汇编语言笔记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都是这么干的)通过中断向量表,来记录每个中断信息对应的中断处理程序的入口地址(段地址+偏移地址的形式)。中断向量,就是中断处理程序的入口地址,而中断向量表,即是中断处理程序入口地址的列表。中断向量表在内存中存在的形式如图

QQ截图20200605161817

​ 8086cpu规定 内存0000:0000到 0000:03ff这1024个内存单元存放着向量表,每个表项占 1024/256项=4Byte 也就是2个字,高地址存放段地址,低地址存放偏移地址

QQ截图20200605162221

​ 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号中断处理程序的功能:

QQ截图20200605174836

编程处理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开始,就空了

QQ截图20200605180308

​ 所以我们可以用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	

​ 上述代码运行正确

QQ截图20200605184807

​ 这个代码运行调试时有以下几点需要注意:

​ 1关于栈,我们没有设置ss和sp,但是中断过程开始需要用到栈,原来栈会被自动设置,如图程序开始时ss就被设置成0769 sp是0000

QQ截图20200606090357

​ 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 的原因,因为单步中断也不响应。