TOP

C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一)
2014-11-23 22:59:17 】 浏览:10049
Tags:特性 匿名 函数 Lambda 表达式 汇编 实现 分析

C++11新特性中提供了对匿名函数(称为Lambda表达式)的支持,本文将对其底层的汇编代码实现作简要分析,如有雷同,纯属巧合~


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(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指针,然后下面比较关键:


那么绕了半天做了什么事,其实就是相当于下面的代码:


s = *this;


那么这个this确实是指向了main里面的a,如何办到的?


查看main栈内存发现,传给闭包的this是指向下图中选中部分,而红框中是变量a:


可见,a在main的栈空间被复制了一次,而不是闭包的栈空间,那么复制发生在哪个时候,为什么this恰好就指向了a的副本?


再调用闭包函数之前,还做了一些事情:


i
C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一) https://www.cppentry.com/bencandy.php?fid=54&id=25276

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Python base64解码TypeError: Inc.. 下一篇C语言找出100万以内的质数