汇编语言:基于x86处理器-学习笔记-第五章
第五章学习笔记
堆栈操作
《汇编语言:基于x86处理器(原书第7版)》 Page 108
堆栈数据结构 (stack data structure) 的原理与盘子堆栈相同:新值添加到栈顶,删除值也在栈顶移除。堆栈也被称为 LIFO 结构 (后进先出,Last-In First-Out),其原因是,最后进入堆栈的值也是第一个出堆栈的值。
运行时堆栈(32位模式)
运行时堆栈是内存数组,CPU 用 ESP (扩展堆栈指针,extended stack pointer) 寄存器对其进行直接管理,该寄存器被称为堆栈指针寄存器 (stack pointer register)。
32位模式下,ESP寄存器存放的是堆栈中某个位置的 32 位偏移量。ESP 基本上不会直接被程序员控制,反之,它是用 CALL、RET、PUSH 和 POP 等指令间接进行修改。
入栈操作
32位入栈操作把栈顶指针减 4,再将数值复制到栈顶指针指向的堆栈位置。
出栈操作
出栈操作从堆栈删除数据。数值弹出堆栈后,栈顶指针增加(按堆栈元素大小),指向堆栈中下一个最高位置。
堆栈应用
运行时堆栈在程序中有一些重要用途:
- 当寄存器用于多个目的时,堆栈可以作为寄存器的一个方便的临时保存区。在寄存器被修改后,还可以恢复其初始值。
- 执行 CALL 指令时,CPU 在堆栈中保存当前过程的返回地址。
- 调用过程时,输入数值也被称为参数,通过将其压入堆栈实现参数传递。
- 堆栈也为过程局部变量提供了临时存储区域。
PUSH 和 POP 指令
PUSH 指令
使用 PUSH 指令将数据压入栈内。
PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈。操作数是 16 位的,则 ESP 减 2,操作数是 32 位的,则 ESP 减 4。
例如 push eax
指令执行的过程可以分为两步:
- 指向栈顶的寄存器
esp
进行一个减法操作sub esp, 4
。 - 将需要保存的元素复制到新的栈顶位置
mov [esp], %eax
。
POP 指令
使用 POP 指令从内存中读取数据,并且修改栈顶指针。
POP 指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值。如果操作数是 16 位的,ESP 加 2,如果操作数是 32 位的,ESP 加 4。
例如 pop ebx
指令就是将栈顶保存的数据复制到寄存器 ebx
中,该指令同样也可以分解成两步:
- 从栈顶的位置读出数据,复制到寄存器
ebx
:mov ebx, [esp]
。 - 将栈顶的指针加8(因为
q
表示的是8个字节):add esp, 4
。
PUSHFD 和 POPFD 指令
PUSHFD 指令把 32 位 EFLAGS 寄存器内容压入堆栈。
POPFD 指令则把栈顶单元内容弹出到 EFLAGS 寄存器。
1 |
|
PUSHAD 和 POPAD 指令
PUSHAD 指令按照 EAX、ECX、EDX、EBX、ESP(执行 PUSHAD 之前的值)、EBP、ESI 和 EDI 的顺序,将所有 32 位通用寄存器压入堆栈。
POPAD 指令按照相反顺序将同样的寄存器弹出堆栈。
1 |
|
如果编写的过程会修改 32 位寄存器的值,则在过程开始时使用 PUSHAD 指令,在结束时使用 POPAD 指令,以此保存和恢复寄存器的内容。
PUSHA 和 POPA 指令
与之相似,PUSHA 指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI) 将 16 位通用寄存器压入堆栈。
POPA 指令按照相反顺序将同样的寄存器弹出堆栈。在 16 位模式下,只能使用 PUSHA 和 POPA 指令。
定义并使用过程
《汇编语言:基于x86处理器(原书第7版)》 Page 112
在汇编语言中,通常用术语过程 (procedure) 来指代子程序。在其他语言中,子程序也被称为方法或函数。
PROC 伪指令
可以把过程非正式地定义为:以返回语句结束的命令语句块。
过程使用 PROC 伪指令和 ENDP 伪指令来声明,另外还必须给过程定义一个名字。
程序启动过程之外的其他过程以 RET 指令结束,以强制 CPU 返回到过程被调用的地方:
1 |
|
但是启动过程(main)是个特例,它以 exit 语句结束。如果程序中使用了 INCLUDE Irvine32.inc
语句的话,exit 语句实际上就是对 ExitProcess 函数的调用,ExitProcess 是用来终止程序的系统函数:INVOKE ExitProcess, 0
。
CALL 和 RET 指令
CALL 指令指挥处理器在新的内存地址执行指令,以实现过程的调用。过程使用 RET(从过程返回)指令使处理器返回到程序过程被调用的地方继续执行。
从底层细节角度来讲,CALL 指令把返回地址压入堆栈并把被调用过程的地址复制到指令指针寄存器 (EIP / IP) 中。当程序返回时,RET 指令从堆栈中弹出返回地址并送到指令指针寄存器中。
在 32 位模式下,CPU 总是执行 EIP(指令指针寄存器)所指向的内存出的指令;在 16 位模式下,CPU 总是执行 IP 寄存器指向的指令。
需要注意的是,进入过程后如果在没有 PUSH 操作的情况下直接 POP,会将之前存入的 CALL 指令后一条指令的地址弹出,从而导致 RET 指令无法正确的返回调用该过程的地址。
程序示例
下面的程序通过传递一个 32 位整数数组的偏移量和长度来测试 ArraySum 过程。调用 ArraySum 之后,程序将过程的返回值保存在变量 theSum 中。