堆栈帧的基本结构

时间:2013-04-03 10:32:20

标签: assembly stack frame x86-64 stackframe

我正在玩游戏,检查堆栈帧,试图了解其工作原理。在阅读了一些总是解释一般结构的文章之后:

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。

干杯, 砖

2 个答案:

答案 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个字节。 buffer1buffer2可能是8字节对齐的,实际上分别需要8和16个字节,可能还需要4个字节来对齐它们。我可能错了 - 我手头没有规格。所以这是36或40字节。然后调用对齐需要一个16字节对齐48个字节。

我认为关闭堆栈保护并检查此叶子函数的堆栈帧会更有启发性,并参考x86-64 ABI规范了解局部变量的对齐要求等。

答案 1 :(得分:0)

它取决于编译器。您可以尝试使用“extern”关键字关闭优化或标记函数(强制使用默认调用Convencion)。

使用寄存器是因为这比通过堆栈发送参数要快得多。