我是如何学习Java的~中断例程和端口

任何一个通用的 CPU,都具备一种能力,可以在执行完当前正在执行的指令之后,检测到从 CPU 外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,称为中断信息。CPU 接收到中断信息时,就会转去处理这个中断信息。

中断信息有两种,内中断和外中断。

内中断

当 CPU 内部有如下情况发生时,就会产生中断信息:

  1. 除法错误,如,执行除法时产生溢出错误
  2. 单步执行
  3. 执行 into 指令
  4. 执行 int 指令

CPU 在收到中断信息后,需要对中断信息进行处理。可是,CPU 是怎么识别中断信息和对中断信息进行相应的处理呢?

CPU 在设计时,对中断信息和处理程序入口地址之间建立了一种联系,中断信息中包含有识别中断源的类型码,中断类型码的作用就是用来定位中断处理程序的。这种关系被放在了一个中断向量表中,CPU根据中断类型码在中断向量表中查找到对应的处理程序入口地址。

对于8086 PC机来说,中断向量表指定放在内存地址 0 处。从内存 0000:0000 到 0000:03FF 的1024 个单元中存放着中断向量表。

程序的入口地址由段地址和偏移地址定位,占用两个字,所以对于一个中断码对应的中断向量表中的数据来说,低地址存放偏移地址,高地址存放段地址。

通过debug可以查看dos中的中断向量表部分如下:

我是如何学习Java的~中断例程和端口_第1张图片

根据中断码,可以知道对应的程序入口在向量表中的位置:

0号中断码:
	对应的入口地址在向量表中的偏移地址为0
	取两个字单元,低地址为偏移地址,高地址为段地址,即 CS:IP = 0F000H:1060H
	所以,获取 CS 和 IP 的值时,IP在向量表中的偏移地址为 0 ,CS在向量表中的偏移地址为 2
	
1号中断码:
	对应的入口地址在向量表中的偏移地址为4
	取两个字单元,低地址为偏移地址,高地址为段地址,即 CS:IP = 0192H:08EDH
	所以,获取 CS 和 IP 的值时,IP在向量表中的偏移地址为 4 ,CS在向量表中的偏移地址为 6
	
2号中断码:
	对应的入口地址在向量表中的偏移地址为8
	取两个字单元,低地址为偏移地址,高地址为段地址,即 CS:IP = 0070H:0008H
	所以,获取 CS 和 IP 的值时,IP在向量表中的偏移地址为 8 ,CS在向量表中的偏移地址为 10

3号中断码:
	对应的入口地址在向量表中的偏移地址为12,取两个字单元,低地址为偏移地址,高地址为段地址,
	取两个字单元,低地址为偏移地址,高地址为段地址,即 CS:IP = 0192H:08E6H
	所以,获取 CS 和 IP 的值时,IP在向量表中的偏移地址为 12 ,CS在向量表中的偏移地址为 14
	
n号中断码:
	对应的入口地址在向量表中的偏移地址为 4 * n
	取两个字单元,低地址为偏移地址,高地址为段地址
	所以,获取 CS 和 IP 的值时,IP在向量表中的偏移地址为 4 * n ,CS在向量表中的偏移地址为 4 * n + 2

CPU 根据中断类型码查找到中断处理逻辑程序并执行后,还需要返回到中断的地方继续执行接下来的程序,所以CPU需要在执行中断程序前保存当前 CS 和 IP 的值,中断过程如下:

  1. 从中断信息中获取中断类型码 N
  2. 标志寄存器入栈
  3. 设置标志位IF =0 和 TF = 0
  4. CS 内容入栈
  5. IP 内容入栈
  6. 从向量表中获取入口地址并设置 IP 和 CS
  7. 执行中断处理程序

在中断过程中,设计到两个标志位的设置:

TF:
	单步中断标志位,当 TF=1 时,允许 CPU 进行单步中断处理,否则不进行对单步中断的处理
	产生单步中断的类型码为1,debug中的 T 等指令就是通过单步中断来实现的
	该标志位和单步类型中断为我们提供了调试程序的功能
	之所以设置TF=0,是为了防止在执行中断的时候,一直响应单步中断指令,导致当前中断无法正常处理完成

	对于单步中断,有些指令不会立即响应中断,如
	mov sp, ax
	mov ss, 16
	只有执行完后才能响应单步中断。对于栈来说,sp 和 ss 的设置是一体的,但以下情况就会引发中断:
	mov sp,ax
	mov ax, 15
	mov ss, ax
	当执行到第一条指令时发生中断,就会去执行中断的程序。
	所以我们在涉及到栈的修改时,要保证两个指令的连续。
	
IF	;和外中断有关

