将局部变量的地址压入堆栈(程序集)的目的是什么

what's the purpose of pushing address of local variables on the stack(assembly)

让我们有一个函数:

int caller()
{
   int arg1 = 1;
   int arg2 = 2
   int a = test(&arg1, &arg2)
}
test(int *a, int *b)
{
    ...
}

所以我不明白为什么 &arg1 和 &arg2 也必须像这样压入堆栈

我可以理解,我们可以通过使用

获取被调用者中的arg1和arg2的地址
movl  8(%ebp), %edx
movl  12(%ebp), %ecx

但如果我们不将这两个压入堆栈, 我们也可以使用以下方式获取他们的地址:

leal 8(%ebp), %edx
leal 12(%ebp), %ecx 

那么为什么还要将 &arg1 和 &arg2 压入堆栈?

如果您直接访问arg1arg2,则意味着您正在访问不属于该函数的堆栈部分。这就是当有人使用 buffer overflow attack 从调用堆栈访问额外数据时发生的事情。

当您的调用有参数时,参数被压入堆栈(在您的情况下为 &arg1&arg2)并且函数可以将它们用作此函数的有效参数列表。

在一般情况下,当您向它传递任意指针时,test 必须工作,包括指向 extern int global_var 或其他任何东西。然后 main 必须根据 ABI / 调用约定调用它。

所以 test 的 asm 定义不能假设任何关于 int *a 点的地方,例如它指向其调用者的堆栈框架。

(或者您可以将其视为优化掉本地引用调用中的地址,因此调用者必须将指向的对象放在 arg 传递槽中,并且 return 堆栈内存的这 2 个双字保存 *a*b 的潜在更新值。)

您在禁用优化的情况下编译。特别是对于调用者将指针传递给局部变量的特殊情况,此问题的解决方案是内联整个函数,启用优化后编译器将执行此操作。

编译器 允许 制作 test 的私有克隆,它通过值、寄存器或编译器想要的任何自定义调用约定获取其参数采用。但是,大多数编译器实际上并没有这样做,而是依靠内联而不是私有函数的自定义调用约定来消除参数传递开销。

或者如果它被声明为 static test,那么编译器就已经知道它是私有的,理论上可以使用它想要的任何自定义调用约定,而无需使用 test.clone1234 这样的名称进行克隆. gcc 有时确实会为不断传播而这样做,例如如果调用者传递了编译时常量但 gcc 选择不内联。 (或者不能,因为你使用了 __attribute__((noinline)) static test() {}


顺便说一句,使用像 x86-64 System V 这样的良好寄存器参数调用约定,调用者会做 lea 12(%rsp), %rdi / lea 8(%rsp), %rsi / call test 之类的。 i386 System V 调用约定陈旧且效率低下,传递堆栈上的所有内容强制 store/reload.

您基本上已经确定了 stack-args 调用约定具有更高开销并且通常很糟糕的原因之一。