1. 堆栈
SS:SP 是堆栈顶端的逻辑地址。注意不能直接用 [sp]
或 [sp+常数或其他寄存器]
的方式引用堆栈中的元素,需要用 BX、BP、SI、DI 中的某个替换。
push op
可以将 op 压入堆栈。CPU 会先让 SP 寄存器减少 op 的宽度,再将 op 的值保存到 SS:SP。pop op
可以将 op 弹出堆栈。CPU 会先将 SS:SP 的值保存到 op,再将 SP 寄存器增加 op 的宽度。
注意这里 op 不能是一个 8 位的寄存器或变量。注意如果这里的 op 是内存地址的话,因为没有另一个操作数可以作宽度参考,必须用 word ptr
或 dword ptr
指明宽度。
pusha
和 popa
是 push
和 pop
的变体,在 16 位系统中,pusha
会依次将以下寄存器放入到堆栈中:AX、CX、DX、BX、SP、BP、SI、DI,popa
则是对应的反向弹出。
2. 函数
2.1. 函数的定义
2.1.1. 用标号定义函数
标号名: ; 定义近函数
; 或写成“标号名 label near”
...
retn ; 可简写为ret
标号名 label far ; 定义远函数
...
retf
2.1.2. 用 proc 定义函数
函数名 proc near ; 定义近函数
...
retn ; 可简写为ret
函数名 endp
函数名 proc far ; 定义远函数
...
retf
函数名 endp
2.2. 函数的调用
函数定义后可以用 call
指令调用。
- 近调用
call near ptr dest
,近返回retn
(在多数情况下可以简写为ret
); - 远调用
call far ptr dest
,远返回retf
。
使用调用进行跳转之后一定要返回,否则栈空间会出现问题。
2.2.1. 近调用的原理
执行 call
时,CPU 首先把下一条要执行的指令压入堆栈中,然后跳转到目标地址。
执行 ret
时,CPU 将从堆栈中取出地址,跳转到目标地址。这可以理解为 pop ip
指令 (回顾:ip 寄存器即指令寄存器,cs:ip 指向 CPU 下一条要执行的地址。但 ip 寄存器不能被直接引用,故只能写 ret
)。
2.2.2. 远调用的原理
远调用时会依次把下一条指令的段地址和偏移地址压入堆栈中,远返回时会倒序弹出(先弹出后压入的 ip 寄存器)。这里 retf
的弹出是通过硬件实现的,所以对 cs 和 ip 寄存器的修改是同时进行的。
2.3. 函数的参数传递
函数的参数传递方式主要有:
- 寄存器传递(一般推荐使用 AX 寄存器)
- 变量传递
- 堆栈传递
使用变量传递或寄存器传递的方式意味着函数在运行过程中使用全局变量,这会导致该函数在运行过程中不可重入(reentrant),因为该函数在第一次调用尚未结束时被再次调用时会造成全局变量的覆盖,意为着该函数不能递归或者在多线程程序中被使用。这就是为什么我们需要介绍堆栈传递。
2.3.1. __cdecl
规范
参数按从右到左的顺序传入堆栈,由调用者通过 add sp, 常数
指令清理堆栈中的参数,或者 pop 寄存器
指令弹出到不使用的寄存器中。
在调用函数之前把需要传递的参数压入堆栈中。调用结束之后不要忘记将堆栈中的元素弹出,可以用 pop
指令传递给一个不用的寄存器,也可以直接用 add sp, 2
指令实现隐式的 pop。
通过 push bp
及 move bp sp
的组合来构成堆栈框架(stack frame),从而引用堆栈中的参数。由于 sp 寄存器不能被直接使用,这里 bp 寄存器取 ss:[bp+4] 这一地址,即我们希望传递给函数的参数。这也是这里使用 bp 寄存器的意义。(这里 ss: [bp] 存放原来 bp 寄存器的值;ss:[bp+2] 指向 ret 函数需返回的地址)
这里必须用 push bp
和 pop bp
来保护 bp 寄存器的原值,否则容易出现问题。如:在函数 f 中使用 ss:[bp+4] 来获取参数,但是使用之后调用了不保护 bp 的函数 g,这时如果还想再使用 ss:[bp+4] 来获取参数就可能出现问题。
如果需要使用多个参数,则可都压入堆栈中然后类比地使用 ss:[bp+6] 等。这也是为什么 C 语言中的函数参数执行顺序是从右到左的原因之一。
同时注意 C 语言的函数中要求保护 BP、BX、SI、DI 寄存器不被修改。
2.3.2. __pascal
规范
参数按照从左到右顺序传入堆栈,由调用者通过 ret 常数
指令清理参数。
2.3.3. __stdcall
规范
2.4. 动态变量
在进入函数体前,用 mov bp, sp
暂存 SP 寄存器,通过 sub sp, 字节数
划出分配给局部动态变量的内存空间。在函数体结束后,用 mov sp, bp
指令恢复原来的 SP 寄存器即可。
这样实现的话,一般通过 [bp+4/6/...]
来访问函数参数,通过 [bp-2/4/...]
来访问局部动态变量,具体可以参考示例代码。
把上面的 C 语言代码转化为汇编: 执行上述代码时,堆栈布局如下: 例:实现 C 语言中的动态局部变量
int f(int a, int b) {
int c; // c是局部动态变量
c = a + b;
return c;
}
f:
push bp; (4)
mov bp, sp
sub sp, 2; (5) 这里挖的坑就是给变量c的
mov ax, [bp+4]
add ax, [bp+6]
mov [bp-2], ax
mov ax, [bp-2]
mov sp, bp; (6)此时变量c死亡
pop bp; (7)
ret; (8)
main:
mov ax, 20
push ax; (1)
mov ax, 10
push ax; (2)
call f; (3)
here:
add sp, 4;(9)此时参数a,b死亡
ss:1FF6 [30] (5) 变量c
ss:1FF8 old bp<- bp(4)(6)
ss:1FFA here <- (3)(7)
ss:1FFC 10 <- (2)(8)
ss:1FFE 20 <- (1)
ss:2000 <-(9)
3. 中断程序设计
中断指令的格式 int n
,其中 n 的取值范围是 [0, 0FFh]。
0:0 到 0:3FF 共 400h 内存空间存储中断向量表,初始由保存调用中断时需要跳转到的段地址和偏移地址(注意这里都是小端存储的),计算方法如下(如 dword ptr 0:[84h]
就是 int 21h
的中断向量):
1000:2000 int 21h ; 当CPU执行这条指令时,会做一下动作
pushf
push cs ; 即1000h
push 下条指令的偏移地址 ; 即2002h
jmp dword ptr 0:[21h*4] ; jmp mem32指令,假设存放着1234:5678
1000:2002 mov ah, 4ch
1234:5678 cmp ah, 9 ; 对应的中断函数从这里开始
1234:567A ...
1234:5688 iret ; 当CPU执行这条指令时,会做以下动作
pop ip
pop cs
popf
4. 混合语言编程
还在路上。
5. 内存分配与文件操作
咕咕咕。
6. 保护模式程序设计
咕咕咕。