1、PC系统的中断机制和原理
2、理解操作系统内核对异步事件的处理方法
3、掌握中断处理编程的方法
4、掌握内核中断处理代码组织的设计方法
5、了解查询式I/O控制方式的编程方法
1、知道PC系统的中断硬件系统的原理
2、掌握x86汇编语言对时钟中断的响应处理编程方法
3、重写和扩展实验三的的内核程序,增加时钟中断的响应处理和键盘中断响应。
4、编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
(1)编写x86汇编语言对时钟中断的响应处理程序:设计一个汇编程序,在一段时间内系统时钟中断发生时,屏幕变化显示信息。在屏幕24行79列位置轮流显示’|’、’/’和’\’(无敌风火轮),适当控制显示速度,以方便观察效果,也可以屏幕上画框、反弹字符等,方便观察时钟中断多次发生。将程序生成COM格式程序,在DOS或虚拟环境运行。
(2)重写和扩展实验三的的内核程序,增加时钟中断的响应处理和键盘中断响应。,在屏幕右下角显示一个转动的无敌风火轮,确保内核功能不比实验三的程序弱,展示原有功能或加强功能可以工作.
(4) 扩展实验三的的内核程序,但不修改原有的用户程序,实现在用户程序执行期间,若触碰键盘,屏幕某个位置会显示”OUCH!OUCH!”。
(5)编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
x86中断调用允许我们用int来进行编程级别的中断调用, 这个调用可以是bios调用, 用于读字符和写扇区等, 也可以是用户自己定义的中断. 我们知道中断处理程序会在保留现场后去查中断向量表, 找到并跳转到中断服务程序. 我们要做的就是通过自定义中断服务程序并把它烧入中断向量表, 这样就能随时使用int执行我们想要的中断.
我们先尝试按照上面所说, 重写int 8h中断, 实现基于计数器的风火轮程序. 这个计数器会在每次中断时减一, 如果减到零就会让风火轮发生改变(写字符到显存). 这里我们直接在之前的弹跳程序里做改动, 让程序执行过程中还会感知时钟变化.
user.asm(部分)
BITS 16
org 0100h
%macro setIVT 2
push es
push si
mov ax, 0000H
mov es, ax
mov ax, %1
mov bx, 4
mul bx
mov si, ax
mov ax, %2
mov [es:si], ax
add si, 2
mov ax, cs
mov [es:si], ax
pop si
pop es
%endmacro
_init:
call Reset
mov ax,0B800h ; 文本窗口显存起始地址
mov gs,ax ; GS = B800h
…
setIVT 8, int8 ;自定义8号中断
Entrance:
…
int8:
cli ;关中断
pusha ;保存寄存器
dec dword[Timer]
jnz int8end
mov dword[Timer],timerdelay
call spin ;打印风火轮
int8end:
mov al, 20H
out 20H, al
out 0a0H, al
popa ;恢复寄存器
sti ;开中断
iret
spin:
inc word[wheel_idx]
mov ax, 4
cmp ax, word[wheel_idx]
jne WriteWheel
mov ax, 0
mov word[wheel_idx], ax
WriteWheel:
mov bx, wheel
add bx, word[wheel_idx]
mov al, byte[bx]
mov ah, 0Fh
mov [gs:((80*23+78)*2)],ax
ret
wheel: db "-\|/"
wheel_idx dw 0
timerdelay equ 20000
Timer dd timerdelay
这样的一个程序实现每100k个时间单位就会让弹球移动一次, 同时, 每一个时间单位都会让中断计数器减一, 如果减到0就让风火轮旋转45度, 这里设定计数器初始值是20k. 在dos仿真环境测试程序, 可以看到
屏幕右下角的风火轮和上面弹球的更新拥有异步的时序, 风火轮的刷新频率是弹球的5倍.
我们除了上面的感知时钟信号的时钟中断以外还希望实现能够感知键盘的键盘中断, 这个技巧我们之前已经实现过了, 我们用重定义int 20h的方法, 让系统在运行用户程序时, 能够在键盘输入ctrl+z时返回内核. 这里我们也可以用类似的方法自定义感知任意键盘输入的中断.
此外, 为了更好地显示内核的控制权, 我们把用户程序设计成每次执行一个时间单位后返回, 这样就能在内核中用循环观察到同样的效果, 而且如果在循环中使用键盘中断和时钟中断就能起到监控的效果. 一旦出现键盘中断就在屏幕上显示一些信息, 时钟中断可以用来控制我们的信息显示时长. 综上, 我们可以设计这样的中断用于执行.
kernel.asm(部分)
_start:
setIVT 8h, int8h
setIVT 21h, int21h
setIVT 20h, int20h
mov ax,0B800h ; 文本窗口显存起始地址
mov gs,ax ; GS = B800h
_CALL cs, main
jmp $
;void CallProgram()
CallProgram:
dec dword[count]
jz Ent
int 8h
int 20h
int 21h
cmp word[exitflag], 1
je ExitProgram ;如果识别到int 20h传来的返回信息, 则退出程序
jmp CallProgram
Ent:
mov dword[count],delay
_CALL cs, OffSetOfUserPrg
jmp CallProgram
ExitProgram:
mov dword[count],delay
dec word[exitflag]
retf;
; 自定义的中断号
int20h:
mov ah, 01h ;缓冲区检测
int 16h
jz noclick ;缓冲区无按键
mov dword[ouchcount], ouchdelay
print ouch, 10, 20, 60
mov ah, 00h
int 16h
cmp ax, 2c1ah ; 检测Ctrl + Z
jne noclick
inc word[exitflag] ;只是作为一个标志
iret
noclick:
iret
;------------------------------
int21h:
cmp dword[ouchcount], 0
je ret21h
dec dword[ouchcount]
jz clearouch
jmp ret21h
clearouch:
print space, 10, 20, 60
ret21h:
iret
;------------------------------
其中int20h, 我们每次调用它都会读键盘缓冲区, 如果键盘缓冲区有字符就
我们可以编写特殊的用户服务程序, 来让单个用户程序实现我们之前四个用户程序所实现的功能. 我们之前的四个程序是在四个子屏幕执行弹球逻辑, 并显示不同的字符串.
那么怎样用中断的形式, 改变用户程序的内部值呢? 一般我们的内核是无法直接访问用户程序的变量的, 所以我们需要在用户程序内使用中断, 参考int 10h的字符串输出调用, 我们传递字符串的地址, 打印位置, 字符串长度和颜色等等, 都是通过寄存器来传参的. 既然如此我们也可以用寄存器来传递要修改的变量地址, 为了一个程序顶四个程序用, 我的用户程序需要修改的变量有四个, 字符串, 字符串长度, 还有弹球要反弹的上下左右四个边界, 那么我们就把该修改的变量直接压栈, 并存下ds. 存储完毕后我们调用中断, 中断服务程序把这些地址对应的变量数值修改成我们需要的数值. 修改完毕后, 用户程序就能表现出多样性.
user.asm(部分)
INITDATA:
cmp word[intflag], 0
je _intdata1
jmp _intdata2
INITXY:
cmp word[initflag], 0
je _init
_intdata1:
mov ax, Message
push ax
mov bx, Strlen
push bx
mov ax, ds
int 33h
pop bx
pop ax
inc word[intflag]
jmp INITXY
_intdata2:
mov ax, Message
push ax
mov bx, Strlen
push bx
mov ax, ds
int 34h
pop bx
pop ax
dec word[intflag]
jmp INITXY
kernel.asm(部分)
int33h:
push es
push si
mov es, ax
mov ax, word[esp+12]
mov si, ax
mov byte[es:si], 'a'
mov ax, word[esp+10]
mov si, ax
mov byte[es:si], 1
pop si
pop es
iret
int34h:
push es
push si
mov es, ax
mov ax, word[esp+12]
mov si, ax
mov byte[es:si], 'b'
mov ax, word[esp+10]
mov si, ax
mov byte[es:si], 1
pop si
pop es
iret
这样的用户程序交替执行int 33h和34h中断, 中断会直接修改字符串内容和长度, 从而我们可以在执行用户程序时看到下面的输出.
当然我们也可以每调用一次中断就让程序切换到新的逻辑.
user.asm
BITS 16
org 0100h
%macro print 4 ; string, length, x, y
push ds
push es
push bp
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov bp, %1
mov cx, %2
mov ax, 1301h
mov bx, 000fh
mov edx,0
mov dh, %3
mov dl, %4
int 10h
pop bp
pop es
pop ds
%endmacro
INITDATA:
dec word[count]
jz _intdata
INITXY:
cmp word[initflag], 0
je _init
Entrance:
jmp BoundaryCheckx ;边界检测与速度更新
DispStr:
print Message,word[Strlen],byte[x],byte[y]
Updatexy:
mov al, byte[x]
add al, byte[vx]
mov byte[x], al
mov al, byte[y]
add al, byte[vy]
mov byte[y], al
jmp Exit ;退出
BoundaryCheckx:
mov al, byte[x]
add al, byte[vx] ;预测下一刻的x
cmp al, byte[upper] ;如果x小于上边界
jl Changevx ;更新vx
cmp al, byte[lower] ;如果x大于下边界
jg Changevx ;更新vx
BoundaryChecky:
mov al, byte[y]
add al, byte[vy]
cmp al, byte[left] ;如果y小于左边界
jl Changevy ;更新vy
add al, byte[Strlen];预测下一刻的yr=y+字符串长
cmp al, byte[right] ;如果yr大于下边界
jg Changevy ;更新vy
jmp DispStr ;如果不需要更新vx vy就继续打印流程
Changevx:
neg byte[vx]
jmp BoundaryChecky
Changevy:
neg byte[vy]
jmp DispStr
_init:
mov al, byte[upper]
add al, 5
mov byte[x],al
mov al, byte[left]
add al, 5
mov byte[y],al
inc word[initflag]
jmp Entrance
_intdata:
mov ax, left
push ax
mov ax, Strlen
push ax
mov ax, Message
push ax
mov word[count],20
mov word[initflag], 0
cmp word[intflag], 3
jle _intdata2
mov word[intflag], 0
_intdata2:
mov ax, ds
cmp word[intflag], 0
je call33h
cmp word[intflag], 1
je call34h
cmp word[intflag], 2
je call35h
cmp word[intflag], 3
je call36h
_intdata3:
pop ax
pop ax
pop ax
inc word[intflag]
jmp INITXY
call33h:
int 33h
jmp _intdata3
call34h:
int 34h
jmp _intdata3
call35h:
int 35h
jmp _intdata3
call36h:
int 36h
jmp _intdata3
Exit:
retf
Message: db "17310031"
Strlen dw $-Message
initflag dw 0
intflag dw 0
count dw 1
vx db 1
vy db 1
left db 0
upper db 1
right db 39
lower db 12
x db 0
y db 0
times 510-($-$$) db 0
dw 0xaa55
kernel.asm(部分)
int33h:
push es
push si
mov es, ax
mov ax, word[esp+10]
mov si, ax
mov byte[es:si], 'a'
mov ax, word[esp+12]
mov si, ax
mov byte[es:si], 1
mov ax, word[esp+14]
mov si, ax
mov byte[es:si], 0
inc si
mov byte[es:si], 1
inc si
mov byte[es:si], 39
inc si
mov byte[es:si], 12
pop si
pop es
iret
int34h:
push es
push si
mov es, ax
mov ax, word[esp+10]
mov si, ax
mov byte[es:si], 'b'
inc si
mov byte[es:si], 'c'
mov ax, word[esp+12]
mov si, ax
mov byte[es:si], 2
mov ax, word[esp+14]
mov si, ax
mov byte[es:si], 40
inc si
mov byte[es:si], 1
inc si
mov byte[es:si], 79
inc si
mov byte[es:si], 12
pop si
pop es
iret
int35h:
push es
push si
mov es, ax
mov ax, word[esp+10]
mov si, ax
mov byte[es:si], 'B'
inc si
mov byte[es:si], 'C'
mov ax, word[esp+12]
mov si, ax
mov byte[es:si], 2
mov ax, word[esp+14]
mov si, ax
mov byte[es:si], 0
inc si
mov byte[es:si], 13
inc si
mov byte[es:si], 39
inc si
mov byte[es:si], 24
pop si
pop es
iret
int36h:
push es
push si
mov es, ax
mov ax, word[esp+10]
mov si, ax
mov byte[es:si], 'A'
mov ax, word[esp+12]
mov si, ax
mov byte[es:si], 1
mov ax, word[esp+14]
mov si, ax
mov byte[es:si], 40
inc si
mov byte[es:si], 13
inc si
mov byte[es:si], 79
inc si
mov byte[es:si], 24
pop si
pop es
iret
可以看到同一个程序执行四种逻辑的屏幕输出, 而且两个程序之间有速度的继承关系.
实验比起上个实验的难度减少了不少, 不需要太多踩坑的过程. 主要要掌握的技术是通过编程直接修改IVT表来自定义中断, 又因为中断的全局性, 我们可以在任意子程序内使用中断.
中断比起一般的函数call做了一些其他的工作, 它会进行标志寄存器的压栈, 进行查表来跳转到目标中断服务程序. 如果要把服务程序作为一般的函数用栈传参, 则需要一些额外的处理.