NOTE: 以下讨论使用的平台为 x86-64,使用的编译器为 gcc,以下提供的伪汇编码 采用 intel 格式。
文章中提到的代码,可以在 Code 找到。
一些重要的寄存器和指令
函数调用过程中,比较重要的寄存器主要有三个 rip rbp rsp。
rip寄存器存放的为下一条要执行指令的地址,Instruction Pointer。rbp为基寄存器,存放的为前一个rbp的值,Base Pointer。rsp为栈寄存器,一直指向函数调用栈的底部,Stack Pointer。
比较重要的指令有 call push pop leave ret。
call addr 指令会首先将 call 返回之后要执行的地址压入栈中(返回地址),设置 rip
的值为 addr,然后跳转的这个位置去执行,大致可以等效于一下指令。
# 假设当执行到 call 指令时 CPU 就会自动设置 rip 寄存器为下一条要执行的指令
push rip
mov rip, addr
# 按理设置了 rip 寄存器,CPU 就会去执行 rip 地址的指令,这里写 jmp 只是想要更清晰的表示 call 的流程
jmp addr
push rbp 指令会将 rsp 寄存器下移一个 word 的长度,rbp 中的内容移动到 rsp
寄存器所指向的地址中。
sub rsp, 0x8
mov QWORD PTR [rsp], rbp
pop rbp 指令会将当前 rsp 寄存器所指向地址中的内容移动到 rbp 寄存器中,并将 rsp
寄存器上移一个 word 的长度。
mov rbp, QWORD PTR [rsp]
add rsp, 0x8
leave 指令会将 rsp 寄存器指向 rbp 当前指向的位置(这个位置存储的为前一个 rbp
的内容),然后将 rbp 寄存器的内容设置为 前一个 rbp 的位置。
mov rsp, rbp
pop rbp
ret 指令会将返回地址设置到 rip 寄存器中,然后跳转到该位置执行。
pop rip
jmp rip
实例
// file name: call.c
int func1(int a, int b, int *c) {
return a + b;
}
void func2(int *c) {
int a = 99;
int b = 1;
int d = func1(a, b, c);
a = a + d;
}
int main() {
int *c = 0;
func2(c);
return 0;
}
使用 gcc -g -no-pie -o call call.c 编译以上代码。然后使用 gdb -q call 调试。
进入 gdb 后首先,set disassembly-flavor intel 将 disassemble 命令转换出
的汇编码设置成 intel 格式,当然如果熟悉 AT&T 汇编格式可以不执行这个命令。
然后 b main 在 main 函数处设置断点。使用 disassemble main 可以查看 main 函数
的汇编码。
0x0000000000401202 <+0>: endbr64
0x0000000000401206 <+4>: push rbp
0x0000000000401207 <+5>: mov rbp,rsp
0x000000000040120a <+8>: sub rsp,0x10
=> 0x000000000040120e <+12>: mov QWORD PTR [rbp-0x8],0x0
0x0000000000401216 <+20>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040121a <+24>: mov rdi,rax
0x000000000040121d <+27>: call 0x401122 <func2>
0x0000000000401222 <+32>: mov eax,0x0
0x0000000000401227 <+37>: leave
0x0000000000401228 <+38>: ret
可以使用 p/x $rbp p/x $rsp p/x $pc 寄存器中的的内容。
(gdb) p/x $rbp
$5 = 0x7fffffffe290
(gdb) p/x $rsp
$6 = 0x7fffffffe280
(gdb) p/x $pc
$7 = 0x40120e
在 call 命令之前的 mov rdi,rax 命令为函数的参数传递。函数的参数传递不同的
类型所使用的寄存器也有所不同,如 integer 类型,使用的寄存器依次为 rdi rsi
rdx rcx r8 r9,如果 integer 参数多于可以使用的寄存器个数会使用栈来进行
参数传递,对于不同类型所使用的寄存器这个 PDF 有详细的介绍。
使用 si 来进行指令级别的单步进入调试。当执行完 call 命令后再次查看三个寄存器
的内容。
(gdb) p/x $rbp
$8 = 0x7fffffffe290
(gdb) p/x $rsp
$9 = 0x7fffffffe278
(gdb) p/x $pc
$10 = 0x401122
使用 disassemble func2 来查看 func2 函数的汇编码。
=> 0x0000000000401122 <+0>: endbr64
0x0000000000401126 <+4>: push rbp
0x0000000000401127 <+5>: mov rbp,rsp
0x000000000040112a <+8>: sub rsp,0x18
0x000000000040112e <+12>: mov QWORD PTR [rbp-0x18],rdi
0x0000000000401132 <+16>: mov DWORD PTR [rbp-0xc],0x63
0x0000000000401139 <+23>: mov DWORD PTR [rbp-0x8],0x1
0x0000000000401140 <+30>: mov rdx,QWORD PTR [rbp-0x18]
0x0000000000401144 <+34>: mov ecx,DWORD PTR [rbp-0x8]
0x0000000000401147 <+37>: mov eax,DWORD PTR [rbp-0xc]
0x000000000040114a <+40>: mov esi,ecx
0x000000000040114c <+42>: mov edi,eax
0x000000000040114e <+44>: call 0x401106 <func1>
0x0000000000401153 <+49>: mov DWORD PTR [rbp-0x4],eax
0x0000000000401156 <+52>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000401159 <+55>: add DWORD PTR [rbp-0xc],eax
0x000000000040115c <+58>: nop
0x000000000040115d <+59>: leave
0x000000000040115e <+60>: ret
p/x *(long *)$rsp 可以查看当前 rsp 寄存器所指向的位置的内容 0x401222。
可以看到这个地址就是 main 函数中 call 指令之后的那条指令的地址,也就是返回地址。
然后使用 si 继续执行,当执行完 push rbp 指令后,继续查看这三个寄存器中的内容。
(gdb) p/x $rbp
$12 = 0x7fffffffe290
(gdb) p/x $rsp
$13 = 0x7fffffffe270
(gdb) p/x $pc
$14 = 0x401127
同样使用 p/x *(long *)$rsp 查看 rsp 寄存器所指向的位置中的内容 0x7fffffffe290。
这个值就是现在 rbp 寄存器中的内容,而下一条指令 mov rbp,rsp 就是将 rbp 指向
当前 rsp 所指向的位置。
(gdb) p/x $rbp
$17 = 0x7fffffffe270
(gdb) p/x $rsp
$18 = 0x7fffffffe270
下面的一部分命令就是将函数的参数和局部变量放入栈中。
接下来,直接跳转到 func2 函数中的 leave 指令位置,当执行完 leave 指令后再次
查看三个寄存器中的值。
(gdb) p/x $rbp
$17 = 0x7fffffffe290
(gdb) p/x $rsp
$18 = 0x7fffffffe278
(gdb) p/x $pc
$19 = 0x40115e
可以看到 rbp 寄存器又指向了在 func2 函数调用之前所指向的位置,rsp 寄存器目前指向 的为返回地址的位置。
然后执行 ret 指令后,三个寄存器全部变为调用 func2 之前的状态,对 func2 的函数
调用过程执行完成。
(gdb) p/x $rbp
$21 = 0x7fffffffe290
(gdb) p/x $rsp
$22 = 0x7fffffffe280
(gdb) p/x $pc
$23 = 0x401222
上述过程的图示。

