为什么c程序为局部变量保留的空间没有被使用



我正在阅读从头开始编程。pdf地址:http://mirror.ossplanet.net/nongnu/pgubook/ProgrammingGroundUp-0-8.pdf

我很好奇Page37为局部变量保留的空间。他说,我们需要2个单词的内存,所以把堆栈指针下移2个单词。执行这个指令:subl$8,%esp所以,在这里,我想我明白了。

但是,我写了c代码来验证这个保留空间。

#include <stdio.h>
int test(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, int a12) {
printf("a1=%#x, a2=%#x, a3=%#x, a4=%#x, a5=%#x, a6=%#x, a7=%#x, a8=%#x, a9=%#x, a10=%#x, a11=%#x, a12=%#x", a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);
return 0;
}
int main(void){
test(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12);
printf("Wick is me!");
return 0;
}

然后,我使用gcc转换为可执行文件gcc -Og -g,并使用gdb调试器。

我使用disass来执行main函数,并复制了下面的一些asm代码。

0x000055555555519d <+0>: endbr64 
0x00005555555551a1 <+4>: sub    $0x8,%rsp  # reserve space?
0x00005555555551a5 <+8>: pushq  $0x12
0x00005555555551a7 <+10>:    pushq  $0x11
0x00005555555551a9 <+12>:    pushq  $0x10
0x00005555555551ab <+14>:    pushq  $0x9
0x00005555555551ad <+16>:    pushq  $0x8
0x00005555555551af <+18>:    pushq  $0x7
0x00000000000011b1 <+20>:    mov    $0x6,%r9d
0x00000000000011b7 <+26>:    mov    $0x5,%r8d
0x00000000000011bd <+32>:    mov    $0x4,%ecx
0x00000000000011c2 <+37>:    mov    $0x3,%edx
0x00000000000011c7 <+42>:    mov    $0x2,%esi
0x00000000000011cc <+47>:    mov    $0x1,%edi
0x00000000000011d1 <+52>:    callq  0x1149 <test>
0x00000000000011d6 <+57>:    add    $0x30,%rsp
0x00000000000011da <+61>:    lea    0xe89(%rip),%rsi        # 0x206a
0x00000000000011e1 <+68>:    mov    $0x1,%edi
0x00000000000011e6 <+73>:    mov    $0x0,%eax
0x00000000000011eb <+78>:    callq  0x1050 <__printf_chk@plt>
0x00000000000011f0 <+83>:    mov    $0x0,%eax
0x00000000000011f5 <+88>:    add    $0x8,%rsp
0x00005555555551f9 <+92>:    retq

我怀疑这是否是保留空间指令。然后,我逐行执行汇编代码并检查堆栈中的内容。

为什么这个指令只有子8字节,而0x7fffffffe390似乎是主函数的返回地址。这不应该是保留空间吗

下面是rsp地址附近的内容。i r $rsp, x/40xb rsp address

0x7fffffffe390: 0x00    0x52    0x55    0x55    0x55    0x55    0x00    0x00   => after sub
0x7fffffffe398: 0xb3    0x20    0xdf    0xf7    0xff    0x7f    0x00    0x00   => before sub

然后,我执行所有的pushq指令,并使用x/64xb 0x7fffffffe360

0x7fffffffe360: 0x07    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe368: 0x08    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe370: 0x09    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe378: 0x10    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe380: 0x11    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffe388: 0x12    0x00    0x00    0x00    0x00    0x00    0x00    0x00
above is local variables
==========================
0x7fffffffe390: 0x00    0x52    0x55    0x55    0x55    0x55    0x00    0x00
0x7fffffffe398: 0xb3    0x20    0xdf    0xf7    0xff    0x7f    0x00    0x00

我认为0x7fffffffe390~0x7fffffffe398是为局部变量保留空间,但它没有改变!我的考试方式错了吗?

执行环境:

  • GDB版本:9.2
  • GCC版本:9.4.0
  • os:x86_64 GNU/Linux

x86-64 SysV ABI要求在调用时堆栈16对齐。由于调用指令将一个8字节的返回地址推送到堆栈,因此在函数开始时堆栈总是错位8,如果要进行嵌套调用,则调用方需要将一个奇数的8字节推送到堆栈以使其再次对齐16。

由于函数接受12个整数参数,其中6个以8字节的形式进入堆栈,因此需要在堆栈参数之前将额外的8字节推送到堆栈,以便在调用之前堆栈16对齐。

如果您的函数采用了11个参数(或任何其他6个(寄存器参数)+奇数堆栈数量的参数),则不需要额外的堆栈推送。

