分析一个奇怪的程序

题目

分析下面的程序,在运行前思考:这个程序可以正确返回吗?

运行后再思考:为什么是这种结果?

通过这个程序加深对相关内容的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assume cs:code
code 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
code ends
end start

设想过程

首先我把编译后的结果放出来,方便下面理解

image-20240224024035523

刚看到这个题目时所设想的程序执行过程为:

  1. 从start开始先执行 mov ax,0
  2. 继续执行 s里面的内容,这里把s2的地址传给了s
  3. 执行到s0的 jmp short s,这时由于上一步将s2的地址传给了s,所以 jmp short s执行过后会执行s2的jmp short s1
  4. 下一步跳到s1,执行mov ax,0 int 21h 程序终止

但是实际debug的结果并非上述所示。下面我们一步一步看。

调试过程

程序开始时cs:ip在0005 mov ax,0的位置

image-20240224021626480

之后也执行了s里面的内容

image-20240224022056426

image-20240224022202654

至此已经执行了红框里面的指令

image-20240224022322306

下一步执行 jmp short s 将会跳转到s的位置,也就是0008的位置

image-20240224022902966

但是注意此时0008位置的指令已经变成了s2的 jmp short s1,按理说下一步执行 jmp short s1时会跳转到s1,也就是0018,但是执行jmp 0008后的结果不是jmp 0018而是变成了jmp 0000,这是为什么呢?

image-20240224024437247

分析原因

我们看一下jmp 0008执行完成之后程序的反编译的结果:

image-20240224034124563

此时 0008 处的机器码为 EBF6 (jmp 0000)

还记得上面的图吗?

这张图里面0020处的机器码是EBF6(jmp 0018)。这就奇怪了, 为什么机器码明明都是EBF6,但为什么一个是jmp 0000另一个是jmp 0018呢?这就要说到jmp short指令了。

jmp指令

​ jmp为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。

​ jmp指令要给出两种信息:

  1. 转移的目的地址
  2. 转移的距离(段间转移、段内短转移、段内近转移)

​ 不同的给出目的地址的方法,和不同的转移位置,对应有不同格式的jmp指令。

依据位移进行转移的jmp指令

​ jmp short 标号(转到标号处执行指令)

​ 这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127,也就是说,它向前转移时最多可以越过128个字节,向后转移可以最多越过127个字节。

​ 我们再来看上面的jmp指令所对应的机器码,都是EB F6。这说明CPU在执行jmp指令时并不需要转移的目的地址

那么CPU在执行 EB F6的时候是根据什么修改的IP,使其指向目标指令的呢?就是根据指令码中的F6。在jmp short 标号指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移。这个位移是编译器根据汇编指令中的标号计算出来的,具体计算方法如图所示。

image-20240224043416917

​ 实际上,jmp short 标号 的功能为:(IP)=(IP)+8位位移,其中:

  1. 8位位移 = 标号处的地址 - jmp指令后的第一个字节的地址;
  2. short 指名此处的位移为8位位移;
  3. 8位位移的范围为 -128~127,用补码表示(第一位为1表示负数,第一位为0表示正数,正数的补码取反+1=原码,复数的补码取反+1=该数的绝对值);
  4. 8位位移由编译器在编译时算出;

还有一种和jmp short 标号功能相似的指令格式,jmp near ptr 标号,它实现的是段内近转移。

jmp near 标号 的功能为:(IP)=(IP)+16位位移,其中:

  1. 16位位移 = 标号处的地址 - jmp指令后的第一个字节的地址;
  2. near 指名此处的位移为16位位移;
  3. 8位位移的范围为 -32768~32767,用补码表示;
  4. 8位位移由编译器在编译时算出;

回到之前的问题:此时可以算出0018H-0022H = -AH,用补码表示为F6。所以EB F6 表示向前转移10个字节。这就可以解释了为什么在0020时会jmp到0018,而在0008时jmp到0000。

注:此文部分摘抄自王爽 - 《汇编语言(第四版)》,若有侵权,可留言删除。