Variable Length Array(VLA) 和 alloca
在 ISO C99 之后 C 语言支持 variable length array,就是使用变量作为数组的大小。 而这个数据依然是放在 stack 当中,alloca 的实现方式和 VLA 基本相同放在一起探讨。
使用下面的 C 代码来进行演示
// file name: vla.c
int func1(int n) {
int b = 0xbe;
int a[n];
int c = 0xef;
a[n - 1] = n * 2;
a[n - 1] += 2;
return a[n - 1];
}
int main() {
int a = func1(10);
a = func1(100);
return 0;
}
同样使用 gcc -g -no-pie -o vla vla.c,然后使用 gdb -q vla 进行调试,
使用 disassemble func1 可以得到它的如下汇编码
0x0000000000401136 <+0>: endbr64
0x000000000040113a <+4>: push rbp
0x000000000040113b <+5>: mov rbp,rsp
0x000000000040113e <+8>: push rbx
0x000000000040113f <+9>: sub rsp,0x38
0x0000000000401143 <+13>: mov DWORD PTR [rbp-0x34],edi
0x0000000000401146 <+16>: mov rax,QWORD PTR fs:0x28
0x000000000040114f <+25>: mov QWORD PTR [rbp-0x18],rax
0x0000000000401153 <+29>: xor eax,eax
0x0000000000401155 <+31>: mov rax,rsp
0x0000000000401158 <+34>: mov rsi,rax
0x000000000040115b <+37>: mov DWORD PTR [rbp-0x30],0xbe
0x0000000000401162 <+44>: mov eax,DWORD PTR [rbp-0x34]
0x0000000000401165 <+47>: movsxd rdx,eax
0x0000000000401168 <+50>: sub rdx,0x1
0x000000000040116c <+54>: mov QWORD PTR [rbp-0x28],rdx
0x0000000000401170 <+58>: movsxd rdx,eax
0x0000000000401173 <+61>: mov r8,rdx
0x0000000000401176 <+64>: mov r9d,0x0
0x000000000040117c <+70>: movsxd rdx,eax
0x000000000040117f <+73>: mov rcx,rdx
0x0000000000401182 <+76>: mov ebx,0x0
0x0000000000401187 <+81>: cdqe
0x0000000000401189 <+83>: lea rdx,[rax*4+0x0]
0x0000000000401191 <+91>: mov eax,0x10
0x0000000000401196 <+96>: sub rax,0x1
0x000000000040119a <+100>: add rax,rdx
0x000000000040119d <+103>: mov ebx,0x10
0x00000000004011a2 <+108>: mov edx,0x0
0x00000000004011a7 <+113>: div rbx
0x00000000004011aa <+116>: imul rax,rax,0x10
0x00000000004011ae <+120>: mov rcx,rax
0x00000000004011b1 <+123>: and rcx,0xfffffffffffff000
0x00000000004011b8 <+130>: mov rdx,rsp
0x00000000004011bb <+133>: sub rdx,rcx
0x00000000004011be <+136>: cmp rsp,rdx
0x00000000004011c1 <+139>: je 0x4011d5 <func1+159>
0x00000000004011c3 <+141>: sub rsp,0x1000
0x00000000004011ca <+148>: or QWORD PTR [rsp+0xff8],0x0
0x00000000004011d3 <+157>: jmp 0x4011be <func1+136>
0x00000000004011d5 <+159>: mov rdx,rax
0x00000000004011d8 <+162>: and edx,0xfff
0x00000000004011de <+168>: sub rsp,rdx
0x00000000004011e1 <+171>: mov rdx,rax
0x00000000004011e4 <+174>: and edx,0xfff
0x00000000004011ea <+180>: test rdx,rdx
0x00000000004011ed <+183>: je 0x4011ff <func1+201>
0x00000000004011ef <+185>: and eax,0xfff
0x00000000004011f4 <+190>: sub rax,0x8
0x00000000004011f8 <+194>: add rax,rsp
0x00000000004011fb <+197>: or QWORD PTR [rax],0x0
0x00000000004011ff <+201>: mov rax,rsp
0x0000000000401202 <+204>: add rax,0x3
0x0000000000401206 <+208>: shr rax,0x2
0x000000000040120a <+212>: shl rax,0x2
0x000000000040120e <+216>: mov QWORD PTR [rbp-0x20],rax
0x0000000000401212 <+220>: mov DWORD PTR [rbp-0x2c],0xef
0x0000000000401219 <+227>: mov eax,DWORD PTR [rbp-0x34]
0x000000000040121c <+230>: lea edx,[rax-0x1]
0x000000000040121f <+233>: mov eax,DWORD PTR [rbp-0x34]
0x0000000000401222 <+236>: lea ecx,[rax+rax*1]
0x0000000000401225 <+239>: mov rax,QWORD PTR [rbp-0x20]
0x0000000000401229 <+243>: movsxd rdx,edx
0x000000000040122c <+246>: mov DWORD PTR [rax+rdx*4],ecx
0x000000000040122f <+249>: mov eax,DWORD PTR [rbp-0x34]
0x0000000000401232 <+252>: lea edx,[rax-0x1]
0x0000000000401235 <+255>: mov rax,QWORD PTR [rbp-0x20]
0x0000000000401239 <+259>: movsxd rdx,edx
0x000000000040123c <+262>: mov eax,DWORD PTR [rax+rdx*4]
0x000000000040123f <+265>: mov edx,DWORD PTR [rbp-0x34]
0x0000000000401242 <+268>: sub edx,0x1
0x0000000000401245 <+271>: lea ecx,[rax+0x2]
0x0000000000401248 <+274>: mov rax,QWORD PTR [rbp-0x20]
0x000000000040124c <+278>: movsxd rdx,edx
0x000000000040124f <+281>: mov DWORD PTR [rax+rdx*4],ecx
0x0000000000401252 <+284>: mov eax,DWORD PTR [rbp-0x34]
0x0000000000401255 <+287>: lea edx,[rax-0x1]
0x0000000000401258 <+290>: mov rax,QWORD PTR [rbp-0x20]
0x000000000040125c <+294>: movsxd rdx,edx
0x000000000040125f <+297>: mov eax,DWORD PTR [rax+rdx*4]
0x0000000000401262 <+300>: mov rsp,rsi
0x0000000000401265 <+303>: mov rdx,QWORD PTR [rbp-0x18]
0x0000000000401269 <+307>: sub rdx,QWORD PTR fs:0x28
0x0000000000401272 <+316>: je 0x401279 <func1+323>
0x0000000000401274 <+318>: call 0x401040 <__stack_chk_fail@plt>
0x0000000000401279 <+323>: mov rbx,QWORD PTR [rbp-0x8]
0x000000000040127d <+327>: leave
0x000000000040127e <+328>: ret

