第九章 转移指令的原理

我们8086cpu的转移行为有以下几类:

1
2
3
只修改IP,成为段内转移,比如:jamp ax
同时修改CS和IP时,成为段间转移,比如jmp 1000:0

对于IP的修改范围不同,段内转移又分为:短转移和近转移。

短转移的IP的修改范围为-128~127。

近转移IP的修改范围为-32768~32767。

操作符offset

功能为取得标号的偏移地址,比如下面的程序:

1
2
3
4
assume cs:cosdeg
cosdeg segment
start:mov ax,offset start相当于mov ax,0,即取得了start的偏移地址
s:mov ax,offset s相当于mov ax,3

下面看问题

image-20230320154038039

所以只需要添加:

1
2
mov ax,cs:[si]#先将s处的偏移地址放进ax
mov cs:[di],ax#再把s处的偏移地址送入s0处,这也就是我们前面定义si,di的原因,就是为了获取偏移地址便于传参。

jmp指令

给出两种信息:

1
2
转移的目的地址
转移的距离。

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

比如

1
jmp short 标号#表示短转移,因为对IP修改的范围为-128~127,向后最多越过127个字节。

转移指令结束后,CS:IP应该指向标号处的指令

比如如下程序:

1
2
3
4
5
6
7
8
9
10
assume cs:codesg

codesg segment

start:mov ax,0
jmp short s#也就是直接指向s的地方,跳过了add ax,1
add ax,1
s:inc ax
codesg ends
end start

我们看一下jmp指令执行的具体过程。举个例子

image-20230320211144676

可以看到汇编指令中的立即数(idata)不论是一个数据还是一个内存单元的偏移地址,都会在机器指令中出现。

image-20230320212220750

看来jmp 0008也就表示jmp short s,但是他的机器码却之后EB03,不包含转移的目的地址

那么我们再来看一个例子

image-20230320212413627

看到本程序的机器码,对于执行jmp竟然也是EB03,说明cpu在执行jmp指令的时候不需要转移的目的地址

我们可以具体看一下执行过程,就明白为什么了:

1
2
3
4
5
首先(CS)=0BBDH,(IP)=0006H,CS:IP指向EB03
然后读取EB03进入指令缓冲器
(IP)=(IP)+所读取指令的长度(IP)=(IP)+2=0008H,CS:IP指向add ax,1
CPU执行指令缓冲器中的指令EB03
指令EB03执行后(IP)=000BH,CS:IP指向inc ax

其实EB03中告诉了CPU要转移的位移。也就是让IP向后移动3个字节。

而这个位移,是编译器根据汇编指令中的“标号”计算出来的,如图所示

image-20230320213037861

这张图要好好回味,画得十分抽象!我来详细解释一下:也就是我们的CPU在执行指令缓冲期中的EBF7时,这时的cs:ip要提前向下读取,也就是已经指向了0009然而在EBF7处执行的指令是跳转到s也就是让IP指向0000,所以两者之间相差0-9=-9取它的补码也就是F7。

所以说“jmp short 标号”的功能为

1
2
(IP)=(IP)+标号处的地址-jmp指令后的第一个字节的地址。
而位移的范围正好也就是-128127!(8位位移)

还有一种和jmp short 功能相似的指令:jmp near ptr 标号,其功能为:(IP)=(IP)+16位移

1
2
(IP)=(IP)+标号处的地址-jmp指令后的第一个字节的地址。
而位移的范围正好也就是-3276832767!(8位位移)

转移的目的地址在指令中的jmp指令

1
jmp far ptr 标号  实现的是段间转移,也就是“远转移”

指明了指令用标号的段地址和偏移地址修改CS和IP。

看如下的程序:

image-20230320214127528

注意,远转移和短转移可就不一样了,这个指令直接读取了目的地址并且直接执行跳转到目的地址,不用位移的形式了。如图所示:

image-20230320215236468

当然后面那一大堆都是定义的dup(0)不用管他。

我们看到jmp far ptr s所对应的机器码

1
EA0B01 BD0B在这里执行的数据也就是0B01 BD0B我们按照高低的顺序读,BD0B也就是新的段地址,而0B01也就是偏移地址。

转移地址在寄存器中的jmp指令

指令:jmp 16位reg

功能:(ip)=(16位reg)

这种指令很好理解,不再多说。

转移指令在内存中的jmp指令

第一种格式

1
2
jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始 存放一个字,是转移的目的偏移地址。

例如:

1
2
3
4
mov ax,0123h
mov ds:[0],ax
jmp word ptr ds:[0]#显然也就是要跳到0123h的偏移地址处。
执行后(ip)=0123h

或者

