为啥这个函数序言中没有“sub rsp”指令,为啥函数参数存储在负 rbp 偏移量?

Posted

技术标签:

【中文标题】为啥这个函数序言中没有“sub rsp”指令,为啥函数参数存储在负 rbp 偏移量?【英文标题】:Why is there no "sub rsp" instruction in this function prologue and why are function parameters stored at negative rbp offsets?为什么这个函数序言中没有“sub rsp”指令,为什么函数参数存储在负 rbp 偏移量? 【发布时间】:2020-07-31 20:31:36 【问题描述】:

这就是我通过阅读一些内存分割文档的理解:在调用函数时,有几条指令(称为函数序言)将帧指针保存在堆栈上,将堆栈指针的值复制到基指针中并为局部变量节省一些内存。

这是我尝试使用 GDB 调试的简单代码:

void test_function(int a, int b, int c, int d) 
    int flag;
    char buffer[10];

    flag = 31337;
    buffer[0] = 'A';


int main() 
    test_function(1, 2, 3, 4);

调试此代码的目的是了解调用函数时堆栈中发生的情况:因此我必须在程序执行的各个步骤(调用函数之前和执行期间)检查内存。虽然我通过检查基指针设法看到了返回地址和保存的帧指针之类的东西,但我真的不明白我在反汇编代码之后要写什么。

拆解:

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000400509 <+0>: push   rbp
   0x000000000040050a <+1>: mov    rbp,rsp
   0x000000000040050d <+4>: mov    ecx,0x4
   0x0000000000400512 <+9>: mov    edx,0x3
   0x0000000000400517 <+14>:    mov    esi,0x2
   0x000000000040051c <+19>:    mov    edi,0x1
   0x0000000000400521 <+24>:    call   0x4004ec <test_function>
   0x0000000000400526 <+29>:    pop    rbp
   0x0000000000400527 <+30>:    ret    
End of assembler dump.
(gdb) disassemble test_function 
Dump of assembler code for function test_function:
   0x00000000004004ec <+0>: push   rbp
   0x00000000004004ed <+1>: mov    rbp,rsp
   0x00000000004004f0 <+4>: mov    DWORD PTR [rbp-0x14],edi
   0x00000000004004f3 <+7>: mov    DWORD PTR [rbp-0x18],esi
   0x00000000004004f6 <+10>:    mov    DWORD PTR [rbp-0x1c],edx
   0x00000000004004f9 <+13>:    mov    DWORD PTR [rbp-0x20],ecx
   0x00000000004004fc <+16>:    mov    DWORD PTR [rbp-0x4],0x7a69
   0x0000000000400503 <+23>:    mov    BYTE PTR [rbp-0x10],0x41
   0x0000000000400507 <+27>:    pop    rbp
   0x0000000000400508 <+28>:    ret    
End of assembler dump.

我知道“将帧指针保存在堆栈上”是由“push rbp”完成的,“将堆栈指针的值复制到基指针中”是由“mov rbp, rsp”完成的,但我得到了什么困惑的是缺少“为局部变量节省一些内存”的“sub rsp $n_bytes”。我在很多展览中都看到了这一点(甚至在 *** 上的某些主题中)。

我还读到参数应该与基指针有一个正偏移(在它被堆栈指针值填充之后),因为如果它们位于调用者函数中并且堆栈向低地址增长,那么当基指针用堆栈指针值更新,编译器通过添加一些正数返回堆栈。但是我的代码似乎将它们存储在负偏移量中,就像局部变量一样。我也不明白为什么将它们放在那些寄存器中(主要)..它们不应该直接保存在 rsp“偏移量“?

也许这些差异是由于我使用的是 64 位系统,但我的研究并没有让我找到任何可以解释我所面临问题的东西。

【问题讨论】:

你能在你的 test_function 中添加对另一个函数的调用吗?看起来 rsp 没有更新,因为它没有在您的函数中使用。如果你调用另一个函数,它必须被更新。 是的,我制作了另一个名为“second_test()”的简单函数,只打印了一个字符串。现在在 test_function 中,我有 rsp 的子指令!谢谢 【参考方案1】:

x86-64 的 System V ABI 指定 red zone 低于 %rsp 的 128 个字节。这 128 个字节属于该函数,只要它不调用任何其他函数(它是一个 叶函数)。

信号处理程序(以及由调试器调用的函数)需要遵守红色区域,因为它们实际上是非自愿的函数调用。test_function(叶函数)的所有局部变量都适合红色区域,因此无需调整%rsp。 (此外,该函数没有可见的副作用,并且会在任何合理的优化设置上进行优化)。

您可以使用-mno-red-zone 进行编译,以阻止编译器使用堆栈指针下方的空间。内核代码必须这样做,因为硬件中断没有实现红区。

【讨论】:

中断处理程序在内核堆栈上运行。 信号 处理程序将是异步“您的进程正在临时运行一些其他代码”事件的更好示例。 @PeterCordes:引用AMD64 ABI Draft 1.0, Section 3.2.2 The Stack Frame: [...]The 128-byte area beyond the location pointed to by %rsp is considered to be reserved and shall not be modified by signal or interrupt handlers[...]。注意“或中断处理程序”。 好的,但是 int 处理程序避免在任何保护内核免受用户空间影响的系统上进行修改的方式是拥有一个单独的内核堆栈。否则,您只需在系统调用或其他 ISR -> pwned 期间从另一个线程修改堆栈。或使用%rsp = NULL 进行系统调用-> 崩溃。 ***.com/a/12912556/224132 了解更多。因此,ABI 的这一部分仅对于不使用虚拟内存或不执行 priv sep 的操作系统值得一提。或者我猜它与内核线程有关,但与用户线程无关。 更新:Kernel code can't use a red-zone 特别是因为 x86-64 中断处理不能尊重红色区域。堆栈上只能有一个不用于硬件中断的红色区域。因此,ABI 中的措辞告诉我们,用户堆栈根本不用于中断。但是在多任务操作系统上,出于安全原因,我们已经知道这是正确的,例如在 32 位 Linux 甚至 Windows 中。最多只有软件驱动的异步事件,如信号或 GDB print foo() 函数评估。【参考方案2】:

但我的代码似乎将它们存储在负偏移中,就像局部变量一样

第一个 x86_64 参数通过寄存器而不是堆栈传递。所以当rbp 设置为rsp 时,它们不在堆栈上,也不能在正偏移量上。

他们只被推送到:

为第二个函数调用保存寄存器状态。

在这种情况下,这不是必需的,因为它是leaf function。

使寄存器分配更容易。

但是如果没有memory spill,优化的分配器可以做得更好。

如果你有以下情况,情况会有所不同:

x86_64 函数有很多参数。那些不适合寄存器的进入堆栈。 IA-32,每个参数都在堆栈中。

缺少用于“为局部变量节省一些内存”的“sub rsp $n_bytes”。

问题的叶函数部分红色区域上缺少的sub rsp 已在以下位置提出:Why does the x86-64 GCC function prologue allocate less stack than the local variables?

【讨论】:

以上是关于为啥这个函数序言中没有“sub rsp”指令,为啥函数参数存储在负 rbp 偏移量?的主要内容,如果未能解决你的问题,请参考以下文章

为啥还要写呢?——北漂18年序言

为啥我不能将指令更改为以前的值(函数调用)OllyDbg

为啥在 Angular 指令的链接函数中“this”为空?

为啥这个函数没有在反应中呈现

为啥包装在函数中的 GAS 内联汇编为调用者生成的指令与纯汇编函数不同

为啥在这个单元测试中没有调用 finalize 函数?