内中断的产生
任何一个通用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的值,这个过程称为中断过程,步骤如下:
- (从中断信息中)取得中断类型码
- 标志寄存器的值入栈(暂存)pushf
- 设置标志寄存器第8位TF和第9位IF的值为0 TF=0,IF=0
- CS内容入栈 push cs
- IP内容入栈 push ip
- 在中断向量表中找到对应的CS和IP值并设置 (ip)=(N4),(cs)=(N4+2)
这么做的目的是,在中断处理之后还要回复CPU的现场(各个寄存器的值),所以先把那些入栈。
中断处理程序和iret指令
运行中的CPU随时都可能接收到中断信息,所以CPU随时都可能执行中断程序,执行的步骤:
- 保存用到的寄存器
- 处理中断
- 回复用到的寄存器
- 用iret返回
iret的指令功能是:pop ip pop cs popf(前面说到了,这三个寄存器的入栈是硬件自动完成的,所以iret是和硬件自动完成的步骤配合使用的)。
可以看到,在中断过程中,寄存器入栈的顺序是标志寄存器、CS、IP,而iet的出栈顺序是IP、CS、标志寄存器,刚好和其相对应,实现了用执行中断处理程序前的CPU现场恢复标志寄存器和CS、P的工作。
iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序。
以处理0号除法溢出中断为例,我们想要编写除法溢出的中断处理程序需要解决如下几步问题:
- 编写程序
- 找到一段没有使用的内存空间
- 将程序写入到内存
- 将内存中的程序的入口写入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 n,n就是int引发的中断类型码,int中断的执行过程:
从中断信息中获取中断类型码n
标志寄存器入栈(因为在中断过程中要改变标志寄存器的值,所以需要将其先保存在栈中)
设置标志寄存器第八位TF和第九位IF为0(当cpu检测到可屏蔽中断信息时,若IF为1则要在cpu执行完当前指令后响应中断)
CS段寄存器内容入栈
IP寄存器内容入栈
从内存地址为”中断类型码 4”和”中断类型码 4 + 2”的两个字单元中读取中断处理程序的入口地址设置IP和CS(即用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址)
(ip)=(n*4),(cs)=(n*4+2)
执行n号中断的程序
当一个系统调用的参数个数大于5时(因为5个寄存器 eax, ebx, ecx, edx,esi 已经用完了),执行int 0x80指令时仍需将系统调用功能号保存在寄存器eax中,所不同的只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针
所以我们可以使用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系统检测初始化程序中会设置中断向量表中的值。