第九章 转移指令的原理
8086CPU的转移指令分为以下几类:
1.无条件跳转指令(如:jmp)
2.条件跳转指令
3.循环指令(如:loop)
4.过程,就像C语言中的函数
5.中断
9.1 操作符offset
操作符offset(伪指令)在汇编语言中由编译器处理,它的功能是取标号的偏移地址。
assume cs :codesg
codesg segment
start : mov ax,offset start;相当于mov ax,0
s : mov ax,offset s;相当于mov ax,3
codesg ends
end start
在上面的程序中,offset操作符取得了标号start和s的偏移地址0和3,所以指令:mov ax,offset start相当于指令mov ax,0,因为start是代码段中的标号,它所标记的指令是代码段中的第一条指令,偏移地址为0;
mov ax,offset s相当于指令mov ax,3,因为s是代码段中的标号,它所标记的指令是代码段中的第二条指令,第一条指令长度为3个字节,则s的偏移地址为3。
使该程序在运行中将s处的一条指令复制到s0处。
分析:
(1) s 和s0 处的指令所在的内存单元的地址是多少? cs:offset s和 cs:offset s0。
(2)将s 处的指令复制到s0 处,就是将cs:offset s 处的数据复制到cs:offset s0处。
(3)段地址已知在cs 中,偏移地址 offset s和 offset s0已经送入si和di中。
(4)要复制的数据有多长? mov ax,bx指令的长度为两个字节,即1个字。
assume cs : codesg
codesg segment
s : mov ax,bx
mov ax,bx的机器码占两个字节
mov si, offset s
mov di, offset s0
mov ax,cs: [si]
mov cs: [di] ,ax
s0 :nop ; nop的机器码占一个字节
nop
codesg ends
end s
9.2 jmp指令
1.无条件转移,可以只修改ip,也可以同时修改cs和ip
1.【jmp 段地址:偏移地址】 可以用来同时修改CS和IP
指令中的段地址修改CS
偏移地址修改IP
这种用法编译器不认识,只能做在debug中使用
2.【jmp 某一合法的寄存器】 仅修改IP的内容
比如:jmp ax 或者 jmp bx(类似于mov IP ax)
2.jmp指令要给出两种信息:
1.转移的目的地址
2.转移的距离(段间转移、段内短转移、段内近转移)
9.3 依据位移进行转移的jmp指令
1.jmp short 标号【转到标号处执行指令,段内短转移】
此格式实现的是:段内短转移,它对ip的修改范围为-128~127
2.也就是说,它向前转移时可以最多越过128个字节,负数使用补码表示
向后转移可以最多越过127个字节
assume cs : codesg ;程序9.1
codesg segment
start : mov ax,0
jmp short s
add ax,1
s: inc ax
codesg ends
end start
上面的程序执行后,ax中的值为1,因为执行 jmp short s后,越过了add ax,1,IP指向了标号s处的inc ax。也就是说,程序只进行了一次ax加1操作。
汇编指令jmp short s对应的机器指令应该是什么样的呢?我们先看一下别的汇编指令和其相对应的机器指令。
assume cs : codesg ;程序9.2
codesg segment
start : mov ax ,0
mov bx, 0
jmp short s
add ax,1
s : inc ax
codesg ends
end start
(1) (CS)-0BBDH,(IP)-0006H,CS:IP指向EB 03(jmp short s 的机器码);(2)读取指令码EB 03进入指令缓冲器;
(3) (IP)=(IP)+所读取指令的长度=(IP)+2-0008H,CS:IP指向add ax,1;(4)CPU执行指令缓冲器中的指令EB 03;
(5)指令EB03执行后,(IP)=000BH,CS:IP指向inc ax。
从上面的过程中我们看到,CPU 将指令EB 03读入后,IP指向了下一条指令,即CS:0008处的add ax,1,接着执行EB 03。如果EB03没有对P进行修改的话,那么,接下来CPU将执行add ax,1,可是,CPU执行的EB 03却是一条修改IP的转移指令,执行后(IP)=000BH,CS:IP指向inc ax,CS:0008处的add ax,1没有被执行。
3.CPU不需要目的地址就可以实现对ip的修改
jmp指令的机器码中不包含目的地址,但是可以实现跳转
实现的方式,是在原地址的基础上进行一个偏移量,即位移
f7 = 11110111 减一11110110 再取反 00001001 这里是9(-9的补码是f7),所以jmp s本来下一条指令是0009指向的nop,但是收到了修改ip的指令,EB F7,0009-9 =0 跳到了s处地址的位置。
4.还有一种和指令“jmp short 标号”功能类似的指令格式:
jmp near ptr 标号,它实现的是段内近转移
功能为:(ip)=(ip)+16位位移
jmp short 标号是8位的位移(范围为-128~127),而jmp near ptr 标号是16位位移(范围为-32768~32767)
9.4 转移的目的地址在指令中的jmp指令
前面讲的jmp指令,其对应的机器码中并没有转移的目的地址,而是相对于当前ip的转移位移
1.指令“jmp far ptr 标号”
实现的是段间转移,又称为远转移,这时机器码中应该明确给出【段地址】
2.指令“jmp far ptr 标号”功能如下:
(CS)=标号所在段的段地址
(IP)=标号所在段中的偏移地址
far ptr 指明了指令用标号的段地址和偏移地址修改cs和ip
assume cs: codesg
codesg segment
start : mov ax, 0
mov bx, 0
jmp far ptr s
db 256 dup (0)
s: add ax,1
inc ax
codesg ends
end start
如图中所示,源程序中的db 256 dup (0),被 Debug解释为相应的若干条汇编指令。这不是关键,关键是,我们要注意一下jmp far ptr s所对应的机器码:EA 0B 01 6A 07,其中包含转移的目的地址。“0B 01 6A 07 ”是目的地址在指令中的存储顺序,高地址的“ 6A 07”是转移的段地址: 07 6A H,低地址的“0B01”是偏移地址:010BH。
9.5 转移地址在寄存器中的jmp指令
指令格式:jmp 16位寄存器
功能:修改ip寄存器中的值,把16位寄存器中的值送入到ip寄存器中
9.6 转移地址在内存中的jmp指令
转移地址在内存中的jmp指令有两种格式:
1.jmp word ptr 内存单元地址(段内转移)
功能:将内存中的那个字视为一个偏移地址,然后跳转到那个偏移地址
与【jmp 寄存器】功能相似
内存单元地址可用寻址方式的任意格式给出
mov ax,0123H
mov ds: [ 0] , ax
jmp word ptr ds : [ 0]
;执行后,(IP)=0123H.又比如,下面的指令:
mov ax,0123H
mov [bx ] ,ax
jmp word ptr [bx];执行后,(IP)=0123H
2.jmp dword ptr 内存单元地址(段间转移)
(ip)=(内存单元地址) ;双字中的低位字是给ip的
(cs)=(内存单元地址+2) ;双字中的高位字是给cs的
跟【jmp 段地址:偏移地址】功能类似
内存单元地址可用寻址方式的任意格式给出
mov ax,0123H
mov ds: [0] ,ax
mov word ptr ds: [2],0
jmp dword ptr ds: [ 0]
;执行后,(CS)=0,(IP)=0123H,CS:IP指向0000:0123又比如,下面的指令:
mov ax,0123H
mov [bx ] ,ax
mov word ptr [bx+2],0
jmp dword ptr [bx]
;执行后,(CS)=0,(IP)=0123H,CS:IP指向0000:0123
补充:不能直接向内存单元中加入立即数要通过寄存器,把立即数加进去
9.7 jcxz指令
1.有条件跳转指令,所有的有条件跳转指令都是短转移
对应的机器码中包含转移的位移,而不是目的地址。对ip的修改范围都为:-128~127(用补码表示)
另一个有条件跳转指令【loop指令】
2.指令格式:jcxz 标号
如果(cx)=0,则跳转到标号处执行
3.jcxz 标号 指令的操作:
1.当(cx)=0时,(ip)=(ip)+8位位移
2.当(cx)!=0时,什么也不做(程序继续向下执行)
if ( (cx) ==0 jmp short标号;
;补全编程,利用jcxz 指令,实现在内存2000H 段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start : mov ax,2000H
mov ds , ax
mov bx,0
s:
mov ch,0
mov cl,[bx]
jcxz ok ;当cx=0时,CS:IP指向OK
inc bx
jmp short s
ok : mov dx,bx
mov ax,4c00h
int 21h
code ends
end start
9.8 loop指令
1.循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对ip的修改范围都为:-128~127(用补码表示)
2.指令格式:loop 标号
3.指令的内部操作
1.cx=cx-1
2.如果cx!=0,(ip)=(ip)+8位位移,跳转
3.(cx)=0,什么也不做,程序向下执行(和jcxz相反)
cx用来控制循环的次数
;利用loop指令,实现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start : mov ax,2000H
mov ds, ax
mov bx,0
s: mov cl,[bx]
mov ch, 0
inc cx ;要+1避免减之前就是0了。
inc bx
loop s
ok: dec bx;dec指令的功能和inc相反,dec bx进行的操作为:(bx)=(bx)-1
mov dx , bx
mov ax,4c00h
int 21h
code ends
end start
9.9 根据位移进行转移的意义
1.根据位移进行转移,这样设计,方便了程序段在内存中的浮动装配
可以实现代码的复用
2.如果在机器码中直接给出【段地址:偏移地址】,
这段程序在内存中换一个位置,则会运行不正确
3.段内近转移、段内短转移都是根据位移进行转移,一共有四种方式
1.jmp short ptr 标号
2.jmp near ptr 标号
3.jcxz 标号
4.loop 标号
这段程序装在内存中的不同位置都可正确执行,因为loop s在执行时只涉及s的位移(-4,前移4个字节,补码表示为FCH),而不是s的地址。如果 loop s的机器码中包含的是s 的地址,则就对程序段在内存中的偏移地址有了严格的限制,因为机器码中包含的是s的地址,如果s 处的指令不在目的地址处,程序的执行就会出错。而loop s的机器码中包含的是转移的位移,就不存在这个问题了,因为,无论s 处的指令的实际地址是多少,loop指令的转移位移是不变的。
9.10 编译器对转移位移超界的检测
注意,根据位移进行转移的指令,他们的转移范围会受到限制
如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错
比如,下面的程序将引起编译错误:
assume cs:code
code segment
start: jmp short s
db 128 dup (0)
s : mov ax,0ffffh
code ends
end start
jmp short s的转移范围是-128~127,IP最多向后移动127个字节。
jmp short s的转移范围是-128~127,IP最多向后移动127个字节。
【实验八、九】
;实验8 分析下面的程序,在运行前思考:这个程序可以正确返回吗? 答案:可以。
assume cs:codesg
codesg segment
mov ax,4c00h ;最终跳到了这里
int 21h
start: mov ax,0
s: nop ;jmp short s1 F6EB H 也就是eb f6 -10的补码
nop
mov di, offset s
mov si,offset s2
mov ax,cs : [si] ; F6EB H
mov cs: [di] ,ax ;jmp short s1 机器码放到了上面
s0: jmp short s
s1: mov ax,0 ;jmp short s1本以为会跳回来,但是是加偏移地址-10
int 21h
mov ax,0
s2: jmp short s1 ;
nop
codesg ends
end start
实验九 编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串 ‘welcome tomasm!’。
编程所需的知识通过阅读、分析下面的材料获得。
80×25彩色字符模式显示缓冲区(以下简称为显示缓冲区)的结构:
内存地址空间中,B8000H~BFFFFH 共32KB的空间,为80×25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将立即出现在显示器上。
在80x25彩色字符模式下,显示器可以显示25行,每行80个字符(160byte),每个字符可以有256种属性(背景色、前景色、闪烁、高亮等组合信息)。
这样,一个字符在显示缓冲区中就要占两个字节,分别存放字符的ASCII码和属性。80x25模式下,一屏的内容在显示缓冲区中共占4000个字节。
显示缓冲区分为8页,每页4KB(~4000B),显示器可以显示任意一页的内容。一般情况下,显示第О页的内容。也就是说通常情况下,B8000H~B8F9FH中的4000个字节的内容将出现在显示器上。
在一页显示缓冲区中:
偏移00009F对应显示器上的第1行(80个字符占160个字节);偏移0A013F对应显示器上的第2行;
偏移140~1DF对应显示器上的第3行;
依此类推,可知,偏移F00~F9F对应显示器上的第25行。
在一行中,一个字符占两个字节的存储空间(一个字),低位字节存储字符的 ASCII码,高位字节存储字符的属性。一行共有80个字符,占160个字节。
即在一行中:
0001单元对应显示器上的第1列;0203单元对应显示器上的第2列;04~05单元对应显示器上的第3列;
依此类推,可知,9E~9F单元对应显示器上的第80列。例:在显示器的0行0列显示黑低绿色的字符串’ABCDEF’('A’的ASCII码值为41H,02H表示黑底绿色)
显示缓冲区里的内容为:
注意,闪烁的效果必须在全屏 DOS方式下才能看到。
assume cs:code , ds:data , ss : stack
data segment
db 'welcome to masm!' ;定义要显示的字符串(共16字节加属性就是32字节)
db 82h,24h,71h;定义三种颜色属性
data ends
stack segment
dw 8 dup(0)
stack ends
code segment
start:
mov ax ,data
mov ds , ax
mov ax,stack
mov ss,ax
mov sp ,10h
xor bx, bx ;bx清零,用来索引颜色
mov ax ,0b872h ;算出屏幕第12行中间的显存的段起始位置放入ax中 也就是屏幕中第一个w的位置。
;B8000H 前11行*每行160byte + 64byte(一行中间的位置80-一半的字符串字节16) = 1760+64= 1824=720H+b8000h=b8720h
mov cx,3 ;s3循环控制行数,外循环为3次,因为要显示三个字符串
s3:
push cx;三个进栈操作为外循环s3保存相关寄存器的值
push ax;以防止它们的值在内循环中被破坏
push bx
mov es,ax;此时es为屏幕第12行中间的显存的段起始位置
mov si ,0;si用来索引代码列的字符
mov di ,0;ai用来定位目标列
mov cx,10h
;s1循环控制存放的字符,内循环为10h次,因为一个字符串中含10h个字节
s1:
mov al,ds: [si]
mov es : [di] ,al
inc si
add di ,2
loop s1;此循环实现偶地址中存放字符
mov di,1;di的值设为1,从而为在显存奇地址中存放字符的颜色属性做准备
pop bx
mov al,ds : 10h [bx];取颜色属性
inc bx
mov cx,10h;第二个内循环也为10h次
s2:
mov es:[di] , al
add di ,2
loop s2;此循环实现奇地址中存放宇符的颜色属性
;以下4句为下一趟外循环做准备
pop ax
add ax ,0ah;将显存的段起始地址设为当前行的下一行
; [在段地址中加oah,相当于在偏移地址中加了0a0h (=160d) ]
pop cx
loop s3
mov ax,4c00h
int 21h
code ends
end start
用cls清屏后的效果