我正在阅读从头开始编程。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中,调用指令执行以下操作:
-
推送RIP的当前值,这是函数返回时将执行的下一条指令。(将RSP向下移动memory-回想一下,在x86_64中,堆栈逐渐减少,因此RBP>RSP)。
-
推送RBP的当前值,该值用于帮助恢复调用方的堆栈帧。(再次降低RSP)
-
将当前底部指针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,它们都在做同样的事情。你需要回忆一下
函数的参数从右到左求值,因此要测试的最后一个参数首先求值
前六个自变量在64位代码中的寄存器中传递,因此a1->a6在你看到的寄存器中被传递。
超过六个参数的任何东西都被推送到堆栈上,因此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字节堆栈空间,但由于的几个原因,它可能会有所不同
- 计算结果
3*a' and
2*b'需要存储在堆栈或寄存器中的某个位置 - a和b的值是文字,可以存储在寄存器中
- 编译器可能能够推断出3a+2b是一个常数,并在编译时执行数学运算,同时优化
a' and
b',并将"c"设置为35
使用-O0或-Og以及使用-m32(强制使用32位处理器的代码)可能会消除其中的一些问题。
更新:
我把-Og
误读为-O0
。随着优化的进行,还有一些额外的复杂性(例如GCC如何准确地选择传递参数,它是为本地保留空间还是将这些本地保持在寄存器中,等等)
要了解发生了什么,您应该首先了解没有优化的图片。
保留空间在哪里?
;保留空间";在x86_64
:上的堆栈上
push ...
sub ...,%rsp
enter ...
还有几种方法可以";未送达";它是:pop ...
,add ...,%rsp
,leave
。
在您的情况下,是pushq
指令同时将一个值放入堆栈槽并为该值保留空间。
你没有展示retq
之前发生的事情,但我怀疑你的";未送达";看起来有点像add $68,%rsp
。
附言:你有一个0x01, 0x02 ..., 0x09, 0x10, ...
序列。请注意,这些是而不是连续数字:0x09
之后的下一个数字是0x0a
。