汇编语言笔记12内中断
汇编语言笔记12内中断
0x00 中断简介
中断interput
cpu可以在执行完当前正在执行的指令后(当前的单条指令执行完后),检测到从cpu外部挥着内部产生的一种特殊信息,并且能够立即对所接受到的信息进行处理。这种特殊的信息称为中断信息,中断即 cpu不再接着执行下去,而是转去处理中断信息。
内中断
cpu内部有以下4中情形时,将产生相应的中断信息。(1)除法错误,比如div指令产生除法溢出 (2)单步执行 (3)执行into指令 (4)执行int指令
8086cpu用中断类型码 来标识中断信息的来源(产生中断信息的事件即为中断的来源 简称中断源),上述4种中断源对应的中断类型码:
除法错误 0 单步执行 1 执行into指令 4 执行int 指令,该指令的格式为int n ,n即为其对应中断类型码
中断类型码为8位,0-255
中断处理程序
cpu收到中断信息后,需要对中断信息进行处理,而如何处理,则由我们编写的中断处理程序决定,一般要对不同的中断信息编写不同的处理程序。
8086cpu(其他cpu都是这么干的)通过中断向量表,来记录每个中断信息对应的中断处理程序的入口地址(段地址+偏移地址的形式)。中断向量,就是中断处理程序的入口地址,而中断向量表,即是中断处理程序入口地址的列表。中断向量表在内存中存在的形式如图
8086cpu规定 内存0000:0000到 0000:03ff这1024个内存单元存放着向量表,每个表项占 1024/256项=4Byte 也就是2个字,高地址存放段地址,低地址存放偏移地址
3号中断源对应的中断程序的 入口地址039d:0016h
偏移地址 4*n 段地址 4 * n +2
中断过程
cpu从取得中断信息 到 返回原来执行的地方这一过程称为中断过程,详细展开如下:
(1)从中断信息取得中断类型码
(2)标志寄存器flag入栈(因为中断过程中要改变标志寄存器的值,所以先保存在栈中)
(3)设置标志寄存器的第8位TF和第9位IF
(4)cs入栈 ip入栈
(5)用内存地址4*n中的内容设置ip 4 * n+2设置cs (n位中断类型码)
上述5个步骤都是由cpu硬件自动完成的,然后开始执行我们编写的中断处理程序
0x01 编写中断处理程序
由于cpu随时都可能检测到中断信息,所以中断处理程序必须一直储存在内存中,而且中断处理程序的入口地址,即中断向量 必须写入对应的中断向量表表项中
中断处理程序的编写和子程序相似:
(1)保存用到的寄存器(push入栈)
(2)处理中断
(3)恢复用到的寄存器(pop出栈)
(4)iret指令返回(该指令由cpu硬件完成,不用写入中断程序)
iret 指令的功能是 pop ip,pop cs, popf(从栈中弹出数据,送入标志寄存器)
除法中断错误的处理
mov ax,1000h mov bh,1 div bh 会产生除法溢出错误,因为 16位/8位 商8位储存在al,而这个商 1000h8位保存不了 发生溢出错误,将产生中断类型码位0的中断信息,上述代码运行如下,系统默认的0号中断处理程序的功能:
编程处理0号中断
发生除法溢出时,依次进行下列工作
(1)产生中断信息,中断类型码0
(2)cpu将标志寄存器入栈 tf if设置0 cs ip 入栈 ip=4n cs=4n+2
(3)调用中断处理程序
我们的中断处理函数该放在内存的哪里呢?如果我们还是想以前一样 写在向操作系统申请的段,出于两个原因不这么做①如果这样做我们程序结束时,申请的内存会被释放,所以写的中断处理程序可能没了②学汇编是为了获得对计算机底层的编程体验,尽可能不去理会操作系统,而直接面向硬件资源
内存0000:0000到0000:03ff是用来存放中断向量表的,大小1kb 8086支持256个中断,但是实际上系统中已经声明的中断远远不到256个,因此中断向量表里有许多是空的,如下图 从0000:01e0开始,就空了
所以我们可以用0000:0200到02ff 这256字节 (按每个指令2字节平均算,能有100+指令 够用了)
所以我们写的程序要实现以下三个功能
(1)可以显式“overflow!”的中断处理程序do0
(2)将do0这个程序复制到 0000:2000处
(3)将0000:2000储存在中断向量表0项处
三个功能中(1)之前已经写过,但是要注意要显示的字符串”overflow!”应该硬编码到do0中,一起复制到目的地址,如果仍然放在程序中,那么程序结束后可能被释放,中断程序do0无法读取它了
(2)可以用 rep movsb指令 ds:[si]->es:[di] 那么ds应该是cs(因为我们程序写在代码段) si 则是offset do0 es是0000h di 是 0200h 那么长度cx 是多少呢?我们可以让编译器自己计算do0长度
start: mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
mov cx,offset e_do0-offset do0;设置长度
cld;自动递增si di
rep movsb
do0: ...
...
...
e_do0: iret
如上 ‘-’是编译器支持的运算符号,可以用它来进行两个常数的减法,比如mov ax,8-4 -> mov ax,4等同,实际上 编译器可以处理常数的表达式 比如mov ax,(5+3)*5/10 -> mov ax,4
(3)就更简单了 不多说上完整代码
assume cs:code
code segment
start: mov ax,cs
mov ds,ax
mov si,offset do0
mov ax,0
mov es,ax
mov di,200h
mov cx,offset edo0-offset do0
cld
rep movsb;安装do0
mov es:[0],200h;设置中断向量表对应项的中断向量
mov word ptr es:[2],0
mov ax,1000h
mov bl,1
div bl;产生溢出 用来测试
mov ax,4c00h
int 21h
do0: jmp short do0_start;这里是为了将字符串编码进代码,但是又不能被执行
db 'overflow!'
do0_start:push ax
push ds
push es
mov ax,cs;ds si指向字符串
mov ds,ax
mov si,202h; 202h的由来是do0 的jmp short占两个字节
mov ax,0b800h
mov es,ax
mov di,12*160+36*2;直接用的常数表达式
mov cx,9
s: mov al,ds:[si]
mov es:[di],al
mov byte ptr es:[di+1],2;绿色显式
inc si
add di,2
loop s
mov ax,4c00h;中断处理程序返回也要写
int 21h
edo0: nop
code ends
end start
上述代码运行正确
这个代码运行调试时有以下几点需要注意:
1关于栈,我们没有设置ss和sp,但是中断过程开始需要用到栈,原来栈会被自动设置,如图程序开始时ss就被设置成0769 sp是0000
2 中断处理程序 do0在最后是返回dos ,而不是返回我们的程序,所以最后用的是mov ax,4c00h 和 int 21h
0x02 单步中断
cpu在执行完一条指令后,如果检测到标志寄存器的TF位(trace flag,trace追踪、痕迹)为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1:
(1)取得中断类型码1
(2)标志寄存器入栈,TF IF设置为0
(3)CS IP入栈
(4)IP=1*4 cs=1 * 4+2
为什么要提供单步中断呢?我们debug的t命令,即是利用单步中断,在执行一条指令后就显式各个寄存器,由于进入单步中断时设置了TF为0,所以在单步中断的中断处理程序中不会再发生单步中断
总的来说,单步调试提供了程序调试的机制
0x03 响应中断的特殊情况
有些情况下,cpu在执行完当前指令后,即便时发生中断,也不会响应,举例如:
在执行完 向ss寄存器传送数据的指令后 (mov ss,ax) 即使是发生中断,cpu也不会响应,这样做的原因是ss:sp 联合指向栈顶,所以对他们的设置应该连续完成(防止在设置ss后 cpu响应中断,而中断过程又必然要呀标志寄存器 CS IP入栈 而sp未设置 ss:sp不是正确的栈顶,将引起错误)。
所以这就是为什么我们把设置ss和sp的指令放一起的原因,这也是为什么t命令 不能停止 mov ss,al 的原因,因为单步中断也不响应。