首页 > 解决方案 > 汇编代码如何确定变量在堆栈中的位置?

问题描述

我正在尝试理解一些基本的汇编代码概念,但我一直在纠结汇编代码如何确定在堆栈上放置东西的位置以及给它多少空间。

为了开始使用它,我在 godbolt.org 的编译器资源管理器中输入了这个简单的代码。

int main(int argc, char** argv) {
  int num = 1;  
  num++;  
  return num;
}

并得到了这个汇编代码

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     QWORD PTR [rbp-32], rsi
        mov     DWORD PTR [rbp-4], 1
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

所以这里有几个问题:

  1. 参数不应该在调用之前放在堆栈上吗?为什么 argc 和 argv 放置在距当前堆栈帧的基指针的偏移量 20 和 32 处?如果我们只需要一个局部变量 num 的空间,这似乎真的很远。所有这些额外空间是否有原因?

  2. 局部变量存储在基指针下方的 4 处。因此,如果我们在堆栈中可视化它并说当前指向 0x00004000 的基指针(只是作为一个例子,不确定这是否现实),那么我们将值放在 0x00003FFC,对吗?而一个整数大小为4字节,那么是占用0x00003FFC向下到0x00003FF8的内存空间,还是占用0x00004000到0x00003FFC的内存空间呢?

  3. 看起来堆栈指针从未向下移动以为该局部变量留出空间。我们不应该sub rsp, 4为本地int腾出空间吗?

然后,如果我修改它以添加更多本地人:

int main(int argc, char** argv) {
  int num = 1; 
  char *str1 = {0};
  char *str2 = "some string"; 
  num++;  
  return num;
}

然后我们得到

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-36], edi
        mov     QWORD PTR [rbp-48], rsi
        mov     DWORD PTR [rbp-4], 1
        mov     QWORD PTR [rbp-16], 0
        mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

所以现在主要参数被推到离基指针更远的地方。为什么前两个局部变量之间的空间是 12 个字节,而后两个局部变量之间的空间是 8 个字节?那是因为类型的大小吗?

标签: cassemblymemoryx86stack

解决方案


我只回答这部分问题:

参数不应该在调用之前放在堆栈上吗?为什么 argc 和 argv 放置在距当前堆栈帧的基指针的偏移量 20 和 32 处?

的参数main确实是由调用的代码设置的main

这似乎是根据x86 的 64 位 ELF psABI编译的代码,其中任何函数的前几个参数都在寄存器中传递,而不是在堆栈上。当控制到达main:标签时,argcwill in ediargvwill be in rsi,第三个通常称为的参数envp将 in rdx。(你没有声明那个参数,所以你不能使用它,但是调用的代码main是通用的并且总是设置它。)

我相信您所指的说明

    mov     DWORD PTR [rbp-20], edi
    mov     QWORD PTR [rbp-32], rsi

编译器书呆子称之为溢出argc指令:他们将原始寄存器中的和参数的初始值复制argv到堆栈中,以防万一这些寄存器用于其他用途。正如其他几个人指出的那样,这是未优化的代码;如果您打开了优化,这些说明是不必要的,也不会发出。当然,如果你打开了优化,你会得到根本不接触堆栈的代码:

main:
    mov     eax, 2
    ret

在此 ABI 中,允许编译器将保存寄存器值的“溢出槽”放置在堆栈帧内的任何位置。它们的位置不一定有意义,并且可能因编译器而异,从同一编译器的补丁级别到补丁级别,或者对源代码的明显不相关的更改。

(一些 ABI 确实在某些细节上指定了堆栈帧布局,例如 IIRC,32 位 Windows ABI 这样做,以促进“展开”,但现在这并不重要。)

(为了强调 to 的参数main在寄存器中,这是我从中得到的程序-O1

int main(int argc) { return argc + 1; }

main:
    lea     eax, [rdi+1]
    ret

仍然没有对堆栈做任何事情!(除此之外ret。))


推荐阅读