讀Kernel感悟-Linux內核啓動-setup輔助程序

文章來源:http://www.top-e.org/jiaoshi/class/

 

我們發現,在起點與終點之間,還有幾個中轉站。最近的一站叫作MBR。BIOS,帶你到MBR後,說:“對不起,只能送你到這裏了。”

那其它幾個中轉站是什麼呢?我們知道,在x86上,保護模式有兩種,32位頁式尋址的保護模式和32位段式尋址的保護模式。顯然,32位頁式尋址的保護模式要求系統中已經構造好頁表。從16位實地址模式直接到32位頁式尋址的保護模式是很有難度的,因爲你要在16位實地址模式下構造頁表。所以不妨三級跳,先從16位實地址模式跳到32位段式尋址的保護模式,再從32位段式尋址的保護模式跳到32位頁式尋址的保護模式。

我們需要這樣一個程序,負責從16位實地址模式跳到32位段式尋址的保護模式,然後設置eip,啓動內核。這個程序的確存在,就是arch/i386/boot/setup.S。最後彙編成setup程序。

事實上,平時所見的壓縮內核映象bzImage,其實由三部分組成。這可以從arch/i386/boot/tools/build.c中看出來。build.c是用戶態工具build的源代碼,後者用來把bootsect(MBR),setup(輔助程序)和vmlinux.bin(壓縮過的內核)拼接成bzImage。

setup有了,它可以啓動內核,然而新的問題來了。誰來啓動setup呢?第一個想到的是MBR。可惜,MBR只有512個字節,而且有64個字節來存放主分區表。這樣看來,MBR的功能就很有限了。所以,最好在setup和MBR之間再架一座橋樑。引導程序,對,用它來引導setup再合適不過了。現有的引導程序如grub,lilo不僅功能強大,而且還提供了人機交互的功能。再合適不過了。

所以,我們理清了大概思路,查閱相關資料可知:

1.CPU加電,從0xffff0處,執行BIOS(可以理解爲“硬件”引導BIOS)

2.BIOS執行掃描檢測設備的操作,然後將MBR讀到物理地址爲0x7c00處。然後從MBR頭部開始執行(可以理解爲BIOS引導MBR)

3.MBR上的代碼跳轉到引導程序,開始執行引導程序的代碼,例如grub(引以理解爲BIOS引導boot loader)。

4.引導程序把內核映象(包括bootsect,setup,vmlinux)讀到內存中,其中setup位於0x90200處,如果是zImage,則vmlinux.bin位於0x10000(64K)處。如果是bzImage,則vmlinux.bin位於0x100000(1M)處。然後執行setup(可以理解爲boot loader引導setup)

5.setup負責引導linux內核vmlinux.bin

現在,讓我們看看setup做了些什麼。

首先,運行一下file setup

它報告說這是一個MS DOS的可執行程序。看來,file也有不正常工作的時候。不過有一點是肯定的。setup不是普通的elf可執行程序。事實上,gcc可以編譯出各種格式的程序,具體可以運行objdump -i,查看ld(gcc的鏈接器)支持的輸出格式。其中有一個是binary格式。

從start開始,setup做了很多操作,例如,如果是zImage,則把它從0x10000拷貝到0x1000,調用bios功能,查詢硬件信息,然後放在內存中供將來的內核使用,然後建立臨時的idt和gdt,負責把16位實地址模式轉化爲32位段式尋址的保護模式。前面這些我們不關心。我們關心的是後者:

_

832         movw    $1, %ax                         # protected mode (PE) bit

833         lmsw    %ax                             # This is it!

正是以下指令開啓了保護模式的大門。

最後,再臨門一腳

對bzImage來說:

jmpi    0x100000,__BOOT_CS

對zImage來說:

jmpi    0x1000,__BOOT_CS

就大功告成了。

不過,且慢,由於當時CS寄存器還沒有設置爲__BOOT_CS,所以,儘管在保護模式下,也仍然不能用通常的方式訪問大於1M的內存。

不過,x86處理器提供了特殊的手段來訪問大於1M的內存,那就是在指令前加前綴0x66。由於跳轉地址與內核大小相關(zImage和bzImage不一樣)所以用一個小技巧,即把該指令當作數據處理。在計算機看來,指令和數據是沒什麼區別的,只要ip寄存器指向內存中某地址,計算機就把地址中的數據當作指令來看待。

854         .byte 0x66, 0xea                        # prefix + jmpi-opcode

855 code32: .long   0x1000                          # will be set to 0x100000

856                                                 # for big kernels

857         .word   __BOOT_CS

我不僅發出感慨:看setup.S的代碼真是一種痛苦的體驗,並且有以下感悟:

1.爲什麼不用C寫呢?

原因很簡單,因爲setup要儘量短小精悍,由於種種原因,它的大小不能超過63.5K.啓動時內存分配如下:

0x00000~0x00400 BIOS中斷向量表

0x00400~0x01000

0x01000~0x10000

0x10000~0x8f000 用來存放zImage所以最多508K

0x8f000~0x90000 引導程序的命令行參數以及bios查詢的信息

0x90000~0x90200 MBR

0x90200~0xA0000 setup

0xA0000~0x100000 映射到BIOS和外設硬件等

0x100000~ 存放bzImage(如果大於508K)

2.如何引用變量?

在setup.S中,定義了不少全局變量。在編譯生成setup文件時,鏈接參數

LDFLAGS_setup    := -Ttext 0x0 -s --oformat binary -e begtext

意爲text段的地址爲0,輸出格式爲binary(而不是默認的elf32-i386),入口地址begtext。

由於基本上處於16位實模式下,所以只要設置好CS等段寄存器就可以正確地尋址了。

現在我們跳到vmlinux.bin的開頭(位於0x1000或者0x100000),也就是startup_32(),執行相應代碼。

3.helloworld,vmlinux,arch/i386/boot/compressed/vmlinux,setup,bootsect的區別與聯繫。

這幾個可執行文件都是由gcc編譯生成。只是格式不一樣。

其中,helloworld,vmlinux,arch/i386/boot/compressed/vmlinux都是elf32_i386格式的可執行文件。setup,bootsect是binary格式的可執行文件,它們的區別在於

1).helloworld是普通的elf32_i386可執行文件。它的入口是_start。運行在用戶態空間。變量的地址都是32位頁式尋址的保護模式的地址,在用戶態空間。由shell負責裝載。

2).vmlinux是未壓縮的內核,它的入口是startup_32(0x100000,線性地址),運行在內核態空間,變量的地址是32位頁式尋址的保護模式的地址,在內核態空間。由內核自解壓後啓動運行。

3).arch/i386/boot/compressed/vmlinux是壓縮後的內核,它的入口地址是startup_32(0x100000,線性地址).運行在32位段式尋址的保護模式下,變量的地址是32位段式尋址的保護模式的地址。由setup啓動運行。

4).setup是裝載內核的binary格式的輔助程序。它的入口地址是:begtext(偏移地址爲0。運行時需要把cs段寄存器設置爲0x9020)。運行在16位實地址模式下。變量的地址等於相對於代碼段起始地址的偏移地址。由boot loader啓動運行。

5).bootsect是MBR上的引導程序,也爲binary格式。它的入口地址是_start(),由於裝載到0x7c00處,運行時需要把cs段寄存器設置爲0x7c0。運行在16位實地址模式下。變量地址等於相對於代碼段起始地址的偏移地址。由BIOS啓動運行。

問題出現了。startup_32有兩個,分別在這兩個文件中:

arch/i386/boot/compressed/head.S

arch/i386/kernel/head.S

那究竟執行的是哪一個呢?難道編譯時不會報錯麼?

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