所以在中断处理程序中,需要对用到的寄存器进行保存,同时还应恢复用到的寄存器:

  1. 保存用到的寄存器
  2. 处理中断
  3. 恢复用到的寄存器
  4. 用 iret 指令返回

iret 指令进行了以下操作:

  1. pop ip
  2. pop cs
  3. popf

这样,在执行完中断处理程序后,CPU 依旧能恢复到原来中断的地方继续执行。

如,当程序中出现除法溢出问题时,会造成中断,中断类型码为0:

assume cs:code

code segment
	start:
		mov ax, 0FFFFH
		mov bl, 1
		div bl
		mov ax, 4c00H
		int 21H
code ends

end start

使用dosbox进行debug调试:
我是如何学习Java的~中断例程和端口_第2张图片
可以看出,当执行div产生溢出时,程序跳转到了F000H:1060H,查看0号中断码所处的中断向量表位置:
我是如何学习Java的~中断例程和端口_第3张图片
从中断向量表中可以看到,0号中断码的处理程序入口为:F000H:1060H,程序跳转到该处进行执行。但对于dosbox来说,并没有对该中断进行处理。这个时候我们可以手动的去修改该处的中断处理程序,使0号中断码执行我们设置的程序,可是该怎么做呢?

根据中断执行的过程,我们可以通过以下方式来进行控制:

  1. 更改向量表来指向我们的中断处理程序
  2. 覆盖原来中断处理程序入口处的代码,替换为我们的中断处理程序。但这样可能会覆盖掉其它功能的代码,导致一些问题

在此我们通过修改向量表来执行我们自己的中断处理程序,当发生0号中断时,在屏幕中央显示Divide overflow!:

assume cs:code

data segment
	db 'Divide Overflow!'
data ends

code segment
	start:
		
		mov ax, 0
		mov ds, ax
		mov bx, 0
		mov ax, offset div0
		mov ds:[bx], ax
		mov ax, seg div0	;seg获取标号处的段地址
		mov ds:[bx + 2], ax
			
		mov ax, 0FFFFH
		mov bl, 1
		div bl
		
		mov ax, 4c00H
		int 21H

	div0:
		push ax
		push es
		push di
		push ds
		push si
		push cx
		
		mov ax, 0b800h
		mov es, ax
		mov di, 160 * 12 + 74

		mov ax, data
		mov ds, ax
		mov si, 0
		
		mov cx, 16
		
	s:
		mov al, ds:[si]
		mov ah, 2
		mov es:[di], ax
		inc si
		add di, 2
		loop s
		
		pop cx
		pop si
		pop ds
		pop di
		pop es
		pop ax
		
		mov ax, 4c00H
		int 21H
		
code ends

end start

dosbox运行如下:
我是如何学习Java的~中断例程和端口_第4张图片
除了出现相应的程序错误时,可以触发中断外,还可以通过 int 指令来触发中断:

int 中断类型码n

如,触发0号中断:

assume cs:code

data segment
	db 'Divide Overflow!'
data ends

code segment
	start:
		
		mov ax, 0
		mov ds, ax
		mov bx, 0
		mov ax, offset div0
		mov ds:[bx], ax
		mov ax, seg div0	;seg获取标号处的段地址
		mov ds:[bx + 2], ax
			
		int 0
		
		mov ax, 4c00H
		int 21H

	div0:
		push ax
		push es
		push di
		push ds
		push si
		push cx
		
		mov ax, 0b800h
		mov es, ax
		mov di, 160 * 12 + 74

		mov ax, data
		mov ds, ax
		mov si, 0
		
		mov cx, 16
		
	s:
		mov al, ds:[si]
		mov ah, 2
		mov es:[di], ax
		inc si
		add di, 2
		loop s
		
		pop cx
		pop si
		pop ds
		pop di
		pop es
		pop ax
		
		mov ax, 4c00H
		int 21H
		
code ends

end start

结果如下:
我是如何学习Java的~中断例程和端口_第5张图片
在DOSBOX中,也提供了自己的中断例程,如我们常用的21H中断码:

mov ax 4c00h
int 21h

对于21H中断码,实际是由多个子程序组成,通过不同的传值可以调用不同的中断功能。

在21H中断码处理程序中,当设置 ah=4CH,会调用相应的子程序,而4CH对应的子程序功能就是程序返回,al定义返回值。

同时,在我们电脑的系统主板上,也存放着一套系统,即BIOS(基本输入输出系统),BIOS主要由以下几部分组成:

  1. 硬件系统的检测和初始化程序
  2. 外部中断和内部中断的中断例程
  3. 用户对硬件设备进行I/O操作的中断例程
  4. 其它和硬件系统相关的中断例程

