使用mov [rbp-XX]而非push/pop属于规范的x86-64栈使用吗?
环境与测试代码
我在Fedora x64 Linux系统上,使用GCC 12.2.1编译器并添加-Wall和-g3标志编译了一段C程序,随后用gdb-gef进行反汇编。源代码如下:
#include <stdio.h> int addNumbers(int a,int b,int c,int d, int e, int f, int g, int h); int main(void) { printf("%d\n", addNumbers(5,17,900,2483,24,67,98,4923)); return 0; } int addNumbers(int a, int b, int c, int d, int e, int f, int g, int h) { int local1 = 16; int local2 = 24; return a + b + c + d + e + f + g + h + local1 + local2; }
反汇编结果
gef➤ disass addNumbers Dump of assembler code for function addNumbers: 0x0000000000401172 <+0>: push rbp 0x0000000000401173 <+1>: mov rbp,rsp 0x0000000000401176 <+4>: mov DWORD PTR [rbp-0x14],edi 0x0000000000401179 <+7>: mov DWORD PTR [rbp-0x18],esi 0x000000000040117c <+10>: mov DWORD PTR [rbp-0x1c],edx 0x000000000040117f <+13>: mov DWORD PTR [rbp-0x20],ecx 0x0000000000401182 <+16>: mov DWORD PTR [rbp-0x24],r8d 0x0000000000401186 <+20>: mov DWORD PTR [rbp-0x28],r9d => 0x000000000040118a <+24>: mov DWORD PTR [rbp-0x4],0x10 0x0000000000401191 <+31>: mov DWORD PTR [rbp-0x8],0x18 0x0000000000401198 <+38>: mov edx,DWORD PTR [rbp-0x14] 0x000000000040119b <+41>: mov eax,DWORD PTR [rbp-0x18] 0x000000000040119e <+44>: add edx,eax 0x00000000004011a0 <+46>: mov eax,DWORD PTR [rbp-0x1c] 0x00000000004011a3 <+49>: add edx,eax 0x00000000004011a5 <+51>: mov eax,DWORD PTR [rbp-0x20] 0x00000000004011a8 <+54>: add edx,eax 0x00000000004011aa <+56>: mov eax,DWORD PTR [rbp-0x24] 0x00000000004011ad <+59>: add edx,eax 0x00000000004011af <+61>: mov eax,DWORD PTR [rbp-0x28] 0x00000000004011b2 <+64>: add edx,eax 0x00000000004011b4 <+66>: mov eax,DWORD PTR [rbp+0x10] 0x00000000004011b7 <+69>: add edx,eax 0x00000000004011b9 <+71>: mov eax,DWORD PTR [rbp+0x18] 0x00000000004011bc <+74>: add edx,eax 0x00000000004011be <+76>: mov eax,DWORD PTR [rbp-0x4] 0x00000000004011c1 <+79>: add edx,eax 0x00000000004011c3 <+81>: mov eax,DWORD PTR [rbp-0x8] 0x00000000004011c6 <+84>: add eax,edx 0x00000000004011c8 <+86>: pop rbp 0x00000000004011c9 <+87>: ret
栈内存转储
gef➤ telescope $rbp-0x20 0x00007fffffffda10│+0x0000: 0x00000384000009b3 0x00007fffffffda18│+0x0008: 0x0000000500000011 0x00007fffffffda20│+0x0010: 0x0000000000000000 0x00007fffffffda28│+0x0018: 0x0000000000000000 0x00007fffffffda30│+0x0020: 0x00007fffffffda50 → 0x0000000000000001 ← $rsp, $rbp 0x00007fffffffda38│+0x0028: 0x0000000000401156 → <main+48> add rsp, 0x10 0x00007fffffffda40│+0x0030: 0x0000000000000062 ("b"?) 0x00007fffffffda48│+0x0038: 0x000000000000133b 0x00007fffffffda50│+0x0040: 0x0000000000000001 0x00007fffffffda58│+0x0048: 0x00007ffff7c29550 → <__libc_start_call_main+128> mov edi, eax
疑问
在addNumbers函数的初始指令中,编译器按照System V调用约定,将前6个参数通过rdi、rsi、rdx、rcx、r8d、r9d寄存器接收,剩余参数存于栈中,随后通过mov DWORD PTR [rbp-0x14], edi这类指令将寄存器中的参数存入栈内存区域。我有两个疑问:
- 这种使用
mov指令而非传统push/pop操作的行为,是否属于规范的栈使用? - 此处编译器未先调整
rsp扩展栈帧,就直接通过mov访问栈内存区域(此时rbp与rsp指向同一地址,mov操作的区域不在rbp和rsp之间的栈帧范围内),推测是因为mov指令效率更高,但这种操作是否违反栈约定或规则?
解答
这种用法完全符合x86-64 System V ABI的规范,不存在违反栈约定的问题,原因如下:
栈帧的本质与访问权限
x86-64的栈是向下增长的内存区域,只要是rsp以下(更低地址)的内存,在保证不会被其他上下文(比如信号处理、嵌套函数调用)覆盖的前提下,程序可以合法访问。ABI并没有强制要求必须通过push指令分配栈空间,也没有要求必须先调整rsp才能使用栈内存——只要编译器能确保该内存区域的安全性即可。编译器的栈空间预分配优化
GCC在这里采用了栈空间预分配的优化策略:函数开头先建立rbp栈帧(push rbp; mov rbp,rsp),随后直接通过mov向rbp偏移的地址写入数据,而非先执行sub rsp, xxx扩展栈帧。这种做法的优势在于:
- 减少指令数量:
mov可直接写入目标地址,无需先调整rsp - 避免栈指针的频繁修改,提升执行效率
实际上,编译器会在函数的合适时机(比如调用其他函数前)统一调整rsp,或者在函数结束时通过leave指令(等价于mov rsp,rbp; pop rbp)自动回收栈空间,确保栈指针最终恢复正确。
参数存储的合规性
根据System V ABI,前6个整数参数通过寄存器传递,但如果函数需要保存这些参数(比如后续要修改寄存器,或者需要通过栈访问参数),编译器可以选择将寄存器中的参数写入栈帧。用mov而非push完成这一操作,只是实现方式的差异,完全符合ABI对参数存储的要求——ABI仅规定参数的传递方式,并未限制参数存入栈时使用的指令。栈内存的安全性保障
从栈内存转储可以看到,rbp-0x28到rbp之间的区域在函数执行时是空闲的,编译器可以安全使用这些空间,因为:
- 父函数的栈帧在
rbp之上(更高地址),不会被当前函数的操作覆盖 - 当前函数未调用其他函数时,
rsp不需要向下扩展,不会覆盖这些预使用的空间 - 如果后续需要调用其他函数,编译器会提前调整
rsp,确保有足够的栈空间供被调用函数使用
总结:GCC的这种实现是完全合规的优化手段,既符合System V ABI的约定,也能提升代码执行效率。
内容的提问来源于stack exchange,提问作者the_endian

