看下面一段程序:计算data中第一行的数的立方存在第二行
assume cs:code 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 si,0 mov di,16 mov cs,8 s:mov bx,[si] call cube mov [di],ax mov [di].2,dx add si,2 add di,4 loop s mov ax,4c00h int 21h cube:mov ax,bx mul bx mul bx ret code ends end start
寄存器冲突
观察下面将data中的数据全转化为大写的代码:
assume cs:code data segment db 'word',0 db 'unix',0 db 'wind',0 db 'good',0 data ends code segment start:mov ax,data mov ds,ax mov bx,0 mov cx,4 s:mov si,bx call capital add bx,5 loop s mov ax,4c00h int 21h capital:mov cl,[si] mov ch,0 jcxz ok and byte ptr [si],11011111b inc si jmp short capital ok:ret code ends end start
这段代码有一个问题出在,主函数部分使用cx设置循环次数4次,在循环中调用了子函数,而子函数中有一个判断语句jcxz也是用了cx,并且在之前修改了cx的值,造成逻辑错误。虽然修改的方法有很多,但我们应遵循以下的标准:
编写调用子程序的程序不必关心子程序使用了什么寄存器
编写子程序不用关心调用子程序的程序使用了什么寄存器
不会发生寄存器冲突
针对这三点,我们可以如下修改代码:
··· capital: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 ···
虽然和上面的程序中没有冲突的是si,但我们保险起见,在子程序开始时将子程序用到的所有的寄存器的内容存入栈中,在返回之前在出栈回到相应寄存器中。这样无论调用子程序的程序使用了什么寄存器,都不会产生寄存器冲突。
标志寄存器
标志寄存器
CPU中有一种特殊的寄存器——标志寄存器(不同CPU中的个数和结构都可能不同),主要有以下三种作用:
1.存储相关指令的某些执行结果
2.为CPU执行相关质量提供行为依据
3.控制CPU相关工作方式
8086CPU中的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW),标志寄存器以下简称为flag。标志位如下:
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
如上所示,1,3,5,12,13,14,15位没有使用,没有任何意义,而其他几位都有不同的含义。
ZF标志
ZF位于flag第6位,零标志位,功能是记录相关指令执行后结果是否为0,如果结果为0,则ZF=1,否则ZF=0。如:
mov ax,1
sub ax,1
执行后结果为0,ZF=1。一般情况下,运算指令(如add,sub,mul,div,inc,or,and)影响标志寄存器,而传送指令(如mov,push,pop)不影响标志寄存器。
PF标志
flag的第2位是PF标志位,奇偶标志位,功能是记录相关指令执行后,其结果的所有bit中1的个数是否为偶数,若1的个数是偶数,pf=1,如果是奇数,fp=0。如:
mov al,1
add al,10
执行后结果为00001011b,有3个1,所以PF=0。
SF标志
flag的第7位是SF标志位,符号标志位,它记录相关指令执行后,结果是否为负,如果结果为负,则sf=1,结果为正,sf=0。计算机中通常用补码表示数据,一个数可以看成有符号数或无符号数,如:
00000001B,可以看成无符号1或有符号+1
10000001B,可以看成无符号129或有符号-127
也就是说对于同一个数字,可以当做有符号数运算也可以当做无符号数运算。如:
mov al,10000001b
add al,1
这段代码结果是(al)=10000010b,可以将add指令进行的运算当做无符号运算,那么相当于129+1=130,也可以当做有符号运算,相当于-127+1=-126。SF标志就是在进行有符号运算的时候记录结果的符号的,当进行无符号运算的时候SF无意义(但还会影响SF,只是对我们来说没有意义了)。
CF标志
flag的第0位是CF标志位,进位标志位,一般情况下载进行无符号运算时,他记录了运算结果的最高有效为向更高为的进位值,或从更高位的借位值。加入一个无符号数据是8位的,也就是0-7个位,那么在做加法的时候就可能造成进位到第8位,这时并不是丢弃这个进位,而是记录在falg的CF位上。如:
mov al,98h
add al,al
执行后al=30h,CF=1。当两个数据做减法的时候有可能向更高位借位,如97h-98h借位后相当于197h-198h,CF也可以用来记录借位,如:
mov al,98h
add al,al
执行后(al)=FFH,CF=1记录了向更高位借位的信息。
OF标志
在进行有符号运算的时候,如果结果超过了机器能表示的范围称为“溢出”。机器能表示的范围是指如8位寄存器存放或一个内存单元存放,表示范围就是-128~127,16位同理。如果超出了这个范围就叫做溢出,如:
mov al,98 add al,99 mov al,0F0H add al,088H
第一段代码(al)=(al)+99=98+99=197超过了8位能表示的有符号数的范围,第二段代码结果(al)=(al)+(-120)=(-16)+(-12-)=-136也超过了8位有符号的范围,所以计算的结果是不可信的。如第一段代码计算之后(al)=0C5H,换成补码表示的是-59,98+99=-59很明显是不正确的结果。
flag的第11位是OF标志位,溢出标志位,一般情况下,OF记录有符号数运算结果是否溢出,如果溢出则OF=1,如果没有溢出,OF=0。所以CF是对无符号数的标志,OF是对有符号的标志。但对于一个运算指令,他们是同时生效的,只不过这个指令究竟是有符号还是无符号,是看实际的操作的。有符号CF无意义,无符号OF无意义。
adc指令
adc是带进位加法指令,利用了CF标志位上记录的进位值。格式:adc 操作对象1,操作对象2。功能:操作对象1=操作对象1+操作对象2+CF。如abc ax,bx实现的是(ax)=(ax)+(bx)+CF,如:
mov ax,2 mov bx,1 sub bx,ax adc ax,1
注意这段代码,首先ax中的值是2,bx中的值是1,然后进行(bx)-(ax)的计算,结果是-1造成了无符号的借位,此时CF=1,在进行adc ax,1时,进行的是(ax)+1+CF=2+1+1=4。仔细分析一下就可以发现,如果把整个加法分开,低位先相加,然后高位相加再加上进位CF, 就是一个完整的加法运算,也就是说add ax,dx这个指令可以拆分为:
add al,bl adc ah,bh
所以有了adc这个指令我们就可以完成一些更庞大的数据量的加法运算。如计算1EF000H+000H的值:
mov ax,001eh mov bx,0f000h add bx,1000h adc ax,0020h
注:inc和loop指令不影响CF位。
sbb指令
sbb和adc类似,是带借位的减法,格式:sbb 操作对象1,操作对象2,执行的功能是操作对象1=操作对象1-操作对象2-CF,如:sbb ax,bx即(ax)=(ax)-(bx)-CF。sbb指令影响CF。
cmp指令
cmp是比较指令,cmp的功能相当于减法,只是不保存结果。cmp执行后影响标志寄存器,其他相关指令通过识别被影响的标志位来得知结果。格式:cmp 操作对象1,操作对象2,执行功能是计算对操作对象1-操作对象2但不保存结果,仅仅根据结果对标志位进行设置,如:
若(ax)=(bx)则(ax)-(bx)=0,zf=1
若(ax)!=(bx)则(ax)-(bx)!=0,zf=0
若(ax)<(bx)则(ax)-(bx)产生借位,cf=1
若(ax)>=(bx)则(ax)-(bx)不产生借位,cf=0
若(ax)>(bx)则(ax)-(bx)既不产生借位,结果又不为0,cf=0且zf=0
若(ax)<=(bx)则(ax)-(bx)既可能借位,结果可能为0,cf=1或zf=1
但实际上往往会出现溢出,如34-(-96)=82H(82H是-126的补码),但应该等于130超出了补码表示的范围,所以sf=1。我们可以同时检验sf和of两个来验证cmp的结果:cmp ah,bh
若sf=1,of=0说明没有溢出,那么sf的计算结果正确(ah)<(bh)
若sf=1,of=1说明出现了溢出,那么sf结果相反(ah)>(bh)
若sf=0,of=1说明有溢出,那么sf结果相反(ah)<(bh)
若sf=0,of=0说明没有溢出,那么结果正确(ah)>=(bh)
检测比较结果的条件转移指令
下面几条指令和cmp一起使用,检测不同的标志位来达到不同的条件跳转效果:
指令 |
含义 |
检测的标志位 |
je |
等于则转移 |
zf=1 |
jne |
不等于转移 |
zf=0 |
jb |
小于转移 |
cf=1 |
jnb |
不小于转移 |
cf=0 |
ja |
大于转移 |
cf=0且zf=0 |
jna |
不大于转移 |
cf=1或zf=1 |
指令中的字母含义如下:
e:equa; ne:not equal b:below nb:not below a:above na:not above
上面的检测都是在cmp进行无符号比较时的检测位,有符号数检测原理一样,只是检测的标志位不同而已。下面看一个例子,如果(ah)=(bh)则(ah)=(ah)+(ah),否则(ah)=(ah)+(bh)
cmp ah,bh je s add ab,bh jmp short ok s:add ah,ah ok:···
这里注意的是,je检测的是zf位,而不管之前执行的是什么指令,只要zf=1就会发生转移,所以cmp的位置需要仔细的把控,当然是否和cmp配合使用也是取决于编程者,下面例子实现了统计data中数值为8的字节个数,然后用ax保存:
··· data segment db 8,11,8,1,8,5,63,38 data ends ··· mov ax,data mov ds,ax mov bx,0 mov ax,0 mov cx,8 s:cmp byte ptr [bx],8 jne next inc ax next:inc bx loop s ···
DF标志位和串传送指令
flag的第10位是DF标志位,方向标志位,在串处理中,每次操作si,di的增减。
df=0每次操作后si,di递增
df=1每次操作后si,di递减
串传送指令,movsb,这个指令相当于执行:
1.((es)*16+(di))=((ds)*16+(si))
2.如果df=0:(si)=(si)+1,(di)=(di)+1
如果df=1:(si)=(si)-1,(di)=(di)-1
可以看出,movsb是将DS:SI指向的内存单元中的字节送入ES:DI中,然后根据DF的值对SI和DI增减1
同理mobsw就是将DS:SI指向的内存单元中的字送入ES:DI中,然后根据DF的值对SI和DI增减2
但一般来说,movsb和movsw都是和rep联合使用的,格式:rep movsb,这相当于:
s:movsb loop s
所以rep的作用是根据cx的值重复执行后面的串传送指令,由于每次执行movsb之后si和di都会自行增减,所以使用rep可以完成(cx)个字节的传送。movsw也一样。
由于DF位决定着串传送的方向,所以这里有两条指令用来设置df的值:
cld:df=0 std:df=1
例子:使用串传送指令将data段中第一个字符串复制到他后面的空间中:
··· data segment db 'Welcome to masm!' db 16 dup (0) data ends mov ax,data mov ds,ax mov si,0 mov es,ax mov di,16 mov cx,16 cld rep movsb ···
pushf和popf
pushf的功能是将标志寄存器的值入栈,popf是出栈标志寄存器。有了这两个命令,就可以直接访问标志寄存器了,如:
mov ax,0 push ax popf
标志寄存器在Debug中的表示
Debug中-r查看寄存器信息,最后有一段表示,下面列出我们已知的寄存器在Debug里的表示:
标志 |
值1的标记 |
值0的标记 |
of |
OV |
NV |
sf |
NG |
PL |
zf |
ZR |
NZ |
pf |
PE |
PO |
cf |
CY |
NC |
df |
DN |
UP |
内中断
内中断的产生
任何一个通用CPU都拥有执行完当前正在执行的指令后,检测到从CPU发来的中断信息,然后立即去处理中断信息的能力。这里的中断信息是指几个具有先后顺序的硬件操作,当CPU出现下面请看时会产生中断信息,相应的中断信息类型码(供CPU区分来源,是字节型,共256种)如下:
除法错误,如执行div指令出现除法溢出 0
单步执行 1
执行into指令 4
执行int指令 指令执行的int n后面的n就是一个字节型立即数,即为中断类型码
中断处理和中断向量表
CPU接收到中断信息之后,往往要对中断信息进行处理,而如何处理使我们编程决定的。而CPU通过中断向量表来根据中断类型找到处理程序的入口地址(CS:IP)也称为中断向量。
中断向量表中存放着不同的中断类型对应的中断向量(处理程序的入口地址),中断向量表存放在内存中,8086PC指定必须放在内存地址0处,从0000:0000到0000:03FF的1024个单元存放中断向量表,每个表项占两个字,四个字节。
CPU会自动根据中断类型找到对应的中断向量并设置CS和IP的值,这个过程称为中断过程,步骤如下:
1.(从中断信息中)取得中断类型码
2.标志寄存器的值入栈(暂存)pushf
3.设置标志寄存器第8位TF和第9位IF的值为0 TF=0,IF=0
4.CS内容入栈 push cs
5.IP内容入栈 push ip
6.在中断向量表中找到对应的CS和IP值并设置 (ip)=(N*4),(cs)=(N*4+2)
这么做的目的是,在中断处理之后还要回复CPU的现场(各个寄存器的值),所以先把那些入栈。
中断处理程序和iret指令
运行中的CPU随时都可能接收到中断信息,所以CPU随时都可能执行中断程序,执行的步骤:
1.保存用到的寄存器
2.处理中断
3.回复用到的寄存器
4.用iret返回
iret的指令功能是:pop ip pop cs popf(前面说到了,这三个寄存器的入栈是硬件自动完成的,所以iret是和硬件自动完成的步骤配合使用的)。
以处理0号除法溢出中断为例,我们想要编写除法溢出的中断处理程序需要解决如下几步问题:
1.编写程序
2.找到一段没有使用的内存空间
3.将程序写入到内存
4.将内存中的程序的入口写入0号中断的向量表位置
我们可以采取下面框架来完成这个过程:
··· start do0安装程序 设置中断向量表 mov ax,4c00h int 21h do0 程序部分 mov ax,4c00h int 21h ···
可以看出我们分成了两部分,第一部分称之为“安装”,第二部分是代码实现。安装部分的函数实现思路如下:
设置es:di至项目的地址
设置ds:si指向源地址
设置cx为传输长度
设置传输方向为正
rep movsb
设置中断向量表
实现如下:
start:mov ax,cs mov ds,ax mov si,offset do0 mov ax,0 es,ax mov di,200h mov cx,offset do0end-fooset do0 cld rep movsb ··· do0:代码 do0end:nop
这里offset do0end-fooset do0的意思是do0到do0end的代码长度,-是编译器可以识别并运算的符号,也就是说编译器可以再编译时处理表达式,如8-4等。还要注意的是,假如代码部分要输出“owerflow!”的话,需要将输出的内容写在代码部分并写入选择的内存中,否则如果单单在这个安装程序开始开辟一段data段的话,是会在程序返回时被释放。如:
do0:jmp short do0start db "overflow!" do0start: ··· do0end:nop
单步中断
当标志寄存器的TF标志位为1的时候,CPU会在执行一条语句之后将资源入栈,然后去执行单步中断处理程序,如Debug就是运行在单步中断的条件下的,它能让CPU每执行一条指令都暂停,然后我们可以查看CPU的状态。但CPU可以防止在运行单步中断处理程序的时候再发生中断然后又去调用单步中断处理程序……CPU可以将TF置零,这样就不会再中断了。CPU提供这个功能就是为了单步跟踪程序的执行。
但需要注意的是,CPU并不会每次接收中断信息之后立即执行,在某些特定情况下它不会立即响应中断,如设置ss寄存器的时候如果接收到了中断信息,就不会响应。因为我们需要连续设置ss和ip的值,在debug中单步执行的时候也是,mov ss,ax和mov sp,0是在一步之内执行的,所以我们需要灵活使用这个特性,sp紧跟着ss执行,而不要在设置ss和sp之间插入其他指令。
int指令
int指令
int指令也会引发中断,使用格式是int n,n就是int引发的中断类型码,int中断的执行过程:
1.获取类型码n
2.标志寄存器入栈,if=0,tf=0
3.cs,ip入栈
4.(ip)=(n*4),(cs)=(n*4+2)
5.执行n号中断的程序
所以我们可以使用int指令调用任何一个中断的中断程序,如int 0调用除法溢出中断。一般情况下,系统将一些具有一定功能的小程序以中断的方式提供给程序调用,当然也可以自己编写,可以简称为中断例程。
编写中断例程
如编写中断7ch的中断例程,实现word型数据的平方,返回dx和ax中。求2*3456^2,代码:
assume cs:code code segment start mov ax,3456 int 7ch add ax,ax adc dx,dx mov ax,4c00h int 21h code ends end start
接下来写7ch的功能和安装程序,并修改7ch中断向量表:
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
编写7ch中断实现loop指令,主程序输出80个“!”:
··· start mov ax,0b800h mov es,ax mov di,160*12 mov bx,offset s-offset se mov cx,80 s:mov byte ptr es:[di],'!' add di,2 int 7ch se:nop ··· 7ch实现部分: lp:push bp mov bp,sp dec cx jcxz lpret add [bp+2],bx lpret:pop bp iret
因为bx里面是需要专一的偏移地址,而使用bp的时候默认段寄存器是ss,所以add [bp+2],bx就可以实现将栈中的sp的值修改回s处,自行推导一下就ok。
BIOS和DOS提供的中断例程
系统ROM中存放着一套程序,称为BIOS,除此之外还有DOS都提供了一套可以供我们调用的中断例程,不同历程有不同的中断类型码,并且还能根据传入的参数不同而实现不同的功能,也就是说同一个类型码的中断例程可以实现很多不同功能,如int 10h是BIOS提供的包含了多个和屏幕输出相关子程序的中断例程。传参数如下面例子:
···
mov ah,2 ;置光标
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,12 ;dl中放列号
int 10h
BIOS和DOS安装历程的过程是,开机后CPU一加电,自动初始化CS为0FFFFH,IP为0,而在这个地方有一个跳转指令,挑战到BIOS和系统检测初始化程序。在BIOS系统检测初始化程序中会设置中断向量表中的值。
端口
端口的概念
各种存储器都要和CPU的地址线,数据线,控制线相连,在CPU看来,总线就是一个由若干个存储单元构成的逻辑存储器,称之为内存地址空间。除了各种存储器,通过总线和CPU相连的还有下面三种芯片:
各种接口卡(如网卡显卡)上的接口芯片,他们控制接口卡工作
主板上的接口芯片,CPU通过它们访问外部设备
其他芯片,用来存储相关系统信息,或进行相应的输入输出
上面的芯片中都有一种由CPU读写的寄存器,它们都和CPU的总线相连(通过各自的芯片),CPU对他们进行读写时候都通过控制线向他们所在的芯片发出端口读写指令。
所以,对于CPU来说,将这些寄存器都当做端口,对他们进行统一编址,建立了一个端口地址空间,每一个端口拥有一个地址,所以CPU可以直接读取下面三个地方的数据:
CPU内部的寄存器
内存单元
端口
端口的写
因为端口所在的芯片和CPU通过总线相连,所以端口地址和内存地址一样通过地址总线传送,并且在PC系统中,CPU最多可以定位64KB个不同的端口,所以端口地址范围是0~65535。
对端口的读写不能使用mov,push,pop等内存读写指令,端口的读写指令只有两个:in和out分别用于从端口读取数据和往端口写入数据。
访问端口的步骤:
1.CPU通过地址总线降低至信息60h发出
2.CPU通过控制线发出端口读命令,选中端口所在芯片,并通知它要从中读取数据
3.端口所在芯片将目标端口中的数据通过数据线送入CPU
注:在in和out指令中,只能通过ax或al来存放从端口中读入的数据或要发送到端口中的数据,且访问8位端口时,用al,访问16位端口用ax。
对0~255以内的端口进行读写时:
in al,20h out 20h,al
对256~65535的端口进行读写时,需要将端口号写在dx中:
mov dx,3f8h in al,dx out dx,al
CMOS RAM芯片
PC中有一个叫做CMOS RAM的芯片,称为CMOS,有如下特征:
包含一个实时钟和一个有128个存储单元的RAM存储器(早期的计算机64个字节)
靠电池供电,关机后内部的实时钟仍可继续工作,RAM中的信息不丢失
128个字节的RAM中,内部实时钟占用0~0dh单元保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取,BIOS也提供了相关的程序可以让我们在开机时配置CMOS中的系统信息。
芯片内部有两个端口70h和71h,CPU通过这两个端口读写CMOS
70h为地址端口,存放要访问CMOS单元的地址,71h为数据端口,存放从选定的单元中读取的数据,或写入的数据。
所以可以看出,想要从CMOS中读取数据,应分两步,先将单元号送入70h,然后再从71h读出对应号的数据。
shl和shr指令
shl和shr是逻辑移位指令,shl是逻辑左移,功能为:
1.将一个寄存器或内存单元中的数向左移位
2.将最后移出的一位写入CF
3.最低位补0
如:mov al,01001000b shl al,1执行结束后(al)=10010000b,CF=0。
注:如果移动位数大于1,那么必须将移动位数写在cl中。
mov al,01010001b mov cl,3 shl al,cl
执行后(al)=10001000b,最后移出的一位是0,所以CF=0。可以看出左移操作相当于x=x*2。
右移shr同理,最高位用0补充,移出的写入CF,若移动位数大于1,也要写在cl中,相当于x=x/2
在CMOS中存放着当前时间的年月日时分秒,分别存在下面的单元内:
秒 |
分 |
时 |
日 |
月 |
年 |
0 |
2 |
4 |
7 |
8 |
9 |
每个信息使用一个字节存放,以BCD码的形式,BCD码是对0-9这几个数字使用二进制表示,如:
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
0000 |
0001 |
0010 |
0011 |
0100 |
0101 |
0110 |
0111 |
1000 |
1001 |
如果要表示一个两位数如13,就是一个字节高四位是十位1的BCD码,低四位是个位3的BCD码,表示为00010011b。下面程序获取当前月份:
··· mov al,8 out 70h,al ;要从8号单元读取数据,所以先将8号单元送入70h端口 in al,71h ;从71h端口拿数据 mov ah,al ;复制一下 mov cl,4 shr ah,cl ;ah右移四位,ah里面的就是月份的十位 and al,00001111b ;al里面剩下的就是月份的个位
外中断
接口芯片和端口
CPU除了需要拥有运算的能力,还要拥有I/O(输入输出)能力,我们键入一个字母,要能处理,所以我们需要面对的是:外部设备随时的输入和CPU何处得到外部设备的输入。
外部设备拥有自己的芯片连接到主板上,这些芯片内部由若干寄存器,而CPU将这些寄存器当做端口访问,外设的输入或CPU向外设输出都是送给对应的端口然后再由芯片处理送给目标(CPU或外设)。
外中断
CPU提供外中断来处理这些如随时可能出现的来自外设的输入,在PC系统中,外中断源有以下两类:
可屏蔽中断:CPU可以不响应的外部中断,CPU是否响应看标志寄存器IF的设置,如果IF=1,CPU执行完当前指令后响应中断,如果IF=0,则不响应。可屏蔽中断的执行步骤和内部中断类似:
1.获取中断类型码n(从外部通过总线输入)
2.标志寄存器入栈,IF=0,TF=0
3.CS,IP入栈
4.(IP)=(n*4),(CS)=(n*4+2)
可见,将IF置零的原因是以免在处理中断程序的时候再发生中断。当然我们也可以选择处理,下面两个指令可以改变IF的值:sti,设置IF=1,cli,设置IF=0。
不可屏蔽中断:CPU必须响应的外部中断,CPU检测到不可屏蔽中断后执行完当前指令立即响应中断。8086CPU中不可屏蔽中断的中断类型码固定位2,所以中断过程中不需要获取中断类型码,步骤:
1.标志寄存器入栈,IF=0,TF=0
2.CS,IP入栈
3.(IP)=(8),(CS)=(0AH)
几乎所有由外设引发的外中断都是可屏蔽中断,如键盘输入,不可屏蔽中断通常是在系统中又必须处理的紧急情况发生时通知CPU的中断信息。
PC键盘处理过程
键盘上每个按键都相当于一个开关,按下就是开关接通,抬起就是开关断开。键盘上有一个芯片对键盘中每一个键盘的状态进行扫描,开关按下生成一个扫描码——通码,记录按下的按键位置,开关抬起也会产生一个扫描——断码,码记录松开的位置,都是送入60h端口。通码的第7位为0,断码第7位为1,也就是说断码=通码+80h。P247表。
当键盘输入送达60h时,相关新品就会向CPU发送中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息之后,如果IF=1,响应中断,引发中断过程并执行int9的中断例程。BIOS中int9的中断程序用来进行基本的键盘输入处理,步骤如下:
1.读出60h的扫描码
2.如果是字符的扫描码,将对应的字符的ASCII吗存入内存中的BIOS键盘缓冲区,如果是控制键(Ctrl)和切换键(CapsLock)扫描码,则将其转换为状态字(二进制位记录控制键和切换键状态的字节)写入内存中的存储状态字节的单元。
3.对键盘系统进行相关控制,如向新平发出应答
BIOS中键盘缓冲区能存储15个键盘输入,每个键盘输入两字节,高位存放扫描码,低位存放字符。此外,0040:17单元存放键盘状态字节,记录了控制键和切换键的状态,记录信息如下:
位 |
含义 |
0 |
右shift,1表示按下 |
1 |
左shift,1按下 |
2 |
Ctrl,1按下 |
3 |
Alt,1按下 |
4 |
ScrollLock状态,1表示指示灯亮 |
5 |
NumLock状态,1表示小键盘输入的是数字 |
6 |
CapsLock状态,1表示大写字母 |
7 |
Insert状态,1表示处于删除状态 |
可以看书P276的一个改写int 9的中断例程。
直接定址表
描述单元长度的标号
我们可以使用下面的标号来表示数据的开始:
··· code segment a:db 1,2,3,4,5,6,7,8 b:dw 0 ··· code ends ···
a,b都是代表对应数据的起始地址,但并不能判断数据的长度或类型。下面一段程序将a中的8个数累加存入b中:
assume cs:code code segment a db 1,2,3,4,5,6,7,8 b dw 0 start mov si,0 mov cx,8 s:mov al,a[si] mov ah,0 add b,ax inc si loop s mov ax,4c00h int 21h code ends end start
code段中a和b后并没有”:”号,这种写法同时描述内存地址和单元长度的标号。a描述了地址code:0和从这个地址开始后的内存单元都是字节单元,而b描述了地址code:8和从这个地址开始以后的内存单元都是字单元。所以b相当于CS:[8],a[si]相当于CS:0[si],使用这种标号,我们可以间接地访问内存数据。
其它段中使用数据标号
刚说的第一种标号即加”:”号的标号,只能使用在代码段中,不能在其他段中使用。如果想要在其它段中(如data段)使用标号可以使用第二种:
assume cs:code,ds:data data segment a db 1,2,3,4,5,6,7,8 b dw 0 data ends ··· start mov ax,data mov ds,ax mov si,0 mov al,a[si] ···
如果想在代码段中直接使用数据标号访问数据,需要使用assume伪指令将标号所在段和一个寄存器联系起来,是让寄存器明白,我们要访问的数据在ds指向的段中,但编译器并不会真的将段地址存入ds中,我们做了如下假设之后,编译器在编译的时候就会默认ds中已经存放了data的地址,如下面的编译例子:
mov al,a[si]
编译为:mov al,[si+0]
可以看出编译器默认了a[si]在ds所在的段中。所以我们需要手工指定ds指向data:
mov ax,data
mov ds,ax
也可以这么使用:
data segment a db 1,2,3,4,5,6,7,8 b dw 0 c a,b data ends
c处存放的是a和b的偏移地址,相当于c dw offset a,offset b。同理c dd a,b相当于c dw offset a,seg a,offset b,seg b即存的是a和b的段地址和偏移地址。
直接定址表
使用查表的方法编写相关程序,如输出一个字节型数据的16进制形式(子程序):
showbyte jmp short show table db '0123456789ABCDEF' show:push bx push es mov ah,al she ah,1 she ah,1 she ah,1 she ah,1 ;右移四位,位移子程序限制使用的寄存器数,只能这么移 and al,00001111b mov bl,al mov bh,0 mov ah,table[bx] ;高四位作为相对于table的偏移,取得对应字符 mov bx,0b800h mov es,bx mov es:[160*12+40*2],ah mov bl,al mov bh,0 mov al,table[bx] mov es:[160*12+40*2+2],al pop es pop bx ret
可见我们直接使用需要的数值和地址的映射关系来寻找需要的数据。
程序入口地址的直接定址表
可以看书P296的例程,主要思想是,编写多个子程序实现不同功能,每个子程序有自己的标号,如sub1,sub2···等。将它们存在一个表中:
table dw sub1,sub2,sub3,sub4
然后按照之前的方法使用如:
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] sret:pop bx ret
使用BIOS进行键盘输入和磁盘读写
int 9中断例程对键盘输入的处理
键盘处理依次按下A,B,C,D,E,shift_A,A的过程:
我们知道,键盘有16字的缓冲区,可以存放15个按键的扫描码和对应的ASCII码值,如下:
| | | | | | | | | | | | | | | | |
我们按下A时,引发键盘中断,CPU执行int 9中断例程,从60h端口读出A键通码,然后检测状态字,看是否有控制键或切换键按下,发现没有,将A的扫描码1eh和对应的ASCII码’a’61h写在缓冲区:
|1e61| | | | | | | | | | | | | | | | |
然后BCDE同理:
|1e61|3062|2e63|2064|1265| | | | | | | | | | | | | | | | |
在按下shift之后引发键盘中断,int 9程序接受了shift的通码之后设置0040:17处状态字第一位为1,表示左shift按下,接下来按A间,引发中断,int 9中断例程从60h端口督导通码之后检测状态字,发现左shift被按下,于是将A的键盘扫描码1eh和’A’的ASCII41h写入缓冲区:
|1e61|3062|2e63|2064|1265|1e41| | | | | | | | | | | | | | | |
松开shift,0040:17第一位变回0,之后又按下A和之前一样。
int 16h读取键盘缓冲区
int 16h可以供程序员调用,编号为0的功能是从键盘缓冲区读一个键盘输入,(ah)=扫描码,(al)=ascii码。如:
mov ah,0 int 16h |3062|2e63|2064|1265|1e41| | | | | | | | | | | | | | | |
执行后,缓冲区第一个没了,然后ah中是1eh,al中是61h。如果缓冲区为空的时候执行,那么会循环等待知道缓冲区有数据,所以int 16h的0号功能的步骤是:
1.检测键盘缓冲区是否有数据
2.没有则继续1
3.读取第一个单元的键盘输入
4.扫描码送ah,ascii码送al
int 13h读写磁盘
3.5寸软盘分为上下两面,每面80个磁道,每个磁道18个扇区,每个扇区512字节,共约1.44MB。磁盘的实际访问时磁盘控制器进行的,我们通过控制磁盘控制器来控制磁盘,只能以扇区为单位读写磁盘,每次需要给出面号,磁道号,和扇区号,面号和磁道号从0开始,扇区号从1开始。BIOS提供int 13h来实现访问磁盘,读取0面0道1扇区的内容到0:200的程序:
mov ax,0 mov es,ax mov bx,200h mov al,1 ;读取的扇区数 mov ch,0 ;磁道号 mov cl,1 ;扇区号 mov dl,0 ;驱动器号,0开始,0软驱A,1软驱B,磁盘从80h开始,80h硬盘C,81h硬盘D mov dh,0 ;磁头号(软盘面号) mov ah,2 ;13h的功能号,2表示读扇区 int 13h
es:bx指向接收数据的内存区。操作成功(ah)=0,(al)=读入的扇区数,操作失败(ah)=错误代码。将0:200的数据写入0面0道1扇区:
mov ax,0 mov es,ax mov bx,200h mov al,1 ;读取的扇区数 mov ch,0 ;磁道号 mov cl,1 ;扇区号 mov dl,0 ;驱动器号,0开始,0软驱A,1软驱B,磁盘从80h开始,80h硬盘C,81h硬盘D mov dh,0 ;磁头号(软盘面号) mov ah,2 ;13h的功能号,2表示读扇区 int 13h es:bx指向写入磁盘的数据,操作成功(ah)=0,(al)=写入的扇区数,操作失败(ah)=错误代码