汇编语言访问堆栈参数详解
高级语言有多种方式来对函数调用的参数进行初始化和访问。以 C 和 C++ 语言为例,它们以保存 EBP 寄存器并使该寄存器指向栈顶的语句为开始 (prologue)。
然后,根据实际情况,它们可以把某些寄存器入栈,以便在函数返回时恢复这些寄存器的值。在函数结尾 (epilogue) 部分,恢复 EBP 寄存器,并用 RET 指令返回调用者。
AddTwo 在其他寄存器入栈时,不用通过 EEP 来修改堆栈参数的偏移量。数值会改变的是 ESP,而 EBP 则不会。
main 部分试图忽略这个问题,并希望程序能正常结束。但是,如果循环调用 AddTwo,堆栈就会溢出。因为每次调用都会占用 12 字节的堆栈空间——每个参数需要 4 个字节,再加 4 个字节留给 CALL 指令的返回地址。如果在 main 中调用 Example1,而它又要调用 AddTwo 就会导致更加严重的问题:
RET 指令把整数 5 加载到指令指针寄存器,尝试将控制转移到内存地址为 5 的位置。假设这个地址在程序代码边界之外,那么处理器将给出运行时异常,通知 OS 终止程序。
然后,根据实际情况,它们可以把某些寄存器入栈,以便在函数返回时恢复这些寄存器的值。在函数结尾 (epilogue) 部分,恢复 EBP 寄存器,并用 RET 指令返回调用者。
AddTwo示例
下面是用 C 编写的 AddTwo 函数,它接收了两个值传递的整数,然后返回这两个数之和:int AddTwo( int x, int y ) { return x + y; }现在用汇编语言实现同样的功能。在函数开始的时候,AddTwo 将 EBP 入栈,以保存其当前值:
AddTwo PROC
push ebp
AddTwo PROC
push ebp
mov ebp,esp
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
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 终止程序。