系統引導

實驗目的

  • 建立對操作系統引導過程的深入認識;
  • 掌握操作系統的基本開發過程;
  • 能對操作系統進行簡單的控制,揭開操作系統的神祕面紗。

實驗內容

此次實驗的基本內容是:編寫一個放入引導扇區的操作系統引導程序bootsect.s,和一個進入保護模式前的設置程序setup.s,並將該bootsect.s和setup.s編譯後在Bochs中運行,進行實驗。

編寫的引導程序bootsect.s和setup.s主要完成如下三個部分的功能:

  1. bootsect.s能在屏幕上打印一段提示信息“XXX is booting...”,其中XXX是你給自己的操作系統起的名字,例如LZJos、Sunix等(可以上論壇上秀秀誰的OS名字最帥)。(實驗者也可以 做一個特色logo並顯示在屏幕上,以表示自己操作系統的與衆不同,當然這要花費一定的時間,也不屬於加分內容,鼓勵大家在將來有時間的時候做一下。)
  2. bootsect.s能完成setup.s的載入,並跳轉到setup.s開始地址執行。而setup.s向屏幕輸出一行“Now we are in SETUP”。
  3. setup.s能獲取基本的硬件參數(如內存參數、顯卡參數、硬盤參數等,在本實驗中只要獲取一個參數就取分,獲取多個參數不加分,但後面的實驗中會用到 某些參數,如實驗六的終端設備需要顯卡的參數,所以在將來需要的時候能再回來修改),將這些參數放在內存的特定地址,留着將來使用,並輸出到屏幕上。

實驗報告

完成實驗後,在實驗報告中回答如下問題:

  1. 你覺得boot的過程複雜嗎?爲什麼?
  2. 摒棄一切清規戒律,嘗試設計一個更簡潔的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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章