汇编语言——更多功能
转移指令及其原理
可以修改IP,或同时修改cs和IP的指令统称为转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。
8086CPU的转移行为有以下几类:
-
只修改IP时,称为段内转移,比如:jmp ax
-
同时修改cs和IP时,称为段间转移,比如:jmp 1000:0,由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移。
-
短转移IP的修改范围为-128~127。
-
近转移IP的修改范围为-32768~32767。
8086CPU的转移指令分为以下几类
- 无条件转移指令(如:jmp)
- 条件转移指令
- 循环指令(如:loop)
- 过程
- 中断
这些转移指令转移的前提条件可能不同,但转移的基本原理是相同的。我们在这一章主要通过深入学习无条件转移指令jmp来理解CPU执行转移指令的基本原理。
操作符 offset和空操作指令nop
操作符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
空操作指令nop
,只占用一个byte的空间,作为指令不会被执行。
jmp指令
jmp指令是无条件跳转指令,可以只修改ip,也可以cs和ip都修改
jmp
指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移、段内近转移)
不同的给出目的地址的方法,和不同的转移位置,对应有不同格式的jmp
指令,下面分情况讨论。
依据位移进行转移的jmp指令
jmp short 标号(转到标号处执行指令)
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127,也就是说,它向前转移时可以最多越过128个字节,向后转移可以最多越过127个字节。jmp指令中的"short"符号,说明指令进行的是短转移。jmp指令中的“标号”是代码段中的标号,指明了指令要转移的目的地,转移指令结束后,CS:IP应该指向标号处的指令。
assume cs:code
code segment
start: mov ax,0
jmp short s
add ax,1
s:inc ax
mov ax,4c00h
int 21h
code ends
end
上面的代码执行完之后,ax中的值为1,因为jmp short
指令跳过了add ax,1
。
在其他的用立即数操作的指令中,如mov ax,ds:[0123h]
,在debug中可以看到对应的机器码为B82301
,可以发现操作数就在机器码中。
在debug中查看jmp short s
发现对应代码为jmp 0008
,但机器码确是EB03
,可以发现没有和0008直接有关。那么他是如何实现修改IP的呢?
;debug 机器码
076C:0000 B80000 mov ax,0
076C:0003 EB03 jmp short s
076C:0005 050100 add ax,1
076C:0008 40 inc ax
076C:0009 B8004C mov ax,4c00h
076C:000C CD21 int 21h
可以很容易发现0005和0008之间隔了3,回想一下CPU如何处理指令,加载到缓冲区,IP+(指令长度),然后执行指令。不妨做个猜想,CPU读取到jmp short s
之后,IP=IP+3=5,IP指向了add ax,1
,执行jmp short s
后变为0008,实际上是通过对当前IP的加减操作,IP=IP+3。为了验证,我们在原来代码的基础上,jmp short s
后再加一条nop空指令
来占据一个byte。再次debug可以发现jmp short s
的机器码为EB04
,证明我们的猜想是对的。
实际上,jmp short 标号
的功能为:(IP)=(IP)+8位位移。
- 8位位移=标号处的地址-jmp指令后的第一个字节的地址;
- short指明此处的位移为8位位移;
- 8位位移的范围为-128~127,用补码表示
- 8位位移由编译程序在编译时算出。
还有一种和jmp short 标号
功能相近的指令格式,jmpnearptr标号,它实现的是段内近转移。jmp near ptr 标号
的功能为:(IP)=(IP)+16位位移。
- 16位位移=标号处的地址-jmp指令后的第一个字节的地址;
- nearptr指明此处的位移为16位位移,进行的是段内近转移;
- 16位位移的范围为-32768~32767,用补码表示;
- 16位位移由编译程序在编译时算出。
转移的目的地址在指令中的jmp 指令
前面说到的jmp指令对应的机器指令中并没有转移的目的地址,而是相对于其当前IP的转移位移。jmp far ptr 标号
实现的是段间转移,又称为远转移。
assume cs:code
code segment
start: mov ax,0
jmp far ptr s
db 256 dup(0)
s:inc ax
mov ax,4c00h
int 21h
code ends
end
上面这段代码用debug查看机器码可以看到jmp far ptr s
的机器码为EA08016C07
,执行这条指令后IP为0108
,CS为076C
。
转移地址在寄存器中的jmp指令
可以在查看之前的讲解
转移地址在内存中的jmp指令
转移地址在内存中的jmp指令有两种格式:
- jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。内存单元地址可用寻址方式的任一格式给出。比如,下面的指令:
mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]
执行后,(IP)=0123H。
下面的指令也可以达到相同的效果:
mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
- jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
(CS)=(内存单元地址+2)
(IP)=(内存单元地址)
内存单元地址可用寻址方式的任一格式给出。比如,下面的指令:
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]
jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:-128~127。
指令格式:jcxz 标号
(如果cx==0,转移到标号处执行。操作:当(cx)=0时,(IP)=(IP)+8位位移;
8位位移=标号处的地址-jcxz指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当cx!=0时,什么也不做(程序向下执行)。
loop指令
虽然在前面经常用,但还是说一下,loop指令也是短转移指令,机器码中的数字是位移,而不是目的地址。
编译器对转移位移超界的检测
编译器会对超界的位移报错,程序无法通过编译,例如下面这段程序在编译时会报错error A2053 jump out of range by 129 byte(s)
。因为jmp short
最多向后127个,但是却有256的差距,所以越界了129byte。
assume cs:code
code segment
start: mov ax,0
jmp short ptr s
db 256 dup(0)
s:inc ax
mov ax,4c00h
int 21h
code ends
end
CALL和RET指令
call和ret指令都是转移指令,这它们都修改IP或同时修改CS和IP。他们共同用来实现子程序的设计。
ret和retf
ret
用栈中的数据,修改IP,实现近转移retf
用栈中的数据,修改IP和CS,实现远转移
CPU执行ret指令时,进行下面两步操作:
IP = ss*16+sp
sp=sp+2
CPU执行retf指令时,进行下面4步操作:
IP=((ss)*l6+(sp))
sp=(sp)+2
CS=((ss)*l6+(sp))
sp=(sp)+2
用不正确汇编指令表示出来就是这个
------RET-------
POP IP
------RETF-------
POP IP
POP CS
CALL指令
CPU执行CALL指令时,会把当前的IP或CS和IP压入栈中,随后转移。
CALL指令除了不能短转移(short 位移)之外和jmp指令很相似,不同的用法在下面分别讲述
依据位移进行转移的call指令
call 标号
(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
sp=sp-2
ss*16+sp=IP
IP=IP+16位位移
16位位移=标号处的地址-call指令后的第一个字节的地址;
16位位移的范围为-32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
从上面的描述中,可以看出,如果我们用汇编语法来解释此种格式的call指令,则:
CPU执行"call标号”时,相当于进行:
push IP
jmp near ptr 标号
转移的目的地址在指令中的call指令
上一个call指令的用法,对应的机器码只有当前IP的偏移值,没有指定的目的地址,CALL far ptr 标号
实现的是段间转移。
CPU执行此种格式的call指令时,进行如下的操作。
sp=sp-2
ss16+sp=CS
sp=sp-2
ss16+sp=IP
CS=标号所在段的段地址
IP=标号在段中的偏移地址
从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的call指令,则:
CPU执行"callfarptr标号”时,相当于进行:
push CS
push IP
jmp far ptr 标号
转移的目的地址在寄存器和内存中的call指令
大致用法与jmp指令并无不同。
CALL指令和RET指令配合使用
在开始的时候我们说到call和ret是用来实现子程序的,那么该如何使用呢?
call是用来保存当前即将执行到指令的偏移地址并且转到标号处执行别的程序,ret可以用来恢复IP。通常我们用以下结构实现程序执行到一半去执行别的程序再回来接着执行剩余的指令
.
.
.
many instructions
.
.
.
call sub
.
.
.
many instructions
.
.
.
sub:
.
.
.
many instructions
.
.
.
ret ;返回原程序继续执行
参数和结果传递的问题
子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
比如,设计一个子程序,可以根据提供的N,来计算N的3次方。这里面就有两个问题:
(1)将参数N存储在什么地方?
(2)计算得到的数值,存储在什么地方?
很显然,可以用寄存器来存储,可以将参数放到bx中;因为子程序中要计算N X N X N
,可以使用多个mul指令,为了方便,可将结果放到dx和ax中。子程序如下。
;说明:计算N的3次方
;参数:(bx)=N
;结果:(dx:ax)=N3
cube: mov ax,bx
mul bx
mul bx
ret
mul是乘法指令,用法与div指令相似,提供8位乘法和16位乘法,8位乘法是,一个乘数默认在AL,另一个可以在8位寄存器或者内存中,结果高位在AH,低位在AL,16位乘法是,AX中保存一个乘数,另一个可以在16位寄存器或者内存中,结果高位在DX,低位在AX。
批量数据的传递
在上面的例子中,只有一个参数,如果有很多的参数,寄存器跟不够用,该怎么办呢?
通常把数据存储在内存中,然后将它们所在内存空间的首地址放在寄存器里,再把寄存器传给子程序,返回是也是如此。
寄存器的冲突
在之前的实现双重循环的时候,会出现cx寄存器的冲突,在构建子程序的时候,同样也会遇到这样的问题,会出现寄存器的冲突,解决方法是在子程序的开始,将使用到的寄存器保存到堆栈,在程序返回的时候再复原。
标志寄存器
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下3种作用。
- 用来存储相关指令的某些执行结果;
- 用来为CPU执行相关指令提供行为依据;
- 用来控制CPU的相关工作方式。
这种特殊的寄存器在8086CPU中,被称为标志寄存器。8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW)。我们已经使用过8086CPU的ax、bx、ex、dx、si、di、bp、sp、IP、cs、ss、ds、es等13个寄存器了,本章中的标志寄存器(以下简称为flag)是我们要学习的最后一个寄存器。
flag和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。
下面是8086flag的结构
|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
|--|--|
| | | | |OF|DF|IF|TF|SF|ZF| |AF| |PF| |CF|
在这一章中,我们学习标志寄存器中的CF、PF、ZF、SF、OF、DF标志位,以及一些与其相关的典型指令。
ZF
flag的第6位是ZF,零标志位。它记录相关指令执行后,其结果是否为0。如果结果为0,那么zf=l;如果结果不为0,那么zf=0。
如下面的程序,执行之后结果为0,zf为1:
mov ax,2
sub ax,2
注意,在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如,mov、push、pop等,它们大都是传送指令。在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响。
PF
flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有bit位中1
的个数是否为偶数。如果1的个数为偶数,pf=l,如果为奇数,那么pf=O。比如,指令:
mov al,1
add al,10
执行后,结果为00001011B,其中有3(奇数)个1,则pf=0;
mov al,1
or al,2
执行后,结果为00000011B,其中有2(偶数)个1,则pf=1;
SF
flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果是否为负。如果结果为负,sf=1;如果非负,sf=0。
计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。比如:
00000001B,可以看作为无符号数1,或有符号数+1;
10000001B,可以看作为无符号数129,也可以看作有符号数-127。
这也就是说,对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算。比如:
mov al,10000001B
add al,1
结果:(al)=10000010B。
可以将add指令进行的运算当作无符号数的运算,那么add指令相当于计算129+1,结果为130(10000010B);也可以将add指令进行的运算当作有符号数的运算,那么
add指令相当于计算-127+1,结果为-126(10000010B)。
不管我们如何看待,CPU在执行add等指令的时候,就已经包含了两种含义,也将得到用同一种信息来记录的两种结果。关键在于我们的程序需要哪一种结果。
SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值。
这也就是说,CPU在执行add等指令时,是必然要影响到SF标志位的值的。至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算了。
sf为什么值,代表了假如我们进行了有符号数的计算,结果是否为负数。
CF
flag的第0位是CF,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。
我们知道,当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。比如,两个8位数据:98H+98H,将产生进位。由于这个进位值在8位数中无法保存,我们在前面的课程中,就只是简单地说这个进位值丢失了。其实CPU在运算的时候,并不丢弃这个进位值,而是记录在一个特殊的寄存器的某一位上。8086CPU就用flag的CF位来记录这个进位值。比如,下面的指令:
mov al,98h
add al,al
执行后结果为30H,CF为1。
而当两个数据做减法的时候,有可能向更高位借位。比如,两个8位数据:97H-98H,将产生借位,借位后,相当于计算197H-98H。而falg的CF位也可以用来记录这个借位值。
mov al,98h
sub al,98h
执行后结果为FFH,CF为1。
OF
我们先来谈谈溢出的间题。在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出。
那么,什么是机器所能表示的范围呢?
比如说,指令运算的结果用8位寄存器或内存单元来存放,比如,addal,3,那么对于8位的有符号数据,机器所能表示的范围就是—128127。同理,对于16位有符号数据,机器所能表示的范围是-32768~32767,如果运算结果超出了机器所能表达的范围,将产生溢出。
注意,这里所讲的溢出,只是对有符号数运算而言。下面我们看两个溢出的例子。
mov al,98
add al,99
执行后将产生溢出。因为add al,99
进行的有符号数运算是:
(al)=(al)+99=98+99=197。
而结果197超出了机器所能表示的8位有符号数的范围:-128~127。
add指令运算的结果是(al)=0C5H
,因为进行的是有符号数运算,所以
有符号数,而0C5H
是有符号数-59
的补码。指令进行的是有符号数运算,则98+99=-59这样的结果让人无法接受,造成这种情况的原因,就是实际的结果197,在8位寄存器al中存放不下。
由于在进行有符号数运算时,可能发生溢出而造成结果的错误。则CPU需要对指令执行后是否产生溢出进行记录。
flag的第11位是OF,溢出标志位。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。如果发生溢出,OF=1,如果没有,OF=0。
一定要注意CF和OF的区别:CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位。比如:
mov al,98
add al,99
add指令执行后:CF=0,0F=1。前面我们讲过,CPU在执行add等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。对于无符号数运算,CPU用CF位来记录是否产生了进位;对于有符号数运算,CPU用0F位来记录是否产生了溢出,当然,还要用SF位来记录结果的符号。对于无符号数运算,98+99没有进位,CF=0;对于有符号数运算,98+99发生溢出,0F=1。
mov al,0F0H
add al,88H
add指令执行后:CF=1,0F=1。对千无符号数运算,0F0H+88H有进位,CF=1;对于有符号数运算,0F0H+88H发生溢出,0F=1。
mov al,0F0H
add al,78H
add指令执行后:CF=1,0F=0。对于无符号运算,0F0H+78H有进位,CF=1;对于有符号数运算,0F0H+78H不发生溢出,0F=0。
我们可以看出,CF和0F所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,它们之间没有任何关系。
adc指令
adc指令是带进位加法的指令,利用了CF中保存的进位信息。
例如:
mov ax,2
mov bx,1
sub bx,ax
adc ax,1
结果ax=4,因为ax=ax+bx+cf。
可以看出,adc指令比add指令多加了一个CF位的值。为什么要加上CF的值呢?
在进行大的数字计算的时候,可能无法直接相加,如32位数的相加,可以拆成2个16位相加,但是低位的相加,进位需要保存下来,在高位计算的时候用adc指令补进去。
sbb指令
sbb是带借位减法指令,它利用了CF位上记录的借位值。
指令格式:sbb操作对象1,操作对象2
功能:操作对象l=操作对象l—操作对象2—CF
比如指令sbb ax,bx
实现的功能是:(ax)=(ax)-(bx)-CF
sbb和adc是基于同样的思想设计的两条指令,在应用思路上和adc类似。在这里,我们就不再进行过多的讨论。通过学习这两条指令,我们可以进一步领会一下标志寄存器CF位的作用和意义。
cmp指令
cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
cmp指令格式:cmp 操作对象1,操作对象2
功能:计算操作对象1-操作对象2,但并不保存结果,仅仅根据计算结果对标志寄存器进行设置。
比如,指令cmp ax,ax,做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响
flag的相关各位。指令执行后:zf=1,pf=1,sf=0,cf=0,of=0。
对于无符号数比较,我们通过cmp指令执行后,相关标志位的值就可以看出比较的结果。
if ax =bx, then ZF=1
if ax!=bx, then ZF=0
if ax >bx, then ZF=0, CF=0
if ax <bx, then CF=1
if ax<=bx, then CF=1 or ZF=1
if ax>=bx, then CF=0
但是对有符号数这么比较是有漏洞的,对于有符号数,判断是否相等可以直接用ZF标志位。对于有符号数计算,通过判断SF标志符号,可以知道结果表示为有符号数的正负,但是这里同样有个漏洞,例如:
mov al,22h
mov bl,0a0h
cmp al,bl
34-(-96)=130得到的结果是82H,-126的补码是82H(10000010B),SF标志位的值为1,这样的情况不能直接判断是大于还是小于,思考一下,为什么出现上述的情况会无法只用SF判断,原因是因为溢出了(130>127),因为溢出导致了无法正确判断,在没有发生溢出时(OF=0),可以直接判断,所以:
if OF=1 and SF=1 ax>bx
if OF=0 and SF=1 ax<bx
if OF=1 and SF=0 ax<bx
if OF=0 and SF=0 ax>bx
检测比较结果的条件转移指令
在前面的时候,我们有说到关于条件的跳转指令都是有关CX寄存器的,现在来说一下关于标志寄存器的,根据cmp指令修改标志位后,检查指定标志位确定是否进行跳转,两者配合使用,类似于call和ret指令。
因为cmp分为无符号数字比较和有符号数字比较,所以,跳转指令也分为对无符号数比较的跳转指令和有符号数字比较的跳转指令
;无符号跳转
je ;含义:相等跳转
jne ;含义:不相等跳转
jb ;含义:小于跳转
jnb ;含义:不小于跳转
ja ;含义:大于跳转
jna ;含义:不大于跳转
DF标志和串传送指令
falg的第10位是DF,方向标志位。在串处理指令中,控制每次操作后si、中的增减。
df=0每次操作后si、di递增;
df=1每次操作后si、di递减。
我们来看下面的一个串传送指令。
格式:movsb
功能:执行movsb指令相当于进行下面几步操作。
(1)((es)X16+(di))=((ds)Xl6+(si))
(2)如果df=0则:(si)=(si)+1,(di)=(di)+1
(3)如果df=1则:(si)=(si)-1,(di)=(di)-1
该指令实现了内存中数据段中的数据复制到另一处位置,可以从一个指定位置开始,如果df为0则正向进行,否则反向进行。
当然也支持字传输:
格式:movsw
功能:执行movsw指令相当于进行下面几步操作。
(1)((es)X16+(di))=((ds)Xl6+(si))
(2)如果df=0则:(si)=(si)+2,(di)=(di)+2
(3)如果df=1则:(si)=(si)-2,(di)=(di)-2
movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,格式如下:
rep movsb
用汇编语法来描述rep movsb
的功能就是:
s: movsb
loop s
可见,rep的作用是根据cx的值,重复执行后面的串传送指令。由于每执行一次
movsb指令si和小都会递增或递减指向后一个单元或前一个单,rep movsb
就可以循环实现(cx)个字符的传送。对于movsw也是同理。
对标志寄存器的保存和恢复
对于一半的寄存器可以直接 push 寄存器名
实现把寄存器的值保存在堆栈中,对标志寄存器,使用pushf
指令可以把标志寄存器压入堆栈,popf
从堆栈中弹出一个字给标志寄存器