
正文
ESP、EBP、CALL 指令与局部变量浅析
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
概述
函数调用是计算机程序中一个最重要的概念之一,从汇编的角度看,能更加直观地理解函数调用的原理,理解 CALL 指令调用过程中 ESP、EBP 寄存器的作用。
我们先从一段简陋的 C 语言代码说起,我们首先调用了 printf 函数,为什么要调用 printf 函数呢?实际上是为了更方便地在 OllyDBG 反汇编工具中断点,能更好地定位到 fun 函数的位置(因为 fun 函数的 CALL 将紧跟着 printf 函数的 CALL)。
在 fun 函数中,我们定义两个 int 型变量(两个 4 字节的变量),给 a 赋值了 0,给 b 赋值了 2。
我们用 OllyDBG 载入,在 printf 函数处下断点,运行在断点处暂停下来,在堆栈窗口中按 Enter 跟随到反汇编处,我们来到了下图所示的位置,第一行就是 main 函数这个 CALL 的段首,首先把 EBP 压栈,为了保存外层代码的 EBP 的值,使这个 CALL 不影响外层代码的 EBP 的值。然后第二行,程序将 ESP 赋值给 EBP。此时,EBP 与 ESP 重合。
第三行是一个 PUSH 指令,它将指向字符串的指针压栈,这个指针指向的是“this is a break point”字符串,第四行程序执行 CALL 指令调用 printf 函数,printf 函数将从堆栈中取出参数进行处理,最终在终端中打印出字符串。换言之,参数通过堆栈传递给 printf 函数,就像 C 语言那样。
第五行将 ESP 增加了 4,然后下一行程序直接 CALL 了 fun 函数。我们按 Enter 键跟进到 fun 函数内部,如下图。
在 fun 函数内部,EBP 又被压栈,然后 EBP 被赋值成 ESP 的值(有点像之前 main 函数段首的情形)
注意第三行,ESP 被减少了 8,这样的话,EBP 就比 ESP 多 8。第四行与第五行是 MOV 赋值指令,首先立即数 0 赋值给了 EBP – 4 的位置,然后立即数 2 赋值给了 EBP – 8 的位置。
让我们来仔细分析一下,不难看出,ESP 减少 8,使得 ESP 与 EBP 相隔 8 字节的地址位置,换言之,ESP 与 EBP 中间有 8 字节的空隙,此时 ESP 比 EBP 小 8。而接下来的两行 MOV 指令将 EBP – 4 这个地址,EBP – 8 这个地址,分别赋值了 0 和 2,这两个 4 字节的 int 整数型数据,a 和 b 两个局部变量刚好填满了 ESP 与 EBP 中间 8 字节的空隙。我们可以作图理解它的结构:
局部变量的堆栈赋值完成以后,函数的基本功能就执行完了。这时,ESP 被赋值成 EBP 的值(跟 fun 函数开头刚好相反,开头是 EBP 被赋值成 ESP) ,相当于将 ESP 还原函数调用之前的状态。下一行,从堆栈中弹出 EBP 的值(也就是 fun 函数开头 PUSH 的 EBP 值),也相当于将 EBP 还原成函数调用之前的状态,最后执行 RETN 指令返回到 CALL 处的下一行,fun 函数就执行完毕了。
补充
本例中并没有演示函数参数在堆栈中的存放规则,只演示了局部变量的。
实际上,函数参数存放在比 EBP 大的位置,和局部变量存放的地址增长方向恰恰相反。也就是说, 如果多定义一个局部变量,ESP 会越来越小于 EBP,它们之间的“空隙”会越来越大,以存放更多的局部变量 。但是, 如果给函数多增加一个参数,函数调用者的 ESP (也就是被调用者的 EBP)会越来越大于 ESP,“空隙”也将存放更多的参数,因为函数的参数会在调用者去 CALL 被调用者之前被压栈,ESP 将增大,这个 ESP 将成为被调用者的 EBP。 最终,我们看到的就是函数中局部变量的地址比 EBP 小,而函数的参数的地址比 EBP 大,它们以 EBP 为界,且地址增长方向相反。







