You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用mov [rbp-XX]而非push/pop属于规范的x86-64栈使用吗?

关于x86-64 System V调用约定下栈使用规范的疑问

环境与测试代码

我在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这类指令将寄存器中的参数存入栈内存区域。我有两个疑问:

  1. 这种使用mov指令而非传统push/pop操作的行为,是否属于规范的栈使用?
  2. 此处编译器未先调整rsp扩展栈帧,就直接通过mov访问栈内存区域(此时rbp与rsp指向同一地址,mov操作的区域不在rbp和rsp之间的栈帧范围内),推测是因为mov指令效率更高,但这种操作是否违反栈约定或规则?

解答

这种用法完全符合x86-64 System V ABI的规范,不存在违反栈约定的问题,原因如下:

  1. 栈帧的本质与访问权限
    x86-64的栈是向下增长的内存区域,只要是rsp以下(更低地址)的内存,在保证不会被其他上下文(比如信号处理、嵌套函数调用)覆盖的前提下,程序可以合法访问。ABI并没有强制要求必须通过push指令分配栈空间,也没有要求必须先调整rsp才能使用栈内存——只要编译器能确保该内存区域的安全性即可。

  2. 编译器的栈空间预分配优化
    GCC在这里采用了栈空间预分配的优化策略:函数开头先建立rbp栈帧(push rbp; mov rbp,rsp),随后直接通过mov向rbp偏移的地址写入数据,而非先执行sub rsp, xxx扩展栈帧。这种做法的优势在于:

  • 减少指令数量:mov可直接写入目标地址,无需先调整rsp
  • 避免栈指针的频繁修改,提升执行效率

实际上,编译器会在函数的合适时机(比如调用其他函数前)统一调整rsp,或者在函数结束时通过leave指令(等价于mov rsp,rbp; pop rbp)自动回收栈空间,确保栈指针最终恢复正确。

  1. 参数存储的合规性
    根据System V ABI,前6个整数参数通过寄存器传递,但如果函数需要保存这些参数(比如后续要修改寄存器,或者需要通过栈访问参数),编译器可以选择将寄存器中的参数写入栈帧。用mov而非push完成这一操作,只是实现方式的差异,完全符合ABI对参数存储的要求——ABI仅规定参数的传递方式,并未限制参数存入栈时使用的指令。

  2. 栈内存的安全性保障
    从栈内存转储可以看到,rbp-0x28到rbp之间的区域在函数执行时是空闲的,编译器可以安全使用这些空间,因为:

  • 父函数的栈帧在rbp之上(更高地址),不会被当前函数的操作覆盖
  • 当前函数未调用其他函数时,rsp不需要向下扩展,不会覆盖这些预使用的空间
  • 如果后续需要调用其他函数,编译器会提前调整rsp,确保有足够的栈空间供被调用函数使用

总结:GCC的这种实现是完全合规的优化手段,既符合System V ABI的约定,也能提升代码执行效率。


内容的提问来源于stack exchange,提问作者the_endian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 02:40:31