CPU从机器LEVEL上就是一个指令执行机器,它在时钟触发模式不停的处理这些指令。这些指令代表了特定的意义,这些意义是由编写指令的人来规定的。CPU是不知道具体含义的。CPU最基本的指令就是运算指令,人们可以其它一些指令将一些看似运算代表的意义变成其它含义,如前面所说的端口,我们可以从端口读取一个数据并将之显示在屏幕的指定位置。在这个处理过程中,CPU只是从一个端口读进一个数据并将之放在一块特定内存处。它并不知道这个过程的含义是进行屏幕显示。而显卡芯片会将这块特定内存上的内容显示在屏幕上。所以说单凭一个CPU不能完全一些特定的功能,它还需要一些其它设备,这些设备如键盘、显示器等这些就是外设。这些外设一般拥有自己的接口卡和端口寄存器,它们与总线相连。CPU只可以访问端口,而且外设的芯片也只能访问端口,它们也不能直接与CPU打交道。可见CPU与外设只有一个端口的联系通道。但是这里有一个问题,就是端口中的数据随时都有可能被外设改变,CPU如何及时处理理呢?显然它不能时时刻刻监听端口,通常它应该采用响应-中断的方式去处理来自于这些外设的数据,因此同前面内中断一样,中断源来自己CPU外部的就是外中断。外中断就是当CPU外部有需要处理的事件发生的时候,相关芯片将向CPU发出中断信息,CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。因为是外中断,CPU不可能每种外中断都要进行处理,所以外中断通常分成不可屏蔽中断和可屏蔽中断。前面的内中断大部分是不可屏蔽中断,而大部分外中断是可屏蔽中断。CPU如何响应可屏蔽中断与不可屏蔽中断的呢,它是CPU根据标志寄存器的IF位置值来决定是否响应后续中断,如是IF=1,当CPU检测到中断信息时,如果是可屏蔽需要立即处理,否则就不处理。通常内中断过程的第二步都会将标志寄存器入栈,并且IP=0,TF=0也就是禁止可屏蔽中断干挠不可屏蔽的中断例程。当然如果在中断例程 中需要处理可屏蔽中断时,可以通过两条指令来改变IF值, sti, 设置IF=1; cli 设置IF=0。外中断的中断类型码与内中断的类型码不同,通常不可屏蔽的外中断类型码固定为2,可屏蔽中断的类型码是通过数据总线送入CPU的。其它的处理过程同前面所说的内中断基本一样,比喻标志符寄存器入栈,比喻保存当前CS:IP,比喻设置当前CS:IP为中断例程的入口地址。下面以键盘的输入作例:
键盘作为一种外设,它的每一键都会产生一个开关信号量,这个信号量会被芯片处理成一个扫描码。扫描码分两种,一种是按下状态称之为通码,一种是松下状态称之为断码。通常断码与通码只在第7位有差异,断码第7位是1,通码第7位是0.也就是说断码=通码+80h。 如g的通码22h,断码为a2h.如下图所示是一些常用的通码表(扫描码)。
键盘与CPU交换信息的端口是60h,同时键盘芯片提供INT 9号中断例程。9号中断例程它不是CPU提供的,它是BIOS提供的。其中主功能是读取60h中扫描码,并将这个扫描码与通过查询ASCII码表得到ASCII码一并放入BIOS键盘缓冲区(对一些控制键如SHIFT则是改变状态字节,这是在0040:17这个特定单元中,用一个字节记录7个键的状态信息)。BIOS键盘缓冲区是系统启动后,BIOS用于存放int 9中断例程所接收的键盘盘输入的内存区,可以存储15个键盘输入,一共是30个字节,一个键盘单元用一个字单元存入,高位字节存扫描码,低码是字符码。如下衅所示:
对外设键盘的读入处理,可以通过读取60h端口中的值来判断内容,另外一方面也可以编写中断响应过程,实现编程响应9号中断并调用已有的9号中断例程。如下所示:
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
;将BIOS内置的INT 9中断例程的入口地址保存在ds:0-4中
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
;恢复中断向量表中int 9的中断入口地址
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,1000h
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
pushf
pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0]
cmp al,1
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
到此为止有关8086CPU及汇编的一些相关概念基本全部学完,但是还是有一些语法上的技巧,还需要特别强调一下,这些语法技巧不一定适用于所有编译器版本,需要根据编译器版本进行对应修改。我们现在学的虽然是最简单的8086CPU,其实对80386\ARM、PPC其实思想都差不多,只是一些约定的规定不一样。换个说法就是一些指令上和CPU架构上的一些差异,但其主要思想一定是相同的,因为我们目前的机器运行的思想都是图灵机。图灵机就是时序控制下的指令机。只是这个指令机实现方式的不同。但原理肯定一样。
1)描述单元长度的标号,传统的标号带:只表示地址,而未带:除了表示地址之外,还表示长度(类型长度)。有点类似于数组的概念。如下图所示:
注意两点,第一点:上面两种写法在代码段都没问题,但是不在代码段时只有不带:的才支持。第二点是如果在数据段中使用a db 1,2,3,4,5,6,7,8这样的定义,则需要将assume ds,data进行关联,但关联不代表ds起始地址就是data还是需要在代码中手工赋值。如下图所示:
2)直接定址表 同高级语言中数组功能一样,实际上对一些需要进行映射转换功能的代码可以直接采用定义一个数值表,然后就可以通过查表的方式进行翻译。实际上这是一种编程技巧。省去转换的计算。