1
2
3
4
mov ax,0123h
mov [bx],ax
jmp word ptr [bx]
#执行后(ip)=0123h

第二种格式

1
jmp dword ptr 内存单元地址(段间转移)#这里写了double字也就是两个字,高地址处的字是转移的目的段地址,低地址处的字是转移的偏移地址。

比如下面的程序

1
2
3
4
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]

执行之后(cs)=0(也就对应了ds:[2]处的一个字被改为0了),(ip)=0123h。所以cs:ip指向0000:0123h

再看下面的指令

1
2
3
4
mov ax,0123h
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]

也就是[bx]处的两个内存单元被设置为0123h,高地址的两个内存单元被设置为0

执行后结果与上面相同。

我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:code

data segment

data ends

code segment
start:mov ax,data
mov ds,ax
mov bx,0
jmp word ptr [bx+1]
code ends
end start

若要使程序中的jmp指令执行后,cs:ip指向程序的第一条指令,则data段中应该定义哪些数据

我们自习思考,结果应该如下:

1
first_instruction dw offset start

这里的offset start也就是显示出start的偏移地址

所以也就是定义了一个字,正好两个字节,读取为跳转的目的ip,非常巧妙!

再来看下一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:code

data segment
dd 12345678h
data ends

code segment

start:mov ax,data
mov ds:ax
mov bx,0
mov [bx],__
mov [bx+2],__
jmp dword ptr ds:[0]
code ends

ends start

补全程序jmp执行之后cs:ip指向程序第一条指令。

显然要读取ds的前四个字节也就是两个字作为cs:ip,显然先读取的是ip应该设置为offset start,后面两个字节的cs应该设置为直接写cs。

再来看最后一个练习

image-20230323163327455

显然执行过后(cs)=0006, (ip)=00BE

jcxz指令

为条件转移指令,所有的条件转移指令都是短转移。用位移表示。

与short s的执行过程一样,都是偏移一个8位的位移。

jcxz指令相当于:

1
if((cx)==0) jmp short 标号

来看一个例题:

利用jcxz指令,实现在内存2000H段中查找第一个值为0的字节。找到后,将它的偏移地址存储在dx中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
assume cs:code
code segment

start:mov ax,2000h
mov ds,ax
mov bx,0
s:________
________
________
________
jmp short s
ok:mov dx,bx
mov ax,4c00h
int 21h
code ends
end start

看我如下示例(不一定正确,因为我并没有正确答案

1
2
3
4
s:mov cl,[bx]
mov ch,0
jxcz ok#这里也就是只要cl便利【bx】找到0,那么直接跳转到ok标识符
inc bx

loop指令

循环指令,所有的转移都为短转移。在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:-128~127

loop指令的功能相当于

1
2
(cx)--;
if((cx)!=0) jmp short 标号

来看如下程序:利用loop指令,实现在内存2000h中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。(也就是换个指令玩玩)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
assume cs:code
code segment
start:mov ax,2000h
mov ds,ax
mov bx,0
s:mov cl,[bx]
mov ch,0
_______
inc bx
loop s

ok:dec bx
mov dx,bx
mov ax,4c00h
int 21h
code ends
end start

这里我给出

1
jcxz ok#因为只要前面cl找到0,就可以利用jcxz直接跳出去,如果没找到0,也就不跳出去继续bx+1然后执行循环继续查找

根据位移进行转移的意义

我们总结一下根据位移进行转移的指令

1
2
3
4
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号

如果我们都不用位移而是用地址,标号的地址,那么就会对程序段在内存中的偏移地址有严格的限制!

编译器对转移位移超界的检测

比如下面的跳转超过了IP可以跳转的界限,就会报错!

image-20230323190559996

image-20230323190606245

以为jmp short s的转移范围是-128-127所以最多向后移动127个字节!

实验:分析一个很难的程序!是否能成功返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
assume cs:codesg 
codesg segment

mov ax,4c00h
int 21h
start:mov ax,0//先ax置为0开始
s:nop
nop

mov di,offset s//这里将s的ip赋值给di
mov si,offset s2//这里将s2的ip赋值给si
mov ax,cs:[si]//将s2的数据(指令)给ax
mov cs:[di],ax//把s2的指令再由ax传输到s的指令,原来是nop,现在被写入为jmp short了,但是应该是跳转固定位移!

s0:jmp short s//跳转到s开始执行,此时s被写入jmp指令,经计算正好跳转到mov ax,4c00h然后程序结束。

s1:mov ax,0
int 21h
mov ax,0

s2:jmp short s1//应该是EB 然后接位移,位移为s1所在的地址减去下一句话也就是nop所在的ip
nop

codesg ends
end start