歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C++11新特性中的匿名函數Lambda表達式的匯編實現分析

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

日期:2017/3/1 9:43:10   编辑:Linux編程

C++11新特性中提供了對匿名函數(稱為Lambda表達式)的支持,本文將對其底層的匯編代碼實現作簡要分析,如有雷同,純屬巧合~

C++11新特性:Lambda函數(匿名函數) http://www.linuxidc.com/Linux/2013-12/93367p2.htm

C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm

讀C++ Primer 之構造函數陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm

讀C++ Primer 之智能指針 http://www.linuxidc.com/Linux/2011-08/40177.htm

讀C++ Primer 之句柄類 http://www.linuxidc.com/Linux/2011-08/40175.htm

C++11 獲取系統時間庫函數 time since epoch http://www.linuxidc.com/Linux/2014-03/97446.htm

C++11中正則表達式測試 http://www.linuxidc.com/Linux/2012-08/69086.htm

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

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

按照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,並按引用捕獲b

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

[&] 按引用捕獲在lambda表達式所在函數的函數體中提及的全部自動儲存持續性變量

[=] 按值捕獲在lambda表達式所在函數的函數體中提及的全部自動儲存持續性變量

[] 什麼也沒有捕獲

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表達式。

更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2014-06/103088p2.htm

Copyright © Linux教程網 All Rights Reserved