實驗目的
- 建立對操作系統引導過程的深入認識;
- 掌握操作系統的基本開發過程;
- 能對操作系統進行簡單的控制,揭開操作系統的神祕面紗。
實驗內容
此次實驗的基本內容是:編寫一個放入引導扇區的操作系統引導程序bootsect.s,和一個進入保護模式前的設置程序setup.s,並將該bootsect.s和setup.s編譯後在Bochs中運行,進行實驗。
編寫的引導程序bootsect.s和setup.s主要完成如下三個部分的功能:
- bootsect.s能在屏幕上打印一段提示信息“XXX is booting...”,其中XXX是你給自己的操作系統起的名字,例如LZJos、Sunix等(可以上論壇上秀秀誰的OS名字最帥)。(實驗者也可以 做一個特色logo並顯示在屏幕上,以表示自己操作系統的與衆不同,當然這要花費一定的時間,也不屬於加分內容,鼓勵大家在將來有時間的時候做一下。)
- bootsect.s能完成setup.s的載入,並跳轉到setup.s開始地址執行。而setup.s向屏幕輸出一行“Now we are in SETUP”。
- setup.s能獲取基本的硬件參數(如內存參數、顯卡參數、硬盤參數等,在本實驗中只要獲取一個參數就取分,獲取多個參數不加分,但後面的實驗中會用到 某些參數,如實驗六的終端設備需要顯卡的參數,所以在將來需要的時候能再回來修改),將這些參數放在內存的特定地址,留着將來使用,並輸出到屏幕上。
實驗報告
完成實驗後,在實驗報告中回答如下問題:
- 你覺得boot的過程複雜嗎?爲什麼?
- 摒棄一切清規戒律,嘗試設計一個更簡潔的boot過程
評分標準
- bootsect顯示正確,15%
- bootsect正確讀入setup,15%
- setup顯示正確,10%
- setup獲取硬件參數正確,15%
- setup正確顯示硬件參數,15%
- tools/build.c修改正確,10%
- 實驗報告,20%
實驗提示
操作系統的boot代碼有很多,並且大部分是相似的,所以可以仿照這些代碼編寫。本實驗將仿照Linux-0.11/boot目錄下的bootsect.s和setup.s進行,以剪裁它們爲主線。當然,如果能完全從頭編寫來實現實驗所要求的功能是再好不過了。
同濟大學趙炯博士的《Linux內核0.11完全註釋》一書的第3章是非常有幫助的參考,可以在“資料和文件下載”中下載到電子版。實驗中可能遇到的各種問題,在這裏幾乎都能找到答案。校友謝煜波撰寫的《操作系統引導探究》也是一份很好的參考。
需要注意的是,Linux的彙編代碼使用AS86編譯,語法上和我們在彙編課上所學的彙編稍有不同,請注意觀察。
下面將給出一些更具體的“提示”,沒自信者要認真閱讀,強者可忽略它們,牛人向後轉去找更肥的草吧。
Linux 0.11相關代碼詳解
boot/bootsect.s、boot/setup.s和tools/build.c是本實驗會涉及到的程序。它們的功能詳見《Linux內核0.11完全註釋》的3.3、3.4和13.2節。
如果使用Windows下的環境,那麼要注意Windows環境裏提供的build.c是一個經過修改過的版本。Linus Torvalds的原版是將0.11內核的最終目標代碼輸出到標準輸出,由make程序將數據重定向到Image文件,這在Linux、Unix和 Minix等系統下都是非常有效的。但Windows本身的缺陷(也許是特色)決定了在Windows下不能這麼做,所以flyfish修改了 build.c,將輸出直接寫入到Image(flyfish是寫入到Boot.img文件,我們爲了兩個環境的一致,也爲了最大化地與原始版本保持統 一,將其改爲Image)文件中。同時爲了適應Windows下的一些特殊情況,他還做了一些其它小的修改。
完成bootsect.s的屏幕輸出功能
首先來看完成屏幕顯示的關鍵代碼如下:
! 首先讀入光標位置 mov ah,#0x03 xor bh,bh int 0x10 ! 顯示字符串“LZJos is running...” mov cx,#25 ! 要顯示的字符串長度 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10 inf_loop: jmp inf_loop ! 後面都不是正經代碼了,得往回跳呀 ! msg1處放置字符串 msg1: .byte 13,10 ! 換行+回車 .ascii "LZJos is running..." .byte 13,10,13,10 ! 兩對換行+回車 !設置引導扇區標記0xAA55 .org 510 boot_flag: .word 0xAA55 ! 必須有它,才能引導
接下來,將完成屏幕顯示的代碼在開發環境中編譯,並使用linux-0.11/tools/build.c將編譯後的目標文件做成Image文件。
編譯和運行
Ubuntu上先從終端進入~/oslab/linux-0.11/boot/目錄。Windows上則先雙擊快捷方式“MinGW32.bat”,將打開一個命令行窗口,當前目錄是oslab。無論那種系統,都執行下面兩個命令:
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去除最後生成的可執行文件中的符號信息。
如果這兩個命令沒有任何輸出,說明編譯與鏈接都通過了。Ubuntu下用ls -l可列出下面的信息:
-rw--x--x 1 root root 544 Jul 25 15:07 bootsect -rw------ 1 root root 257 Jul 25 15:07 bootsect.o -rw------ 1 root root 686 Jul 25 14:28 bootsect.s
Windows下用dir可列出下面的信息:
2008-07-28 20:14 544 bootsect 2008-07-28 20:14 924 bootsect.o 2008-07-26 20:13 5,059 bootsect.s
其中bootsect.o是中間文件,沒有用處。bootsect是編譯、鏈接後的目標文件。
需要留意的文件是bootsect的文件大小是544字節,而引導程序必須要正好佔用一個磁盤扇區,即512個字節。造成多了32個字節的原因是ld86 產生的是Minix可執行文件格式,這樣的可執行文件處理文本段、數據段等部分以外,還包括一個Minix可執行文件頭部,它的結構如下:
struct exec { unsigned char a_magic[2]; //執行文件魔數 unsigned char a_flags; unsigned char a_cpu; //CPU標識號 unsigned char a_hdrlen; //頭部長度,32字節或48字節 unsigned char a_unused; unsigned short a_version; long a_text; long a_data; long a_bss; //代碼段長度、數據段長度、堆長度 long a_entry; //執行入口地址 long a_total; //分配的內存總量 long a_syms; //符號表大小 };
算一算:6 char(6字節)+1 short(2字節)+6 long(24字節)=32,正好是32個字節,去掉這32個字節後就可以放入引導扇區了(這是tools/build.c的用途之一)。
對於上面的Minix可執行文件,其a_magic[0]=0x01,a_magic[1]=0x03,a_flags=0x10(可執行文件),a_cpu=0x04(表示Intel i8086/8088,如果是0x17則表示Sun公司的SPARC),所以bootsect文件的頭幾個字節應該是01 03 10 04。爲了驗證一下,Ubuntu下用命令“hexdump -C bootsect”可以看到:
00000000 01 03 10 04 20 00 00 00 00 02 00 00 00 00 00 00 |.... ...........| 00000010 00 00 00 00 00 00 00 00 00 82 00 00 00 00 00 00 |................| 00000020 b8 c0 07 8e d8 8e c0 b4 03 30 ff cd 10 b9 17 00 |.........0......| 00000030 bb 07 00 bd 3f 00 b8 01 13 cd 10 b8 00 90 8e c0 |....?...........| 00000040 ba 00 00 b9 02 00 bb 00 02 b8 04 02 cd 13 73 0a |..............s.| 00000050 ba 00 00 b8 00 00 cd 13 eb e1 ea 00 00 20 90 0d |............. ..| 00000060 0a 53 75 6e 69 78 20 69 73 20 72 75 6e 6e 69 6e |.Sunix is runnin| 00000070 67 21 0d 0a 0d 0a 00 00 00 00 00 00 00 00 00 00 |g!..............| 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000210 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| 00000220
Windows下用UltraEdit把該文件打開,果然如此。
圖1 用UltraEdit打開文件bootsect
接下來幹什麼呢?是的,要去掉這32個字節的文件頭部(tools/build.c的功能之一就是這個)!隨手編個小的文件讀寫程序都可以去掉它。不過,懶且聰明的人會在Ubuntu下用命令:
$ dd bs=1 if=bootsect of=Image skip=32
生成的Image就是去掉文件頭的bootsect。
Windows下可以用UltraEdit直接刪除(選中這32個字節,然後按Ctrl+X)。
去掉這32個字節後,將生成的文件拷貝到linux-0.11目錄下,並一定要命名爲“Image”(注意大小寫)。然後就“run”吧!
圖2 bootsect引導後的系統啓動情況
bootsect.s讀入setup.s
首先編寫一個setup.s,該setup.s可以就直接拷貝前面的bootsect.s(可能還需要簡單的調整),然後將其中的顯示的信息改爲:“Now we are in SETUP”。
接下來需要編寫bootsect.s中載入setup.s的關鍵代碼。原版bootsect.s中下面的代碼就是做這個的。
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, !我們不需要那麼多,我們設置爲2 int 0x13 !應用0x13號BIOS中斷讀入2個setup.s扇區 jnc ok_load_setup !讀入成功,跳轉到ok_load_setup: ok - continue mov dx,#0x0000 !軟驅、軟盤有問題纔會執行到這裏。我們的鏡像文件比它們可靠多了 mov ax,#0x0000 !否則復位軟驅 reset the diskette int 0x13 jmp load_setup !重新循環,再次嘗試讀取 ok_load_setup: !接下來要幹什麼?當然是跳到setup執行。
所有需要的功能在原版bootsect.s中都是存在的,我們要做的僅僅是刪除那些對我們無用的代碼。
再次編譯
現在有兩個文件都要編譯、鏈接。一個個手工編譯,效率低下,所以藉助Makefile是最佳方式。
在Ubuntu下,進入linux-0.11目錄後,使用下面命令(注意大小寫):
$ make BootImage
Windows下,在命令行方式,進入Linux-0.11目錄後,使用同樣的命令(不需注意大小寫):
make BootImage
無論哪種系統,都會看到:
Unable to open 'system' make: *** [BootImage] Error 1
有Error!這是因爲make根據Makefile的指引執行了tools/build.c,它是爲生成整個內核的鏡像文件而設計的,沒考慮我們只需要bootsect.s和setup.s的情況。它在向我們要“系統”的核心代碼。爲完成實驗,接下來給它打個小補丁。
修改build.c
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的尾部,請斟酌。
當按照前一節所講的編譯方法編譯成功後,run,就得到了如圖3所示的運行結果,和我們想得到的結果完全一樣。
圖3 用修改後的bootsect.s和setup.s進行引導的結果
setup.s獲取基本硬件參數
setup.s將獲得硬件參數放在內存的0x90000處。原版setup.s中已經完成了光標位置、內存大小、顯存大小、顯卡參數、第一和第二硬盤參數的保存。
用ah=#0x03調用0x10中斷可以讀出光標的位置,用ah=#0x88調用0x15中斷可以讀出內存的大小。有些硬件參數的獲取要稍微複雜一些,如磁盤參數表。在PC機中BIOS設定的中斷向量表中int 0x41的中斷向量位置(4*0x41 = 0x0000:0x0104)存放的並不是中斷程序的地址,而是第一個硬盤的基本參數表。第二個硬盤的基本參數表入口地址存於int 0x46中斷向量位置處。每個硬盤參數表有16個字節大小。下表給出了硬盤基本參數表的內容:
表1 磁盤基本參數表
位移 | 大小 | 說明 |
0x00 | 字 | 柱面數 |
0x02 | 字節 | 磁頭數 |
… | … | … |
0x0E | 字節 | 每磁道扇區數 |
0x0F | 字節 | 保留 |
所以獲得磁盤參數的方法就是複製數據。
下面是將硬件參數取出來放在內存0x90000的關鍵代碼。
mov ax,#INITSEG mov ds,ax !設置ds=0x9000 mov ah,#0x03 !讀入光標位置 xor bh,bh int 0x10 !調用0x10中斷 mov [0],dx !將光標位置寫入0x90000. !讀入內存大小位置 mov ah,#0x88 int 0x15 mov [2],ax !從0x41處拷貝16個字節(磁盤參數表) 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
現在已經將硬件參數(只包括光標位置、內存大小和硬盤參數,其他硬件參數取出的方法基本相同,此處略去)取出來放在了0x90000處,接下來的工作是將這些參數顯示在屏幕上。這些參數都是一些無符號整數,所以需要做的主要工作是用匯編程序在屏幕上將這些整數顯示出來。
以十六進制方式顯示比較簡單。這是因爲十六進制與二進制有很好的對應關係(每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位十六進制的形式顯示出 來。
下面是完成顯示16進制數的彙編語言程序的關鍵代碼,其中用到的BIOS中斷爲INT 0x10,功能號0x0E(顯示一個字符),即AH=0x0E,AL=要顯示字符的ASCII碼。
!以16進制方式打印棧頂的16位數 print_hex: mov cx,#4 ! 4個十六進制數字 mov dx,(bp) ! 將(bp)所指的值放入dx中,如果bp是指向棧頂的話 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 ret
這裏用到了一個loop指令,每次執行loop指令,cx減1,然後判斷cx是否等於0。如果不爲0則轉移到loop指令後的標號處,實現循環;如果爲0 順序執行。另外還有一個非常相似的指令:rep指令,每次執行rep指令,cx減1,然後判斷cx是否等於0,如果不爲0則繼續執行rep指令後的串操作 指令,直到cx爲0,實現重複。
!打印回車換行 print_nl: mov ax,#0xe0d ! CR int 0x10 mov al,#0xa ! LF int 0x10 ret
只要在適當的位置調用print_bx和print_nl(注意,一定要設置好棧,才能進行函數調用)就能將獲得硬件參數打印到屏幕上,完成此次實驗的任 務。但事情往往並不總是順利的,前面的兩個實驗大多數實驗者可能一次就編譯調試通過了(這裏要提醒大家:編寫操作系統的代碼一定要認真,因爲要調試操作系 統並不是一件很方便的事)。但在這個實驗中會出現運行結果不對的情況(爲什麼呢?因爲我們給的代碼並不是100%好用的)。所以接下來要複習一下彙編,並 學學在Bochs中如何調試操作系統代碼。
我想經過漫長而痛苦的調試後,大家一定能興奮地得到下面的運行結果:
圖4 用可以打印硬件參數的setup.s進行引導的結果
Memory Size是0x3C00KB,算一算剛好是15MB(擴展內存),加上1MB正好是16MB,看看Bochs配置文件bochs/bochsrc.bxrc:
…… megs: 16 …… ata0-master: type=disk, mode=flat, cylinders=410, heads=16, spt=38 ……
這些都和上面打出的參數吻合,表示此次實驗是成功的。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
http://blog.sina.com.cn/s/blog_4b3646350100an8l.html
bootsect.s只是做了修改:
SYSSIZE = 0x3000
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = 0x9000 ! we move boot here - out of the way
SETUPSEG = 0x9020 ! setup starts here
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
ROOT_DEV = 0x306
entry _start
_start:
!-------------------------
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
!---------------------------
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
!-----------------------------
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
!---------------------------
ok_load_setup:
! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#29
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
jmpi 0,SETUPSEG
msg1:
.byte 13,10
.ascii "Kerberos is Loading ..."
.byte 13,10,13,10
.org 508
root_dev:
.word ROOT_DEV
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
setup.s也只是修改了一下:
INITSEG = 0x9000 ! we move boot here - out of the way
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
SETUPSEG = 0x9020 ! this is the current segment
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
entry start
start:
! -------打印字符串----------------
mov ax,cs;
mov ds,ax;
mov es,ax;
!--------代碼段與數據段、附加段在一個位置--------
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#22
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1
mov ax,#0x1301 ! write string, move cursor
int 0x10
!--------打印光標位置---------------
mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
mov cx,#13
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#cursor1
mov ax,#0x1301 ! write string, move cursor
int 0x10
!----------調用函數用數字打印------------
push [0]
call print_hex
call print_nl
pop [0];
!----------打印內存信息--------------------------
! Get memory size (extended mem, kB)
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10
mov cx,#14
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#memory
mov ax,#0x1301 ! write string, move cursor
int 0x10
mov ah,#0x88
int 0x15
mov [2],ax
push [2]
call print_hex
mov ax,#0xe4b !K
int 0x10
mov al,#0x42 !B
int 0x10
call print_nl
pop [0];
!----------------------------
msg1:
.ascii "Ahh! I'm in SETUP..."
.byte 13,10
cursor1:
.byte 13,10
.ascii "Cursor Pos:"
memory:
.byte 13,10
.ascii "Memory Size:"
!--------將16進制輸出到屏幕上的函數----------------
print_hex:
mov bp,sp;
add bp,#2;
mov dx,(bp) ! 將(bp)所指的值放入dx中,如果bp是指向棧頂的話
mov cx,#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
ret
!打印回車換行
print_nl:
mov ax,#0xe0d ! CR
int 0x10
mov al,#0xa ! LF
int 0x10
ret
.text
endtext:
.data
enddata:
.bss
endbss