汇编代码如何确定向下放置变量到堆栈的距离?

问题描述 投票:3回答:2

我试图理解一些基本的汇编代码概念,并且陷入了汇编代码如何确定将事物放在堆栈上的位置以及提供多少空间的问题。

为了开始玩它,我在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个字节?这是因为类型的大小?

c assembly memory x86 stack
2个回答
3
投票

我只想回答这部分问题:

在通话之前,参数是否不应放在堆栈上?为什么argc和argv位于当前堆栈帧的基址指针的偏移量20和32处?

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

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

我相信你指的是指示

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

是编译器书呆子调用溢出指令的原因:它们将argcargv参数的初始值从其原始寄存器复制到堆栈,以防其他东西需要这些寄存器。正如其他几位人士指出的那样,这是未经优化的代码;这些说明是不必要的,如果您已启用优化,则不会发出这些指令。当然,如果您已经进行了优化,那么您已经获得了根本不接触堆栈的代码:

main:
    mov     eax, 2
    ret

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

(有些ABI确实详细说明了堆栈帧布局,例如IIRC 32位Windows ABI这样做,以便于“展开”,但现在这并不重要。)

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

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

:

main:
    lea     eax, [rdi+1]
    ret

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


2
投票

这是“编译器101”,你想要研究的是“调用约定”和“堆栈框架”。细节依赖于编译器/ OS /优化。简而言之,传入参数可以在寄存器中或堆栈上。输入函数时,它可能会创建一个堆栈帧来保存一些寄存器。然后它可以定义一个“帧指针”来引用堆栈本地和堆栈参数的帧指针。有时堆栈指针也用作帧指针。

对于寄存器,通常有人(公司)会定义一个调用约定并指定哪些寄存器是“易失性的”,这意味着它们可以被例程使用而没有问题,并且“保留”,这意味着如果例程使用它们,它们将会必须在功能进入和退出时保存和恢复。调用约定还指定哪些寄存器(如果有)用于参数传递和函数返回。

© www.soinside.com 2019 - 2024. All rights reserved.