操作系統實驗一
從零開發能使用的操作系統需要的可能不只是C語言,還需要彙編的支持。我沒有接觸過X86彙編,不過我學過MIPS的彙編,兩者其實有很多相似之處,主要的差別應該是在X86的寄存器上。除此之外其實X86還有串操作的語句,方便我們直接操作字符串或數組。這裏我們做一個練手的項目,設計一個能讓字符串在屏幕上按照左上、左下、右上、右下四種方向自動彈動的程序。
另外,我們還希望能夠通過把這段彙編程序的可執行二進制程序裝入軟盤,來實現裸機控制權的接管。這也可以說成是一個最簡單的操作系統。
打印字符串
main:
org 07c00h
mov ax, cs
mov ds, ax
mov es, ax
call DispStr
jmp $
DispStr:
mov ax, BootMessage
mov bp, ax
mov cx, [Strlen] ;字符串長
mov ax, 01301h ;寫模式
mov bx, 000fh ;頁號0,黑底白字
mov dl, 0 ;行=列=0
int 10h ;10h號接口
ret
BootMessage: db "huangwx8 17310031"
Strlen db 17
times 510-($-$$) db 0
dw 0xaa55
這段代碼其實相當簡潔,最開始有一行org 07c00h,這個指令會讓編譯器把代碼裝載到0x7c00,如果我們把它編譯成BIN文件,在PC上運行時,PC識別到扇區末的0xaa55代號,認爲它是引導扇區;然後它會把這個扇區放到7c00h的地址處,把程序計數器設爲7c00h,從這裏開始執行程序,從此bios不再控制PC,而是我們的程序接管PC。
然後我們把ds和es指向與cs相同的段,是爲了後面偏移尋址找數據不會出錯。
DispStr部分,我們會把Message(一個指向字符串的地址)塞到es:bp,然後我們會爲ax賦一個值,高8位是13h,定義01h的功能爲寫字符串,低8位是01h,定義寫模式。然後爲bx賦一個值,高8位是頁號,這裏設爲0;低8位是控制顏色,高4位是底色,低四位是字色。0fh的意義就是黑底白字。cx是字符串長度,dx控制位置。
我們call int 10h,10h中斷號就是call系統的17號中斷向量。然後屏幕上就會打印出字符串了。
最後ret,然後 jmp 是“當前行被彙編後的地址”。$$是”程序被編譯後的首地址”.因此jmp $是一個死循環,相當於halt。而times 510-($-$$) db 0則是爲了填滿內存從開頭到其後的510個字節爲0,再加上0xaa55的2字節,以重置512字節即一個扇區的空間。方便我們直接把這個東西燒到軟盤裏,讓bios識別並執行這段引導程序。
我們用nasm編譯器編譯這段代碼,就能得到一個512字節的bin程序。然後我們把這段程序複製到虛擬機所搭載的啓動軟盤的第一個扇區,虛擬機會執行程序邏輯,我們就能在屏幕上看見這樣的字符輸出啦,我的姓名和學號。
任意位置打印字符
上面說過了,設置dh和dl的數值就能控制打印位置。我們只需要用一個循環,在幾個不同的位置打印字符串,觀察一下輸出怎麼樣。
main:
org 07c00h
mov ax, cs
mov ds, ax
mov es, ax
call DispStr
jmp $
DispStr:
dec word[count] ; 遞減計數變量
jnz DispStr ; >0:跳轉;
mov word[count],delay
dec word[dcount] ; 遞減計數變量
jnz DispStr
mov word[count],delay
mov word[dcount],ddelay
;用簡單的雙循環控制時延,這裏時延50000x580個時間單位
mov ax, BootMessage
mov bp, ax
mov cx, [Strlen] ;字符串長
mov ax, 01301h ;寫模式
mov bx, 000fh ;頁號0,黑底白字
mov dh, [boundary] ;行=b
mov dl, [boundary] ;列=b
int 10h ;10h號接口
dec byte[boundary]
jnz DispStr
ret
BootMessage: db "huangwx8 17310031"
Strlen dw 17
delay equ 50000
ddelay equ 580
count dw delay
dcount dw ddelay
boundary db 10
times 510-($-$$) db 0
dw 0xaa55
正如我們希望的,字符串在對角線方向被打印了10次。
彈球邏輯
我們剛學C語言的時候做過的最簡單的邏輯大概就是類似這種的邏輯了。這裏我們設置初始方向是向右下方,一旦觸壁就改變方向。即初始的速度vx、vy爲1,位置x、y爲0,0,除了上面的打印函數要調整以外,還要設置位置更新函數和邊界判斷函數;每次打印前判斷邊界並更新速度,打印時按照位置打印字符串,打印後更新位置。
org 07c00h
mov ax, cs
mov ds, ax
mov es, ax
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:
jmp BoundaryCheckx
DispStr:
mov ax, BootMessage ;打印字符串
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
BootMessage: db "Welcome!"
Strlen db 8
delay equ 50000
ddelay equ 580
count dw delay
dcount dw ddelay
x db 0
y db 0
vx db 1
vy db 1
left db 0
upper db 0
right db 75
lower db 24
boundary db 10
times 510-($-$$) db 0
dw 0xaa55
對於比較複雜的邏輯要使用跳轉語句來實現簡單的遞歸調用。上面用了jg和jl作爲邏輯判斷語句,沒有用call和ret而是用了jmp實現遞歸調用和返回。總體上還是一個死循環(檢測-打印-更新位置),結果如圖。
字符串持續打印並按照一個速度運動,一旦遇到邊界就會讓一個方向的速度反轉。
小結
這個實驗難度並不大,儘管我沒有x86基礎,但經過了兩個小時的學習和兩個小時的編程實驗,這個項目也很快被完成了。實驗的目的主要是爲了讓我們理解PC開機後,硬件軟件做了什麼;以及讓我們熟悉x86彙編編程,爲以後更復雜的操作系統開發打下基礎。