8 数据处理
8.1 div指令
除数:由8位和16位两种,在一个reg或内存单元中。
被除数:当除数位8位则被除数为16位,那被除数默认放在AX中;当除数位16位则被除数位32位,那被除数将被放在AX和DX中,其中DX存放高16位AX存放低16位。
结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,AX存储除法操作的商,DX存储除法操作的余数。
格式:div reg;div 内存单元
内存单元的办法有很多种,下面示例:
- 示例1:
div byte ptr ds:[0]
含义是(AL)=(ax)/((ds)*16+0)
的商,(AH)=(ax)/((ds)*16+0)
的余数。 - 实列2:
div byte ptr [bx+si+8]
含义是(AL)=(ax)/((ds)*16+(bx)+(si)+8)
的商,(AH)=(ax)/((ds)*16+(bx)+(si)+8)
的余数 - 示例3:
div word ptr es:[0]
含义是(AX)=(dx*10000H+ax)/((es)*16+0)
的商,(DX)=(dx*10000Hax)/((es)*16+0)
的余数 - 示例4:
div word ptr [bx+si+8]
含义是(AX)=[(dx)*10000H+(ax)]/((ds)*16+(bx)+(si)+8)
的商,(DX)=[(ds)*10000H+(ax)/((ds)*16+(bx)+(si)+8)]
的余数
下面代码示例:除法指令计算1001/100
mov ax,1001
mov bl,100
div bl
;执行后的结果,(al)=0AH,(ah)=1H。就是商为10,余数为1
8.2 伪指令 dd
之前都是用的dw和db定义字型数据和字节数据。dd是用来定义(dword word,双字)型数据的,如下
data segment
db 1
dw 1
dd 1
data ends
在data段中定义了3个数据:
- 第一个数据是01H,在
data:0
处占一个字节,8位。 - 第二个数据是0001H,在
data:1
处占一个字,16位。 - 第三个数据是00000001H,在
data:3
处占两个字,32位。
8.9 dup
dup是一个操作符,在汇编中和db,dw,dd一样,也是给编译器处理的符号。他是和db,dw,dd等数据定义伪指令配合使用的,用来操作数据的重复。比如:
db 3 dup (0)
定义了3个字节,他们的值都是0,相当于 db 0,0,0
。
db 3 dup (0,1,2)
定义了9个字节,他们是0、1、2、0、1、2、0、1、2,相当于 db 0,1,2,0,1,2,0,1,2
。
db 3 dup ('abc','ABC')
定义了18个字节,它们是'abcABCabcABCabcABC',相当于db 'abcABCabcABCabcABC'
。
所以dup的作用是db 重复次数 dup(重复的数据)
。
dup还可以用来定义栈段,如db 200 dup (0)
。定义了一个200位的栈
9 转移指令的原理
可以修改IP或者同时修改CS和IP的指令统称位转移指令。就是转移指令可以扩展CPU执行内存中的某处代码的指令。
8086CPU 的转移行为由以下几类。
- 只修改IP时,称为段内转移,比如:jmp ax。
- 同时修改CS和IP时,称为段间转移,比如:jmp 1000:0。
转移指令对IP修改的范围不同,段内转移又分为:短转移和近转移。
- 短转移IP的修改范围为
-128~127
。 - 近转移IP的修改范围为
-32768~32767
。
转移指令分为以下几类。
无条件转移指令(如:jmp)、条件转移指令、循环指令(如:loop)、过程、中断。
9.1 操作符 offset
offset是回忆起处理的符号,它的功能是取得标号的偏移地址。如下:
assume cs:code
code segment
start:
mov ax,offset start ;相当于 mov ax,0
s:
mov ax,offset s ;相当于 mov ax,3
code ends
end start
程序中offset获取到标号start和s的偏移地址分别为0和3。
9.2 jmp 指令
jmp为无条件转移指令,可以修改CS和IP。
jmp指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内转移、段内近转移)
格式有jmp [地址];jmp 标签
9.2.1 依据位移进行转移的jmp指令
jmp short 标号 (转移到标号处执行指令)
这个格式是一个短转移,最多穿越127个字节。short符号就是说明进行短转移。s是代码段中的一个标签,指明了指令要转移的目的地,转移指令结束之后,CS:IP
应该指向标签处的指令。如下
assume cs:code
code segment
start:
mov ax,0
jmp short s;跳转到s处,也就是下一个指令是inc ax,不是add ax,1
add ax,1
s:
inc ax
code ends
end start
使用debug
我们发现机器码那边是EB03,说明CPU在执行jmp指令的时候并不需要转移的目的地址。
问题:地址是根据CS:IP在指向指令的,但是jmp机器码中只有EB03没有(IP)=0008H的地址,CPU怎么指明目标指令呢?
下面推演jmp short s
指令的读取和指向过程:
(CS)=076AH,(IP)=0003H
,CS:IP指向EB 03(jmp short s
的机器码);- 读取指令码EB 03进入指令缓冲器;
- (IP)=(IP)+所读取指令的长度=(IP)+2=0005H,CS:IP指向add ax,1;
- CPU执行指令缓冲器中的指令EB 03;
- 指令EB 03执行后,(IP)=0008H,CS:IP指向inc ax。
其中在进行EB 03后(IP)=0008H指向inc ax。看下一个程序
CPU通过EB 06指向mov dx,1234H
。所以可以得知是(IP)=(IP)+6=(000C+2)+6=0014H
。在上一个程序是0005H+3=0008H
。
这其中也可以知道为什么短转移是-127~126
、近转移是-32768~32767
。
其中(IP)=(IP)+2+FA=A+2+FA=106。正确的算法是6-C=-06补码是FA
其中+2是执行的jmp的IP地址。
所以jmp short 标号
指令所对应的机器码中不包含转移目的地的地址,而包含的是转移的位移。
jmp short 标号
的功能是(IP)=(IP)+8位位移- 8位位移=标号处的地址-jmp指令后的第一个字节的地址;
short
指明此处的位移为8位位移,进行的是段内的短位移;- 8位位移的范围为
-128~127
,用补码表示; - 8位位移由编译程序在编译时算出。
jmp near ptr 标号
的功能是(IP)=(IP)+16位位移- 16位位移=标号处的地址-jmp指令后的第一个字节的地址;
near ptr
指明此处的位移为8位位移,进行的是段内的近位移;- 16位位移的范围为
-32768~32767
,用补码表示; - 16位位移由编译程序在编译时算出。
9.2.2 转移的目的地址在指令中的jmp指令
前面的jmp指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。
jmp far ptr 标号
实现的是段间转移,又称为远转移。功能如下:
(CS)=标号所在段的段地址:(IP)=标号在段中的偏移地址
far ptr
指明了指令用标号的段地址和偏移地址修改CS和IP。
如下程序:
assume cs:code
code segment
start:
mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0)
s:
add ax,1
inc ax
code ends
end start
发现jmp far ptr s
所对应的机器码:EA 0B 01 BD 0B
,其中包含转移的目的地址0B 01 BD 0B
是目的地址在指令中的存储顺序,高位地址BD 0B
是转移的段地址:0BBDH
,低地址0B01
是偏移地址:010BH
。
9.2.3 转移地址在内存中的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]
执行后,(IP)=0123H
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 ds:[bx+2],0
jmp dword ptr ds:[bx]
执行后,(CS)=0,(IP)=0123H,CS:IP指向0000:0123
9.3 jcxz指令
jcxz指令是有条件转移指令,所有有条件转移指令都是短地址,在对应机器码中包含转移的位移,而不是目的地址。对IP的修改范围是-128~127
。
指令格式:jcxz 标号(如果(CX)=0
,转移到标号处执行);
操作:当(CX)=0
时,(IP)=(IP)+8
位位移;
当(CX)≠0
时程序向下执行,jcxz什么也不做;
9.4 loop 指令
loop指令为循环指令,所有循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围是-128~127
。
操指令格式:loop 标号((CX)=(CX)-1
,如果(CX)≠0
,转移到标号处继续执行);
操作:首先(CX)=(CX)-1
;如果(CX)≠0,(IP)=(IP)+8
位位移;
当(CX)≠0
时程序向下执行,loop什么也不做;
9.5 转移位移超界的检测
根据位移进行转移指令,他们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围越界的问题,在编译的时候,编译器会报错。如下:
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字节。
10 CALL和REL指令
这两个都是转移指令,他们都修改IP,或同时修改CS和IP。一般被用来实现子程序的设计。
10.1 ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
执行ret指令时,进行的操作:
(IP)=((ss)*16+(sp))
(sp)=(sp)+2
相当于进行了pop IP
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;
(IP)=((ss)*16+(sp))
(sp)=(sp)+2
(CS)=((ss)*16+(sp))
(sp)=(sp)+2
相当于进行了pop IP
pop CS
如下代码
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
mov ax,4C00H
inr 21H
start:
mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push ax
mov bx,0
ret
code ends
end start
10.2 CALL指令
call指令实现转移的方法和jmp指令的原理相同,除了call指令不能实现短转移。
call 标号
(将当前IP压栈后,转到标号出执行指令)
CPU执行call指令时的操作:
(sp)=(sp)-2
((ss)*16+(sp)=(IP))
(IP)=(IP)+16位位移
相当于push IP
jmp near ptr 标号
call far ptr 标号
CPU执行指令时的操作:
(sp)=(sp)-2
((ss)*16+(sp))=(cs)
(sp)=(sp)-2
((ss)*16+(sp))=(ip)
- (cs)=标号所在段的段地址
- (ip)=标号所在段中的偏移地址
相当于:push cs
push ip
jmp far ptr 标号
call 16位reg
CPU执行指令时的操作:
(sp)=(sp)-2
((ss)*16+(sp))=(ip)
(ip)=(16位reg)
相当于:push ip
jmp 16位reg
call word ptr 内存单元地址
CPU执行指令时的操作:
相当于:push ip
jmp word ptr 内存单元地址
call dword ptr 内存单元地址
CPU执行指令时的操作:
相当于:push cs
push ip
jmp dword ptr 内存单元地址
10.3 mul指令
mul指令是一个乘法指令,使用乘法的时候需要注意两点
- 两个相乘的数:两个数要么都是8位,要么都是16位。
- 8位:一个默认放在AL中,另一个放在8位reg或内存字节单元中;
- 16位:一个默认放在AX中,另一个放在16位reg或内存字节单元中;
- 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中放。
格式如下:
mul reg
mul 内存单元
内存单元可以用不用的寻址方式给出,比如:
mul byte ptr ds:[0]
含义:(ax)=(al)*((ds)*16+0)
mul word ptr [bx+si+8]
含义:
(ax)=(ax)*((ds)*16+(bx)+(si)+8)
结果的低16位
(dx)=(ax)*((ds)*16+(bx)+(si)+8)
结果的高16位
如下:
;计算100*10
;100和10小于255,做8位乘法。如下:
mov al,100
mov bl,10
mul bl
;结果:(ax)=1000(03E8H)
;计算100*10000
;10000大于255,做16位乘法。如下:
mov ax,100
mov bx,10000
mul bx
;结果:(ax)=4240H,(dx)=000FH (F4240H=1000000)
10.4 call和ret实现模块化
使用call和ret实现子程序,如同c语言的函数的使用,如下
assume cs:code,ss:stack
stack segment
db 16 dup (0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,16
mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s:
add ax,ax
loop s
ret
code ends
end start
可以看见call调用了s后面的程序后又通过ret返回cal的下一条指令。这样子使add ax,ax
loop s
封装成一个子程序,实现模块化的程序设计。
这样子就形成一个框架
assume cs:code
code segment
main:
mov ax,1234h
call sub1 ;调用子程序sub1
mov ax,1234h
mov ax,4c00h
int 21h
sub1: ;子程序sub1开始
mov ax,1234h
call sub2 ;调用子程序sub2
ret ;子程序返回
sub2: ;子程序sub2开始
mov ax,1234h
ret ;返回
code ends
end main
10.5 参数和结果的传递
在模块化程序的运行中,往往需要传递参数和返回结果。这些数使用寄存器来存储。比如要计算N的3次方。如下程序
assume cs:code,ds:data
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
code segment
start:
mov ax,data
mov ds,ax
mov cx,8 ;循环的次数
mov si,0 ;数据从si来
mov di,16 ;数据到di去
s:
mov bx,[si] ;
call sub1 ;子程序进行N*N*N的算法,返回ax
mov [di],ax ;将ax存放到第一个dd单元中
add si,2 ;指向下一个word单元
add di,4 ;指向下一个dword单元
loop s
mov ax,4c00h
int 21h
sub1: ;子程序
mov ax,bx
mul bx
mul bx
ret
code ends
end start
程序中使用loop循环传递不一样的参数给sub1子程序,进行3次方的算法。但是这个程序的参数是一个,有时候我们会传递好几个参数。虽然可以用好几个寄存器来存放,但是寄存器是有限的。对于返回值也是相同的问题。
这时候我们可以将批量的数据存放到内存中,在使用寄存器去读取,对于返回的结果也是一样的。
比如下面程序:将data段中的一串小写字母转化为大写。
assume cs:code,ds:data
data segment
;0123456789ABCDEF
db 'word',0
db 'unix',0
db 'wind',0
db 'good',0
data ends
code segment
start:
mov ax,data
mov ds,ax
mov cx,4
mov bx,0
s:
mov si,bx
call sub1
add bx,5
loop s
mov ax,4c00h
int 21h
sub1:
mov cl,[si]
mov ch,0
jcxz OK
and byte ptr [si],11011111B
inc si
jmp short sub1
OK:
ret
code ends
end start
;这个程序虽然是正确的,但是其中的寄存器冲突了
当然也可以通过栈来传递数据
10.6 寄存器冲突问题
我们在编写子程序的时候会遇到寄存器冲突的情况,比如说我们用到了loop需要CX寄存器,子程序再使用jcxz也需要CX寄存器。这样子CX寄存器的使用就会冲突。
解决这个问题的办法就是使用到寄存器的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容。我们对上面的子程序进行修改一下。
sub1:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz OK
and byte ptr [si],11011111B
inc si
jmp short change
OK:
pop si ;注意进栈出栈的顺序
pop cx
ret
实验:
- 名称:show_str
- 功能:再指定的位置,用指定的颜色,显示一个用0结束的字符串
- 参数:(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79),(cl)=颜色,ds:si指向字符串的首地址
- 返回:无
- 应用举例:再屏幕的8行3列,用绿色显示data段中的字符串
assume cs:code,ds:data,ss:stack
data segment
;0123456789ABCDEF
db 'Welcome to masm!',0
data ends
stack segment
db 128 dup (0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
call init_main
mov dh,8
mov dl,3
call init_row
pop ax
add di,ax
pop ax
add di,ax
call show_data
mov ax,4c00h
int 21h
init_main:
mov bx,data
mov ds,bx
mov bx,0B800H
mov es,bx
ret
init_row:
pop bx
mov ax,160
mul dh
push ax
mov ax,2
mul dl
push ax
push bx
ret
show_data:
push di
push si
push cx
mov cx,0
showString:
mov cl,ds:[si]
jcxz showStringRet
mov es:[di],cl
mov cl,2
mov es:[di+1],cl
add di,2
inc si
jmp showString
showStringRet:
pop cx
pop si
pop di
ret
code ends
end start
代码中使用栈来保存数据,避免了寄存器使用的冲突问题,使用的栈来保存数据记得栈的入栈和出栈的顺序问题。
11 标志寄存器
CPU内部的寄存器,有一种特殊的寄存器,具有以下3中作用。
- 用来存储相关指令的某些执行结果;
- 用来为CPU执行相关指令提供行为依据;
- 用来控制CPU的相关工作方式;
8086CPU中的标志寄存器有16位,其中存储信息的通常称为程序状态字(PSW)。
我们已经使用过了ax、bx、cx、dx、si、di、bp、sp、ip、cs、ss、ds、es
等13个寄存器了。下面是最后一个寄存器。
标志寄存器是按位起作用,也就是说每一位都有专门的含义,记录特定的信息。再8086CPU中没有使用1、3、5、12、13、14、15位,不具有任何含义。而0、2、4、6、7、8、9、10、11位都具有特殊的含义。如下图
11.1 ZF标志
标志寄存的第六位是ZF(零标志位)。它记录相关指令执行后的结果,其结果是否为0.如果结果为0,那么zf=1
;如果结果不为0,那么zf=0
。如下:
指令:mov ax,1
sub ax,1
执行后,结果为0,则zf=1
,表示”结果为0“
指令:mov ax,2
sub ax,1
执行后,结果不为0,则zf=0
,表示”结果非0“
由此可见得ZF标志寄存器是用来标记相关指令的计算结果是否为0。(表示为真,0表示否)
11.2 PF标志
标志寄存器的第2位是PF(奇偶标志位),它记录相关指令的执行后的结果,其结果再所有bit位中的1的个数是否为偶数。如果bit位1的个数为偶数,则pf=1
,如果为奇数,那么pf=0
。如下:
指令:mov al,1
add al,10
执行后:结果为00001011B,其中有3个1,则pf=0
指令:mov al,1
or al,2
执行后:结果为00000011B,其中有2个1,则pf=1
11.3 SF标志位
标志寄存器的第7位是SF(符号标志位)。它记录相关指令执行后的结果,其结果是否为负。如果结果为负,则sf=1
,如果结果为正,那么sf=0
。
计算机中通常用补码来表示有符号数据。计算机中的数据看作是有符号数或无符号数。比如:
0000 0001B
,可以看作为无符号数1,或有符号数+1;
1000 0001B
,可以看作为无符号数129,也可以看作有符号数-127;
也就是说对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以单座有符号数据来运算。比如:
指令:mov 10000001B
add al,1
结果:(al)=10000010B
;
把指令当作无符号数的运算:那么add指令相当于是129+1=130(10000010B)
;
把指令当作有符号数的运算:那么add指令相当于是-127+1=126(10000010B)
;
SF标志,就是CPU再对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得治结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关指令影响了它的值。至于我们需不需要这种影响,就得看指令所进行的运算了。
11.4 CF标志
标志寄存器的第0位是CF(进位标志位)。在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位制。或从更高位借位值。如下:
指令:mov al,ffh
add al,al
结果:(al)=feh (cf)=1
。其中进位的1不是消失了而是记录在某一个特殊的寄存器的某一位上。8086CPU用的的CF来记录这个进位值。
指令:mov al,97h
sub al,98h
结果:(al)=ffh (cf)=1
。其中sub执行的是197h-98h=ffh
,因为不够减只能向更高位借位,而cf记录这个借位值。
11.5 OF标志
标志寄存器的第11位是OF(符号标志位)。它记录相关指令执行后的结果,其结果进行有符号的运算结果是否溢出。如果结果溢出,则sf=1
,如果结果没有溢出,那么sf=0
。
CF和OF的区别:
- CF是对无符号数运算有意义的标志位
- OF是对有符号数运算有意义的标志位
溢出:就是数据进行有符号数的运算的时候超过了机器所能表示的范围。对于8位的有符号数据,机器所能表达的范围是-128~127。对于16位的有符号数据,机器所能表达的范围是-32768~32767。如果运算结果超出了机器所能表示的范围,将产生溢出。如下:
指令:mov al,98
mov al,99
执行后:将产生溢出。在(al)=(al)+99=98=99=197
,结果的197超出了机器所能表示的8位有符号数的范围:-128~127。
11.6 adc 指令
adc是带进位加法指令,它利用了CF位上记录的进位值。
指令格式:adc 操作对象1,操作对象2
功能:操作对象1=操作对象1 + 操作对象2 + CF
比如:adc ac,bx
实现的功能是(ax)=(ax)+(bx)+CF
指令:mov ax,1
add ax,ax
adc ax,3
执行后:(ax)=1+1=2 (CF)=0;(ax)=1+3+(cf)=4
;
指令:mov al,98H
add al,al
adc al,3
执行后:(al)=98H+98H=30H CF=1;(al)=30H+3+(CF)=34H
;
可以看出adc比add多一个加CF标志寄存器的值。
作用:adc指令和add指令的配合使用可以对更大数据进行加法运算。
如:计算1EF0001000H+2010001EF0H
,结果放在ax(最高16位),bx(次高16位)。cx(低16位)中。
mov cx,01000H
add cx,01EF0H
mov bx,0F000H
adc bx,01000H
mov ax,0001EH
adc ax,00020H
11.7 sbb指令
sbb是借位减法指令,它利用CF位上记录的借位值。
指令格式:sbb 操作对象1,操作对象2
功能:操作对象1=操作对象1 - 操作对象2 - CF
比如:sbb ac,bx
实现的功能是(ax)=(ax)-(bx)-CF
sbb指令和adc差不多,不同的是sbb根据减法运算查看借位的值。
11.8 cmp指令
cmp是比较指令,cmp的功能相当于减法只是不保存结果。但是cmp指令执行之后会对标志寄存器产生影响。其他相关的指令通过标志寄存器的值看来得知比较的结果。
指令格式:cmp 操作对象1,操作对象2
功能:操作对象1 - 操作对象2 但是不保存结果
,,仅仅根据计算结果对标志寄存器进行设置。
比如:指令cmp ax,ax
实现的功能是(ax)-(ax)
的运算,结果为0,但不在ax中保存,仅影响标志寄存器的相关各位。执行后:zf=1,pf=1,sf=0,cf=0,of=0
。
列如指令:mov ax,8
mov bx,3
cmp ax,bx
执行后:(ax)=8,zf=0,pf=1.sf=0,cf=0,of=0
其实,我们通过cmp指令执行后,相关标志位的值就可以看出比较的结果。
cmp指令就是通过减法运算,影响标志寄存器,标志寄存器的相关位记录了比较的结果。cmp指令也包含两种含义,分别是无符号数的比较和有符号数的比较。
无符号数比较,指令cmp ax,bx
- 如果(ax)=(bx) 则(ax)-(bx)=0,所以:zf=1;
- 如果(ax)≠(bx) 则(ax)-(bx)≠0,所以:zf=0;
- 如果(ax)<(bx) 则(ax)-(bx)将产生借位,所以:cf=1;
- 如果(ax)≤(bx) 则(ax)-(bx)既可能借位,结果可能为0,所以:cf=1或zf=1;
- 如果(ax)>(bx) 则(ax)-(bx)不必借位,结果又不为0,所以:cf=0并且zf=0;
- 如果(ax)≥(bx) 则(ax)-(bx)不必借位,所以:cf=0;
有符号数比较,指令cmp ah,bh
- 如果(ah)=(bh) 则(ah)-(ah)=0,所以:zf=1;
- 如果(ah)≠(bh) 则(ah)-(ah)≠0,所以:zf=0;
列:(ah)=22H (bh)=0A0H
;则(ah)-(ah)=34-(-96)=130=82H
,其中34和-96为补码得到补码82H;而82是-126的补码,所以sf=1
。
这里sf=1,但是34>-96所以不能说明(ah)<(bh)
。其中发生了溢出就是of=1。得到结论,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果为正,反之则反。
11.9 条件转移指令
根据条件决定是否改变IP去进行转移,如jcxz根据cx是否等于0进行修改IP。除了jcxz之外还有其他根据条件转移的指令,其中条件大多数是根据检测标志寄存器来的相关标志位来进行转移指令。通常都会和cmp指令配合使用。
下面是常用的根据无符号数的比较结果进行转移的条件转移指令:
指令 | 含义 | 检测的标志位 |
---|---|---|
je | 等于则转移 | zf=1 |
jne | 不等于则转移 | zf=0 |
jb | 小于则转移 | cf=1 |
jnb | 不小于则转移(大于或等于) | cf=0 |
ja | 大于则转移 | cf=0且zf=0 |
jna | 不大于则转移(小于或等于) | cf=1或zf=1 |
11.10 DF标志和串传送指令
flag的第10位是DF,方向标志位。在串处理指令中,控制每次操作皇后si、di的增减。
df=1
每次操作后si、di递增;df=0
每次操作后si、di递减。
指令:movsb
功能:执行movsb指令相当于进行下面几步操作。
((es)*16+(di))=((ds)*16+(si))
- 如果
df=0
则:(si)=(si)+1 (di)=(di)+1
,如果df=1
则:(si)=(si)-1 (di)=(di)-1
movsb指令就是把ds:si
指向的内存单元中的字节送入es:di
中,然后根据标志寄存器df的值,将si和di递增或递减。
指令:movsw
,和movsb
差不多,不过在递增递减的时候是2。一般来说movsb
和movsw
都是和rep
配合使用,如:rep movsb
相当于s:movsb
loop s
。
rep
指令的作用是根据cx的值,重复执行后面的串传送指令。每执行一次movsb指令si和di都会递增或递减指向后一个单元或前一个单元,则rep movsb
就可以循环实现(cx)个字符的传送。指令:rep movsw
也是相同的。
由于标志寄存器的df位决定串传送指令指向后,si和di改变的方向。所以在CPU中提供了指令来改变df位的设置,使程序员可以改变传送的方向。
- cld指令:将标志寄存器的df位置0;
- std指令:将标志寄存器的df位值1
如下请程序:将data段中的第一个字符串复制到它后面的空间中。
assume cs:code,ds:data
data segment
db 'welcome to masm!'
db 16 dup (0)
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov es,ax
mov di,160
mov cx,16
cld
rep movsb
mov ax,4C00H
int 21H
code ends
end start
11.11 pushf 和 popf
pushf的功能是将标志寄存器的值压栈,popf是从栈中弹出数据,送入标志寄存器中。是一种直接访问标志寄存器的一种方法。
如下程序
mov ax,0 ;ax= 0000 0000 0000 0000
push ax
popf ;pws=0000 0000 0000 0000
mov ax,0fff0h
add ax,0010h
pushf
pop ax
and al,11000101B
and ah,00001000B
实验:将data段的数据显示在屏幕上,并且将字母大写
assume cs:code,ds:data
data segment
db "Beginner's All-purpose Symbolic Instruction Code.",0
db 16 dup (0)
data ends
stack segment
db 128 dup (0)
stack ends
code segment
start:
mov ax,stack
mov ss,ax
mov sp,128
call init_reg
cld
call show_data
mov ax,4C00H
int 21H
init_reg:
mov bx,data
mov ds,bx
mov si,0
mov bx,0B800H
mov es,bx
mov di,160*8
add di,6
ret
show_data:
push di
push si
push ax
mov al,2
mov cx,0
s:
mov cl,ds:[si]
cmp cl,'a' ;97-112
jb s1
cmp cl,'z'
ja s1
movsb
and byte ptr es:[di-1],11011111B
mov es:[di],al
inc di
jmp s
s1:
jcxz end_reg
movsb
mov es:[di],al
inc di
jmp s
end_reg:
pop ax
pop si
pop di
ret
code ends
end start
12 内中断
12.1 内中断的产生
当CPU的内部有什么事发生的时候,将产生相应的中断信息。也叫中断源,以下是4中中断源。
- 除法错误(0),如:执行div指令产生的除法溢出;
- 单步执行(1);
- 执行into指令(4)
- 执行int指令()
这里有4中中断信息,不同的中断信息就需要不同的处理。竟然要进行不同处理CPU就需要知道中断信息的来源。所以在中断信息中必须包含识别来源的编码。
中断类型码:是一个字节型数据,可以表示256种中断信息的来源。
12.2 中断处理程序
- CPU收到中断信息后,需要对中断信息进行处理。而处理需要由我们的编程决定,也就是中断处理程序。不同的中断信息编写不同的处理程序。
- CPU收到中断信息后,去处理中断程序。然而我们去处理其他程序就需要改变CS:IP指向的位置。这就需要中断信息和其他处理程序的位置入口之间建立某种关系了,让CPU根据中断信息可以找到要指向的处理程序。
12.3 中断向量表
中断向量表就是中断处理程序入口地址的列表,存放着256个中断源所对应的中断处理程序入口。对于8086而言,中断向量表指定放在内存地址0处。从内存0000:0000~0000:3FF
的1024个单元中存放这中断向量表(这是一个规定,其中200~2FF一般是空的)。一个表项存放一个中断向量,也就是中断程序的入口地址,其中到位存放CS值、低位存放IP值,也就是一个表项占两个字(4个字节,也就是为什么是1024个单元)。
CPU只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应表项,从而得到中断处理程序的入口地址。
12.4 中断过程
CPU受到中断信息后,要对中断信息进行处理,首先将引发中断过程。硬件在完成中断过程后,CS:IP
将指向中断程序的入口,CPU开始执行中断处理程序。其中执行完中断处理程序后,应该返回原来的执行点继续执行下面的指令。这就需要将之前的CS和IP的值保存起来。在使用call指令调用子程序一样,先保存CS和IP的值然后再设置CS和IP。
下面是CPU在收到中断信息后,所引发的中断过程:
- 从中断信息中取得中断类型码;
- 标志寄存器的值入栈;(因为中断过程会改变标志寄存器的值,所以先将其保存在栈中)
- 设置标志寄存器的第8位TF和第9位IF的值为0;
- CS的内容入栈;
- IP的内存入栈;
- 中断处理程序的入口地址设置IP和CS为内存地址中断类型码4和中断类型码4+2,这两个字单元中读取。
简化过程:
- 取得中断类型码N
pushf
TF=0,IF=0
push CS
push IP
(IP)=(N*4),(CS)=(N*4+2)
12.5 iret指令
CPU随时都可能执行中断处理程序,中断处理程序的入口地址需要存储在内存某段空间中。中断处理程序的入口地址也是中断向量,必须村粗在对应的中断向量表表项中。
中断处理程序的编写和子程序相似,如下:
- 保存用到的寄存器;
- 处理中断;
- 恢复用到的寄存器;
- 用iret指令返回
iret指令:通常和硬件自动完成的中断过程配合使用。在中断过程寄存器的入栈顺序是标志寄存器、CS、IP,而iret的出栈顺序是IP、CS、标志寄存器,刚好对应。也就是是iret指令是恢复中断程序处理之前的执行点继续执行。
iret指令的过程:pop IP
pop CS
popf
。
实验
编写处理0号中断
assume cs:code,ds:data,ss:stack
code segment
start:
mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
mov cx,offset do0end-offset do0
cld
rep movsb
;设置中断向量表
mov ax,0
mov es,ax
mov word ptr es:[0*4],200h
mov word ptr es:[0*4+2],0
mov ax,1000H
mov bh,1
div bh
mov ax,4c00h
int 21h
;=============================
do0:
jmp short do0start
dodata_start:
db "overfyes!"
dodata_end:
nop
do0start:
;显示字符串"overflow!"
mov ax,cs
mov ds,ax
mov si,202h
mov ax,0b800h
mov es,ax
mov di,160*24
mov cx,offset dodata_end - offset dodata_start
s:
movsb
mov byte ptr es:[di],3
inc di
loop s
mov ax,4c00h
int 21h
do0end:
nop
code ends
end start
12.6 单步中断
在CPU执行完一条指令后,如果检测到标志寄存器的TF位为1,则产生单步中断。单步中断的类型码为1,发生单步中断的过程如下:
- 取得中断类型码1;
- 标志寄存器入栈,TF、IF设置为0(避免程序陷入死循环);
- CS、IP入栈;
- (IP)=(14),(CS)=(14+2)。
在debug每次使用t都会显示寄存器的状态,每次使用t都是一次单步中断。所以每次使用t都进行了中断然后显出东西。
12.7 响应中断的特殊情况
CPU在执行完当前指令后如果检测到中断信息,就响应中断,引发中断过程。有时候CPU在执行完当前指令后,即便是发生中断发布会响应的特殊情况。
如:在执行完SS寄存器传送技术的指令后,即使发生中断,CPU也不会响应。
指令:mov ax,1000h
mov ss,ax
mov ax,0
mov sp,0
其中ax=0没有被响应中断,但是指令有执行
原因:ss:sp
联合指向栈顶,对他们的设置是连续完成。在CPU执行完ss的指令后CPU响应中断,引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,sp并为改变,ss:sp
指向的不是正确的栈顶,这会引发错误。所以在执行完设置ss的指令后,不响应中断,给设置ss和sp的指令指向正确的栈顶提供一个机会。使得设置sp的指令紧接着设置ss的指令执行,在此之间CPU不会引发中断过程。
13 int指令
int指令的格式为:int n,n为中断类型码,它的功能是引发中断过程。执行过程如下:
- 取中断类型码n;
- 标志寄存器入栈,
IF=0 TF=0
; - CS、IP入栈;
(IP)=(n*4) (CS)=(n*4+2)
;
去执行n号中断的中断处理程序。
实验
编写一个中断列程,就是中断处理程序,类型吗为7CH。
功能:word数据的平方
参数:(ax)=要计算的数据
返回值:dx,ax中存放结果的高16位和低16位。
题目:求2*3456^2。
assume cs:code
code segment
start:
mov bx,cs
mov ds,bx
mov si,offset do_7c
mov ax,0
mov es,ax
mov di,200h
mov cx,offset do_7c_end - offset do_7c
cld
rep movsb
call break_off
mov ax,3456
int 7ch
add ax,ax
adc dx,dx
mov ax,4c00h
int 21
do_7c:
mul ax
iret
do_7c_end:
nop
break_off:
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h
mov word ptr es:[7ch*4+2],0
ret
code ends
end start
中断列程do_7c最后要使用iret指令,用汇编描述就是pop IP
pop CS
popf
,与执行中断列程的int 7ch
相对应。int指令和iret指令的配合使用与call指令和ret指令的配合使用具有相同的思路。
13.1 对int、iret和栈的深入理解
使用int 7ch
代替loop,来理解int、iret、栈之间的关系。
程序功能是在屏幕中间打出80个感叹号!,程序如下:
assume cs:code,ss:stack
stack segment
db 128 dup (0)
stack ends
code segment
start:
mov bx,stack
mov ss,bx
mov sp,128
call break
mov bx,b800h
mov es,bx
mov di,160*12
mov bx,offset se - offset s
mov cx,80
s:
mov byte ptr es:[di],'!'
add di,2
int 7ch
se:
mov ax,4c00h
int 21h
;===================================================
break:
mov bx,cs
mov ds,bx
mov bx,0
mov es,bx
mov di,200h
mov cx,offset do_7c_end - offset do_7c_start
cld
rep movsb
mov bx,0
mov es,bx
mov word ptr es:[7c*4],200h
mov word ptr es:[7c*4+2],0
ret
;===================================================
do_7c_start:
push bp
mov bp,sp
dec cx
jcxz ok
add [bp+2],bx ;改变栈中的值也就是改变出账是IP的值
ok:
pop bp
iret
do_7c_end:
nop
code ends
end start
int 7ch
将标志寄存器、cs、ip入栈;
iret
对ip、cs、标志寄存器出栈;
中对栈中对应的IP寄存器进行修改值,在出栈的时候就指向标签s了。
13.2 BIOS和DOS的中断列程
- BIOS:基本输入输出系统,是在系统板ROM中存放的一套程序,主要包含以下几部分
- 硬件系统的检测和初始化程序;
- 外部中断和内部中断的中断列程;
- 用于对硬件设备进行I/O操作的中断列程;
- 其他和硬件系统相关的中断列程;
- DOS:操作系统DOS也提供了中断列程,从操作系统的角度来看DOS的中断列程就是操作系统向程序提供编程资源。
- BIOS和DOS在所提供的中断列程中包含了许多子程序,这些子程序实现了程序员在编程的时候经常需要用到的功能。在编程的时候,可以用int指令直接调用BIOS和DOS提供的中断列程,来完成某些工作。
13.3 BIOS和DOS中断例程的安装过程
- 开机后,CPU一加电,初始化
(CS)=0FFFFH
,(IP)=0
,自动从FFFF:0
单元开始执行程序。FFFF:0
处有一条跳转指令。CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。 - 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程入口登记在中断向量表中,注意,对于BIOS所提供的中断例程,只需要将入口地址登记在中断向量表中即可,因为它们是固化到ROM中的程序,一直在内存中存在。
- 硬件系统检测和初始化完成后,调用
int 91h
进行操作系统的引导。从此将计算机交由操作系统控制。 - DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
14 端口
14.1 端口的读写
在访问端口的时候,CPU通过地址来定位端口。因为端口所在的芯片和CPU通过总线相连接,所以端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口,则端口地址的范围为0~65535。
对端口的读写不能用mov、push、pop等内存读写指令。端口的读写指令只有两条:in和out,反别用于端口读取数据和往端口写入数据。
CPU执行内存访问指令和端口访问指令时候,总线上的信息:
- 访问内存:
mov ax,ds:[8]
。执行时与总线相关的操作如下:- CPU通过地址线将地址信息8发出;
- CPU通过控制线发出内存读命令,选中存储芯片,并通过它,将要从中读取数据;
- 存储器将8号单元中的数据通过数据线送入CPU。
- 访问端口:
in al,60h
从60号端口读入一个字节,总线相关操作如下:- CPU通过地址线将地址信息60h发出;
- CPU通过控制线发出端口读命令,选中端口所在芯片,并通知它,将要从中读取数据;
- 端口所在芯片将60h端口中的数据通过数据线送入CPU。
- 注意:在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。访问8位端口时用al,访问16位端口时用ax。
- 对0~255以内的端口进行读写时:
int al,20h
从20h端口读入一个字节out 20h,al
往20h端口写入一个字节
- 对256~65535以内的端口进行读写时:
mov dx,301h
将端口号301h送入dxint al,300h
从300h端口读入一个字节out dx,al
往301h端口写入一个字节
- 对0~255以内的端口进行读写时:
14.2 CMOS RAM 芯片
PC机中,有一个CMOS RAM芯片,一般简称为CMOS。此芯片的特征如下:
- 包含一个实时钟和一个有128个存储单元的RAM存储器。
- 该芯片考电池供电。所以,关机后其内部的实钟仍可正常工作,RAM中的信息不丢失。
- 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息。
- 该芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口来读写CMOS RAM。
- 70h为地址端口,存放要访问的CMOS RAM单元地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。CPU对COMS RAM的读写分两步进行,比如,读CMOS RAM的2号单元:
- 将2送入端口70h;
- 从端口71h读出2号单元的内容。
检测点
- 编程,读取CMOS RAM的2号单元的内容。
mov al,2
out 70h,al
in al,71h
- 编程,向CMOS RAM的2号单元写入0。
mov al,2
out 70,al
mov al,0
out 71h,al
- 总结:in是出,out是入,70h是地址端口,71是数据端口
14.3 shl和shr指令
shl指令是逻辑左移指令,他的功能位:
- 将一个寄存器或内存单元中的数据向左移位;
- 将最后移出的一位写入CF中;
- 最低位用0补充。
指令:mov al,11001000B
shl al,1
执行后:(al)=10010000B
CF=1
。al向左位移一位,最低位补0。
如果移动位数大于1时,必须将位数放在cl中。
比如指令:mov al,11001000B
mov cl,3
shl al,cl
执行后:(al)=01000000B
CF=0
。因为移出的最后一位是0。
左移一位相当于执行了X=X*2
。
shr指令是逻辑右移指令,它和shl所进行的操作相反。
- 将一个寄存器或内存单元中的数据向右移位;
- 将最后移出的一位写入CF中;
- 最高位用0补充。
指令:mov al,10000001B
shr al,1
执行后:(al)=01000000B
CF=1
。右移1位,最高位补0。
逻辑右移相当于X=X/2
。
14.4 CMOS RAM 中存储的时间信息
在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度都为1个字节,存放单元位
时间单位 | 秒 | 分 | 时 | 日 | 月 | 年 |
---|---|---|---|---|---|---|
存放单元 | 0 | 2 | 4 | 7 | 8 | 9 |
这些数据以BCD码的方式存放。如下
十进制数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
BCD码 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1010 |
比如,数值26,用BCD码表示为:0010 0110。
所以,一个字节可表示两个BCD码。CMOS RAM存储时间信息的单元中用了两个BCD码表示两位的十进制数,高4位表示十位,低4位表示个位。
15 外中断
CPU在计算机系统中,除了能够执行指令进行运算以外还能对外部设备进行控制,接收他们的输入,向它们进行输出。就是CPU除了有运算能力外要有I/O能力。
15.1 外中断信息
- 可屏蔽中断
可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽看标志寄存器的IF位的设置。当CPU检测中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断。
中断过程:
1. 取中断类型码n;
2. 标志寄存器入栈,IF=0 TF=0
;
3. CS、IP入栈;
4. (IP)=(n*4) (CS)=(n*4+2)
可屏蔽中断所引发的中断过程,除在第1步的实现上有所不同外,基本上和内中断的中断过程相同。因为可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断类型码是在CPU内部产生的。
将IF设置为0,在进入中断处理程序后,禁止其他的可屏蔽中断。将IF置1,在中断处理程序中需要处理可屏蔽中断。
sti指令,设置 IF=1
cli指令,设置 IF=0
- 不可屏蔽中断
不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。
在8086CPU中不可屏蔽中断的中断类型码固定为2,实验中断过程取中断类型码的过程如下:
中断过程:
1. 标志寄存器入栈,IF=0 TF=0
;
2. CS、IP入栈;
3. (IP)=(8) (CS)=(0AH)
几乎由外设引发的外中断,都是可屏蔽中断。
Comments NOTHING