實驗目的:
1、瞭解監控程序執行用戶程序的主要工作
2、瞭解一種用戶程序的格式與運行要求
3、加深對監控程序概念的理解
4、掌握加載用戶程序方法
5、掌握幾個BIOS調用和簡單的磁盤空間管理
實驗要求:
1、知道引導扇區程序實現用戶程序加載的意義
2、掌握COM/BIN等一種可執行的用戶程序格式與運行要求
3、將自己實驗一的引導扇區程序修改爲3-4個不同版本的COM格式程序,每個程序縮小顯示區域,在屏幕特定區域顯示,用以測試監控程序,在1.44MB軟驅映像中存儲這些程序。
4、重寫1.44MB軟驅引導程序,利用BIOS調用,實現一個能執行COM格式用戶程序的監控程序。
5、設計一種簡單命令,實現用命令交互執行在1.44MB軟驅映像中存儲幾個用戶程序。
6、編寫實驗報告,描述實驗工作的過程和必要的細節,如截屏或錄屏,以證實實驗工作的真實性
實驗內容:
(1) 將自己實驗一的引導扇區程序修改爲一個的COM格式程序,程序縮小顯示區域,在屏幕第一個1/4區域顯示,顯示一些信息後,程序會結束退出,可以在DOS中運行。在1.44MB軟驅映像中制定一個或多個扇區,存儲這個用戶程序a。
相似地、將自己實驗一的引導扇區程序修改爲第二、第三、第四個的COM格式程序,程序縮小顯示區域,在屏幕第二、第三、第四個1/4區域顯示,在1.44MB軟驅映像中制定一個或多個扇區,存儲用戶程序b、用戶程序c、用戶程序d。
(2) 重寫1.44MB軟驅引導程序,利用BIOS調用,實現一個能執行COM格式用戶程序的監控程序。程序可以按操作選擇,執行一個或幾個用戶程序。解決加載用戶程序和返回監控程序的問題,執行完一個用戶程序後,可以執行下一個。
(3)設計一種命令,可以在一個命令中指定某種順序執行若干個用戶程序。可以反覆接受命令。
(4)在映像盤上,設計一個表格,記錄盤上有幾個用戶程序,放在那個位置等等信息,如果可以,讓監控程序顯示出表格信息。
(5)拓展自己的軟件項目管理目錄,管理實驗項目相關文檔
獲取用戶輸入和讀扇區的bios中斷調用
上面的任務要求中包含了根據用戶的操作選擇要執行的用戶COM程序。爲此我們要在引導扇區以外的扇區存儲我們的abcd四個子程序,在需要執行時,boot應該能把對應的子程序放入內存,並用跳轉指令執行。
BIOS的中斷int 16h滿足我們的獲取用戶輸入要求,ah=0時,有
從鍵盤讀入字符送AL寄存器。執行時,等待鍵盤輸入,一旦輸入,字符的ASCII碼放入AL中。若AL=0,則AH爲輸入的擴展碼。
BIOS的中斷int 13h滿足我們的讀扇區要求,設ah=02h,則有
al = 要讀扇區數,ch=柱面(磁道)號,cl=起始扇區號,dh=磁頭號,dl=驅動器號(0表示A盤),ea:bx->數據緩衝區。
我們需要自己定義ch、cl和dh,這個在1.44MB的軟盤上是有給定的公式的
這裏因爲我們的代碼量並不多,我們會把它裝在前幾個扇區方便讀取。我們先嚐試一下使用上面兩個中斷讀取特殊的扇區內容,並把它們打印出來。
input_read_test.asm
OffSetOfUserStr equ 0A100h
org 7c00h
%macro print 4 ; string, length, x, y
mov ax, cs
mov ds, ax
mov bp, %1
mov ax, ds
mov es, ax
mov cx, %2
mov ah, 13h
mov al, 00h
mov bh, 00h
mov bl, 07h ; 黑底白字
mov dh, %3
mov dl, %4
int 10h
%endmacro
datadef:
msg db 'This is a test program. Enter 1~4 to get 10 ascii from target sector.'
msglen equ ($-msg)
msg1 db 'Press any key for continue.'
msglen1 equ ($-msg1)
sectorNum db '1'
Entrance:
call cls
print msg, msglen, 0, 0
mov ah, 0
int 16h ;調用16h中斷,從鍵盤獲取ascii碼輸入並存在al中
cmp al, '1'
jl Entrance
cmp al, '4' ;避免無效鍵盤輸入
jg Entrance
mov [sectorNum], al ;獲得鍵盤輸入的扇區號
mov cl, [sectorNum]
sub cl, '0'-1 ;讀扇區號
mov ax, cs ;定位es
mov es, ax
mov ah, 2 ;讀扇區
mov al, 1 ;讀扇區數
mov dl, 0 ;驅動器號
mov dh, 0 ;磁頭號
mov ch, 0 ;柱面號
mov bx, OffSetOfUserStr ;數據緩衝區
int 13H
print OffSetOfUserStr, 10, 1, 0
print msg1, msglen1, 2, 0
mov ah, 0
int 16h
jmp Entrance
cls:
mov ax, 0B800h
mov es, ax
mov si, 0
mov cx, 80*25 ; 循環次數
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
mov ax, cs
mov ds, ax
mov es, ax
times 510-($-$$) db 0
dw 0xaa55
藉助中斷調用和簡單循環,程序可以讀取目標扇區的內容並打印出來。這裏我把首扇區後的四個扇區用a、b、c和d填滿,在程序的執行中會詢問用戶要訪問的扇區號(1-4),然後程序把一個扇區的512字節都拉到內存的0A100位置,然後打印前10個字符。
執行程序與監控行爲
執行程序事實上比較簡單,只需要在上面的代碼做一些改動,讓程序jmp到我們的內存目標位置運行即可。但是如何用主程序監控子程序的運行是需要技巧的,畢竟我們的編程仍然是串行編程,而我們又不方便在子程序的代碼上直接做更改來實現監控功能(這是編譯器纔會做的事情)。這時我們需要用到的技巧是自定義中斷。
bios的中斷執行會從以0000h爲起點的一張中斷向量表中查找中斷,每個中斷向量佔四字節。2字節用做偏移,2字節用做基址。而bios有很多空的向量可以留給我們自己定義,我們可以在特定的位置裝入特殊的地址作爲中斷服務程序的偏移地址。
我們把上面的程序讀扇區程序做一些改動,在讀入扇區並打印10個ascii字符後就陷入無限循環的沉睡,沉睡過程中我們會不斷調用一種我們自己設計的中斷,檢測是否有CTRL+Z的組合鍵,如果有才跳出循環,回到主界面再次請示用戶要不要讀扇區。
鍵盤輸入在無即時性的輸入請求時,會先放入緩衝區。我們的中斷服務程序做的事情就是掃描鍵盤緩衝區,判斷是不是CTRL+Z的組合鍵,如果是就退出死循環並回到入口。
至於CTRL+Z的掃描碼是多少我們並不知道,但是我們可以寫個程序看一下。我們運行一下int 16h再輸入CTRL+Z,查看ax寄存器的值爲2c1ah,則我們在做判斷時只需要cmp ax和2c1ah即可。
self_defined_int.asm(片段)
mov ax, 0000h ; 中斷向量表從0h開始
mov es, ax
mov ax, 20h ; 重定義20號中斷
mov bx, 4 ; 每個中斷向量是4字節的地址,所以20h乘以4
mul bx
mov si, ax ; 偏移
mov ax, int20h
mov [es:si], ax ; 把我們的int20的偏移地址作爲20號中斷的中斷服務程序
add si, 2
mov ax, cs ; 放入代碼段, 符合中斷向量的格式(前兩字節爲偏移,後兩字節爲代碼段,且爲大端序)
mov [es:si], ax
int20h:
mov ah, 01h ;緩衝區檢測
int 16h
jz noclick ;緩衝區無按鍵
mov ah, 00h
int 16h
cmp ax, 2c1ah ; 檢測Ctrl + Z
jne noclick
jmp Entrance ; 如果是則退出程序
noclick:
iret
可以看見,如果不按這個組合鍵,程序就只能被卡在這裏。而使用組合鍵後就能跳出循環回到主界面,這個功能就是我們的監控程序監控程序運行的方式。我們在子程序中的循環中放置int 20h的中斷請求,這個中斷請求在循環中反覆查看鍵盤緩衝區,直到緩衝區中出現CTRL+Z的組合鍵,程序纔會終止運行。
COM程序執行時的基址和偏移
我們在用上面的13h調用裝入用戶程序之後,會把程序裝到我們指定的內存位置OffSetOfUserPrg=0100h。那麼我們在子程序執行時要怎麼訪問內存中的數據呢?
我們要搞明白的是ORG是什麼,它是一個彙編僞指令,使用的目的是告訴編譯器這段代碼被放在哪裏了。有了這個org,我們在這個子程序中直接訪問內存時,會加上一個固有的偏移。也就是說,org只有在我們需要認爲讀寫內存時纔會生效。以實驗一爲例,bios識別到引導扇區以後,會強行把這段代碼放入內存的7c00h以下的512字節。我們的引導扇區代碼中會有一些數據聲明,比如name db ‘huangwx’。name和代碼摻在一起,是一個偏移地址。org 7c00h給出的是基址地址,這就讓程序執行時,使用的是基址地址+偏移地址來找數據。比如mov ax,[02h],就是把7c00h+02h的16字節的數據放入ax寄存器。
搞懂org是什麼以後,上面的問題就變得不是問題了。我們的監控程序把目標扇區的COM程序裝入內存的0100h位置,如果不在編寫COM子程序時使用org 0100h,子程序的尋址就會出錯。
監控程序實踐
我們按照上面所述,編寫4個COM格式的用戶程序,並把它們塞入第2、3、4、5扇區等待監控程序讀取。這4個用戶程序在運行時定期執行int 20h號中斷來檢測用戶有沒有按下ctrl+z來中斷程序。
bootloader.asm
OffSetOfUserPrg equ 00100h
org 7c00h
%macro print 4 ; string, length, x, y
mov ax, cs
mov ds, ax
mov bp, %1
mov ax, ds
mov es, ax
mov cx, %2
mov ah, 13h
mov al, 00h
mov bh, 00h
mov bl, 07h ; 黑底白字
mov dh, %3
mov dl, %4
int 10h
%endmacro
; 這裏我們做一個字符串打印的封裝簡化代碼
mov ax, 0000h ; 中斷向量表從0h開始
mov es, ax
mov ax, 20h ; 重定義20號中斷
mov bx, 4 ; 每個中斷向量是4字節的地址,所以20h乘以4
mul bx
mov si, ax ; 偏移
mov ax, int20h
mov [es:si], ax ; 把我們的int20的偏移地址作爲20號中斷的中斷服務程序
add si, 2
mov ax, cs ; 放入代碼段, 符合中斷向量的格式(前兩字節爲偏移,後兩字節爲代碼段,且爲大端序)
mov [es:si], ax
begin:
call cls
print msg, msglen, 0, 0
input:
mov ah, 0
int 16h ;調用16h中斷,從鍵盤獲取ascii碼輸入並存在al中
cmp al, '1'
jl input
cmp al, '4' ;避免無效鍵盤輸入
jg input
mov [sectorNum], al ;獲得鍵盤輸入的扇區號
call cls
print msg1, msglen1, 0, 0
print sectorNum, 1, 0, 16
mov cl, [sectorNum]
sub cl, '0'-1 ;從第二個扇區開始
mov ax, cs ;定位es
mov es, ax
mov ah, 2 ;讀扇區
mov al, 1 ;讀扇區號
mov dl, 0 ;驅動器號
mov dh, 0 ;磁頭號
mov ch, 0 ;柱面號
mov bx, OffSetOfUserPrg ;數據緩衝區
int 13H
jmp OffSetOfUserPrg ;跳轉去執行目標程序
cls:
mov ax, 0B800h
mov es, ax
mov si, 0
mov cx, 80*25 ; 循環次數
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
; 這段代碼把B800h(顯存)下的一段內存清空,起清屏作用
; 自定義的中斷號
int20h:
mov ah, 01h ;緩衝區檢測
int 16h
jz noclick ;緩衝區無按鍵
mov ah, 00h
int 16h
cmp ax, 2c1ah ; 檢測Ctrl + Z
jne noclick
jmp begin ; 如果是則退出程序
noclick:
iret
datadef:
msg db 'Welcome to BootLoader, press 1~4 to run a program.'
msglen equ ($-msg)
msg1 db 'This is program 0, press Ctrl + Z to return.'
msglen1 equ ($-msg1)
sectorNum db '1'
times 510-($-$$) db 0
dw 0xaa55
用戶程序a.asm, 在左上角的1/4窗口實行彈球邏輯
org 00100h
mov ax, cs
mov ds, ax
mov es, ax
mov al, byte[upper]
add al, 5
mov byte[x],al
mov al, byte[left]
add al, 5
mov byte[y],al
loop1:
dec word[count] ; 遞減計數變量
jnz loop1 ; >0:跳轉;
mov word[count],delay
dec word[dcount] ; 遞減計數變量
jnz loop1
mov word[count],delay
mov word[dcount],ddelay
; 以上是用一個二重循環實現時延50000*580個單位時間
jmp Entrance ;進行一個週期的工作
jmp $ ;halt
Entrance:
int 20h
jmp BoundaryCheckx
DispStr:
call Clear
call Reset
mov ax, Message ;打印字符串
mov bp, ax
mov cl, byte[Strlen] ;字符串長
mov ch, 0
mov ax, 01301h ;寫模式
mov bx, 000fh ;頁號0,黑底白字
mov dh, byte[x] ;行=x
mov dl, byte[y] ;列=y
int 10h ;10h號接口
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 loop1 ;無限循環
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
Clear:
mov ax, 0B800h
mov es, ax
mov si, 160
mov cx, 80*24 ; 循環次數
mov dx, 0
clsLoop:
mov [es:si], dx
add si, 2
loop clsLoop
ret
;通過直接修改顯存實現的清屏函數
Reset:
mov ax, cs
mov ds, ax
mov ax, ds
mov es, ax
ret
;把cs ds和es指向相同的內存
Message: db "17310031"
Strlen db $-Message
delay equ 50000
ddelay equ 2000
count dw delay
dcount dw ddelay
clearcount db 0
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 512-($-$$) db 0
運行結果
用戶程序運行的任意時刻按下組合鍵都可以中斷程序運行並jmp回主監控程序. 到此, 最基本的監控程序就實現完畢了.
拓展性工作.
- 這裏我們的程序只有不到512字節, 當超過512字節時我們要做的可能是要讀取多個扇區, 比如以鏈式訪問的形式訪問多個不同磁盤位置的扇區, 並把它們裝入內存中連續的一段地址.
- 又或者, 我們可以在監控程序中設計按照一定順序執行幾個用戶程序的自動化監控程序. 比如定義新的中斷, 不再是接收用戶鍵盤組合鍵才停止運行, 而是用一個計數器標識識別什麼時候該觸發中斷, 返回監控程序.
- 再比如, 我們的用戶程序可以放在軟盤的任意扇區, 這個扇區不是我們人爲指定的而是機器指定的, 機器會把程序序號—扇區號的一一對應記錄在一張軟盤裏的表中. 監控程序運行前我們先把這張表加載到內存中, 然後用戶就不需要輸入扇區號才能裝填用戶程序, 只需要輸入用戶程序的序號, 監控程序就自動在內存中查詢到對應的扇區是哪一個