一, 實驗內容
改寫bootsect.s和setup.s, 完成如下主要功能:
1, bootsect.s能夠在屏幕上打印一段提示信息"XXX is booting...", 其中XXX是你給自己的操作系統起的名字,例如LZJos、Sunix等.
2, bootsect.s能夠完成setup.s的載入, 並跳轉到setup.s開始地址執行.
3, setup.s能夠像屏幕輸出一行信息 "Now we are in SETUP"
4, setup.s能獲取至少一個基本的硬件參數(如內存參數、顯卡參數、硬盤參數等),將其存放在內存的特定地址,並輸出到屏幕上。
setup.s不再加載linux內核, 保持上述信息顯示到屏幕上即可
二, 實驗步驟
1, 完成bootsect的屏幕輸出功能
由於不需要加載linux內核,所以就不需要原始的linux代碼那么復雜,比如: 將bootsect自身移動到0x90000處等操作,可以忽略的。
要顯示字符串,那么字符串顯示到屏幕的哪里呢?當然是當前光標的位置了!所以第一步就要先讀取光標的位置,這可以利用10號中斷的3號子程序來完成。要顯示字符串,可以利用10號功能的13號子程序來完成,需要注意的是一定要邊顯示字符邊移動光標,最終的光標要移動到字符串的末尾處。最后要注意用0xAA55來標記引導扇區。代碼如下:
! 文件:bootsect.s
entry _start
_start: ! 首先利用10號中斷的3號功能來讀取光標位置 mov ah,#0x03 xor bh,bh int 0x10 ! 再利用10號中斷的13號功能顯示字符串 mov cx,#50 ! 加上回車和換行,字符串一共包含50個字符,所以設置cx為50 mov bx,#0x0007 mov bp,#msg1 mov ax,#0x07c0 mov es,ax ! es:bp=顯示字符串的地址 mov ax,#0x1301 int 0x10 Inf_loop: jmp Inf_loop ! 無限循環 ! msg1處放置要顯示的字符串 msg1: .byte 13,10 ! 換行+回車 .ascii "AXF OS is booting, my name is Aixiangfei ..." .byte 13,10,13,10 ! 兩對換行+回車 ! 下面是啟動盤具有有效引導扇區的標志. 僅供BIOS中的程序加載扇區時識別使用。 ! 它必須位於引導扇區的最后兩個字節中. .org 510 boot_flag: .word 0xAA55 ! 引導扇區的標記就是0XAA55
編譯和運行: 進入~/oslab/linux-0.11/boot/目錄,編譯並連接:
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
參數說明:-0(注意:這是數字0,不是字母O)表示生成8086的16位目標程序,-a表示生成與GNU as和ld部分兼容的代碼,-s告訴鏈接器ld86去除最后生成的可執行文件中的符號信息。
如果這兩個命令沒有任何輸出,說明編譯與鏈接都通過了。需要留意的生成的bootsect的大小是544字節,而引導程序必須要正好占用一個磁盤扇區,即512個字節。造成多了32個字節的原因是ld86產生的是Minix可執行文件格式,這樣的可執行文件處理文本段、數據段等部分以外,還包括一個Minix可執行文件頭部。所以最后必須要把這多余的32個字節刪掉,可以用linux自帶的工具dd來完成:
dd bs=1 if=bootsect of=Image skip=32
去掉這32個字節后,將生成的文件拷貝到linux-0.11目錄下,並一定要命名為“Image”(注意大小寫)。然后就可以run了
程序有可能需要不斷地修改調試,為了方便,可以將上述的一系列操作寫道一個shell腳本文件 cbootsect.sh 中:
#!/bin/sh as86 -0 -a -o bootsect.o bootsect.s ld86 -0 -s -o bootsect bootsect.o dd bs=1 if=bootsect of=Image skip=32 cp Image ../ ../../run
運行結果:
2, setup的載入
首先要確定setup是在磁盤的0磁道2扇區,linux 0.11中的setup占了4個扇區,而我們最后要寫的setup顯然沒有那么復雜,所以可以只用1個扇區就可以了。另外,由於bootsect位於0x7c00處,占用512個字節,所以可以將setup載入到0x7e00處。要想從磁盤中載入數據到內存,可以利用BIOS提供的13號中斷輕松完成。最后就直接用jumi指令跳轉到0x7e00處即可。對前面的bootsect.s擴展之后的完整代碼如下:
! 文件:bootsect.s SETUPLEN = 1 SETUPSEG = 0x07e0
entry _start _start: ! 首先利用10號中斷的3號功能來讀取光標位置 mov ah,#0x03 xor bh,bh int 0x10 ! 再利用10號中斷的13號功能顯示字符串 mov cx,#50 ! 加上回車和換行,字符串一共包含50個字符,所以設置cx為50 mov bx,#0x0007 mov bp,#msg1 mov ax,#0x07c0 mov es,ax ! es:bp=顯示字符串的地址 mov ax,#0x1301 int 0x10 load_setup: mov dx,#0x0000 ! 設置驅動器和磁頭(drive 0, head 0): 軟盤0磁頭 mov cx,#0x0002 ! 設置扇區號和磁道(sector 2, track 0):0磁頭、0磁道、2扇區 mov bx,#0x0200 ! 設置讀入的內存地址:BOOTSEG+address = 512,偏移512字節 mov ax,#0x0200+SETUPLEN ! 設置讀入的扇區個數(service 2, nr of sectors), ! SETUPLEN是讀入的扇區個數,Linux 0.11設置的是4, ! 我們不需要那么多,我們設置為1 int 0x13 ! 應用0x13號BIOS中斷讀入1個setup.s扇區 jnc ok_load_setup ! 讀入成功,跳轉到ok_load_setup: ok - continue mov dx,#0x0000 ! 軟驅、軟盤有問題才會執行到這里 mov ax,#0x0000 ! 否則復位軟驅 int 0x13 j load_setup ! 重新循環,再次嘗試讀取 ok_load_setup: jmpi 0,SETUPSEG ! msg1處放置要顯示的字符串 msg1: .byte 13,10 ! 換行+回車 .ascii "AXF OS is booting, my name is Aixiangfei ..." .byte 13,10,13,10 ! 兩對換行+回車 ! 下面是啟動盤具有有效引導扇區的標志. 僅供BIOS中的程序加載扇區時識別使用。 ! 它必須位於引導扇區的最后兩個字節中. .org 510 boot_flag: .word 0xAA55 ! 引導扇區的標記就是0XAA55
3, 完成setup的屏幕輸出功能
這個很簡單,跟bootsect是一樣的。代碼如下:
! 文件:setup.s
entry _start _start: ! 首先利用10號中斷的3號功能來讀取光標位置 mov ah,#0x03 xor bh,bh int 0x10 ! 再利用10號中斷的13號功能顯示字符串 mov cx,#26 mov bx,#0x0007 mov bp,#msg mov ax,cs mov es,ax mov ax,#0x1301 int 0x10 Inf_loop: jmp Inf_loop ! 無限循環 msg: .byte 13,10 .ascii "Now we are in SETUP." .byte 13,10,13,10 .org 510 boot_flag: .word 0xAA55
編譯和與運行:現在有兩個文件都要編譯、鏈接。一個個手工編譯,效率低下,所以借助Makefile是最佳方式。linux 0.11中Makefile文件已經幫我們把這件事做好了。進入liux-0.11目錄后,使用命令:
$ make BootImage
但是我們發現竟然出現了錯誤:
原因:這是因為make根據Makefile的指引執行了tools/build.c,它是為生成整個內核的鏡像文件而設計的,沒考慮我們只需要bootsect.s和setup.s的情況。build.c從命令行參數得到bootsect、setup和system內核的文件名,將三者做簡單的整理后一起寫入Image。其中system是第三個參數(argv[3])。當“make all”或者“makeall”的時候,這個參數傳過來的是正確的文件名,build.c會打開它,將內容寫入Image。而“make BootImage”時,傳過來的是字符串"none"。所以,修改build.c的思路就是當argv[3]是"none"的時候,只寫bootsect和setup,忽略所有與system有關的工作,或者在該寫system的位置都寫上“0”。
修改build.c文件很簡單,只需要把第178到183這幾行代碼刪除即可!
// if ((id=open(argv[3],O_RDONLY,0))<0) // die("Unable to open 'system'"); // if (read(id,buf,GCC_HEADER) != GCC_HEADER) // die("Unable to read header of 'system'"); // if (((long *) buf)[5] != 0) // die("Non-GCC header of 'system'");
run
4, 讀取硬件參數
這個部分是最復雜的了。需要打印的硬件信息有:光標位置,內存大小,磁盤的柱面數,磁頭數,每磁道的扇區數。在這個實驗中,這些參數的信息可以保存在內存中的任意位置,在linux 0.11中,這些參數信息是被保存到了0x90000處,所以不妨跟linux 0.11一樣。
(1)獲取硬件參數
獲得光標位置信息,這個很簡單,只需要調用13號中斷的3號子程序就可以得到,前面已經用過了的。
獲得內存大小,可以調用用15號中斷的88號子程序得到,也很簡單。
與磁盤相關的信息稍微復雜一點,這些信息被保存在0x0000:0x0104地址處的16個字節的中,這16個字節的信息叫做“磁盤參數表”。所以獲得磁盤信息的方法就是復制數據。
(2)數字轉字符
現在已經將這些硬件參數取出來放在了0x90000處,接下來的工作是將這些參數顯示在屏幕上。這些參數都是一些無符號整數,所以需要做的主要工作是用匯編程序在屏幕上將這些整數用16進制的形式顯示出來。
因為十六進制與二進制有很好的對應關系(每4位二進制數和1位十六進制數存在一一對應關系),顯示時只需將原二進制數每4位划成一組,按組求對應的ASCII碼送顯示器即可。ASCII碼與十六進制數字的對應關系為:0x30~0x39對應數字0~9,0x41~0x46對應數字a~f。從數字9到a,其ASCII碼間隔了7h,這一點在轉換時要特別注意。為使一個十六進制數能按高位到低位依次顯示,實際編程中,需對bx中的數每次循環左移一組(4位二進制),然后屏蔽掉當前高12位,對當前余下的4位(即1位十六進制數)求其ASCII碼,要判斷它是0~9還是a~f,是前者則加0x30得對應的ASCII碼,后者則要加0x37才行,最后送顯示器輸出。以上步驟重復4次,就可以完成bx中數以4位十六進制的形式顯示出來。
因為在輸出的時候需要調用多次,所以最好把這個功能寫成一個函數print_bx,方便使用。既然要用到函數,故一定要先設置好棧。為了方便,還可以寫一個函數print_nl實現換行的功能。
INITSEG = 0x9000 entry _start _start: ! 在顯示字符串之前必須先獲取當前光標的位置,這樣就可以把字符串顯示到當前光標處了 mov ah,#0x03 xor bh,bh int 0x10 ! 利用10號中斷的13號功能打印字符串"Now we are in SETUP." mov cx,#26 mov bx,#0x0007 mov bp,#msg1 mov ax,cs mov es,ax mov ax,#0x1301 int 0x10 ! 下面開始讀取一些硬件參數 ! 讀入光標位置信息,保存到0x90000處 mov ax,#INITSEG mov ds,ax mov ah,#0x03 xor bh,bh int 0x10 mov [0],ds ! 讀入內存大小位置信息,保存到0x90002處 mov ah,#0x88 int 0x15 mov [2],ax ! 從0x41處拷貝16個字節(磁盤參數表)到0x90004處 mov ax,#0x0000 mov ds,ax lds si,[4*0x41] mov ax,#INITSEG mov es,ax mov di,#0x0004 mov cx,#0x10 rep ! 重復16次 movsb ! 先打印光標位置 ! 打印字符串之前要先讀取光標位置,將字符串打印到當前光標處 mov ah,#0x03 xor bh,bh int 0x10 ! 打印字符串 "Cursor POS:" mov cx,#11 mov bx,#0x0007 mov ax,cs mov es,ax mov bp,#msg2 mov ax,#0x1301 int 0x10 ! 調用打印函數,打印光標位置 mov ax,#0x9000 mov ds,ax mov dx,0x0 call print_hex call print_nl ! 打印內存大小 ! 打印字符串"Memory SIZE:" mov ah,#0x03 xor bh,bh int 0x10 ! 讀取光標位置 mov cx,#12 mov bx,#0x0007 mov ax,cs mov es,ax mov bp,#msg3 mov ax,#0x1301 int 0x10 ! 調用打印函數,打印內存大小信息 mov ax,#0x9000 mov ds,ax mov dx,0x2 call print_hex ! 打印字符串"KB" mov ah,#0x03 xor bh,bh int 0x10 ! 讀取光標位置 mov cx,#2 mov bx,#0x0007 mov ax,cs mov es,ax mov bp,#msg4 mov ax,#0x1301 int 0x10 call print_nl !打印柱面數 ! 打印字符串"Cyls" mov ah,#0x03 xor bh,bh int 0x10 ! 讀取光標位置 mov cx,#5 mov bx,#0x0007 mov ax,cs mov es,ax mov bp,#msg5 mov ax,#0x1301 int 0x10 ! 調用打印函數打印磁盤柱面數 mov ax,#0x9000 mov ds,ax mov dx,0x4 call print_hex call print_nl ! 打印磁頭數 ! 打印字符串"Heads:" mov ah,#0x03 xor bh,bh int 0x10 ! 讀取光標位置 mov cx,#6 mov bx,#0x0007 mov ax,cs mov es,ax mov bp,#msg6 mov ax,#0x1301 int 0x10 ! 調用打印函數打印磁盤磁頭數 mov ax,#0x9000 mov ds,ax mov dx,0x6 call print_hex call print_nl ! 打印每磁道扇區數 ! 打印字符串"sectors" mov ah,#0x03 xor bh,bh int 0x10 ! 讀取光標位置 mov cx,#8 mov bx,#0x0007 mov ax,cs mov es,ax mov bp,#msg7 mov ax,#0x1301 int 0x10 ! 調用打印函數打印扇區數 mov ax,#0x9000 mov ds,ax mov dx,0x12 call print_hex call print_nl Inf_loop: jmp Inf_loop ! 無限循環 ! print_hex函數:將一個數字轉換為ascii碼字符,並打印到屏幕上 ! 參數值:dx ! 返回值:無 print_hex: mov cx,#4 ! 要打印4個十六進制數字,故循環4次 print_digit: rol dx,#4 ! 循環以使低4比特用上 !! 取dx的高4比特移到低4比特處 mov ax,#0xe0f ! ah = 請求的功能值,al = 半字節(4個比特)掩碼 and al,dl ! 取dl的低4比特值 add al,#0x30 ! 給al數字加上十六進制0x30 cmp al,#0x3a jl outp ! 如果是一個不大於十的數字 add al,#0x07 ! 如果是a~f,要多加7 outp: int 0x10 loop print_digit ! 用loop重復4次 ret ! 打印回車換行 print_nl: mov ax,#0xe0d int 0x10 mov al,#0xa int 0x10 ret msg1: .byte 13,10 .ascii "Now we are in SETUP." .byte 13,10,13,10 msg2: .ascii "Cursor POS:" msg3: .ascii "Memory SIZE:" msg4: .ascii "KB" msg5: .ascii "Cyls:" msg6: .ascii "Heads:" msg7: .ascii "Sectors:" .org 510 boot_flag: .word 0xAA55
運行結果: