从汇编的角度揭开C++ this指针的神秘面纱(上)
C++中的this指针一直比较神秘。任何类的对象,都有一个this指针,无处不在。那么this指针的本质究竟是什么?this指针什么时候会被用到?今天通过几段简单的代码,来揭秘一下。
要先揭秘this指针,先来说一下函数调用时参数的传递过程。考虑以下代码:
int sum(int i, int j, int k)
{return i + j + k;
}int main()
{int a, b, c;a = 1;b = 2;c = 3; sum(a,b,c);return 0;
}
这是一段非常简单的函数调用代码。我们生成其汇编代码(x86-64),如下所示:
sum(int, int, int):pushq %rbpmovq %rsp, %rbpmovl %edi, -4(%rbp)movl %esi, -8(%rbp)movl %edx, -12(%rbp)movl -4(%rbp), %edxmovl -8(%rbp), %eaxaddl %eax, %edxmovl -12(%rbp), %eaxaddl %edx, %eaxpopq %rbpret
main:pushq %rbpmovq %rsp, %rbpsubq $16, %rspmovl $1, -4(%rbp)movl $2, -8(%rbp)movl $3, -12(%rbp)movl -12(%rbp), %edxmovl -8(%rbp), %ecxmovl -4(%rbp), %eaxmovl %ecx, %esimovl %eax, %edicall sum(int, int, int)movl $0, %eaxleaveret
我们重点来关注一下函数参数的传递过程。通过分析main函数的汇编函数, 我用类似于C语言的伪代码解释了一下每一行的意思,辅助理解,如下所示。
main pushq %rbpmovq %rsp, %rbp //rbp = rspsubq $16, %rsp //rsp -= 16movl $1, -4(%rbp) //*(rbp-4) = 1movl $2, -8(%rbp) //*(rbp-8) = 2 movl $3, -12(%rbp) //*(rbp-12) = 3movl -12(%rbp), %edx //edx =*(rbp-12)movl -8(%rbp), %ecx //ecx =*(rbp-8) movl -4(%rbp), %eax //eax = *(rbp-4)movl %ecx, %esi //esi = ecx movl %eax, %edi //edi = eaxcall sum(int, int, int)movl $0, %eaxleaveret
在执行这条指令(call sum(int, int, int))前,main函数的栈空间分布如下:
即main函数会存储三个变量: a, b, c. 同时会将其值分别赋值给edi, esi、edx寄存器。那么我们很好奇,将a, b, c三个变量的值赋值给edi, esi、edx寄存器会有什么用呢?我们先来看一下sum函数,我用类似于C语言的伪代码解释了一下每一行的意思,辅助理解,如下所示。
sum(int, int, int):pushq %rbpmovq %rsp, %rbp //rbp = rspmovl %edi, -4(%rbp) //*(rbp-4) = edimovl %esi, -8(%rbp) //*(rbp-8) = esimovl %edx, -12(%rbp) //*(rbp-12) = edxmovl -4(%rbp), %edx //edx = *(rbp-4)movl -8(%rbp), %eax //eax = *(rbp-8)addl %eax, %edx //edx += eax movl -12(%rbp), %eax //eax = *(rbp-12) addl %edx, %eax //eax += edx popq %rbpret
sum函数的栈空间分布如下:
我们重点关注一下这几条指令:
movl %edi, -4(%rbp) //*(rbp-4) = edi
movl %esi, -8(%rbp) //*(rbp-8) = esi
movl %edx, -12(%rbp) //*(rbp-12) = edx
可以看到,在sum函数的栈空间中,其会分配三个存储单元,rbp-4, rbp-8, rbp-12存储1,2,3。而1,2,3这三个值分别又是从edi, esi、edx三个寄存器中拷贝过来的。而这三个寄存器的值又是来自main函数中a, b, c三个变量的赋值。也就是说,这里edi, esi、edx三个寄存器,在函数调用时,完成了参数的传递。那么这种参数传递的现像是不是有什么约定呢? 答案是有的!
在Linux/macOS 等 Unix-like系统中,函数的调用约定标准为System V AMD64 ABI,其参数传递机制:
参数位置 | 整数/指针寄存器 | 浮点寄存器 |
---|---|---|
第 1 个 | RDI | XMM0 |
第 2 个 | RSI | XMM1 |
第 3 个 | RDX | XMM2 |
第 4 个 | RCX | XMM3 |
第 5 个 | R8 | XMM4 |
第 6 个 | R9 | XMM5 |
第 7+ 个 | 栈(右→左) | XMM6-7 |
从这个约定中得知,在传递整数时,第一个参数用的是RDI寄存器,第二个参数用的是RSI寄存器,第三个参数用的是RDX寄存器。上面函数调用的例子中正好符合此调用约定(例子中用的是edi, esi、edx三个寄存器传递第1,第2,第3个参数,而edi, esi、edx正好是RDI、RSI、RDX三个寄存器的低32位)。
<this指针揭秘继续...>