C++11新特性中的匿名函數Lambda表達式的彙編實現分析(一)

Constructs a closure: an unnamed function object capable of capturing variables in scope.html

—— Lambda functions (since C++11) [cppreference.com]express

按照C++11標準的說法,lambda表達式的標準格式以下:數組

[ capture ] ( params ) mutable exception attribute -> ret { body } 
// (1) 完整的聲明

[ capture ] ( params ) -> ret { body }  
//(2) 一個常lambda的聲明:按副本捕獲的對象不能被修改。

[ capture ] ( params ) { body } 
// (3) 省略後綴返回值類型:閉包的operator()的返回值類型是根據如下規則推導出的:若是body僅包含單一的return語句,那麼返回值類型是返回表達式的類型(在此隱式轉換以後的類型:右值到左值、數組與指針、函數到指針)不然,返回類型是void

[ capture ] { body }  
//(4) 省略參數列表:函數沒有參數,即參數列表是()

    capture - 指定哪些在函數聲明處的做用域中可見的符號將在函數體內可見。閉包

    符號表可按以下規則傳入:函數

    [a,&b],按值捕獲a,並按引用捕獲bthis

    [this],按值捕獲了this指針spa

    [&] 按引用捕獲在lambda表達式所在函數的函數體中說起的所有自動儲存持續性變量.net

    [=] 按值捕獲在lambda表達式所在函數的函數體中說起的所有自動儲存持續性變量翻譯

    [] 什麼也沒有捕獲debug

    params - 參數列表,與命名函數同樣

    ret - 返回值類型。若是不存在,它由該函數的return語句來隱式決定(或者是void,例如當它不返回任何值的時候)

    body - 函數體

下面,我將從最簡單的形式開始逐步對各類形式的lambda表達式進行彙編分析。

首先是最簡單的類型(4):

和普通表達式同樣,若單純的一個表達式將被編譯器忽略,這裏將lambda表達式賦值給一個棧變量進行分析。

int main()
{
	auto lambda = []{ };

	return 0;
}

IntelliSense顯示這裏的lambda變量實際上是一個 void lambda(),編譯後被解析是main::__l3::void<lambda>(void)類型,debug查看彙編代碼,發現本句並無在main函數裏產生任何彙編代碼,但並不表明這個表達式沒有意義,

...省略...
	auto lambda = []{ };

	return 0;
        xor         eax,eax  
}
...省略...

若使用sizeof(lambda)計算其所佔字節數將獲得1,稍微在main代碼上面一點,能夠發現[]{}是做爲一個函數被編譯:

 push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 push        ecx  
 lea         edi,[ebp-0CCh]  
 mov         ecx,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 
 pop         ecx  
 mov         dword ptr [this],ecx  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret  
 int         3  
 int         3

可見,就像普通函數同樣,[]{}表達式內部被編譯爲一個函數,該函數內有一個this指針做爲棧變量,它指向調用函數時的寄存器ecx。

下面咱們執行這個lambda表達式,進入閉包內部分析,同時,爲了好說明,在函數內增長一條賦值語句。

int main()
{
	auto lambda = []{
		int s = 0xA;
	};
	lambda();
	return 0;
}

對應有彙編代碼:

auto lambda = []{
		int s = 0xA;
	};
	lambda();
 lea         ecx,[ebp-5]  
 call        001E1570  
	return 0;

能夠看到,有一個地址傳送,[ebp-5]的地址送給ecx,而後直接調用閉包函數。

[ebp-5]是main的一個棧變量,佔用4字節,他的值沒有被初始化,debug版本默認是(0xcccccccc)。

將其地址&[ebp-5]送入ecx究竟有什麼含義,不妨先進入閉包函數內部看看:

 push        ebp  
 mov         ebp,esp  
 sub         esp,0D8h  
 push        ebx  
 push        esi  
 push        edi  
 push        ecx  
 lea         edi,[ebp+FFFFFF28h]  
 mov         ecx,36h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 pop         ecx  
 mov         dword ptr [ebp-8],ecx  
		int s = 0xA;
 mov         dword ptr [ebp-14h],0Ah  
	};
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret

可見,剛纔的ecx被push保存,而後又在函數初始化棧完成後(rep stos後),被彈出並寫入局部變量[ebp-8]中,而這個[ebp-8]其實就是上面說到的this指針。也就是說,這個this指針指向main中的一個局部變量。

