實驗四:具有中斷處理的內核
實驗目的:
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, 我們每次調用它都會讀鍵盤緩衝區, 如果鍵盤緩衝區有字符就
- 在屏幕上顯示一串”ouch!ouch!”的字符串, 併爲顯示它的延遲計數器賦值.
- 清掉緩衝區的這個字符, 如果該字符是ctrl+z就退出當前用戶程序的執行.
int21h是一個時鐘中斷, 它的計數器和int20h修改的計數器是同一個. - 檢測計數器的值是否爲0, 如果不爲0就減一.
- 如果當前剛好減到0, 那麼屏幕上就不應該再顯示”ouch!ouch!”了, 我們用一段空格覆蓋掉它.
然後我們把上面寫過的int8h也放入內核, 並把用戶程序改寫成每call一次執行一步, 就能在運行程序的基礎上, 看見屏幕右下角的風火輪旋轉和在任意時刻按下鍵盤, 都能看見屏幕顯示出ouch,ouch.
個性化用戶程序的服務程序
我們可以編寫特殊的用戶服務程序, 來讓單個用戶程序實現我們之前四個用戶程序所實現的功能. 我們之前的四個程序是在四個子屏幕執行彈球邏輯, 並顯示不同的字符串.
那麼怎樣用中斷的形式, 改變用戶程序的內部值呢? 一般我們的內核是無法直接訪問用戶程序的變量的, 所以我們需要在用戶程序內使用中斷, 參考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做了一些其他的工作, 它會進行標誌寄存器的壓棧, 進行查表來跳轉到目標中斷服務程序. 如果要把服務程序作爲一般的函數用棧傳參, 則需要一些額外的處理.