操作系统实验:具有中断处理的内核

 Max.C     2020-05-31   7042 words    & views

一、实验题目

实验四:具有中断处理的内核

二、实验目的

  1. PC系统的中断机制和原理
  2. 理解操作系统内核对异步事件的处理方法
  3. 掌握中断处理编程的方法
  4. 掌握内核中断处理代码组织的设计方法
  5. 了解查询式I/O控制方式的编程方法

三、实验要求

  1. 知道PC系统的中断硬件系统的原理。
  2. 掌握x86汇编语言对时钟中断的响应处理编程方法。
  3. 重写和扩展实验三的的内核程序,增加时钟中断的响应处理和键盘中断响应。
  4. 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性。

实验内容

  1. 编写x86汇编语言对时钟中断的响应处理程序:设计一个汇编程序,在一段时间内系统时钟中断发生时,屏幕变化显示信息。在屏幕24行79列位置轮流显示’ ’、’/’和’\’(无敌风火轮),适当控制显示速度,以方便观察效果,也可以屏幕上画框、反弹字符等,方便观察时钟中断多次发生。将程序生成COM格式程序,在DOS或虚拟环境运行。
  2. 重写和扩展实验三的的内核程序,增加时钟中断的响应处理和键盘中断响应,在屏幕右下角显示一个转动的无敌风火轮,确保内核功能不比实验三的程序弱,展示原有功能或加强功能可以工作。
  3. 扩展实验三的的内核程序,但不修改原有的用户程序,实现在用户程序执行期间,若触碰键盘,屏幕某个位置会显示”OUCH!OUCH!”。
  4. 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性。

四、实验方案

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中,包含子文件夹exampleos

  • example:时钟中断COM程序相关程序;
  • os:独立内核相关文件。

五、实验过程

1、时钟中断响应处理程序

外部中断处理涉及中断向量表IVT、标志寄存器,在中断处理过程有对栈的操作,所以,设计出来的处理程序有两大部分:

  1. 中断向量表IVT的修改,设置时钟中断向量(08h);
  2. 新的时钟中断处理过程;

实模式中,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运行,显示效果如下,风火轮在最右侧来回跳动:

5

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]形式,将部分语法细节修改一下即可通过编译,成功运行。

由于中断过程了段寄存器dses,在中断的开始阶段将其压栈,结束时出栈,保护原本的段寄存器值。(如果不加上这一步,在用户程序执行时调用中断会出错)

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代码中函数进行了重构,减少了部分没必要的函数,增加了strcmpstrlen等函数,便于对字符串进行处理;
  • 优化了帮助信息、程序信息的显示:
  • 使用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会随着键盘输入在最下方来回移动,风火轮会随着时钟中断在最右侧上下移动。

9

10

六、实验总结

中断处理过程中有不少细节需要注意:

  • 中断过程中如果需要使用段寄存器,需要将段寄存器的原值依次入栈,在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中断系统原理与编程