对于BIOS和DOS中的中断例程需要安装到内存中,步骤如下:

  1. 开机后,CPU 初始化 CS=0FFFFH 和 IP=0,自动从FFFFH:0单元开始执行程序。FFFFH:0处有一条跳转指令,CPU 执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序
  2. 初始化程序将BIOS所支持的中断处理程序地址写入中断向量表中
  3. 硬件系统检测和初始化完成后,调用 int 19h 进行操作系统的引导,从而将计算机交由操作系统控制
  4. DOS启动后,将自己的中断处理程序装入内存,并在中断向量表中建立关系。

BIOS提供的中断例程中,包含了多个和屏幕输出相关的子程序:

assume cs:code

code segment
	start:
		
		mov ah, 9	;在光标位置处显示字符
		mov al, 'a'	;显示的字符
		mov bl, 7	;字符的颜色
		mov bh, 0	;显示的页码
		mov cx, 3	;字符的重复个数
		int 10h
		
		mov ax, 4c00H
		int 21H
		
code ends

end start

运行结果如下:
我是如何学习Java的~中断例程和端口_第6张图片

同时,调用dos的21H中断例程,也可以实现向屏幕输出内容:

assume cs:code

data segment
	db 'Hello World!$'	;要显示的字符串需要以$作为结束符
data ends

code segment
	start:
		
		mov ax, data
		mov ds, ax
		mov dx, 0	;ds:dx指向需要显示的字符串地址
		
		mov ah, 9	;在光标位置处显示字符
		int 21h
		
		mov ax, 4c00H
		int 21H
		
code ends

end start

结果如下:
我是如何学习Java的~中断例程和端口_第7张图片

端口

在 PC 机系统中,除了与CPU相连的各种存储器外,还有以下3种芯片:

  1. 各种接口卡上的接口芯片,如网卡、显卡上的接口芯片
  2. 主板上的接口芯片,CPU 通过他们对部分外设进行访问
  3. 其它芯片,用来存储相关的系统信息或进行相关的输入输出处理

在这些芯片中,都有一组可以由 CPU 读写的寄存器,这些寄存器:

  1. 都和 CPU 的总线相连,这种连接是通过它们所在的芯片进行的
  2. CPU 对它们的读写时,都是通过控制线向它们所在的芯片发出端口读写命令

所以,CPU 把这些寄存器都当成端口,对它们进行统一的编址,从而建立一个统一的端口地址空间,每个端口在地址空间中都有一个地址。

CPU 在访问端口的时候,通过端口地址来定位端口。而端口所在的芯片是通过总线和CPU相连的,所以端口地址和内存一样,也是通过地址总线来传送的,只是端口不像内存地址一样需要段地址和偏移地址来合成最终的地址。

对于端口的读写,通过指令 in 和 out 实现,读取或写入的数据只能通过寄存器 AX 或 AL 传送。访问的端口只能用 dx 来表示,当访问的是 8 位端口时,也可以用立即数直接表示。所以,对于 8086 CPU 来说,可以定位的端口最大为16位,即 64KB 个不同的端口,端口的范围为0 ~ 65535。

如:

in al, 20h	;从20h端口读入一个字节
out 20h, al	;往20h端口写入一个字节

mov dx, 3f8h	;对256~65535的端口读写,需要把端口号放入dx中
in al, dx	;从3f8h端口读入一个字节
out dx, al	;向3f8h端口写入一个字节

外中断

外设的输入输出都是通过端口来实现的,但是,CPU 如何知道外设的输入并进行处理呢?

CPU 提供了中断机制来满足这种需求。该中断被称为外中断。

外中断源有以下两种:

  1. 可屏蔽中断源

    可屏蔽中断是 CPU 可以不响应的外中断。根据标志位 IF 来判断是否对该中断进行响应。IF=1,则 CPU 在执行完当前指令后响应中断,引发中断过程。否则,不响应可屏蔽中断。

    在中断过程中,CPU 之所以会把 IF 设置为 0,是为了在进入中断处理程序后,禁止其它的可屏蔽中断,直到中断处理完成。

    如果在中断的过程中,需要对可屏蔽中断进行响应,可以更改IF标志位,除了通过 POPF 的命令来更改标志位外,还可以通过以下指令来操作 IF 标志位:

    sti	;设置 IF=1
    cli	;设置 IF=0
    
  2. 不可屏蔽中断

    不可屏蔽中断是 CPU 必须响应的外中断,对于 8086 CPU 来说,不可屏蔽中断的中断码固定为 2,所以在中断过程中不需要获取中断码,除此之外,其它中断过程一致。

几乎所有由外设引发的外中断,都是可屏蔽中断。