那麼,爲了進一步研究這個機制,咱們設法讓這個閉包使用this。不妨猜測一下,this既然是指向main裏面的變量,那麼他多是一個base address用來「捕獲」(lambda中的概念)閉包外層做用域內的某些變量。「捕獲」方式在上面有說到,若將上面的[]改成[=],讓lambda按值捕獲main中的int變量s,再看看有什麼變化:

int main()
{
	int a = 0xB;
	auto lambda = [=]{
		int s = a;
	};
	lambda();
	return 0;
}

閉包內對應彙編代碼:

 pop         ecx  
 mov         dword ptr [ebp-8],ecx  
		int s = a;
 mov         eax,dword ptr [ebp-8]  
 mov         ecx,dword ptr [eax]  
 mov         dword ptr [ebp-14h],ecx  
	};

一樣的,先放置this指針,而後下面比較關鍵:

  1. 把this臨時放到eax

  2. 而後再取eax地址對應的值放到臨時ecx寄存器中,這裏就是a

  3. 而後賦值給[ebp-14h]就是s

那麼繞了半天作了什麼事,其實就是至關於下面的代碼:

s = *this;

那麼這個this確實是指向了main裏面的a,如何辦到的?

查看main棧內存發現,傳給閉包的this是指向下圖中選中部分,而紅框中是變量a:

可見,a在main的棧空間被複制了一次,而不是閉包的棧空間,那麼複製發生在哪一個時候,爲何this剛好就指向了a的副本?

再調用閉包函數以前,還作了一些事情:

int a = 0xB;
 mov         dword ptr [ebp-8],0Bh  
	auto lambda = [=]{
		int s = a;
	};
 lea         eax,[ebp-8]  
 push        eax  
 lea         ecx,[ebp-14h]  
 call        010E1BE0  
	lambda();
 lea         ecx,[ebp-14h]  
 call        010E1C20  
	return 0;

發現還call了一個帶參函數:

  1. a的地址送入eax並壓棧,至關於給下面的函數傳參&a

  2. 將給後面閉包用的this保存在ecx中,可能會給下面的一個call使用

上面的操做至關於下面的僞代碼:

call 010E1BE0(  &a , this); //固然,this並非做爲參數傳入的,這裏只是方便理解

能夠預見,010E1BE0函數的做用應該是拷貝a,並讓this指向a,空口無憑,進去看看:

push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 push        ecx  
 lea         edi,[ebp+FFFFFF34h]  
 mov         ecx,33h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 
 pop         ecx  
 mov         dword ptr [ebp-8],ecx  
 mov         eax,dword ptr [ebp-8]  
 mov         ecx,dword ptr [ebp+8]  
 mov         edx,dword ptr [ecx]  
 mov         dword ptr [eax],edx  
 mov         eax,dword ptr [ebp-8]  
 
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         esp,ebp  
 pop         ebp  
 ret         4

先後的代碼循序漸進,主要是中間:

  1. ecx是this不用說了。

  2. 先把this保存到該函數的棧空間再說

  3. this放進eax,預見下面的[eax]就是*this,和上面說到的同樣

  4. 而後是[ebp+8]這塊,送給ecx臨時保存,而後取值,送入edx臨時保存,可見[ebp+8]裏面應該是一個地址

  5. edx送給*this

  6. 最後那個mov eax,[ebp-8] ,又把this做爲返回值

關於[ebp+8]:還記得傳入該函數的參數&a嗎?沒錯,[ebp+8]保存的是就是&a。

簡單翻譯一下這個函數的意思:

fun(&a,this);

int* fun(int* in,int* this)

{

    *this = *in;

    return this;

}

注意這裏的this傳遞實際上是經過寄存器的方式。

好了,說了半天,剛纔那個問題,差很少也知道答案了。

調用閉包函數前,「捕獲者」this指針被放在main中,並對其指向的內存塊拷貝閉包中要用到的變量值,調用時,this經過寄存器送入閉包中,閉包經過this訪問外層做用域(這裏是main)的已捕獲對象(這裏是a)。

可見,若是閉包要按捕獲main中多個變量,那麼事先要調用一個複製函數,依次複製全部要用的變量,而後經過this尋址訪問main中變量的副本,而不是把全部變量拷貝到閉包的棧空間內


上面說的都是最簡單的形式,也即:[=]{ },以後的文章將分析更復雜的lambda表達式。今天先說到這。

C++11新特性中的匿名函數Lambda表達式的彙編實現分析(二)

參考資料:

C++11中的匿名函數(lambda函數,lambda表達式)

Lambda函數

相關文章
相關標籤/搜索