一、实验题目
实验四:具有中断处理的内核
二、实验目的
- PC系统的中断机制和原理
- 理解操作系统内核对异步事件的处理方法
- 掌握中断处理编程的方法
- 掌握内核中断处理代码组织的设计方法
- 了解查询式I/O控制方式的编程方法
三、实验要求
- 知道PC系统的中断硬件系统的原理。
- 掌握x86汇编语言对时钟中断的响应处理编程方法。
- 重写和扩展实验三的的内核程序,增加时钟中断的响应处理和键盘中断响应。
- 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性。
实验内容
-
编写x86汇编语言对时钟中断的响应处理程序:设计一个汇编程序,在一段时间内系统时钟中断发生时,屏幕变化显示信息。在屏幕24行79列位置轮流显示’ ’、’/’和’\’(无敌风火轮),适当控制显示速度,以方便观察效果,也可以屏幕上画框、反弹字符等,方便观察时钟中断多次发生。将程序生成COM格式程序,在DOS或虚拟环境运行。 - 重写和扩展实验三的的内核程序,增加时钟中断的响应处理和键盘中断响应,在屏幕右下角显示一个转动的无敌风火轮,确保内核功能不比实验三的程序弱,展示原有功能或加强功能可以工作。
- 扩展实验三的的内核程序,但不修改原有的用户程序,实现在用户程序执行期间,若触碰键盘,屏幕某个位置会显示”OUCH!OUCH!”。
- 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性。
四、实验方案
1、实验环境
实验所使用的计算机配置为:
- CPU:Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
- RAM:8.00GB
- 系统:Windows 10 - x64 10.0.17763
使用的开发工具如下:
- Oracle VM VirtualBox - 6.0.14 r133895 (Qt5.6.2):虚拟机软件,相比起VMware,有着小巧轻便的特点,功能也很齐全。用于创建、运行虚拟机,创建虚拟软盘,引导虚拟软盘启动。
- WinHex - 19.6 x64:16进制文件编辑器,用于查看、编辑虚拟软盘文件的内容。
- Visual Studio Code - 1.44.2 :代码编辑器,可以根据需要配置插件,用于编辑代码。
- bochs:x86硬件平台的开源模拟器,可以在运行过程中停止,实验中用于跟踪寄存器、堆栈,调试虚拟机。
- DOSBox version 0.74-3:DOS 模拟程序,用于tcc、tasm等16位工具的使用,运行com文件。
- TCC+TASM+Tlink:本次实验使用的汇编和C组合环境,完成独立内核的实现。
- NASM version 2.14.02 :COM格式程序可以直接使用nasm编译。
2、实验思路
根据实验要求和相关知识,我们按照以下思路完成实验:
- 使用nasm完成COM时钟中断程序,学习中断机制和实现方法。
- 将com程序中的框架移植到内核中,此时需要使用tasm+tcc编程,为内核设计新的时钟中断和键盘中断。
- 对内核进行扩展完善。
3、程序设计
在时钟中断程序中,我们要实现右下角的无敌风火轮,尝试设计改变风火轮的位置,让风火轮在最右边(第79列)上下来回移动。
独立内核有时钟中断和键盘中断,尝试实现如下功能:
- 时钟中断:将上面的程序移植进内核;
- 键盘中断:第一次按下键盘,屏幕最下方会显示”OUCH!”,之后每当按下键盘时,”OUCH!”会在最下方来回移动。
内核完善:
- 实现滚屏;
- 对屏幕显示效果进行优化;
4、实验文件列表
文件位于/src
中,包含子文件夹example
,os
:
example
:时钟中断COM程序相关程序;os
:独立内核相关文件。
五、实验过程
1、时钟中断响应处理程序
外部中断处理涉及中断向量表IVT、标志寄存器,在中断处理过程有对栈的操作,所以,设计出来的处理程序有两大部分:
- 中断向量表IVT的修改,设置时钟中断向量(08h);
- 新的时钟中断处理过程;
在实模式中,IVT在内存的低位区(0~0x3FFH)共1KB的空间内,时钟中断向量08h的位置是从8*4 = 32 = 20h开始的4B,通过如下代码修改地址:
; 设置时钟中断向量(08h),初始化段寄存器
xor ax,ax ; AX = 0
mov es,ax ; ES = 0
mov word [es:20h],Timer ; 设置时钟中断向量的偏移地址
mov ax,cs
mov word [es:22h],ax ; 设置时钟中断向量的段地址=CS
mov ds,ax ; DS = CS
mov es,ax ; ES = CS
修改后,对时钟中断的响应会跳转至我们所编写的处理过程中,下面的过程涉及三个循环:
- 循环1:计数循环,由于时钟中断过快,设置为每四次中断进行一次无敌风火轮的显示,由
Timer
部分实现; - 循环2:显示循环,循环显示的字符为
char db '|\-/'
,使用计数变量num
令其循环显示; - 循环3:移动循环,为了让风火轮上下来回移动,使用变量
flag
表示移动方向,变量Y
表示纵坐标位置,并判断是否到达边界。
数据段:
data:
Y dw 0
flag dw 160
num dw 0
char db '|\-/'
count db delay ; 计时器计数变量,初值=delay
; 常数定义
delay equ 4 ; 计时器延迟计数
代码段:
; 时钟中断处理程序
Timer:
dec byte [count] ; 递减计数变量
jnz end ; >0:跳转
inc byte[num]
cmp byte[num], 4
jne Timer2
mov byte[num], 0
Timer2:
; 清除最后一行
mov ax,0600h
mov ch,0 ; 左上角的行号
mov cl,79 ; 左上角的列号
mov dh,24 ; 右下角的行号
mov dl,79 ; 右下角的行号
mov bh,07h ; 属性
int 10h
; 显示字符
mov bx, [num]
mov al, byte[char+bx] ; AL = 显示字符值(默认值为20h=空格符)
mov ah, 07h
mov bx, [Y]
mov [gs:(79*2+bx)],ax ; 更新显示字符
mov byte[count],delay ; 重置计数变量=初值delay
; 位置移动
mov bx, [flag]
add [Y], bx
xor bx, bx
cmp word [Y], -160
je int8jmp
cmp word [Y], 160*25
je int8jmp
end:
mov al,20h ; AL = EOI
out 20h,al ; 发送EOI到主8529A
out 0A0h,al ; 发送EOI到从8529A
iret ; 从中断返回
int8jmp:
neg word [flag]
mov bx, [flag]
add [Y], bx
mov bx, [flag]
add [Y], bx
jmp end
完成后使用nasm编译成com文件,使用DOSBox运行,显示效果如下,风火轮在最右侧来回跳动:
2、独立内核对时钟中断的响应处理
独立内核中的中断实现思路与com程序相同,将IVT设置写至函数_setIntr08
,时钟中断处理过程写至_Timer
即可,对中断向量表的修改在内核开始运行时执行:
start:
mov ax, cs
mov ds, ax ; DS = CS
mov es, ax ; ES = CS
mov ss, ax ; SS = CS
mov sp, 100h
; 光标设置
call _updateCursor
; 中断设置
call _setIntr08
call _setIntr09
;调用C的主函数
call near ptr _main
移植过程不需要改动代码框架,只需要根据tasm的语法将内存单元的[ds:num]
修改至ds:[num]
形式,将部分语法细节修改一下即可通过编译,成功运行。
由于中断过程了段寄存器ds
和es
,在中断的开始阶段将其压栈,结束时出栈,保护原本的段寄存器值。(如果不加上这一步,在用户程序执行时调用中断会出错)
3、独立内核对键盘中断的响应处理
键盘中断为09号中断,对应中断向量表的位置为从9*4 = 36 = 24h开始的4B,写一个函数_setIntr09
用于修改中断向量表。这次我们需要将原本的09号中断向量保存起来(保存到0:200h
位置,即紧接在中断向量表之后),在新的键盘中断中还需要用到它。
; 设置键盘中断向量09h
_setIntr09:
xor ax,ax ; AX = 0
mov es,ax ; ES = 0
mov ax,es:[24h]
mov es:[200h],ax
mov ax,es:[26h]
mov es:[202h],ax
mov es:[24h], offset _Keyboard
mov ax,cs
mov es:[26h], ax ; 设置时钟中断向量的段地址=CS
mov ds,ax ; DS = CS
mov es,ax ; ES = CS
ret
我的设计思路是——按下键盘时,”OUCH!”会在最下方左右来回移动,实现思路与时钟中断的来回移动相似,这里也不多赘述;
; 键盘中断处理程序
_Keyboard:
push ds
push es
mov ax,cs
mov ds,ax ; DS = CS
mov ax, 0
mov es, ax ; ES = 0B800h
; 调用原本的中断
pushf
call dword ptr es:[200h]
; 清除当前行
mov ax,0600h
mov ch,24 ; 左上角的行号
mov cl,0 ; 左上角的列号
mov dh,24 ; 右下角的行号
mov dl,78 ; 右下角的行号
mov bh,07h; 属性
int 10h
; 显示ouch
mov ax, 0B800h
mov es, ax ; ES = 0B800h
mov bx, ds:[ouch]
mov ah, 07h
mov al, 'O'
mov es:[((80*24+0)*2)+bx],ax ; 更新显示字符
mov al, 'U'
mov es:[((80*24+1)*2)+bx],ax ; 更新显示字符
mov al, 'C'
mov es:[((80*24+2)*2)+bx],ax ; 更新显示字符
mov al, 'H'
mov es:[((80*24+3)*2)+bx],ax ; 更新显示字符
mov al, '!'
mov es:[((80*24+4)*2)+bx],ax ; 更新显示字符
mov bx, ds:[flag]
add ds:[ouch], bx
xor bx, bx
cmp ds:[ouch], -2
je int9jmp
cmp ds:[ouch], 150
je int9jmp
int9end:
pop es
pop ds
mov al, 20h ; AL = EOI
out 20h, al ; 发送EOI到主8529A
out 0A0h,al ; 发送EOI到从8529A
iret ; 从中断返回
int9jmp:
neg ds:[flag]
mov bx, ds:[flag]
add ds:[ouch], bx
mov bx, ds:[flag]
add ds:[ouch], bx
jmp int9end
在一开始的处理程序中,我没有调用原本的中断,导致内核只接受了一次键盘中断,就不再响应任何键盘输入。通过查询9号中断的资料,得知需要调用原本的中断,对键盘输入进行一定的处理,才能响应新的键盘输入。所以加上了这一段代码:
由于原本的中断是iret
结束,所以我们需要返回时保证栈中有IP、CS、FLAGS,因此,先用pushf
将标志寄存器压栈,然后使用call跳转。
; 调用原本的中断
pushf
call dword ptr es:[200h]
完成后编译、写盘,风火轮和OUCH都成功显示。
4、内核完善
这次实验,我也将实验三的独立内核做了一些完善,包括以下几方面:
- 将C代码中函数进行了重构,减少了部分没必要的函数,增加了
strcmp
、strlen
等函数,便于对字符串进行处理; - 优化了帮助信息、程序信息的显示:
- 使用int 10h实现了滚屏,当指令到达最后一行时,再次输入会进行滚屏显示;清屏需要使用命令
clear
实现;
清屏:
_clear proc near
push bp
; 清屏
mov ax,0600h
mov ch,0 ; 左上角的行号
mov cl,0 ; 左上角的列号
mov dh,23 ; 右下角的行号
mov dl,79 ; 右下角的行号
mov bh,07h; 属性
int 10h ; 中断调用,清屏
; 光标设置
mov ds:[_X], 0
mov ds:[_Y], 0
call _updateCursor
pop bp
ret ; 返回C程序
_clear endp
滚屏:
_roll proc near
push bp
; 清屏
mov ax,0601h
mov ch,0 ; 左上角的行号
mov cl,0 ; 左上角的列号
mov dh,23 ; 右下角的行号
mov dl,79 ; 右下角的行号
mov bh,07h; 属性
int 10h ; 中断调用,清屏
; 光标设置
add ds:[_Y], -1
call _updateCursor
pop bp
ret ; 返回C程序
_roll endp
- 对光标进行了更加明确的管理:使用
_X db 0
和_Y db 0
实时记录光标的位置;可以对其赋值,并调用_updateCursor
手动更新光标位置。
; 汇编代码中手动更新光标位置
mov ds:[_X], 0
mov ds:[_Y], 0
call _updateCursor
// C代码中手动更新光标位置
X = 0;
Y = 0;
updateCursor();
updateCurso
代码:
_updateCursor proc near
; 更新光标位置
push bp
mov ah, 02h
mov bh, 0 ; 页号
mov dh, ds:[_Y] ; 行
mov dl, ds:[_X] ; 列
int 10h ; BIOS的10h
pop bp
ret ; 返回
_updateCursor endp
5、运行结果
OUCH会随着键盘输入在最下方来回移动,风火轮会随着时钟中断在最右侧上下移动。
六、实验总结
中断处理过程中有不少细节需要注意:
- 中断过程中如果需要使用段寄存器,需要将段寄存器的原值依次入栈,在
iret
之前依次取出恢复,才能保证从不同段起始地址都可以进入中断处理并正确运行。(比如独立内核和用户程序的段起始地址不同,需要都能成功执行中断处理过程) - 中断的跳转机制:中断指令int外部中断会将IP、CS和FLAGS压栈,中断结束
iret
返回时需要保证栈顶依次为IP、CS和FLAGS。 - 对中断向量表进行修改时,如果有必要,注意将原中断向量进行保存。
在实验中遇到的问题有两个:一个是在中断中没有修改ds
的值,导致其访问内存单元出错;经过调试,最后中断处理过程使用了如下的代码,保证了中断执行过程的正确性、能够正确返回源程序且返回后能正常运行。
start:
push ds
push es
; code.....
end:
pop es
pop ds
mov al, 20h ; AL = EOI
out 20h, al ; 发送EOI到主8529A
out 0A0h,al ; 发送EOI到从8529A
iret ; 从中断返回
另一个问题是键盘中断的问题,如果没有在新的键盘中断中调用原键盘中断,就不再响应任何键盘输入。猜测是原中断的某些操作在新的中断中没有执行,结合查找到的关于int 9h
的资料,得知需要使用原键盘中断。最后完成了保存、调用原中断的修改,成功完成新的键盘中断。
七、参考文献
- TurboC的命令行编译方法与链接
- PC中断系统原理与编程