对于键盘来说,会引发 9 号中断,键盘和 CPU 的通信通过端口 60H 来实现,当键盘输入一个信息时,会把输入数据放入 60H 端口中,相关芯片就会向 CPU 发送中断类型码为 9 的可屏蔽中断。CPU 检测到该中断信息后,若 IF=1,则响应中断,引发中断过程,执行 9 号中断例程。在此,我们可以模仿这个过程来触发 9 号中断。

在模仿之前,需要了解键盘的输入数据规则。

当键盘上的某个键被按下时,会产生一个扫描码,称为通码,该通码会被放入端口为 60H 的寄存器中,之后引发 9 号中断。同样,当松开一个键时,也会产生一个扫描码,称为断码,也会被送入该寄存器中然后引发 9 号中断。

通码和断码的关系为:断码 = 通码 + 80H。

其中 Esc 键的扫描码为 1 ,通过该键来实现以下功能:

在屏幕中间依次显示 a ~ z,在显示的过程中,按下 Esc 键后,改变显示的颜色:

代码如下:

assume cs:code

data segment
	dw 2 dup(0)
data ends


code segment
	start:
		
		mov ax, data
		mov es, ax
		mov di, 0
		
		mov ax, 0
		mov ds, ax
		mov si, 9 * 4
		
		;原中断程序放入定义的两个字单元中
		mov cx, 2
		cld
		rep movsw
		
		;替换向量表中的9号中断例程入口
		
		mov ax, 0
		mov ds, ax
		mov si, 9 * 4
		
		cli
		mov word ptr ds:[si], offset int_9
		mov word ptr ds:[si + 2], seg int_9
		sti
		
		;向屏幕中间写入字符
		mov ax, 0b800h
		mov ds, ax
		mov bx, 160 * 12 + 80
		mov ah, 'a'
		
	s:	
		mov byte ptr ds:[bx], ah
		inc ah
		call sleep
		cmp ah, 'z'
		jna s
		
		;恢复原中断向量表的内容
		mov ax, data
		mov ds, ax
		mov si, 0
		
		mov ax, 0
		mov es, ax
		mov di, 9 * 4
		
		;原中断程序放入定义的两个字单元中
		cli
		mov cx, 2
		cld
		rep movsw
		sti
	
		mov ax, 4c00H
		int 21H
		
	;定义一个足够长时间的循环,以便看到屏幕上的变化
	sleep:
		push ax
		push dx
		mov ax, 0
		mov dx, 10H
	lp:
		sub ax, 1
		sbb dx, 0
		cmp ax, 0
		jne lp
		cmp dx, 0
		jne lp
		pop dx
		pop ax
		ret
		
	int_9:
		push ax
		push es
		push si
		
		mov ax, data
		mov es, ax
		
		;获取键盘输入的扫描码
		in al, 60h
		
		;调用原中断程序,不影响键盘输入的其它功能的使用
		pushf
		call dword ptr es:[0]
		
		;根据 Esc的扫描码 1 来判断当前输入的是否为Esc
		cmp al, 1
		jne exit
		;变换字体颜色
		mov ax, 0b800h
		mov es, ax
		mov si, 160 * 12 + 81
		inc byte ptr es:[si]
		
	exit:
		pop si
		pop es
		pop ax
		iret
		
		
code ends

end start

键盘的输入可以通过触发 9 号中断码来去获得,CPU 获取到扫描码后,会将其转换为对应的 ascii 码或状态信息,并把扫描码和最终对应的 ascii 码存放在指定的空间中,该空间被称为键盘缓冲区。存放时,会把扫描码当成低位数据,ascii 码当成高位数据存入键盘缓冲区的一个字单元中。以后的每次输入都会按顺序存入缓冲区中。

BIOS 提供了 int 16h 中断例程,功能编号为 0 的子程序供我们调用,来读取键盘缓冲区中的第一个字单元的内容,每次读取完成后,就会把该缓存区相应的字单元内容删除,同时该字单元后的内容会向前移动,以便下次内容的获取

调用方法:

mov ah, 0
int 16h

读取的内容会放入到 ax 中,al 中为对应的 ascii 码,ah 为对应的扫描码。

若当前缓冲区无内容时,会进入循环等待,直到缓冲区中有数据产生。

根据该缓冲区的功能,我们可以很方便的实现把键盘输入的数据打印在屏幕上:

assume cs:code

code segment
	start:
		mov bx, 0b800h
		mov ds, bx
		mov si, 160 * 12
		
		mov cx, 20
		
	s:
		mov ah, 0
		int 16h
		mov ah, 2
		mov ds:[si],ax
		add si, 2
		loop s
		
		mov ax, 4c00h
		int 21h
		
code ends

end start

目录
上一章
下一章

你可能感兴趣的:(汇编,C语言,Java,我是如何学习Java的)