0x0000000000401146 <+16>: mov rax,QWORD PTR fs:0x28
0x000000000040114f <+25>: mov QWORD PTR [rbp-0x18],rax
...
0x0000000000401265 <+303>: mov rdx,QWORD PTR [rbp-0x18]
0x0000000000401269 <+307>: sub rdx,QWORD PTR fs:0x28
0x0000000000401272 <+316>: je 0x401279 <func1+323>
0x0000000000401274 <+318>: call 0x401040 <__stack_chk_fail@plt>
0x0000000000401279 <+323>: mov rbx,QWORD PTR [rbp-0x8]
这两条指令就是设置图片中的 stack guard,用于检查对栈的操作是否越界了。可以看到 在这个函数的最后会检查这个位置的值是否和原始相同,如果相同就会跳转到 0x401279处 执行,否则就会执行 __stack_chk_fail 函数,产生 segmentation falut 错误。这个检查 主要是为了防止修改掉 prev rbp 和返回地址从而导致程序执行错误。
0x0000000000401153 <+29>: xor eax,eax
0x0000000000401155 <+31>: mov rax,rsp
0x0000000000401158 <+34>: mov rsi,rax
设置 rsi 暂存 rsp 当前的值。
从 0x401162 到 0x40120e 为计算和设置 VLA 的开始地址,即图中的蓝色部分,同时也将 rsp 设置到栈底。
从 0x401219 到 0x40124f 对应的为 C 代码中的对 a[n-1] 赋值的两行。
从 0x401252 到 0x40125f 对应的为 C 代码中的 return a[n-1]。
0x0000000000401262 <+300>: mov rsp,rsi
恢复 rsp 寄存器为 0x7fffffffe230。
然后正常退出函数。
为什么在函数开头和结尾 push rbx and mov rbx,QWORD PTR [rbp-0x8]?

根据上图可以看到被调用的函数如果有用到 rbx 寄存器,是有责任保存这个寄存器的。
Ref
- what registers are preserved through a linux x86_64 function call
- what does the endbr64 instruction actually do
- gcc VLA
- x86 and amd64 instruction reference
- x86 what does movsxd rdx edx instruction mean
- x86-64 reference sheet.pdf
- difference between je jne and jz jnz
- why does this memory address fs0x28 fs0x28 have a random value