1. 预备知识
- 汇编语言中的 nop 表示空指令,程序运行到此处时什么也不做,但会占用一个指令的时间。
- 操作符 offset 的功能是取得指定标号的偏移地址。
- CPU 在执行 jmp 指令时并不需要转移的目的地址,而是通过位移来定位转移后指令的地址。这样做的好处是,方便程序段在内存中的浮动装配,使带转移的程序在内存中的不同位置都可以正确执行,因为跳转指令只涉及目的地址的偏移而不是地址。
- 其中,转移位移通过标号处的地址减去 jmp 指令后的第一个字节的地址得到,所以此时的位移可能为负数。在汇编语言中,转移的位移使用补码表示。
2. 实验任务
分析下面程序,在运行前思考:这个程序可以正确返回吗?运行后再思考:为什么是这种结果?
assume cs:codesg
codesg segment
mov ax,4c00h
int 21h
start:
mov ax,0
s:
nop
nop
mov di,offset s
mov si,offset s2
mov ax,cs:[si]
mov cs:[di],ax
s0:
jmp short s
s1:
mov ax,0
int 21h
mov ax,0
s2:
jmp short s1
nop
codesg ends
end start
- end start 指明程序的入口为标号 start 处。
如上图,初始时 CS:IP 指向的地址为 076A:0005,即指令 mov ax,0000,为程序中 start 标号处的指令。
- 标号 s 到标号 s0 间的代码,mov di,offset s 语句将标号 s 处指令的偏移地址赋值给寄存器 DI;mov si,offset s2 语句将标号 s2 处指令的地址赋值给寄存器 SI。后两条语句将标号 s2 处指令的地址赋值到标号 s 处。由于转移指令实际上使用的是机器码,所以实质是将机器码 EBF0 赋值到标号 s 处。而标号 s2 处指令长度为 2 字节,正好覆盖了标号 s 处两个 nop 指令的位置。
s:
nop ;空指令,占一个字节
nop ;空指令,占一个字节
mov di,offset s ;将标号s处指令的偏移地址赋值到寄存器DI
mov si,offset s2 ;将标号s2处指令的偏移地址赋值到寄存器SI
mov ax,cs:[si] ;将标号s2处指令的内容赋值到寄存器AX
mov cs:[di],ax ;将寄存器AX的内容赋值到标号s处,即两个nop指令(两字节)会被jmp short s1覆盖
此时,标号 s 处的指令变为:
- 后续,再次执行指令标号 s0 处的指令 jmp short s。如上图,此时标号 s 处的指令变为 JMP 0000,即跳转到 076A:0000 处,而此处的指令为程序返回指令,程序正常返回。另一方面,FB 用于表示当前指令的性质,后两位表示转移的位移。在机器码中,位移使用补码表示,F6 对应的二进制是 1111 0000,对应的十进制数为 -10,所以 jmp 指令后一指令地址 076A:000A 向前偏移 10 个字节恰好是程序返回指令的地址 076A:0000。
3. 总结
本题的关键是将标号 s2 处的指令 jmp short s1 赋值到标号 s 处,由于转移指令使用位移表示,真正赋值的内容是 FBF6 而非 jmp short s1。