汇编语言访问堆栈参数详解

高级语言有多种方式来对函数调用的参数进行初始化和访问。以 C 和 C++ 语言为例,它们以保存 EBP 寄存器并使该寄存器指向栈顶的语句为开始 (prologue)。

然后,根据实际情况,它们可以把某些寄存器入栈,以便在函数返回时恢复这些寄存器的值。在函数结尾 (epilogue) 部分,恢复 EBP 寄存器,并用 RET 指令返回调用者。

AddTwo示例

下面是用 C 编写的 AddTwo 函数,它接收了两个值传递的整数,然后返回这两个数之和:
int AddTwo( int x, int y )
{
    return x + y;
}
现在用汇编语言实现同样的功能。在函数开始的时候,AddTwo 将 EBP 入栈,以保存其当前值:

AddTwo PROC
    push ebp

接下来,EBP 的值被设置为等于 ESP,这样 EBP 就成为 AddTwo 堆栈帧的基址指针:

AddTwo PROC
    push ebp
    mov ebp,esp

执行了上面两条指令后,堆栈帧的内容如下图所示。而形如 AddTwo(5, 6) 的函数调用会先把第一个参数入栈,再把第二个参数入栈:


AddTwo 在其他寄存器入栈时,不用通过 EEP 来修改堆栈参数的偏移量。数值会改变的是 ESP,而 EBP 则不会。

基址-偏移量寻址

可以使用基址-偏移量寻址 (base-offset addressing) 方式来访问堆栈参数。其中,EBP 是基址寄存器,偏移量是常数。通常,EAX 为 32 位返回值。AddTwo 的实现如下所示,参数相加后,EAX 返回它们的和数:
AddTwo PROC
    push ebp
    mov ebp, esp              ;堆栈帧的基址
    mov eax, [ebp + 12]       ;第二个参数
    add eax, [ebp + 8]        ;第一个参数 pop ebp
    ret
AddTwo ENDP

显式的堆栈参数

若堆栈参数的引用表达式形如 [ebp+8],则称它们为显式的堆栈参数 (explicit stack parameters)。这个名称的含义是:汇编代码显式地说明了参数的偏移量是一个常数。有些程序员定义符号常量来表示显式的堆栈参数,以使其代码更具易读性:
y_param EQU [ebp + 12]
x_param EQU [ebp + 8]

AddTwo PROC
    push ebp
    mov ebp,esp
    mov eax,y_param
    add eax,x_param
    pop ebp
    ret
AddTwo ENDP

清除堆栈

子程序返回时,必须将参数从堆栈中删除。否则将导致内存泄露,堆栈就会被破坏。例如,设如下语句在 main 中调用 AddTwo:

push 6
push 5
call AddTwo

假设 AddTwo 有两个参数留着堆栈中,下图所示为调用返回后的堆栈:


main 部分试图忽略这个问题,并希望程序能正常结束。但是,如果循环调用 AddTwo,堆栈就会溢出。因为每次调用都会占用 12 字节的堆栈空间——每个参数需要 4 个字节,再加 4 个字节留给 CALL 指令的返回地址。如果在 main 中调用 Example1,而它又要调用 AddTwo 就会导致更加严重的问题:
main PROC
    call Example1
    exit
main ENDP

Example1 PROC
    push 6
    push 5
    call AddTwo
    ret                   ;堆栈被破坏了!
Example1 ENDP
当 Example1 的 RET 指令将要执行时,ESP 指向整数 5 而不是能将其带回 main 的返回地址:


RET 指令把整数 5 加载到指令指针寄存器,尝试将控制转移到内存地址为 5 的位置。假设这个地址在程序代码边界之外,那么处理器将给出运行时异常,通知 OS  终止程序。

编程帮,一个分享编程知识的公众号。跟着站长一起学习,每天都有进步。

通俗易懂,深入浅出,一篇文章只讲一个知识点。

文章不深奥,不需要钻研,在公交、在地铁、在厕所都可以阅读,随时随地涨姿势。

文章不涉及代码,不烧脑细胞,人人都可以学习。

当你决定关注「编程帮」,你已然超越了90%的程序员!

编程帮二维码
微信扫描二维码关注