下面的代码是由gcc从一个简单的scanf程序生成的。
我的问题是


为什么这3个变量地址在分配时不连续?观看像add esp, N这样的子句,通常在例行程序的结尾?它与调用约定有关吗?
在此示例中,为什么编译器不生成add esp, 20h? /> asm

#include <stdio.h>
int main() {
  int x;
  printf ("Enter X:\n");
  scanf ("%d", &x);
  printf ("You entered %d...\n", x);
  return 0;
};


#1 楼

您的函数中实际上只有一个局部变量:x。该变量位于您期望的堆栈上,位于ebp-4。 IDA感到困惑,因为该特定函数不是在调用函数之前将变量推入堆栈,而是在移动它们。当它们实际上只是堆栈顶部的位置时,这会欺骗IDA认为它们是局部变量。
这样做,但是我的猜测是您编译时没有优化。这种指令布局可能使调试更容易。

我认为您还将调用约定与局部变量清除混淆了。每个函数都需要清理自己的局部变量区域。您的main()函数正在执行leave指令。调用约定与清理传递给函数的参数有关。

#2 楼

由于使用了请假指令(LEAVE PROCDURE)

引用了ftom intel指令手册

,所以没有添加esp,20我想这是您的问题的另一部分,因为编译器未生成任何推入参数指令,而是利用顶部将args移入堆栈,而底部利用varargs存储。

#3 楼

在此函数中,实际上只有一个堆栈变量:var_4

在上面的反汇编中,IDA错误地将传递给_puts()___isoc99_scanf()_printf()的参数检测为局部堆栈变量。要看到这一点,让我们分析以下代码段:

mov     [esp+20h+var_20], offset aEnterX ; "Enter X:"
call    _puts


var_20在此函数的开头由IDA定义为-20h,因此mov [esp+20h+var_20], offset aEnterX的意思是mov [esp+20h+-20h], offset aEnterXmov [esp], offset aEnterX相同。换句话说,代码只是在调用offset aEnterX之前将_puts()压入堆栈,而IDA不幸地将“替代压入”检测为本地堆栈变量。

#4 楼

(我会更多地处理由Visual C ++生成的代码,因此该答案的某些部分只是有根据的猜测。)



1.为什么这三个变量地址在分配时不连续? br />

变量的对齐方式,甚至在堆栈上的存在取决于编译器,并且可以根据您使用的特定编译器版本,使用的优化选项和其他因素而有所不同。
示例中的3个变量并不都是“真实”变量。 var_4是与x对应的变量,而var_1Cvar_20只是GCC传递参数到更深层函数调用的方法的结果。当您编写scanf("%d", &x);时,GCC知道它将需要将两个4字节变量传递给堆栈上的该函数,因此在进入该函数时,它会抢先为它们保留足够的空间。这样,它不需要将任何东西push放入栈中(如果没有剩余的栈空间,这可能是有问题的...),它只需要mov到该预分配空间的参数即可。但是,这并不能解释为什么两个分配之间存在差距。 GCC还喜欢将堆栈分配对齐为16个字节1,这就是我猜测“真正的局部变量”和“为更深的函数参数保留的空间”所需的大小在总和为的最终值之前独立对齐的地方“保留的堆栈空间”。

1您可以使用-mpreferred-stack-boundary=num来控制此对齐方式。通过观察像add esp, N这样的子句来堆栈,该子句通常位于例程的结尾?它与调用约定有关吗?


如您在本示例中看到的那样,该指令并不总是生成。其对应的sub esp, N是一个更好的指标。由此您可以对局部变量的数量/大小进行有根据的猜测。

调用约定与函数的局部变量无关,它控制参数传递给函数的方式以及其后清理参数的职责。


3.在此示例为什么编译器不生成add esp, 20h


示例中的函数以push ebp; mov ebp, esp开头,它保存了ebpesp的原始值。最后的leave指令执行相反的操作-它恢复了espebp的保存值,因此无需计算任何内容。

保存的ebp也称为帧指针。可以指示编译器不要生成它,在这种情况下,需要使用您提到的计算来恢复esp的原始值。