Lambdas作为闭包采取环境.RIP寄存器的关键作用

Lambdas as closures taking environment. The crucial role of RIP register

本文关键字:寄存器 作用 RIP 环境 闭包 Lambdas      更新时间:2023-10-16

我查看了以下代码段的汇编程序输出,我惊呆了:

int x=0, y=0; // global
// r1, r2 are ints, local.
std::thread t([&x, &y, &r1, &r2](){
x = 1;      
r1 = y;     
});

!std::thread t([&x, &y, &r1, &r2](){
<lambda()>::operator()(void) const+0: push   %rbp
<lambda()>::operator()(void) const+1: mov    %rsp,%rbp
<lambda()>::operator()(void) const+4: mov    %rdi,-0x8(%rbp)
<lambda()>::operator()(void) const+18: mov    -0x8(%rbp),%rax
<lambda()>::operator()(void) const+22: mov    (%rax),%rax
!   x = 1;      
<lambda()>::operator()(void) const()
<lambda()>::operator()(void) const+8: movl   $0x1,0x205362(%rip)        # 0x6062ac <x>
!   r1 = y;     
<lambda()>::operator()(void) const+25: mov    0x205359(%rip),%edx        # 0x6062b0 <y>
<lambda()>::operator()(void) const+31: mov    %edx,(%rax)
!
!});
<lambda()>::operator()(void) const+33: nop
<lambda()>::operator()(void) const+34: pop    %rbp
<lambda()>::operator()(void) const+35: retq   

为什么x的地址,y确定与RIP有关。RIP是一个指令指针,所以它似乎是狂野的。特别是,我从未见过这样的事情。(也许我没有看到很多东西:))。

我脑海中浮现的唯一解释是这样一个事实,即lambda是一个闭包,并且从特定位置获取环境变量与RIP有一些共同之处。

代码在运行时不会移动,一旦加载了代码部分,例程就不会被复制或移动。
静态数据在加载其部分后也会占用相同的地址。
因此,指令和静态变量之间的距离在编译时是已知的,并且在模块基的重新定位下它是不变的(因为指令和数据都以相同的量转换)。

因此,RIP相对寻址不仅不疯狂,而且长期以来一直缺少该功能。
在 32 位代码中,像mov eax, [var]这样的指令是无害的,但在没有 RIP 相对寻址的 64 位中,它需要 9 个字节,1 个用于操作码,8 个用于即时。 使用 RIP 相对寻址时,即时寻址仍然是 32 位。


C++ lamdba 是函数对象的语法糖,其中捕获的变量成为实例变量。
引用捕获的变量作为指针/引用处理。
全局变量在捕获时不需要任何特殊处理,因为它们已经可以访问。

您正确地指出,xy分别作为0x205362(%rip)0x205359(%rip)进行访问。
由于它们是全局的,因此它们的地址在运行时是固定的,并且使用 RIP 相对寻址来访问它们。

但是,您忘了检查如何访问局部捕获的变量r1
它与(%rax)一起存储,rax之前被加载为(优化)movq (%rdi), %rax
%rdi是方法operator()的第一个参数,所以this,刚才提到的指令将第一个实例变量加载到rax中,然后使用该值访问r1
简单地说,它是指向r1的指针(或更好的引用),因为r1存在于堆栈上,其地址在运行时是动态的(这取决于堆栈的状态)。

因此,lambda 同时使用间接寻址和 RIP 相对寻址,从而与 RIP 相对寻址在某种程度上很特殊的假设相矛盾。


请注意,捕获机制不会延长捕获变量的生命周期(如在 ECMAScript 中),因此在 lambda 中通过引用捕获本地变量以进行std::thread几乎总是一个坏主意。