Gcc和clang仍在奇怪地生成sub rsp, 16(gcc)和push rax; sub rsp, 8;(clang)(https://gcc.godbolt.org/z/jGj5WPq8c)。我不明白为什么。

回想一下,在x86_64中,调用指令执行以下操作:

  1. 推送RIP的当前值,这是函数返回时将执行的下一条指令。(将RSP向下移动memory-回想一下,在x86_64中,堆栈逐渐减少,因此RBP>RSP)。

  2. 推送RBP的当前值,该值用于帮助恢复调用方的堆栈帧。(再次降低RSP)

  3. 将当前底部指针RBP移动到当前堆栈指针RSP。(这实际上创建了一个零大小的堆栈,从RSP当前所在的位置开始)

因此,在您显示的内存转储中:

0x7fffffffe390: 0x00    0x52    0x55    0x55    0x55    0x55    0x00    0x00
0x7fffffffe398: 0xb3    0x20    0xdf    0xf7    0xff    0x7f    0x00    0x00

0x7fffffffe390的值是从main返回后要执行的下一个函数的地址。此指令位于0x0000555555555200(请记住,intel处理器是little-endian,因此必须向后读取值)。这个内存地址与您为代码显示的其他内存值一致。

此外,main(RBP)堆栈帧的底部位于0x7ffff7df20b3,看起来与您显示的其他堆栈地址一致。

一旦执行了对"main"的调用,就输入函数的preable,这是反汇编的前三行:

0x000055555555519d <+0>: endbr64 
0x00005555555551a1 <+4>: sub    $0x8,%rsp  # reserve space?
0x00005555555551a5 <+8>: pushq  $0x12

第二行sub $0x8, %rsp从堆栈指针中减去0x8,从而从RBP->RSP。这个空间是为局部变量保留的空间(以及函数执行时可能需要的任何其他空间)

接下来我们有一系列的pushq和mov,它们都在做同样的事情。你需要回忆一下

函数的
  1. 参数从右到左求值,因此要测试的最后一个参数首先求值

  2. 前六个自变量在64位代码中的寄存器中传递,因此a1->a6在你看到的寄存器中被传递。

  3. 超过六个参数的任何东西都被推送到堆栈上,因此a7->a12被推到堆栈上。

所有参数都是文字,因此没有局部变量,这些值直接用于pushq或mov。

下一个组件是

0x00000000000011d1 <+52>:    callq  0x1149 <test>
0x00000000000011d6 <+57>:    add    $0x30,%rsp
0x00000000000011da <+61>:    lea    0xe89(%rip),%rsi        # 0x206a
0x00000000000011e1 <+68>:    mov    $0x1,%edi
0x00000000000011e6 <+73>:    mov    $0x0,%eax

在这里我们看到了实际的测试调用。下一条指令是清理堆栈。回想一下,我们在堆栈上推送6个8字节的值,导致堆栈向下增长48字节。通过向上移动RSP,添加0x30(48位小数)可以有效地从堆栈中删除这6个值。

接下来的两行设置要传递给printf的参数,下一行mov $0x0, %eax清除EAX,这是函数返回值通常所在的位置。

汇编的最后一位(内存地址发生了变化,我怀疑这是来自第二次运行的代码):

0x00000000000011eb <+78>:    callq  0x1050 <__printf_chk@plt>
0x00000000000011f0 <+83>:    mov    $0x0,%eax
0x00000000000011f5 <+88>:    add    $0x8,%rsp
0x00005555555551f9 <+92>:    retq

执行对printf的实际调用,然后清除返回值(printf返回一个带有打印的字符数的int值),最后add $0x8, %rsp撤消在反汇编的第2行执行的减法,有效地破坏了main的堆栈帧。最后一行retq是来自main的返回。

sub $0x8,%rsp为局部变量(或中间值)保留了8个字节,这是正确的。但是,main不使用任何局部变量,所以不会有任何变化。

作为测试,您可以在main中添加一些局部变量:

int a = 5, b = 10, c;
c = 3*a + 2*b;
printf("Wick is me %dn", c);   // <--- note modification in this line

在这种情况下,您应该在第2行看到对从RSP中减去的值的一些修改。我们预计需要额外的24字节堆栈空间,但由于的几个原因,它可能会有所不同

  1. 计算结果3*a' and2*b'需要存储在堆栈或寄存器中的某个位置
  2. a和b的值是文字,可以存储在寄存器中
  3. 编译器可能能够推断出3a+2b是一个常数,并在编译时执行数学运算,同时优化a' andb',并将"c"设置为35

使用-O0或-Og以及使用-m32(强制使用32位处理器的代码)可能会消除其中的一些问题。

更新:

我把-Og误读为-O0。随着优化的进行,还有一些额外的复杂性(例如GCC如何准确地选择传递参数,它是为本地保留空间还是将这些本地保持在寄存器中,等等)

要了解发生了什么,您应该首先了解没有优化的图片


保留空间在哪里?

;保留空间";在x86_64:上的堆栈上

  • push ...
  • sub ...,%rsp
  • enter ...

还有几种方法可以";未送达";它是:pop ...add ...,%rspleave

在您的情况下,是pushq指令同时将一个值放入堆栈槽并为该值保留空间

你没有展示retq之前发生的事情,但我怀疑你的";未送达";看起来有点像add $68,%rsp

附言:你有一个0x01, 0x02 ..., 0x09, 0x10, ...序列。请注意,这些是而不是连续数字:0x09之后的下一个数字是0x0a

最新更新