SeaBIOS實現簡單分析
SeaBIOS是一個16bit的x86 BIOS的開源實現,常用於QEMU等仿真器中使用。本文將結合SeaBIOS Execution and code flow和SeaBIOS的源碼對SeaBIOS的全過程進行簡單分析。需要注意,本文不是深入的分析,對於一些比較復雜和繁瑣的部分直接跳過了。
從整體角度出發,SeaBIOS包含四個階段。
- 加電自檢(Power On Self Test, POST)
- 引導(Boot)
- 運行時(Main runtime phase)
- 繼續運行和重啟(Resume and reboot)
加電自檢階段
QEMU仿真器會將SeaBIOS加電自檢階段的第一條指令放置在F000:FFF0的位置。當QEMU仿真器啟動之后,將會執行這一條指令。為什么放置在F000:FFF0位置呢?
+------------------+ <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\
/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000
這是從MIT 6.828 Lab1當中截取下來的一個圖。PC的物理地址空間根據人們長期實踐下來的約定,往往被按照上面的方式來划分。而BIOS固件,將會被放置在BIOS ROM的區域當中。這一塊區域的范圍是F000:0000~F000:FFFF。剛好是64KB。而為什么放在這64KB區域的頂部?因為英特爾設計8088處理器的時候,就設計成了上電啟動時,將指令指針寄存器IP設置為0xFFF0,將代碼段寄存器CS設置為0xF000(指向BIOS ROM這一段)。所以,將第一條指令放在F000:FFF0位置,啟動后它將立刻被執行。(實際上,這么說不是很嚴謹。其實CPU是從0xFFFFFFF0,也就是32bit地址線可尋址空間的最后16字節位置開始執行代碼的。在剛開機的時候雖然CS為0xF000,但是它的段基址實際上是0xFFFF0000,而不是按照*16方法計算出來的0xF0000。這一點的原因在后面的“關於make_bios_writable”部分介紹)。
這個SeaBIOS里的“第一條指令”就在romlayout.S的reset_vector中。
reset_vector:
ljmpw $SEG_BIOS, $entry_post
通過這條jmp指令,程序跳轉到CS:IP為$SEG_BIOS:$entry_post的位置。這兩個是兩個常量,分別為0xF000和0xE05B。而entry_post是如下定義的:
ORG 0xe05b
entry_post:
cmpl $0, %cs:HaveRunPost // Check for resume/reboot
jnz entry_resume
ENTRY_INTO32 _cfunc32flat_handle_post // Normal entry point
entry_post中,首先通過一條cmpl指令,判斷是否已經經歷過POST階段。如果已經經歷過該階段,意味着當前不應該重新進行,而應該進入繼續運行(Resume)。所以,如果%cs:HaveRunPost不為0,意味着已經經歷過POST階段,則進入繼續運行(entry_resume),具體的過程在第四個階段會介紹。而對於其他情況,就會進入handle_post函數。
handle_post是一個32bit的C函數,在post.c文件中。需要注意,此時機器是在16bit實模式下的。為了調用32bit的C函數,通過ENTRY_INTO32,先將機器切換到保護模式,然后才能調用。
我們進一步分析ENTRY_INTO32是如何實現的。ENTRY_INTO32是一個宏,用於將機器切換到保護模式,然后調用一個C函數。
.macro ENTRY_INTO32 cfunc
xorw %dx, %dx
movw %dx, %ss
movl $ BUILD_STACK_ADDR , %esp
movl $ \cfunc , %edx
jmp transition32
.endm
可以看到,這里的cfunc是一個指向C編譯器生成的函數的Label,被傳遞到edx寄存器中。此外,ENTRY_INTO32還會設置好堆棧段寄存器SS為0,也就是將BIOS ROM程序函數調用中的堆棧保存在Low Memory區域。
transition32將使用到edx寄存器里面的值。下面是transition32的實現:
transition32:
// Disable irqs (and clear direction flag)
cli
cld
// Disable nmi
movl %eax, %ecx
movl $CMOS_RESET_CODE|NMI_DISABLE_BIT, %eax
outb %al, $PORT_CMOS_INDEX
inb $PORT_CMOS_DATA, %al
// enable a20
inb $PORT_A20, %al
orb $A20_ENABLE_BIT, %al
outb %al, $PORT_A20
movl %ecx, %eax
transition32_nmi_off:
// Set segment descriptors
lidtw %cs:pmode_IDT_info
lgdtw %cs:rombios32_gdt_48
// Enable protected mode
movl %cr0, %ecx
andl $~(CR0_PG|CR0_CD|CR0_NW), %ecx
orl $CR0_PE, %ecx
movl %ecx, %cr0
// start 32bit protected mode code
ljmpl $SEG32_MODE32_CS, $(BUILD_BIOS_ADDR + 1f)
.code32
// init data segments
1: movl $SEG32_MODE32_DS, %ecx
movw %cx, %ds
movw %cx, %es
movw %cx, %ss
movw %cx, %fs
movw %cx, %gs
jmpl *%edx
首先,先屏蔽中斷,並清空方向標志位。然后通過向一個端口寫入NMI_DISABLE_BIT的方式屏蔽NMI(這里具體的不探究)。然后這一點是非常重要的——啟動A20 Gate。
啟動A20總線
首先將介紹A20總線。我們知道,8086/8088系列的CPU,在實模式下,按照段地址:偏移地址的方式來尋址。這種方式可以訪問的最大內存地址為0xFFFF:0xFFFF,轉換為物理地址0x10FFEF。而這個物理地址是21bit的,所以為了表示出這個最大的物理地址,至少需要21根地址線才能表示。
然而,8086/8088地址總線只有20根。所以在8086/8088系列的CPU上,比如如果需要尋址0x10FFEF,則會因為地址線數目不夠,被截斷成0x0FFEF。再舉個例子,如果要訪問物理地址0x100000,則會被截斷成0x00000。第21位會被省略。也就是說地址不斷增長,直到0x100000的時候,會回到“0x00000”的實際物理地址。這個現象被稱為“回環”現象。這種地址越界而產生回環的行為被認為是合法的,以至於當時很多程序利用到了這個特性(比如假定訪問0x100000就是訪問0x00000)。
然而,80286到來了。80286具有24根地址總線。對於為8086處理器設計的程序,設計者可能假定第21位會被省略。然而,在具有24根地址總線的80286機器上,則沒有這個特性了。於是,如果不做出一些調整。地址總線數目的增加,可能導致向下兼容性被破壞。於是,當時的工程師們想了一個辦法,設計了A20總線,用來控制第21位(如果最低位編號為0,那第21位的編號就是20)及更高位是否有效。實際上可以想象成,第21位(及更高位)都接入了一個和A20總線的與門。當A20總線為1,則高位保持原來的。當A20總線為0,則高位就始終為0。這樣,當A20總線為0的時候,8086/8088的回環現象將會保持。這么一來舊程序就可以兼容了。
控制A20總線的端口被稱為A20-Gate。使用in/out指令控制,即可控制A20總線是否打開。A20 Gate是0x92端口的第二個bit。先獲得0x92端口的值並存放在al寄存器中,然后通過or將該寄存器的第二個bit設置為1。然后再將al的值寫入0x92端口即可。這就是上面的enable a20部分的原理。
從實模式進入32位保護模式
在16bit實模式下,最多訪問20根地址線。且段內偏移不能超過64KB(16位)。而32位保護模式下,則沒有了最多訪問20根地址線的限制,且段內偏移可以達到4GB(32位)。
此外,保護模式最大的特點是:原先的段基地址:段偏移的尋址方式,變為段選擇符:段偏移的尋址方式。這里不再繼續介紹保護模式,因為篇幅有限。有需要者可以自己查閱資料。
首先,前兩條指令將設定中斷描述符表和全局描述符表。我們重點關注全局描述符表。
// GDT
u64 rombios32_gdt[] VARFSEG __aligned(8) = {
// First entry can't be used.
0x0000000000000000LL,
// 32 bit flat code segment (SEG32_MODE32_CS)
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_B,
// 32 bit flat data segment (SEG32_MODE32_DS)
GDT_GRANLIMIT(0xffffffff) | GDT_DATA | GDT_B,
// 16 bit code segment base=0xf0000 limit=0xffff (SEG32_MODE16_CS)
GDT_LIMIT(BUILD_BIOS_SIZE-1) | GDT_CODE | GDT_BASE(BUILD_BIOS_ADDR),
// 16 bit data segment base=0x0 limit=0xffff (SEG32_MODE16_DS)
GDT_LIMIT(0x0ffff) | GDT_DATA,
// 16 bit code segment base=0xf0000 limit=0xffffffff (SEG32_MODE16BIG_CS)
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_BASE(BUILD_BIOS_ADDR),
// 16 bit data segment base=0 limit=0xffffffff (SEG32_MODE16BIG_DS)
GDT_GRANLIMIT(0xffffffff) | GDT_DATA,
};
// GDT descriptor
struct descloc_s rombios32_gdt_48 VARFSEG = {
.length = sizeof(rombios32_gdt) - 1,
.addr = (u32)rombios32_gdt,
};
先看(從第0項開始的)第1項
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_B
這個GDT項對應的32位段基地址是0x00000000。而長度限制limit為0xFFFFFFFF。並且在32bit保護模式下偏移量也是32bit的。這意味着這個GDT項可以映射到整個物理地址空間(所以叫“Flat” code segment)。
然后Enter protected mode那里則是進入保護模式的經典方法。控制寄存器CR0的最低位(PE位)如果為1,則表示處理器處於保護模式,否則則處於實模式。我們重點關注
orl $CR0_PE, %ecx
這一個指令將PE位置為1。然后再次寫入cr0寄存器。處理器即進入保護模式。下一條指令非常奇怪:
ljmpl $SEG32_MODE32_CS, $(BUILD_BIOS_ADDR + 1f)
這里的1f要區分清楚。指的是前方第一個標簽為“1”的位置,而不是代表十六進制數0x1F。下一個標簽“1”就是這個指令的下一條。所以,看起來這個跳轉是沒有價值的。實際上,在cr0寄存器被設定好之前,下一條指令已經被放入流水線。而再放入的時候這條指令還是在實模式下的。所以這個ljmp指令是為了清空流水線,確保下一條指令在保護模式下執行。
現在,我們已經在保護模式了!在這里,程序進行了這些操作:
.code32
// init data segments
1: movl $SEG32_MODE32_DS, %ecx
movw %cx, %ds
movw %cx, %es
movw %cx, %ss
movw %cx, %fs
movw %cx, %gs
這里實際上含義很明確。就是初始化ds、es、ss、fs、gs寄存器,將數據段的段選擇器傳遞給它們即可。
然后,就交給C語言編譯器編譯產生的代碼啦!通過一個跳轉指令
jmpl *%edx
就完成了跳轉。
從32位保護模式回到實模式
雖然沒有用到這個,但是這里順便分析一下從32位保護模式回到實模式的方法。SeaBIOS是這樣實現的:
transition16:
// Reset data segment limits
movl $SEG32_MODE16_DS, %ecx
movw %cx, %ds
movw %cx, %es
movw %cx, %ss
movw %cx, %fs
movw %cx, %gs
// Jump to 16bit mode
ljmpw $SEG32_MODE16_CS, $1f
.code16
// Disable protected mode
1: movl %cr0, %ecx
andl $~CR0_PE, %ecx
movl %ecx, %cr0
// far jump to flush CPU queue after transition to real mode
ljmpw $SEG_BIOS, $2f
// restore IDT to normal real-mode defaults
2: lidtw %cs:rmode_IDT_info
// Clear segment registers
xorw %cx, %cx
movw %cx, %fs
movw %cx, %gs
movw %cx, %es
movw %cx, %ds
movw %cx, %ss // Assume stack is in segment 0
jmpl *%edx
這里需要注意一些地方。
恢復段描述符高速緩沖寄存器
首先要了解段描述符高速緩沖寄存器(Descriptor cache register)。我們知道,GDT是存在存儲器當中的。每一次在存儲器中存取數據的時候,CPU需要先尋址,而尋址需要先根據段選擇子計算段基址。這個計算又需要對存儲器中的GDT做一次讀取。於是就多了一次存儲器訪問,大大影響程序執行性能。所以,Intel提供的解決方案是為每一個段寄存器配備一個段描述符高速緩沖寄存器。當段寄存器被重新賦值的時候,就根據段選擇子,從存儲器中讀取GDT中的項,然后將段基址以及其他的段描述符信息存儲在這個段寄存器對應的段描述符高速緩沖寄存器中。下一次尋址的時候,就可以直接查詢這個段描述符高速緩沖寄存器。性能好了很多。
然而這個寄存器,在實模式下仍然是有效的。也就是實模式下仍然會查詢該寄存器以獲取段基址(其實值就是當前段寄存器的值*16)。具體表格如下(出處)
這就給我們一個需要注意的地方。我們在從32位保護模式切換到實模式的時候,要先把段描述符高速緩沖寄存器的內容恢復成實模式下的狀態,然后再切換回去。因為在實模式下,我們無法再像保護模式中那樣設定寄存器中的值了。
如何恢復呢?對於非代碼段,其實很簡單。因為我們段基地址全部初始化成0,其他段界限等也都一樣,所以只需要在GDT中新建一個表項目,也就是SeaBIOS源碼中打*的這一項,然后將各個數據段寄存器設置成它即可。
// GDT
u64 rombios32_gdt[] VARFSEG __aligned(8) = {
// First entry can't be used.
0x0000000000000000LL,
// 32 bit flat code segment (SEG32_MODE32_CS)
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_B,
// 32 bit flat data segment (SEG32_MODE32_DS)
GDT_GRANLIMIT(0xffffffff) | GDT_DATA | GDT_B,
// 16 bit code segment base=0xf0000 limit=0xffff (SEG32_MODE16_CS)
GDT_LIMIT(BUILD_BIOS_SIZE-1) | GDT_CODE | GDT_BASE(BUILD_BIOS_ADDR),
// 16 bit data segment base=0x0 limit=0xffff (SEG32_MODE16_DS)
// ************************************************
GDT_LIMIT(0x0ffff) | GDT_DATA,
// 16 bit code segment base=0xf0000 limit=0xffffffff (SEG32_MODE16BIG_CS)
GDT_GRANLIMIT(0xffffffff) | GDT_CODE | GDT_BASE(BUILD_BIOS_ADDR),
// 16 bit data segment base=0 limit=0xffffffff (SEG32_MODE16BIG_DS)
GDT_GRANLIMIT(0xffffffff) | GDT_DATA,
};
具體實現就對應了上面的“Reset data segment limits”部分的代碼。
代碼段寄存器的恢復
我們知道CS寄存器不能通過mov指令修改,於是就不能通過mov指令來恢復CS寄存器對應的段描述符高速緩沖寄存器了。修改CS的唯一方法是通過JMP指令。
SeaBIOS的實現是,創建了一個SEG32_MODE16_CS表項。然后通過一個ljmp指令跳轉,來恢復CS寄存器。
ljmpw $SEG32_MODE16_CS, $1f
關閉PE
在上面代碼的“Disable protected mode”部分,將CR0寄存器的PE位置0即刻關閉保護模式。然后,和前面一樣,通過一個ljmp刷新流水線。確保后面的指令都是在實模式中運行。
進入handle_post函數
通過ENTRY_INTO32 _cfunc32flat_handle_post語句,即先進入保護模式,然后完成對C函數handle_post的調用。
handle_post函數的定義如下:
// Entry point for Power On Self Test (POST) - the BIOS initilization
// phase. This function makes the memory at 0xc0000-0xfffff
// read/writable and then calls dopost().
void VISIBLE32FLAT
handle_post(void)
{
if (!CONFIG_QEMU && !CONFIG_COREBOOT)
return;
serial_debug_preinit();
debug_banner();
// Check if we are running under Xen.
xen_preinit();
// Allow writes to modify bios area (0xf0000)
make_bios_writable();
// Now that memory is read/writable - start post process.
dopost();
}
首先是一些基本的准備工作,比如啟動串口調試等等,這些細節我們就忽略了。從make_bios_writable開始。
關於make_bios_writable
這里不放代碼,僅僅簡單介紹該函數的作用——允許更改RAM中的BIOS ROM區域。在介紹make_bios_writable之前,首先對Shadow RAM做一些介紹。實際上,盡管在啟動的時候,是從F000:FFF0加載第一條指令的,你可能會覺得在啟動的時候代碼段段基址是0xF0000。其實,並不是這樣的。在計算機啟動的時候,代碼段段基地址實際上是是0xFFFF0000(這里就不符合那個乘16的計算方式了)。筆者猜測這一一點的實現方式是通過段描述符高速緩沖寄存器實現的(實模式下也是通過查詢這個寄存器來獲得段基址的),開機的時候代碼段的對應基址項被設置成0xFFFF0000。
為什么從這里開始呢?我們知道BIOS是存儲在ROM當中的。而Intel有一個習慣,將BIOS固件代碼從ROM中映射到可尋址地址的末端(最后64K內)。這里的“映射”,並不是復制,而是當讀取這個地址的時候,就直接讀取ROM存儲器當中的值。在8086時期,可尋址的地址為0x00000-0xFFFFF,所以說它的“末端”確實是從我們理解的0xF0000開始的。所以在8086時期,硬件設備將會將原本存儲於ROM的BIOS映射到F000:0000-F000:FFFF。然而,到了后面有32根地址線,實際上末端應該是0xFFFF0000-0xFFFFFFFF這一部分。此時的計算機,實際上是將BIOS固件代碼映射到0xFFFF0000-0xFFFFFFFF中。
所以,實際上SeaBIOS的這一行指令:
reset_vector:
ljmpw $SEG_BIOS, $entry_post
是位於0xFFFFFFF0的物理地址位置的。但是我們注意到這是一個Long jump指令,這個指令會使CPU重新計算代碼段寄存器,原本的0xFFFF0000基地址,在這一個指令執行之后,就會變成符合乘16計算方式的0xF0000!
讀者可能會想,這不就出問題了嗎?32根地址線的PC,BIOS固件明明在最后呀!實際上,為了保持向前兼容性,機器啟動的時候會自動將ROM的BIOS復制到RAM的BIOS ROM區域當中。所以,通過ljmpw指令跳轉之后,因為已經復制了,在RAM當中也有BIOS固件代碼。所以是不會有問題的。
這個復制高地址處的ROM到低地址處的過程被稱為Shadow RAM技術。然而,在這個過程后,這段內存會被保護起來,無法進行寫入。make_bios_writable函數就用於讓這段內存可寫,從而便於更改一些靜態分配的全局變量值。
進入dopost
剛才已經做好了准備。然后,就可以進入dopost函數了。這個函數是POST過程的主體。depost當中,將會調用maininit函數。下面的“maininit過程”,將作詳細介紹。
maininit過程
dopost函數定義如下
// Setup for code relocation and then relocate.
void VISIBLE32INIT
dopost(void)
{
code_mutable_preinit();
// Detect ram and setup internal malloc.
qemu_preinit();
coreboot_preinit();
malloc_preinit();
// Relocate initialization code and call maininit().
reloc_preinit(maininit, NULL);
}
首先,看code_mutable_preinit。
void
code_mutable_preinit(void)
{
if (HaveRunPost)
// Already run
return;
// Setup reset-vector entry point (controls legacy reboots).
rtc_write(CMOS_RESET_CODE, 0);
barrier();
HaveRunPost = 1;
barrier();
}
這一段的核心是將HaveRunPost設置為1。可以看出,HaveRunPost實際上相當於一個全局變量,在BIOS ROM中實際上是被初始化為0的。然后將ROM映射到RAM中的BIOS ROM區域之后,通過make_bios_writable,使得這一段RAM區域可寫,然后才能更改HaveRunPost的值。
為了初始化內存,SeaBIOS實現了自己的malloc函數。通過malloc_preinit進行初始化,然后通過reloc_preinit函數將自身代碼進行重定位。這些步驟有非常多的工程細節,就忽略不看了。接下來從重要的函數:maininit開始分析。
// Main setup code.
static void
maininit(void)
{
// Initialize internal interfaces.
interface_init();
// Setup platform devices.
platform_hardware_setup();
// Start hardware initialization (if threads allowed during optionroms)
if (threads_during_optionroms())
device_hardware_setup();
// Run vga option rom
vgarom_setup();
sercon_setup();
enable_vga_console();
// Do hardware initialization (if running synchronously)
if (!threads_during_optionroms()) {
device_hardware_setup();
wait_threads();
}
// Run option roms
optionrom_setup();
// Allow user to modify overall boot order.
interactive_bootmenu();
wait_threads();
// Prepare for boot.
prepareboot();
// Write protect bios memory.
make_bios_readonly();
// Invoke int 19 to start boot process.
startBoot();
}
maininit函數中,有很多重要的部分。我們來看一看。
首先是interface_init函數。
void
interface_init(void)
{
// Running at new code address - do code relocation fixups
malloc_init();
// Setup romfile items.
qemu_cfg_init();
coreboot_cbfs_init();
multiboot_init();
// Setup ivt/bda/ebda
ivt_init();
bda_init();
// Other interfaces
boot_init();
bios32_init();
pmm_init();
pnp_init();
kbd_init();
mouse_init();
}
這個函數用於加載內部的一些接口。下面是其步驟。
初始化中斷向量表IVT
中斷向量表(Interrupt Vector Table)是一張在實模式下使用的表。顧名思義,這個表將中斷號映射到中斷過程的一個列表。中斷向量表必須存儲在低地址區域(也就是從0x00000000)開始,大小一般是0x400字節。是一塊由很多個項組成的連續的內存空間。每一項,就對應了一個中斷,如下所示(出處):
+-----------+-----------+
| Segment | Offset |
+-----------+-----------+
4 2 0
每一項被稱為中斷向量(Interrupt Vector)。筆者認為因為每一項都可以寫成(segment, offset)的形式,仿佛一個二維向量坐標,所以被稱為“中斷向量”。可以看出每一項占據4個字節,前兩個字節是段,后兩個字節是偏移。而這一項實際上就對應了一個中斷服務處理程序的入口地址(段:偏移)。只需要修改這個表里的地址,即可更換中斷處理程序。
而每一個中斷都有一個中斷號碼,號碼就是中斷向量的索引。比如這個表里前四個字節對應的項,中斷號就是0,然后4-8字節對應的項中斷號就是1。可以很容易地看出,中斷號*4為首地址的4字節內存區域就對應了該中斷號對應的中斷處理程序位置。
SeaBIOS中,ivt_init就是初始化一些中斷。我們看實現。
static void
ivt_init(void)
{
dprintf(3, "init ivt\n");
// Initialize all vectors to the default handler.
int i;
for (i=0; i<256; i++)
SET_IVT(i, FUNC16(entry_iret_official));
// Initialize all hw vectors to a default hw handler.
for (i=BIOS_HWIRQ0_VECTOR; i<BIOS_HWIRQ0_VECTOR+8; i++)
SET_IVT(i, FUNC16(entry_hwpic1));
for (i=BIOS_HWIRQ8_VECTOR; i<BIOS_HWIRQ8_VECTOR+8; i++)
SET_IVT(i, FUNC16(entry_hwpic2));
// Initialize software handlers.
SET_IVT(0x02, FUNC16(entry_02));
SET_IVT(0x05, FUNC16(entry_05));
SET_IVT(0x10, FUNC16(entry_10));
SET_IVT(0x11, FUNC16(entry_11));
SET_IVT(0x12, FUNC16(entry_12));
SET_IVT(0x13, FUNC16(entry_13_official));
SET_IVT(0x14, FUNC16(entry_14));
SET_IVT(0x15, FUNC16(entry_15_official));
SET_IVT(0x16, FUNC16(entry_16));
SET_IVT(0x17, FUNC16(entry_17));
SET_IVT(0x18, FUNC16(entry_18));
SET_IVT(0x19, FUNC16(entry_19_official));
SET_IVT(0x1a, FUNC16(entry_1a_official));
SET_IVT(0x40, FUNC16(entry_40));
// INT 60h-66h reserved for user interrupt
for (i=0x60; i<=0x66; i++)
SET_IVT(i, SEGOFF(0, 0));
// set vector 0x79 to zero
// this is used by 'gardian angel' protection system
SET_IVT(0x79, SEGOFF(0, 0));
}
首先,ivt_init將所有中斷初始化到一個空處理函數(相當於只有一條return語句)。將所有硬中斷也都初始化到一個默認處理函數。然后是一些默認的中斷處理程序。比如經典的VGA服務INT 10H,經典的磁盤服務INT 13H等等。對於每一項的事先,這里不多介紹。
初始化BIOS數據區域BDA
BDA(BIOS Data Area),是存放計算機當前一些狀態的位置。在SeaBIOS中,其定義如下:
struct bios_data_area_s {
// 40:00
u16 port_com[4];
u16 port_lpt[3];
u16 ebda_seg;
// 40:10
u16 equipment_list_flags;
u8 pad1;
u16 mem_size_kb;
u8 pad2;
u8 ps2_ctrl_flag;
u16 kbd_flag0;
u8 alt_keypad;
u16 kbd_buf_head;
u16 kbd_buf_tail;
// 40:1e
u8 kbd_buf[32];
u8 floppy_recalibration_status;
u8 floppy_motor_status;
// 40:40
u8 floppy_motor_counter;
u8 floppy_last_status;
u8 floppy_return_status[7];
u8 video_mode;
u16 video_cols;
u16 video_pagesize;
u16 video_pagestart;
// 40:50
u16 cursor_pos[8];
// 40:60
u16 cursor_type;
u8 video_page;
u16 crtc_address;
u8 video_msr;
u8 video_pal;
struct segoff_s jump;
u8 other_6b;
u32 timer_counter;
// 40:70
u8 timer_rollover;
u8 break_flag;
u16 soft_reset_flag;
u8 disk_last_status;
u8 hdcount;
u8 disk_control_byte;
u8 port_disk;
u8 lpt_timeout[4];
u8 com_timeout[4];
// 40:80
u16 kbd_buf_start_offset;
u16 kbd_buf_end_offset;
u8 video_rows;
u16 char_height;
u8 video_ctl;
u8 video_switches;
u8 modeset_ctl;
u8 dcc_index;
u8 floppy_last_data_rate;
u8 disk_status_controller;
u8 disk_error_controller;
u8 disk_interrupt_flag;
u8 floppy_harddisk_info;
// 40:90
u8 floppy_media_state[4];
u8 floppy_track[2];
u8 kbd_flag1;
u8 kbd_led;
struct segoff_s user_wait_complete_flag;
u32 user_wait_timeout;
// 40:A0
u8 rtc_wait_flag;
u8 other_a1[7];
struct segoff_s video_savetable;
u8 other_ac[4];
// 40:B0
u8 other_b0[5*16];
} PACKED;
bda_init就是初始化該區域的函數。這里不多介紹。
BOOT階段前最后的准備
然后是一些其他的接口,比如鍵盤、鼠標接口的加載。在interface_init函數的最后部分定義。這里不再介紹。在maininit函數中,剩余的部分
// Run vga option rom
vgarom_setup();
sercon_setup();
enable_vga_console();
// Do hardware initialization (if running synchronously)
if (!threads_during_optionroms()) {
device_hardware_setup();
wait_threads();
}
// Run option roms
optionrom_setup();
// Allow user to modify overall boot order.
interactive_bootmenu();
wait_threads();
// Prepare for boot.
prepareboot();
// Write protect bios memory.
make_bios_readonly();
// Invoke int 19 to start boot process.
startBoot();
用於加載VGA設備、初始化硬件、為用戶提供更改啟動順序的界面。然后,將剛才被設置為可寫的RAM中的BIOS ROM部分,重新保護起來。然后,通過一個startBoot函數,調用INT19中斷,進入Boot狀態。
startBoot
void VISIBLE32FLAT
startBoot(void)
{
// Clear low-memory allocations (required by PMM spec).
memset((void*)BUILD_STACK_ADDR, 0, BUILD_EBDA_MINIMUM - BUILD_STACK_ADDR);
dprintf(3, "Jump to int19\n");
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
call16_int(0x19, &br);
}
當前,CPU出於保護模式。但是在引導至OS Bootloader之前,需要告別保護模式,回到實模式。call16_int使用之前提到的trainsition16回到實模式之后,調用INT 19H中斷,進入BOOT狀態。
引導階段
boot階段的代碼,是從handle_19,也就是的0x19中斷處理程序開始。
void VISIBLE32FLAT
handle_19(void)
{
debug_enter(NULL, DEBUG_HDL_19);
BootSequence = 0;
do_boot(0);
}
do_boot函數:
static void
do_boot(int seq_nr)
{
if (! CONFIG_BOOT)
panic("Boot support not compiled in.\n");
if (seq_nr >= BEVCount)
boot_fail();
// Boot the given BEV type.
struct bev_s *ie = &BEV[seq_nr];
switch (ie->type) {
case IPL_TYPE_FLOPPY:
printf("Booting from Floppy...\n");
boot_disk(0x00, CheckFloppySig);
break;
case IPL_TYPE_HARDDISK:
printf("Booting from Hard Disk...\n");
boot_disk(0x80, 1);
break;
case IPL_TYPE_CDROM:
boot_cdrom((void*)ie->vector);
break;
case IPL_TYPE_CBFS:
boot_cbfs((void*)ie->vector);
break;
case IPL_TYPE_BEV:
boot_rom(ie->vector);
break;
case IPL_TYPE_HALT:
boot_fail();
break;
}
// Boot failed: invoke the boot recovery function
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
call16_int(0x18, &br);
}
可以發現這里為從軟盤、硬盤、CDROM等一系列設備中引導提供了支持。我們重點關注從硬盤引導。
static void
boot_disk(u8 bootdrv, int checksig)
{
u16 bootseg = 0x07c0;
// Read sector
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
br.dl = bootdrv;
br.es = bootseg;
br.ah = 2;
br.al = 1;
br.cl = 1;
call16_int(0x13, &br);
if (br.flags & F_CF) {
printf("Boot failed: could not read the boot disk\n\n");
return;
}
if (checksig) {
struct mbr_s *mbr = (void*)0;
if (GET_FARVAR(bootseg, mbr->signature) != MBR_SIGNATURE) {
printf("Boot failed: not a bootable disk\n\n");
return;
}
}
tpm_add_bcv(bootdrv, MAKE_FLATPTR(bootseg, 0), 512);
/* Canonicalize bootseg:bootip */
u16 bootip = (bootseg & 0x0fff) << 4;
bootseg &= 0xf000;
call_boot_entry(SEGOFF(bootseg, bootip), bootdrv);
}
讀取主引導扇區
首先,通過調用0x13中斷讀取主引導扇區。讀扇區的參數定義如下:
功能描述:讀扇區
入口參數:AH=02H
AL=扇區數
CH=柱面
CL=扇區
DH=磁頭
DL=驅動器,00H~7FH:軟盤;80H~0FFH:硬盤
ES:BX=緩沖區的地址
出口參數:CF=0——操作成功,AH=00H,AL=傳輸的扇區數,否則,AH=狀態代碼
首先,需要了解(復習)磁盤的CHS模式。Cylinder-head-sector(柱面-磁頭-扇區, CHS)是一個定位磁盤數據位置的方法。因為向磁盤讀取數據,要先告訴磁盤讀取哪里的數據。
下面的兩張圖片選自Wikipedia。
首先介紹磁頭。一個磁盤可以看成多個重疊的盤片組成。磁頭可以選擇讀取哪一個盤片上的數據。
其次是柱面。在某一個盤片上,找一個同心圓,這個圓對應的圈叫做磁道。而把所有盤片疊在一起,所有磁道構成的面叫做柱面。所以實際上柱面可以指定在當前磁頭所指向的盤片上磁道的“半徑”。
然后是扇區。扇區的概念非常重要。一個磁道上連續的一段稱為扇區。每個磁道被等分為若干個扇區。一般來說,一個扇區包含512字節的數據。
磁頭、柱面都是從0開始編號的。扇區是從1開始編號的。
從代碼的Read sector部分可以看出,boot_disk將讀取使用bootdrv指定的磁盤驅動器上,0磁頭,0柱面,1扇區為起始位置,扇區數為1(512字節)的一段數據。然后將這段部分復制到0x7c00的內存地址當中。這個內存地址可謂是非常經典。
在checksig的部分,GET_FARVAR那一句的含義就是以bootseg為段來讀取mbr中的signature。而mbr此時指向的地址偏移量為0。我們看mbr_s數據結構的定義:
struct mbr_s {
u8 code[440];
// 0x01b8
u32 diskseg;
// 0x01bc
u16 null;
// 0x01be
struct partition_s partitions[4];
// 0x01fe
u16 signature;
} PACKED;
可以看出signature是MBR主引導扇區代碼的最后兩個字節。這里又是一大經典。可以看出,checksig的部分是用於校驗主引導扇區代碼的最后兩個字節是否為MBR_SIGNATURE。而這個值恰恰就是那個經典的數字:0xAA55。
接近尾聲
引導過程快要接近尾聲了。我們注意到一個“規范化”(Canonicalize)操作:
/* Canonicalize bootseg:bootip */
u16 bootip = (bootseg & 0x0fff) << 4;
bootseg &= 0xf000;
這個操作實際上是為了調用OS Loader的時候,段為0x0000,而偏移為0x7C00。而不是段為0x07C0,而偏移為0x0000。
最后,通過一個call_boot_entry函數,轉移到OS Loader。此時,系統處於實模式下。
static void
call_boot_entry(struct segoff_s bootsegip, u8 bootdrv)
{
dprintf(1, "Booting from %04x:%04x\n", bootsegip.seg, bootsegip.offset);
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
br.code = bootsegip;
// Set the magic number in ax and the boot drive in dl.
br.dl = bootdrv;
br.ax = 0xaa55;
farcall16(&br);
}
這就是引導部分的主要內容。
剩余部分
因為最近事情比較多,最后兩部分還沒有寫完。后面更新。