为什么栈上变量的地址不连续?

逆向工程 拆卸 部件 x86 C
2021-06-11 00:29:06

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

  1. 为什么这3个变量地址在分配时不连续?
  2. 如果不是,我什么时候可以通过观察子句来推测从堆栈生成的变量的数量,例如add esp, N它经常出现在例程的末尾?它与调用约定有关吗?
  3. 在这个例子中,为什么编译器没有add esp, 20h用它生成

C代码

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

汇编

main            proc near
var_20          = dword ptr -20h
var_1C          = dword ptr -1Ch
var_4           = dword ptr -4
                push    ebp
                mov     ebp, esp
                and     esp, 0FFFFFFF0h
                sub     esp, 20h
                mov     [esp+20h+var_20], offset aEnterX ; "Enter X:"
                call    _puts
                mov     eax, offset aD  ; "%d"
                lea     edx, [esp+20h+var_4]
                mov     [esp+20h+var_1C], edx
                mov     [esp+20h+var_20], eax
                call    ___isoc99_scanf
                mov     edx, [esp+20h+var_4]
                mov     eax, off set aYouEnteredD___ ; "You entered %d...\n"
                mov     [esp+20h+var_1C], edx
                mov     [esp+20h+var_20], eax
                call    _printf
                mov     eax, 0
                leave
                retn
main            endp
4个回答

你的函数中实际上只有一个局部变量:x。此变量位于您期望它的堆栈上,位于ebp-4. IDA 感到困惑,因为这个特定的函数不是在调用函数之前将变量压入堆栈,而是移动它们。这使 IDA 认为这些是局部变量,而实际上它们只是堆栈顶部的位置。

mov     [esp+20h+var_1C], edx  <===> push edx
mov     [esp+20h+var_20], eax  <===> push eax

我无法明确解释 gcc 这样做的原因,但我的猜测是您编译时没有进行优化。这种指令布局可能会使调试更容易。

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

由于 leave 指令 (LEAVE PROCDURE ) 指令,因此没有 add esp ,20

引用 ftom intel 说明手册

6.5.2 LEAVE Instruction
The LEAVE instruction, which does not have any operands, reverses the action of the previous ENTER instruction. 
The LEAVE instruction copies the contents of the EBP register into the ESP register to release all stack space allocated to the procedure. Then it restores the old value ofthe EBP register from the stack. This simultaneously 
restores the ESP register to its original value. A subsequent RET instruction then can remove any arguments and 
the return address pushed on the stack by the calling program for use by the procedure.

至于您问题中的另一部分,我想这是因为编译器没有生成任何推送参数指令,它利用顶部将 args 移动到堆栈中,将底部移动到可变参数存储中

mov [esp],%d
mov [esp+4] , ADDR where to store the input ie ADDR of [esp+1c]

这个函数中实际上只有一个堆栈变量: 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 aEnterX,与mov [esp], offset aEnterX. 换句话说,代码只是offset aEnterX在调用之前入堆栈_puts(),不幸的是,IDA 将“替代推入”检测为本地堆栈变量。

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

1.为什么这3个变量的地址在分配时不连续?

变量对齐甚至堆栈上的存在取决于编译器,并且可能因您使用的特定编译器版本、使用的优化选项和其他因素而异。

您示例中的 3 个变量并非都是“真实”变量。var_4是与您的 相对应的变量x,而var_1Cvar_20只是 GCC 将参数传递给更深层次的函数调用的方法的结果。当您编写 时scanf("%d", &x);,GCC 知道它需要将两个 4 字节的变量传递给堆栈上的该函数,因此它会在进入函数时预先为它们预留足够的空间。这样,它不需要push在堆栈上添加任何东西(如果没有剩余的堆栈空间,这可能会出现问题......),它只需要mov将参数放入该预分配的空间中。

但这并不能解释为什么两个分配之间存在差距。GCC 也更喜欢将堆栈分配对齐到 16 个字节1这就是我猜测的地方,“真实局部变量”和“为更深层次的函数参数保留的空间”所需的大小似乎在汇总到最终值之前是独立对齐的“保留堆栈空间”。

1您可以使用 来控制这种对齐方式-mpreferred-stack-boundary=num

2.如果不是,我什么时候可以通过观察像add esp, N通常在例程末尾的子句来推测从堆栈生成的变量数量它与调用约定有关吗?

正如您在此示例中所见,该指令并不总是会生成。它的对应物sub esp, N是一个更好的指标。从中您可以对局部变量的数量/大小进行有根据的猜测。

调用约定是不相关的一个函数的局部变量,它控制的参数传递的方式进入该功能,谁负责清除它们之后。

3.在这个例子中,为什么编译器没有add esp, 20h用它生成

在您的例子中,函数开始push ebp; mov ebp, esp,这节省了原来的数值ebpespleave末指令则相反-它恢复的保存价值espebp,所以没有需要计算什么。

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