話說看linux內核源碼真是一件辛苦的事情啊,爲了弄清楚操做系統,我從linux源碼看到grub源碼,再看到BIOS,真心是傷不起啊。我研究的linux內核版本是2.6.34.13,它不支持從直接從內核啓動,須要一個bootloader。目前linux中使用最普遍的的bootloader是grub,本文就是對grub2.00(下文中簡寫爲grub)在x86下的分析研究。
grub的源碼主要分爲四個部分,分別爲grub-core/boot/i386/pc/boot.S,
grub-core/boot/i386/pc/
diskboot
.S,grub-core/boot/i386/pc/
startup_raw
.S,以及grub的核心代碼
。
- boot.S是MBR中的512個字節,主要工做是加載diskboot.S。
- diskboot.S也是512字節的代碼,它的做用是加載startup_raw.S和grub的核心代碼。
- startup_raw.S的做用是解壓grub的核心代碼。
- grub的核心代碼是grub真正功能實現的地方。
通常
boot.S是放在第一個扇區(即MBR),
diskboot.S是放在第二個扇區,startup_raw.S和grub核心代碼放在接下來的幾十個扇區(可是限於0-63扇區)。下面按照grub的執行流程來研究grub的工做原理。
1. grub-core/boot/i386/pc/boot.S
計算機啓動後(略過BIOS部分)會將硬盤的第一個扇區加載到0000:7C00處,這個扇區的佈局以下圖所示。
此時IP寄存器的值爲7C00,第一條指令爲跳轉指令,跳到BPB(BIOS Parameter Block)後面的代碼處執行。這一部分比較簡單,首先初始化段寄存器,而後從啓動盤讀取第二個扇區到0000:8000處。
/* boot kernel */
jmp *(kernel_address) /* kernel_address = 0000:8000 */
隨這這一條指令的執行,boot.S完成了它的使命,將執行權交給了diskboot.S。
2. grub-core/boot/i386/pc/diskboot.S
diskboot.S的工做跟boot.S相似,它會根據配置信息從啓動盤中讀取很少於62個扇區的數據。它會將這些數據放在0000:8200處的一段內存中,其中開始部分是start_raw.S的代碼,後一部分是壓縮的grub核心代碼。
ljmp $0, $(GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200)
這條跳轉指令是diskboot的最後一條指令(不考慮意外狀況),實際上就是跳轉到0000:8200處。
3. grub-core/kern/i386/pc/startup_raw.S
startup_raw.S的部分關鍵代碼以下,首先須要
設置數據段、堆棧段和擴展段寄存器,以及棧指針。
/* set up %ds, %ss, and %es */
xorw %ax, %ax
movw %ax, %ds
movw %ax, %ss
movw %ax, %es
/* set up the real mode/BIOS stack */
movl $GRUB_MEMORY_MACHINE_REAL_STACK, %ebp
movl %ebp, %esp
進行一些準備工做後開始進入保護模式,
real_to_prot
這個函數的代碼在
grub-core/kern/i386/realmode
.S中
。
/* transition to protected mode */
DATA32 call real_to_prot
進入保護模式後就調用
_LzmaDecodeA解壓壓縮的核心代碼,
_LzmaDecodeA這個函數在
lzma_decode.S中定義
。緊接着的幾條指令的做用是設置參數,而後跳轉到核心代碼。 movl $GRUB_MEMORY_MACHINE_DECOMPRESSION_ADDR, %edi
...
popl %esi
movl LOCAL(boot_dev), %edx
movl $prot_to_real, %edi
movl $real_to_prot, %ecx
movl $LOCAL(realidt), %eax
jmp *%esi
解壓後的核心最開始放在0x00100000處,在上面的指令中,esi寄存器的值就是核心代碼的地址,edx、edi、ecx、eax分別是啓動設備、從保護模式進入實模式函數的地址、從實模式進入保護模式函數的地址、實模式中斷描述符表的地址。
4. grub-core/kern/i386/pc/startup.S
startup.S首先保存傳遞過來的參數,具體實現是下面的三條指令。 movl %ecx, (LOCAL(real_to_prot_addr) - _start) (%esi)
movl %edi, (LOCAL(prot_to_real_addr) - _start) (%esi)
movl %eax, (EXT_C(grub_realidt) - _start) (%esi)
爲何不直接使用這幾個變量的地址呢?這是由於startup.S當前所在地址爲0x00100000,而代碼的目標地址爲0x00008200,因此須要轉換一下變量的地址。
如今須要將核心代碼移動到目標地址,下面的代碼完成了移動的工做。 /* copy back the decompressed part (except the modules) */
movl $(_edata - _start), %ecx
movl $(_start), %edi
rep
movsb
movl $LOCAL (cont), %esi
jmp *%esi
LOCAL(cont):
ecx的值是核心代碼的大小,由_edata減_start得來。edi的值爲_start,也就是目標地址。esi是核心代碼如今所在的地址,在startup_raw.S中設置,到如今一直未有變更。
移動完成之後,jmp指令完成了到目標地址的跳轉,這個跳轉很巧妙,雖然整個代碼移動了位置,但看起來就像沒有移動同樣。 /*
* Call the start of main body of C code.
*/
call EXT_C(grub_main)
接下來清空BSS(Block Started by Symbol
),調用
grub_main進入grub的主函數,這個函數在
grub-core/kern/main.c中
。到這來咱們已經來到了grub的核心代碼部分,這部分主要是grub的模塊化框架的初始化,各類命令的註冊,各類模塊的加載等等。
我比較關心的是grub在用戶選擇指定的系統後怎麼加載linux的,精力有限,其它代碼暫時就不去閱讀了。當用戶選擇指定的內核條目後,
grub-core/commands/boot.c中的命令函數
g
rub_cmd_boot開始執行
。
加載linux的代碼在grub-core/loader/i386目錄下,grub支持16位、32位、64位三種模式啓動linux內核。16位模式下會從新回到實模式,再啓動linux內核。32位和64位兩種模式下不須要再進入實模式,直接在保護模式啓動。
linux.c文件中的
grub_cmd_linux函數就是處理linux內核加載的
。函數
grub_cmd_linux的做用是根據命令行參數生成相關配置數據,而後設置一個回調函數,由
g
rub_cmd_boot調用。這樣作的好處是減少了代碼的耦合度。
g
rub_cmd_boot和
grub_linux_boot兩個函數
將linux內核從文件系統中讀取到內存中,內核代碼分爲兩部分,一部分是實模式代碼,一部分是保護模式代碼。實模式代碼通常放在0x00009000處,保護模式代碼通常放在0x00100000處,在
32位和64位兩種模式啓動實際上不須要實模式部分代碼
。此外,
g
rub爲linux內核設置了
linux_kernel_params
參數,這些參數在linux內核中會用到。
5.
grub_relocatorXX_boot
離進入linux內核就差最後一步了,
grub_linux_boot最後調用
grub_relocatorXX_boot(.../
lib/.../
relocator.c
)
進入內核。顧名思義,最後所要作的事情是重定位。
grub_cmd_linux只是將加載了內核的代碼
,這部分代碼要能使用還必須對代碼的每一個chunk進行重定位。
重定義這部分代碼不是很複雜,須要結合ELF文件格式閱讀,這裏就很少說了。調用
grub_relocator_prepare_relocs重定位好以後,以下面代碼所示,先關閉中斷,而後調用
relst()
就進入到了linux內核,
relst()後面的代碼是永遠不會執行的
。
asm volatile ("cli");
((void (*) (void)) relst) ();
/* Not reached. */
return GRUB_ERR_NONE;
到這裏爲止,本文就算是把grub啓動linux簡要敘述了一遍,其中還有不少細節這裏沒有講,之後有時間再好好看看。