我正在玩游戏,检查堆栈帧,试图了解其工作原理。在阅读了一些总是解释一般结构的文章之后:
local vars< --- SP
低地址
旧BP
< --- BP
ret addr args高地址
我有一个示例程序,它调用一个带有三个参数的函数,并有两个缓冲区作为局部变量:
#include <stdio.h>
void function(int a, int b, int c);
int main()
{
function(1, 2, 3);
return 0;
}
void function(int a, int b, int c)
{
char buffer1[5];
char buffer2[10];
}
我看了一下程序的汇编程序代码,并且惊讶地发现在调用函数时没有找到我期望的东西。我期待的是:
# The arguments are pushed onto the stack:
push 3
push 2
push 1
call function # Pushes ret address onto stack and changes IP to function
...
# In function:
# Push old base pointer onto stack and set current base pointer to point to it
push rbp
mov rbp, rsp
# Reserve space for stack frame etc....
因此,在执行函数时,框架的结构将类似于:
buffers <--- SP low address
old BP <--- BP
ret Addr
1
2
3 high address
但接下来会发生以下情况:
函数调用:
mov edx, 3
mov esi, 2
mov edi, 1
call function
为什么在我们可以推送到堆栈时使用这里的寄存器??? 在我们称之为的实际功能中:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 48
mov DWORD PTR [rbp-36], edi
mov DWORD PTR [rbp-40], esi
mov DWORD PTR [rbp-44], edx
mov rax, QWORD PTR fs:40
mov QWORD PTR [rbp-8], rax
xor eax, eax
mov rax, QWORD PTR [rbp-8]
xor rax, QWORD PTR fs:40
je .L3
call __stack_chk_fail
据我所知,堆栈帧保留了48个字节吗?然后,使用函数调用中的寄存器,将函数的参数复制到堆栈的末尾。所以它看起来像这样:
3 <--- SP
2
1
??
??
old BP <--- BP
return Address
??
我假设缓冲区介于args和旧BP
之间。但我真的不确定究竟在哪里......因为它们总共只有15个字节而48个字节都是保留的...那里不会有一堆未使用的空间吗?
有人可以帮助我概述这里发生的事情吗?这是依赖于处理器的东西吗?我正在使用intel i7。
干杯, 砖
答案 0 :(得分:1)
有几个问题。首先,3个参数由寄存器传递,因为它是ELF ABI规范的一部分。我不确定这些天最新的(x86-64)SysV ABI文档在哪里,(x86-64.org似乎已经不存在了)。 Agner Fog维护了很多优秀的文档,包括calling conventions上的文档。
通过调用__stack_check_fail
使堆栈分配变得复杂,这被添加为检测堆栈粉碎/缓冲区溢出的对策。 ABI的一部分还指定在函数调用之前堆栈必须是16字节对齐的。如果您使用-fno-stack-protector
重新编译,您将更好地了解正在发生的事情。
此外,因为该功能没有做任何事情,所以这不是一个特别好的例子。它存储参数(不必要),需要12个字节。 buffer1
和buffer2
可能是8字节对齐的,实际上分别需要8和16个字节,可能还需要4个字节来对齐它们。我可能错了 - 我手头没有规格。所以这是36或40字节。然后调用对齐需要一个16字节对齐48个字节。
我认为关闭堆栈保护并检查此叶子函数的堆栈帧会更有启发性,并参考x86-64 ABI规范了解局部变量的对齐要求等。
答案 1 :(得分:0)
它取决于编译器。您可以尝试使用“extern”关键字关闭优化或标记函数(强制使用默认调用Convencion)。
使用寄存器是因为这比通过堆栈发送参数要快得多。