参考教程:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_bilibili
一、移位指令
1、8个移位指令
(1)逻辑左移指令SHL:SHL OPR, CNT。
①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制无符号数,向左移位相应的位数,低位补0,最后一个被移出的高位写入CF中。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(2)逻辑右移指令SHR:SHR OPR, CNT。
①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制无符号数,向右移位相应的位数,高位补0,最后一个被移出的低位写入CF中。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(3)循环左移指令ROL:ROL OPR, CNT。
①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制数,向左移位相应的位数,被移出的高位会从低位移入,最后一个被移出的高位写入CF中。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(4)循环右移指令ROR:ROR OPR, CNT。
①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制数,向右移位相应的位数,被移出的低位会从高位移入,最后一个被移出的低位写入CF中。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(5)算数左移指令SAL:SAL OPR, CNT。
①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制有符号数,向左移位相应的位数,低位补0,最后一个被移出的高位写入CF中。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(6)算数右移指令SAR:SAR OPR, CNT。
①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制有符号数,向右移位相应的位数,每移一位时高位补0或1取决于次高位是0或1(与次高位相同),最后一个被移出的低位写入CF中。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(7)带进位循环左移RCL:RCL OPR, CNT。
①OPR为操作数,CNT为左移位数,该指令将OPR视作二进制数,向左移位相应的位数,每移一位时,原高位写入CF,原CF的内容从低位移入。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
(8)带进位循环右移RCR:RCR OPR, CNT。
①OPR为操作数,CNT为右移位数,该指令将OPR视作二进制数,向右移位相应的位数,每移一位时,原低位写入CF,原CF的内容从高位移入。
②当CNT大于1时,必须将其存入寄存器CL中,以寄存器名字CL的形式给出。
2、移位指令使用示例
(1)以逻辑移位指令进行示例:将X逻辑左移一位,相当于执行X = X * 2;将X逻辑右移一位,相当于执行X = X / 2。
(2)汇编程序:
assume cs:code
code segment
main: mov al, 00000001b ;执行后(al)=00000001b=1
shl al, 1 ;执行后(al)=00000010b=2
shl al, 1 ;执行后(al)=00000100b=4
shl al, 1 ;执行后(al)=00001000b=8
mov cl, 3
shl al, cl ;执行后(al)=01000000b=64
mov cl, 2
shr al, cl ;执行后(al)=00010000b=16
mov ax, 4c00h
int 21h
code ends
end main
二、操作显存数据
1、显示的原理
(1)8086的内存空间中有这么一块显存地址空间,屏幕上的显示内容和显存地址空间中的数据一一对应。
(2)通过往显示缓冲区中写入数据,可以实现在屏幕上显示特定属性字符的效果。
2、显示缓冲区的结构
(1)显示缓冲区总共25行80列(单位为字),每个字由两个字节组成,其中低位字节存放要显示符号的ASCII码,高位字节存放要显示字符的属性。
(2)字符的显示属性由8位组成,其中0-2位为前景的RGB参数(三色参数均仅有0或1可选),3位决定是否高亮,4-6位为背景的RGB参数(三色参数均仅有0或1可选),7为决定是否闪烁。
3、举例
(1)目的:编写汇编程序,在屏幕的中间,属性为白底蓝字,显示‘Welcome to masm!’。
(2)汇编程序:
assume cs:code, ds:data
data segment
db ‘Welcome to masm!’
data ends
code segment
main: mov ax, data ;获取数据段地址
mov ds, ax ;将数据段地址送入DS中
mov ax, 0b800h ;获取显示缓冲区首地址
mov es, ax ;将显示缓冲区首地址送入ES中
mov si, 0
mov di, 160*12+80-16
mov cx, 16
w: mov al, [si]
mov es:[di], al ;将字符ASCII码载入缓冲区
inc di ;操作下一个字节
mov al, 71h
mov es:[di], al ;将字符属性载入缓冲区
inc si ;指向数据区字符串的下一个字符
inc di ;操作下一个字节
loop w
mov ax, 4c00h
int 21h
code ends
end main
三、描述内存单元的标号
1、数据标号
(1)代码段中的标号可以用来标记指令、段的起始地址,也可以用来标记数据所在的位置。如下汇编程序,其作用是将a标号处的8个字节数据累加,结果存储到b标号处的字中。
assume cs:code
code segment
a: db 1, 2, 3, 4, 5, 6, 7, 8
b: dw 0
start: mov si,offset a ;获取标号a处“数据堆”的首地址
mov bx,offset b ;获取标号b处“数据堆”的首地址
mov cx,8
s: mov al,cs:[si]
mov ah,0
add cs:[bx],ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
(2)数据标号可以把冒号去掉,此时数据标号不同于仅仅表示地址的地址标号,它同时描述内存地址和单元长度。如下汇编程序,其作用是将a标号处的8个字节数据累加,结果存储到b标号处的字中。
assume cs:code
code segment
a db 1, 2, 3, 4, 5, 6, 7, 8 ;标号a以后的内存单元最小单位都是字节
b dw 0 ;标号b以后的内存单元最小单位都是字
start: mov si,0
mov cx,8
s: mov al,a[si] ;(al) = (cs * 16 + a + si)
mov ah,0
add b,ax ;(cs * 16 + b) = (ax)
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
2、数据的直接定址表
(1)数据标号除了可用于标识代码段中的数据以外,还可以用于标识数据段中的数据。如下汇编程序,其作用是将a标号处的8个字节数据累加,结果存储到b标号处的字中。
assume cs:code, ds:data
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8 ;标号a以后的内存单元最小单位都是字节
b dw 0 ;标号b以后的内存单元最小单位都是字
data ends
code segment
start: mov ax, data
mov ds, ax
mov si,0
mov cx,8
s: mov al,a[si] ;(al) = (ds * 16 + a + si)
mov ah,0
add b,ax ;(ds * 16 + b) = (ax)
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
(2)标号可以当作数据定义,如下所示。
assume cs:code, ds:data
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
c dw offset a, seg a, offset b, seg b
data ends
code segment
start: mov ax, data
mov ds, ax
mov si,0
mov cx,8
s: mov al,a[si] ;(al) = (ds * 16 + a + si)
mov ah,0
add b,ax ;(ds * 16 + b) = (ax)
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
(3)鉴于标号可以当作数据定义,不妨尝试给若干组数据用标号标识,把这些标号全部搁一起,当作一组数据定义,这样就能得到一个数据直接定址表,换句话说,利用数据直接定址表可在两个数据集合之间建立一种映射关系,用查表的方法根据给出的数据得到其在另一集合中的对应数据。
(4)举例:编写程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果。
①解决方案:空间换时间,将所要计算的sin(x) 的结果都存储到一张表中,然后用角度值来查表,找到对应的sin(x)的值,并显示在屏幕上。
②汇编程序:
assume cs:code
code segment
start: mov al,60 ;用ax向子程序传递角度值
call showsin
mov ax,4c00h
int 21h
showsin: jmp short show ;转移至子函数下一条代码处
table dw ag0, ag30, ag60, ag90, ag120, ag150, ag180
ag0 db '0' ,0 ;sin(0)对应的字符串'0'
ag30 db '0.5' ,0 ;sin(30)对应的字符串'0.5'
ag60 db '0.866', 0 ;sin(60)对应的字符串'0.866'
ag90 db '1' ,0 ;sin(90)对应的字符串'1'
ag120 db '0.866' ,0 ;sin(120)对应的字符串'0.866'
ag150 db '0.5', 0 ;sin(150)对应的字符串'0.5'
ag180 db '0', 0 ;sin(180)对应的字符串'0'
show: push bx
push es
push si
mov bx, 0b800h
mov es, bx
mov ah, 0
mov bl, 30
div bl ;用角度值/30作为相对于table的偏移量
mov bl, al
mov bh,0
add bx, bx ;注意table与其它标号描述的内存单元大小
mov bx, table[bx] ;取得对应的字符串的偏移地址,放在bx中
mov si, 160*12+40*2
shows: mov ah, cs:[bx]
cmp ah, 0
je showret
mov es:[si],ah
inc bx
add si,2
jmp shows
showret: pop si
pop es
pop bx
ret
code ends
end start
3、代码的直接定址表
(1)除了数据有直接定址表以外,代码也可以有直接定址表,其实现思路是将若干个功能写成相应的若干个子程序,将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应,对应关系为“功能号 * 2 = 对应的功能子程序在地址表中的偏移”。
(2)举例:
①目标:实现一个子程序setscreen,为显示输出提供如下功能。
[1]清屏。
[2]设置前景色。
[3]设置背景色。
[4]向上滚动一行
②子程序入口参数说明:
[1]用AH寄存器传递功能号,0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行。
[2]对2、3号功能,用AL传送颜色值,(al)∈{0, 1, 2, 3, 4, 5, 6, 7 }。
③各个功能的子程序实现:
[1]清屏:将显存中当前屏幕中的字符设为空格符。
sub1:
push bx
push cx
push es
mov bx, 0b800h
mov es, bx
mov bx, 0
mov cx, 2000
sub1s:
mov byte ptr es:[bx], ' '
add bx, 2
loop sub1s
pop es
pop cx
pop bx
ret ;sub1结束
[2]设置前景色:设置显存中奇地址的属性字节的第0、1、2位。
sub2:
push bx
push cx
push es
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
sub2s:
and byte ptr es:[bx], 11111000b
or es:[bx], al
add bx, 2
loop sub2s
pop es
pop cx
pop bx
ret ;sub2结束
[3]设置背景色:设置显存中奇地址的属性字节的第4、5、6位。
sub3:
push bx
push cx
push es
mov cl, 4
shl al, cl
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000
sub3s:
and byte ptr es:[bx],10001111b
or es:[bx], al
add bx, 2
loop sub3s
pop es
pop cx
pop bx
ret ; sub3结束
[4]向上滚动一行:依次将第n+1行的内容复制到第n行处,并清空最后一行。
sub4:
push cx
push si
push di
push es
push ds
mov si, 0b800h
mov es, si
mov ds, si
mov si,160 ;ds:si指向第n+1行
mov di, 0 ;es:di指向第n行
cld
mov cx, 24 ;共复制24行
sub4s:
push cx
mov cx, 160
rep movsb ;复制1行
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si], ' ' ;清空最后一行
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret ;sub4结束
④主程序与setscreen子程序:
assume cs:code
code segment
start:
mov ah, 2
mov al, 5
call setscreen
mov ax, 4c00h
int 21h
setscreen: ;要在其中再加入新功能,只需要在地址表中加入它的入口地址即可
jmp short set
table dw sub1,sub2,sub3,sub4 ;地址表
set:
push bx
cmp ah,3
ja sret
mov bl,ah
mov bh,0
add bx,bx
call word ptr table[bx] ;根据bx中的功能号索引相应的标号,执行其子程序
sret:
pop bx
ret
;4个功能的子程序放在此处
code ends
end start
四、中断及其处理
1、中断的概念与分类
(1)中断是指CPU不再接着(刚执行完的指令)向下执行,而是转去处理中断信息。
(2)中断的分类:
①内中断:由CPU内部发生的事件而引起的中断。
②外中断:由外部设备发生的事件引起的中断。
2、8086的内中断
(1)CPU内部产生的中断信息:
①除法错误,比如执行DIV指令时产生除法溢出(除0错误)。
②单步执行中断。
③INTO命令。
④INT命令。
(2)8086的中断类型码:
①除法错误:0。
②单步执行中断:1。
③INTO命令:4。
④INT <立即数n>命令:立即数n。
3、中断处理程序
(1)CPU处理中断信息,本质上就是执行中断处理程序。
(2)中断向量表:由中断类型码可查表得到中断处理程序的入口地址(低字节存放IP-偏移地址,高字节存放CS-代码段地址),从而定位中断处理程序。((IP) = (N*4),(CS) = (N*4+2),N为中断类型码)
(3)举例:触发系统的0号中断,CPU会根据中断类型码在中断向量表中找到中断处理程序的入口地址,并根据入口地址设置CS寄存器与IP寄存器,将转至中断服务程序执行。
五、编制中断处理程序
1、中断处理程序及其结构
(1)CPU随时都可能检测到中断信息,所以中断处理程序必须常驻内存(一直存储在内存某段空间之中),中断处理程序的入口地址,也即中断向量,必须存储在对应的中断向量表表项中(0000H:0000H-0000H:03FFH)。
(2)触发并进入中断处理程序的过程:
①取得中断类型码N。
②pushf —— 标志寄存器内容入栈(保存标志寄存器)。
③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发。
④push CS —— 保存原程序断点。
⑤push IP —— 保存原程序断点。
⑥(IP) = (N*4)、(CS) = (N*4+2) —— 转移至N号中断的中断服务程序。
2、编制中断处理程序——以除法错误中断为例
(1)预期效果:编写一个0号中断处理程序do0,它的功能是在屏幕中间显示“overflow!”后,返回到操作系统。
(2)准备工作:
①do0子程序应该存放在内存的确定位置,并且要重新找个地方,不破坏系统,可利用中断向量表中的空闲单元来存放我们的程序。经过估计,do0的长度不可能超过256个字节,就选用从0000:0200至0000:02FF的256个字节的空间。
②0号中断处理程序要有新的入口地址(需说明,实际应用中不要随便自己改写中断处理程序)。
(3)程序框架梳理:
①编写可以显示“overflow!”的中断处理程序do0。
②将do0送入内存0000H:0200H处(安装程序)。
③将do0中断处理程序的入口地址0000H:0200H存储在中断向量表0号表项中。
(4)汇编程序:
assume cs:code
code segment
start:
;安装程序do0
mov ax, cs
mov ds, ax ;do0的段地址送入DS中
mov si, offset do0 ;获取do0的偏移地址
mov ax, 0
mov es, ax
mov di, 200h ;ES:DI指向0000H:0200H
mov cx, offset do0end - offset do0 ;获取do0程序所占用字节数
cld
rep movsb ;将do0下的内容送入内存0000H:0200H处
;设置中断向量表
mov ax, 0
mov es, ax
mov word ptr es:[0*4], 200h
mov word ptr es:[0*4+2], 0
mov ax,4c00h
int 21h
do0:
jmp short do0start
db ‘overflow!’
do0start:
mov ax, cs
mov ds, ax
mov si, 202h
mov ax, 0b800h
mov es, ax
mov di, 12*160+36*2
mov cx, 9
s:
mov al, [si]
mov es:[di], al
inc si
add di, 2
loop s
mov ax, 4c00h
int 21h
do0end: nop
code ends
end start
(5)测试:运行上面的程序,改变中断向量,然后执行DIV指令,除数为0,触发除0错误,观察屏幕现象。
六、单步中断
1、Debug的T命令回顾
(1)Debug利用了CPU提供的单步中断的功能,使用T命令时,Debug会将TF标志设为1,使CPU工作在单步中断方式下。
(2)每使用一次T命令,Debug就会执行一条指令,并显示寄存器中的内容和下一条需要执行的指令(CS:IP指向该条指令)。
2、单步中断处理过程
(1)两个和中断相关的寄存器标志位:
①TF-陷阱标志(Trap flag):当TF=1时,每条指令执行完后产生陷阱,由系统控制计算机;当TF=0时,CPU正常工作,不产生陷阱。(用于调试时的单步方式操作)
②IF-中断标志(Interrupt flag):当IF=1时,允许CPU响应可屏蔽中断请求;当IF=0时,关闭中断。
(2)CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断(中断类型码为1),引发中断过程,执行中断处理程序。
(3)进入中断处理程序时需要将TF置为0,这是因为中断处理程序也由一条条指令组成的,如果在执行中断处理程序之前TF=1,则CPU在执行完中断处理程序的第一条指令后又要产生单步中断,转去执行单步中断的中断处理程序的第一条指令,以此往复,将陷入一个永远不能结束的循环,CPU永远执行单步中断处理程序的第一条指令,所以在进入中断处理程序之前,需要设置TF=0。
(4)一般情况下,CPU在执行完当前指令后,如果检测到中断信息就响应中断,引发中断过程。不过在有些情况下,CPU 在执行完当前指令后,即便是发生中断,也不会响应,如在执行完向SS寄存器传送数据的指令后,即便是发生中断,CPU也不会响应,这是因为SS:SP联合指向栈顶,而对它们的设置应该连续完成(实际上如果不连续设置SS和SP,编译阶段也不会报错,但编程时应养成良好的习惯),以此保证对栈的正确操作。
七、由INT指令引发的中断
1、INT指令介绍
(1)格式:INT <立即数n>。(n为中断类型码)
(2)INT指令可无条件引发任何中断过程,CPU执行“int n”指令,相当于引发一个n号中断的中断过程,执行过程如下:
①取得中断类型码N。
②pushf —— 标志寄存器内容入栈(保存标志寄存器)。
③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发。
④push CS —— 保存原程序断点。
⑤push IP —— 保存原程序断点。
⑥(IP) = (N*4)、(CS) = (N*4+2) —— 转移至N号中断的中断服务程序。
(3)一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。
2、编写供应用程序调用的中断例程
(1)编程时,可以用INT指令调用子程序,此子程序即中断处理程序,简称为中断例程(与一般的子程序一样,需注意保存现场和恢复现场)。可以自定义中断例程,实现特定功能。
(2)举例:写7ch号中断的中断例程,完成特定任务。
①目标:求一个word型数据的平方,用AX进行参数传递,DX、AX中分别存放结果的高16位、低16位。
②任务分解:
[1]编程实现求平方功能的程序。
[2]安装程序,将其安装在0000H:0200H处。
[3]设置中断向量表,将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程。
③知识补充:IRET指令常用于中断处理函数结尾处,它相当于指令“pop ip”、“pop cs”、“popf”(标志寄存器内容出栈)。
④安装中断例程的汇编程序:
assume cs:code
code segment
start: mov ax, cs
mov ds, ax
mov si, offset sqr
mov ax ,0
mov es, ax
mov di, 200h
mov cx, offset sqrend - offset sqr
cld
rep movsb
mov ax, 0
mov es, ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
mov ax,4c00h
int 21h
sqr: mul ax
iret
sqrend: nop
code ends
end start
⑤测试使用的汇编程序:
assume cs:code
code segment
start: mov ax,3456
int 7ch ;引发7ch号中断,计算(ax)^2
add ax,ax
adc dx, dx
mov ax,4c00h
int 21h
code ends
end start
八、BIOS和DOS中断处理
1、BIOS——基本输入输出系统
(1)BIOS是在系统板的ROM中存放着的一套程序,容量为8KB,从FE000H开始。
(2)BIOS中的主要内容:
①硬件系统的检测和初始化程序。
②外部中断和内部中断的中断例程。
③用于对硬件设备进行I/O操作的中断例程。
④其它和硬件系统相关的中断例程。
(3)使用BIOS功能调用,程序员不用了解硬件操作细节,直接使用指令设置参数,并中断调用BIOS例程,即可完成相关工作。
(4)BIOS具体有哪些功能可查找BIOS中断手册,里面有详细的介绍,这里不再赘述。
2、DOS中断
(1)通过执行指令“int 21”,可引发DOS中断类,和硬件设备相关的DOS中断例程中,一般都调用BIOS的中断例程。
(2)BIOS和DOS在所提供的中断例程中包含了许多子程序,这些子程序实现了程序员在编程的时常用到的功能。
3、BIOS和DOS中断例程的安装过程
(1)CPU一上电,初始化(CS)=0FFFFH,(IP)=0,自动从FFFFH:0000H单元开始执行程序。FFFFH:0000H处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
(2)初始化程序将建立BIOS 所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
(3)硬件系统检测和初始化完成后,调用“int 19h”进行操作系统的引导,从此将计算机交由操作系统控制。
(4)DOS启动后,除完成其它工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
九、端口的读写
1、IN指令与OUT指令
(1)CPU可以直接读写3个地方的数据——CPU内部的寄存器、内存单元、端口,从CPU角度,可以将各寄存器当作端口并统一编址,CPU用统一的方法与各种设备通信。
(2)读写端口需要用专门的指令IN和OUT,IN指令用于CPU从端口读取数据,OUT用于CPU往端口写入数据。
(3)“IN <寄存器> <端口地址>”执行的操作是将端口地址(可以其它形式给出,如存储在寄存器中)中的数据读入CPU相应的寄存器中,执行该指令时总线有如下相关操作:
①CPU通过地址线将端口地址信息发出。
②CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知要从中读取数据。
③端口所在的芯片将端口中的数据通过数据总线送入CPU。
(4)“OUT <端口地址> <寄存器>”执行的操作是将CPU相应的寄存器中的数据写入端口地址(可以其它形式给出,如存储在寄存器中)对应的空间中,执行该指令时总线有如下相关操作:
①CPU通过地址线将端口地址信息发出。
②CPU通过控制线发出端口写命令,选中端口所在的芯片,并通知要往里面写入数据。
③CPU通过数据总线将数据送入端口所在的芯片的端口中。
2、8086的I/O端口分配
3、用端口访问外设举例
(1)61h端口地址的设备控制寄存器功能如下所示:
(2)汇编程序:
assume cs:codeseg
codeseg segment
start: mov al, 08h ;设置声音的频率
out 42h, al
out 42h, al
in al, 61h ;读设备控制器端口原值
mov ah, al ;保存原值
or al, 3 ;打开扬声器和定时器
out 61h, al ;接通扬声器,发声
mov cx, 60000 ;延时
delay: nop
loop delay
mov al, ah
out 61h, al ;恢复端口原值
mov ax, 4c00h
int 21h
codeseg ends
end start
十、操作CMOS RAM芯片
1、CMOS RAM芯片介绍
(1)包含一个实时钟和一个有128个存储单元的RAM存储器。
(2)128个字节的RAM中存储:内部实时钟、系统配置信息、相关的程序(用于开机时配置系统信息)。
(3)CMOS RAM 芯片靠电池供电,关机后其内部的实时钟仍可正常工作,RAM中的信息不丢失。
(4)该芯片内部有两个端口,端口地址为70h和71h,CPU通过这两个端口可以读写CMOS RAM。
①70h地址端口存放要访问的CMOS RAM单元的地址。
②71h数据端口存放从选定的单元中读取的数据,或要写入到其中的数据。
2、举例——提取CMOS RAM中存储的月份信息
(1)背景知识:CMOS RAM中以BCD码的形式存储时间信息,其中月份信息存储在8号单元中,具体内容分布如下所示。
(2)任务分解:
①从CMOS RAM的8号单元读出当前月份的BCD码。
②将用BCD码表示的月份以十进制的形式显示到屏幕上。
(3)汇编程序:
assume cs:code
code segment
start: mov al, 8
out 70h, al ;存放要访问的CMOS RAM单元的地址(8号单元)
in al, 71h ;将其中的月份信息读入AL
mov ah, al
mov cl, 4
shr ah, cl
and al, 00001111b
add ah, 30h
add al, 30h
mov bx, 0b800h
mov es, bx
mov byte ptr es:[160*12+40*2], ah
mov byte ptr es:[160*12+40*2+2], al
mov ax, 4c00h
int 21h
code ends
end start
十一、外设连接与中断
1、由外部设备发生的事件引起的中断(外中断)
(1)可屏蔽中断与不可屏蔽中断:
①可屏蔽中断是CPU 可以不响应的外中断,CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置,当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程,如果IF=0,则不响应可屏蔽中断。
②不可屏蔽中断是CPU必须响应的外中断,当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后立即响应,引发中断过程。对于8086CPU,不可屏蔽中断的中断类型码固定为2。
(2)几乎所有由外设引发的外中断都是可屏蔽中断,比如键盘输入、打印机请求;不可屏蔽中断在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。
(3)CPU在执行指令过程中,可以检测到外设发送过来的中断信息,引发中断过程,处理外设的输入。
2、中断的处理过程
(1)可屏蔽中断所引发的中断过程:
①取中断类型码n(可屏蔽中断信息来自于CPU外部,中断类型码通过数据总线送入CPU)。
②pushf —— 标志寄存器内容入栈(保存标志寄存器)。
③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发,并禁止其它可屏蔽中断(如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1)。
④push CS —— 保存原程序断点。
⑤push IP —— 保存原程序断点。
⑥(IP) = (N*4)、(CS) = (N*4+2) —— 转移至N号中断的中断服务程序。
(2)不可屏蔽中断的中断过程:
①中断值固定为2,不必取中断码。
②pushf —— 标志寄存器内容入栈(保存标志寄存器)。
③TF = 0,IF = 0 —— 防止非预期的中断嵌套触发,并禁止其它可屏蔽中断(如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1)。
④push CS —— 保存原程序断点。
⑤push IP —— 保存原程序断点。
⑥(IP)=(8)、(CS)=(0AH)。
3、STI和CLI指令
(1)STI指令无操作数,它用于设置IF=1。
(2)CLI指令无操作数,它用于设置IF=0。
十二、PC机键盘的处理过程
1、第一步——键盘输入
(1)键盘上的每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。
(2)按下一个键时的操作:
①开关接通,该芯片产生一个扫描码,扫描码说明了按下的键在键盘上的位置。
②扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60H。
(3)松开按下的键时的操作:
①产生一个扫描码,扫描码说明了松开的键在键盘上的位置。
②松开按键时产生的扫描码也被送入60H端口中。
(4)扫描码——长度为一个字节的编码:
①按下一个键时产生的扫描码——通码,通码的第7位为0。
②松开一个键时产生的扫描码——断码,断码的第7位为1。
③通码 + 80H = 断码。
2、第二步——引发9号中断
(1)键盘的输入到达60H端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息,CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。
(2)BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9中断例程所接收的键盘输入的内存区,可以存储15 个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
(3)若输入了控制键或切换键,将会修改键盘状态字节,其地址为0040H:0017H,具体定义如下。
3、第三步——执行int 9中断例程
(1)读出60H端口中的扫描码。
(2)根据扫描码分情况对待:
①如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区。
②如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元。
(3)对键盘系统进行相关的控制,如向相关芯片发出应答信息。
4、按照开发需求定制键盘输入处理举例
(1)需求分解:
①在屏幕中间依次显示字母'a'~'z',并可以让人眼看清。
②在显示的过程中,按下Esc键后,改变显示的颜色。
(2)策略说明:
①尽可能忽略硬件处理细节,充分利用BIOS提供的int 9中断例程对这些硬件细节进行处理,由此可以自编int 9中断,自编的中断处理程序在实现需求之余还需调用原来的int 9中断例程,并且要将中断向量表中的int 9中断例程的入口地址改为自编的中断处理程序的入口地址,在新中断处理程序中调用原来的int 9中断例程,还需要是原来的int 9中断例程的地址,这样,按下按键引发int 9中断就能执行定制的中断例程,并且不需要程序员考虑硬件处理细节。
②这个开发需求可能并不是全生命周期都需要的,所以,相关功能处理完以后需要将中断向量表还原为原来的内容,后续按下按键时CPU还是调用原来的int 9中断。
(3)汇编程序分步实现:
①依次显示'a'~'z',并可以让人眼看清,这就需要在字母切换的过程中加塞一堆“无用指令”。
assume cs:code
stack segment
db 128 dup (0)
stack ends
code segment
start: mov ax, stack
mov ss, ax
mov sp, 128
mov ax, 0b800h
mov es, ax
mov ah, 'a'
s: mov es:[160*12+40*2], ah ;显示字符
call delay ;调用延时程序
inc ah
cmp ah, 'z'
jna s
mov ax,4c00h
int 21h
delay: push ax ;延时程序没有什么实质操作
push dx ;纯浪费CPU算力
mov dx, 10h
mov ax, 0
s1: sub ax, 1
sbb dx ,0
cmp ax, 0
jne s1
cmp dx, 0
jne s1
pop dx
pop ax
ret
code ends
end start
②实现按下Esc键后改变显示的颜色。
assume cs:code
stack segment
db 128 dup (0)
stack ends
data segment
dw 0,0
data ends
code segment
start: mov ax, stack
mov ss, ax
mov sp, 128
mov ax, data
mov ds, ax
;更改中断例程入口地址
mov ax, 0
mov es, ax
push es:[9*4] ;保存旧中断例程入口
pop ds:[0]
push es:[9*4+2]
pop ds:[2]
mov word ptr es:[9*4], offset int9 ;设置新中断例程入口
mov es:[9*4+2], cs
;显示字母
mov ax, 0b800h
mov es, ax
mov ah, 'a'
s: mov es:[160*12+40*2], ah ;显示字符
call delay ;调用延时函数
inc ah
cmp ah, 'z'
jna s
;恢复原来中断例程的入口地址
mov ax, 0
mov es, ax
push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2]
mov ax,4c00h
int 21h
;定义延时程序
delay: push ax
push dx
mov dx, 10h
mov ax, 0
s1: sub ax, 1
sbb dx ,0
cmp ax, 0
jne s1
cmp dx, 0
jne s1
pop dx
pop ax
ret
;定义中断例程
int9: push ax
push bx
push es
in al, 60h ;从60h端口读出键盘的输入
pushf
pushf
pop bx
and bh, 11111100b
push bx
popf
call dword ptr ds:[0] ;调用原int 9指令功能
cmp al, 1 ;判断是否为Esc的扫描码,是则改变显示的颜色
jne int9ret
mov ax, 0b800h
mov es, ax
inc byte ptr es:[160*12+40*2+1]
int9ret:pop es
pop bx
pop ax
iret
code ends
end start
十三、用中断响应外设(以键盘为例)
1、键盘相关的中断
(1)硬件中断int 9h:
由键盘上按下或松开一个键时,如果中断是允许的,就会产生int 9h中断,并转到BIOS的键盘中断处理程序。
(2)BIOS中断int 16h:
BIOS中断提供基本的键盘操作,引发该中断时,执行何种功能取决于此时AH中的内容,也即功能号(AH):
00H、10H —从键盘读入字符
01H、11H —读取键盘状态
02H、12H —读取键盘标志
03H —设置重复率
04H —设置键盘点击
05H —字符及其扫描码进栈
(3)DOS中断int 21h:
DOS中断提供丰富、便捷的功能调用,执行何种功能取决于此时AH中的内容,也即功能号(AH):
01H —从键盘输入一个字符并回显
06H —读键盘字符
07H —从键盘输入一个字符不回显
08H —从键盘输入一个字符,不回显,检测CTRL-Break
0AH — 输入字符到指定地址的缓冲区
0BH — 读键盘状态
0CH — 清除键盘缓冲区,并调用一种键盘功能
2、对键盘输入的处理的int 9h中断和int 16h中断
(1)int 9h中断:
①BIOS提供了int 9中断例程,键盘输入将引发9号中断,9号中断会将键盘的输入存入缓冲区或改变状态字。
②int 9中断例程从60h端口读出扫描码,并将其转化为相应的ASCII码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。
③键盘缓冲区中有16个字单元,可以存储15个按键的扫描码和对应的入ASCII码。
(2)int 16h中断:
①BIOS提供了int 16h中断例程供程序员调用,以完成键盘的各种操作。
②举例:当(AH) = 0时,CPU检测键盘缓冲区中是否有数据,无则重复检测,有则从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中删除,将读取的扫描码送入AH,ASCII码送入AL。
(3)B1OS 的int 9 中断例程和int 16h中断例程是一对相互配合的程序,int 9中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区中读出。它们写入和读出的时机不同,int 9中断例程在有键按下的时候向键盘缓冲区中写入数据,而int 16h中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。
3、调用int 16h从键盘缓冲区中读取键盘的输入举例
(1)例1:
①目标:接收用户的键盘输入,输入“r”将屏幕上的字符设置为红色,输入“g”将屏幕上的字符设置为绿色,输入“b”将屏幕上的字符设置为蓝色。
②汇编程序:
assume cs:code
stack segment
db 128 dup (0)
stack ends
code segment
start: mov ah,0
int 16h ;调用中断,等待输入
;识别按键并进行相应跳转
mov ah, 1
cmp al, 'r'
je red
cmp al, 'g'
je green
cmp al, 'b'
je blue
jmp short sret
;设置屏幕颜色
red: shl ah, 1
green: shl ah, 1
blue: mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 2000 ;需要修改整个显示缓冲区的字符属性
s: and byte ptr es:[bx], 11111000b
or es:[bx], ah ;设置属性字节前3位
add bx, 2
loop s
sret: mov ax,4c00h
int 21h
code ends
end start
(2)例2:
①设计一个最基本的字符串输入程序,需要具备下面的功能:
[1]在输入的同时需要显示这个字符串。
[2]一般在输入回车符后,字符串输入结束。
[3]能够用退格键删除已经输入的字符。
②逻辑抽象为计算机语言:
[1]用栈的方式来管理字符串的存储空间,DS:DI指向字符串的存储空间,字符串以’\0’为结尾符。
[2]输入回车符后 ,在字符串中加入’\0’,表示字符串结束。
[3]每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串,即从字符栈的栈底到栈顶,显示所有的字符((dh)、(dl) = 字符串在屏幕上显示的行、列位置)。
③汇编程序:
[1]字符输入后的判断及处理:
assume cs:code, ds:data
data segment
db 32 dup (?) ;字符串的“栈”空间
data ends
code segment
start: mov ax, data
mov ds, ax
mov si, 0
mov dh, 12
mov dl, 20
call getstr
return: mov ax, 4c00h
int 21h
getstr: push ax
getstrs:mov ah, 0
int 16h ;调用int 16h从键盘缓冲区中读取1个字符
cmp al, 20h
jb nochar ;ASCII码小于20h的为非字符,转去处理
mov ah, 0 ;AH存放charstack程序功能号
call charstack ;AL中的字符入栈,显示栈中的字符
jmp getstrs
nochar: ;处理非字符
cmp ah, 0eh ;退格键的扫描码
je backspace
cmp ah, 1ch ;回车键的扫描码
je enter
jmp getstrs
backspace: ;退格
mov ah, 1 ;AH存放charstack程序功能号
call charstack ;字符出栈,显示栈中的字符
jmp getstrs
enter: ;回车
mov al, 0
mov ah, 0 ;AH存放charstack程序功能号
call charstack ;’\0’字符入栈,显示栈中的字符
pop ax
ret
code ends
end start
[2]字符栈的入栈、出栈和显示功能子程序:
charstack:
jmp short charstart
table dw charpush, charpop, charshow
top dw 0 ;栈顶指针
charstart:
push bx
push dx
push di
push es
cmp ah, 2
ja sret
mov bl, ah
mov bh, 0
add bx, bx
jmp word ptr table[bx] ;根据功能号执行相应功能
charpush: ;功能号0,AL中的字符入栈
mov bx, top
mov [si][bx], al
inc top ;栈顶指针自增
jmp sret
charpop: ;功能号1,字符出栈到AL中
cmp top, 0
je sret
dec top ;栈顶指针自减
mov bx, top
mov al, [si][bx]
jmp sret
charshow: ;功能号2,显示字符串
mov bx, 0b800h
mov es, bx
mov al,160
mov ah, 0
mul dh
mov di, ax
add dl, dl
mov dh, 0
add di, dx
mov bx, 0
charshows:
cmp bx, top
jne noempty
mov byte ptr es:[di], ' '
jmp sret
noempty:mov al, [si][bx]
mov es:[di], al
mov byte ptr es:[di+2], ' '
inc bx
add di, 2
jmp charshows
sret: pop es
pop di
pop dx
pop bx
ret
十四、读写磁盘
1、BIOS提供的磁盘直接服务——int 13h
2、用BIOS int 13h对磁盘进行读操作
(1)入口参数:
①(ah) = 2(2表示读扇区)。
②(al) = 读取的扇区数。
③(ch) = 磁道号,(cl) = 扇区号。
④(dh) = 磁头号(对于软盘即面号,一个面用一个磁头来读写)。
⑤(dl) = 驱动器号。软驱从0开始,0——软驱A,1——软驱B;硬盘从80h开始,80h——硬盘C,81h——硬盘D。
⑥ES:BX指向接收从扇区读入数据的内存区。
(2)返回参数:
①操作成功:(ah) = 0,(al) = 读入的扇区数。
②操作失败:(ah) = 出错代码。
3、用BIOS int 13h对磁盘进行写操作
(1)入口参数:
①(ah) = 3(3表示读扇区)。
②(al) = 写入的扇区数。
③(ch) = 磁道号,(cl) = 扇区号。
④(dh) = 磁头号(对于软盘即面号,一个面用一个磁头来读写)。
⑤(dl) = 驱动器号。软驱从0开始,0——软驱A,1——软驱B;硬盘从80h开始,80h——硬盘C,81h——硬盘D。
⑥ES:BX指向将写入磁盘的数据。
(2)返回参数:
①操作成功:(ah) = 0,(al) = 写入的扇区数。
②操作失败:(ah) = 出错代码。