实验2 操作系统的引导
操作系統的引導
實驗目的
- 熟悉hit-oslab實驗環境;
- 建立對操作系統引導過程的深入認識;
- 掌握操作系統的基本開發過程;
- 能對操作系統代碼進行簡單的控制,揭開操作系統的神秘面紗。
實驗內容
此次實驗的基本內容是:
改寫bootsect.s主要完成如下功能:
改寫setup.s主要完成如下功能:
實驗報告
在實驗報告中回答如下問題:
評分標準
- bootsect顯示正確,30%
- bootsect正確讀入setup,10%
- setup獲取硬件參數正確,20%
- setup正確顯示硬件參數,20%
- 實驗報告,20%
實驗提示
操作系統的boot代碼有很多,并且大部分是相似的。本實驗仿照Linux-0.11/boot目錄下的bootsect.s和setup.s,以剪裁它們為主線。當然,如果能完全從頭編寫,并實現實驗所要求的功能,是再好不過了。
同濟大學趙炯博士的《Linux內核0.11完全注釋(修正版V3.0)》(以后簡稱《注釋》)的第6章是非常有幫助的參考,實驗中可能遇到的各種問題,幾乎都能找到答案。可以在“資料和文件下載”中下載到該書的電子版。同目錄中,校友謝煜波撰寫的《操作系統引導探究》也是一份很好的參考。
需要注意的是,oslab中的匯編代碼使用as86編譯,語法和匯編課上所授稍有不同。
下面將給出一些更具體的“提示”。這些提示并不是實驗的一步一步的指導,而是羅列了一些實驗中可能遇到的困難,并給予相關提示。它們肯定不會涵蓋所有問題,也不保證其中的每個字都對完成實驗有幫助。所以,它們更適合在你遇到問題時查閱,而不是當作指南一樣地亦步亦趨。本書所有實驗的提示都是秉承這個思想編寫的。
Linux 0.11相關代碼詳解
boot/bootsect.s、boot/setup.s和tools/build.c是本實驗會涉及到的源文件。它們的功能詳見《注釋》的6.2、6.3節和16章。
如果使用Windows下的環境,那么要注意Windows環境里提供的build.c是一個經過修改過的版本。Linus Torvalds的原版是將0.11內核的最終目標代碼輸出到標準輸出,由make程序將數據重定向到Image文件,這在Linux、Unix和Minix等系統下都是非常有效的。但Windows本身的缺陷(也許是特色)決定了在Windows下不能這么做,所以flyfish修改了build.c,將輸出直接寫入到Image(flyfish是寫入到Boot.img文件,我們為了兩個環境的一致,也為了最大化地與原始版本保持統一,將其改為Image)文件中。同時為了適應Windows的一些特殊情況,他還做了其它一些小修改。
引導程序的運行環境
引導程序由BIOS加載并運行。它活動時,操作系統還不存在,整臺計算機的所有資源都由它掌控,而能利用的功能只有BIOS中斷調用。
完成bootsect.s的屏幕輸出功能
首先來看完成屏幕顯示的關鍵代碼如下:
! 首先讀入光標位置 mov ah, xor bh,bh int 0x10! 顯示字符串“LZJos is running...” mov cx, mov bx, mov bp, mov ax, int 0x10inf_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,用cd命令進入linux-0.11\boot。無論那種系統,都執行下面兩個命令編譯和鏈接bootsect.s:
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.sWindows下用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.| 00000220Windows下用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 BootImageWindows下,在命令行方式,進入Linux-0.11目錄后,使用同樣的命令(不需注意大小寫):
makeBootImage無論哪種系統,都會看到:
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 0x10loop print_digitret 這里用到了一個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 0x10ret只要在適當的位置調用print_bx和print_nl(注意,一定要設置好棧,才能進行函數調用)就能將獲得硬件參數打印到屏幕上,完成此次實驗的任務。但事情往往并不總是順利的,前面的兩個實驗大多數實驗者可能一次就編譯調試通過了(這里要提醒大家:編寫操作系統的代碼一定要認真,因為要調試操作系統并不是一件很方便的事)。但在這個實驗中會出現運行結果不對的情況(為什么呢?因為我們給的代碼并不是100%好用的)。所以接下來要復習一下匯編,并閱讀《Bochs使用手冊》,學學在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 ……這些都和上面打出的參數吻合,表示此次實驗是成功的。
=====================實驗報告=====================
1,完成bootsect.s的屏幕輸出功能
a)cd /home/yuebo/oslab/linux-0.11/boot
b)rm -rf bootsect.o?bootsect.o?
c)修改bootsect.s第246行,修改后如下:
d)修改bootsect.s第98行,修改后如下:注意是字符串的長度+6=25
e)還在這個目錄下編譯、鏈接
as86 -0 -a -o bootsect.o bootsect.s
ld86 -0 -s -o bootsect bootsect.o
f)因為
yuebo@ubuntu:~/oslab/linux-0.11/boot$ ls -hl bootsect
-rwxrwxr-x 1 yuebo yuebo 544 Aug 19 20:39 bootsect
需要留意的文件是bootsect的文件大小是544字節,而引導程序必須要正好占用一個磁盤扇區,即512個字節。造成多了32個字節的原因是ld86產生的是Minix可執行文件格式...(實驗提示中有說明)
g)接下來干什么呢?是的,要去掉這32個字節的文件頭部(tools/build.c的功能之一就是這個),安裝實驗提示用。
dd bs=1 if=bootsect of=Image skip=32
yuebo@ubuntu:~/oslab/linux-0.11/boot$ ls -hl Image?
-rw-rw-r-- 1 yuebo yuebo 512 Aug 19 20:47 Image
h)去掉這32個字節后,將生成的文件拷貝到linux-0.11目錄下,并一定要命名為“Image”(注意大小寫)。然后就“run”吧!
說明:這里要明白最后這個現象,為什么一直停留在這個界面而不動了呢?從李老師的課程中可以知道原因,因為這里的Imag文件里面只有bootset的部分,而setup、system并沒有放進這個Image中,所以這里執行的只是bootsect的代碼。
2,bootsect.s讀入setup.s
???????????? 修改setup.s 依據bootsect.s將其寫成 如下代碼
BIOS中斷0x10功能號 ah=0x03,讀取光標位置。
輸入:bh = 頁號
返回:cx,dx中
BIOS中斷0x10功能號ah = 0x13,顯示字符串。
輸入:al,bl,bh,dh,dl,es:bp此寄存器指向要顯示的字符串的起始位置,cx顯示字符串字符數。
這里比較關鍵的是es這個寄存器不能遺忘,es指向的段就是下面這一段。
SETUPSEG = 0x9020 entry _start _start: mov ax,#SETUPSEG mov es,ax! Print some inane messagemov ah,#0x03 ! read cursor pos xor bh,bh int 0x10mov cx,#25 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10msg1: .byte 13,10 .ascii "Now we are in SETUP" .byte 13,10,13,10.text endtext:說明:讀懂這段代碼基礎是動8086匯編語言,不需要把bootsect.s, head.s, setup.s都讀了再做這個實驗,解決問題就是找線索,而不是把所有的知識都理解了再去解決問題,本質上一個問題的解決是找到幾個關鍵點以及它們之間的聯系,所以大腦要用排除法過濾99%的無用信息,把有用的信息解讀了就行了。至于全部源碼也可以放在做完所有實驗,對操作系統框架很熟悉的基礎上在攻細節。
再make的話會出現問題,就是Non-GCC header of 'system' make: *** [Image] Error 1這種錯誤,問題出在了build.c
解決的辦法就是就是把tool中的build.c中178~181四行注釋掉,其原因參看實驗提示;
修改后再make,run發現成功,截圖如下
3,setup.s獲取基本硬件參數
setup.s到源碼如下:
INITSEG = 0x9000 SETUPSEG = 0x9020 entry _start _start: mov ax,#SETUPSEG mov es,ax mov ax,#INITSEG mov ds,ax!-------print setup_msg---------------------- mov cx,#23 mov bp, #setup_msg call print_string call print_nl!------get parameters----------------------- call get_parameters!------------print cursor------------------ mov cx,#11 mov bp, #cursor_msg call print_string push [0] pop (bp) call print_hex call print_nl !--------------print Memory----------------------- mov cx,#12 mov bp, #memory_size_msg call print_string push [2] pop (bp) call print_hex mov cx,#2 mov bp, #kb_msg call print_string call print_nl!-------------------print Cyls--------------------------- mov cx,#5 mov bp, #cyls_msg call print_string push [4] pop (bp) call print_hex call print_nl!-----------------print Heads--------------------------- mov cx,#6 mov bp, #head_msg call print_string push [6] pop (bp) call print_hex call print_nl !-----------------print Sectors------------------------ mov cx,#8 mov bp, #sector_msg call print_string push [8] pop (bp) call print_hex call print_nlstop:jmp stop setup_msg: .byte 13,10, 13, 10 .ascii "Now this is SETUP" .byte 13,10 cursor_msg: .ascii "Cursor Pos:" memory_size_msg: .ascii "Memory SIZE:" cyls_msg: .ascii "Cyls:" head_msg: .ascii "Heads:" sector_msg: .ascii "Sectors:" kb_msg:.ascii "KB"!-------------display funtions----------------------------- print_string: !input:bp->the start of a string, cx-->numbers of chracterspush bppush cxmov ah,#0x03 ! read cursor pos xor bh,bh int 0x10pop cxpop bpmov bx,#0x0007 ! page 0, attribute 7 (normal) mov ax,#0x1301 ! write string, move cursor int 0x10ret!以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數字加上十六進制0x30cmp al,#0x3ajl outp !是一個不大于十的數字add al,#0x07 !是a~f,要多加7outp: int 0x10loop print_digitretprint_nl:mov ax,#0xe0d ! CRint 0x10mov al,#0xa ! LFint 0x10ret!-----------get parameters functions---------------- get_parameters: push axpush bxpush cxpush dxpush dspush espush sipush dipush bpmov ax,#INITSEG mov ds,ax !設置ds=0x9000mov ah,#0x03 !讀入光標位置xor bh,bhint 0x10 !調用0x10中斷mov [0],dx !將光標位置寫入0x90000.!讀入內存大小位置mov ah,#0x88int 0x15mov [2],ax!從0x41處拷貝16個字節(磁盤參數表)mov ax,#0x0000mov ds,axlds si,[4*0x41]mov ax,#INITSEGmov es,axmov di,#0x0004mov cx,#0x10rep !重復16次movsbpop bppop dipop sipop espop dspop dxpop cxpop bxpop axret運行效果截圖如下:
說明:實驗step3是在實驗step2到基礎上進行到,實驗2理解透徹樓,實驗step3很容易做的出來。實驗step3到任務就是把一段內存中到東西打印出來。實驗step3參考樓《注釋》和int10中斷到使用方法(https://www.cnblogs.com/magic-cube/archive/2011/10/19/2217676.html)。
4,實驗體會
在做實驗step3的時候感覺還是挺困難的,幾次想參考一下別人到答案,可是感覺大部分都沒有作出來,做出來的也寫的不知所云。最后自己堅持每天晚上回去調一個多小時,終于找到了規律。體會如下:一,再困難也不能放棄,也不能看別人到答案,中間有困難、有刺激、有喜悅更多的是收獲;二,王爽《匯編語言》課后習題一定要認真做一下;三,不明白不要緊,多實踐多思考,逐漸就會明白進而解決問題;四,衡量你解決問題的能力標志不是你知道這個問題怎么解決,而是親手解決過多少問題。
總結
以上是生活随笔為你收集整理的实验2 操作系统的引导的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第3章 Python 数字图像处理(DI
- 下一篇: Windows 10下,anaconda