GRUB STARTUP

1 起始程式

GRUB2 的起始程式位於 grub2\grub-core\kern\i386\pc\startup.S,功能是檢驗核心、解壓縮核心、初始化核心變數記憶體,最後跳到核心主程式,執行核心的啟動程序。

當核心映像形成後,核心映像的內容會分成實碼和壓縮碼二個部分,並使用 reed solomon 錯誤更正碼保護之。 實碼部分市可以直接執行的程式碼,包含初始化程式、錯誤更正程式(reed solomon)、多重載入啟動程式、LZMA 解壓縮程式、一些相關的函式和函式庫, 其體積約數 KB,由 GRUB_KERNEL_MACHINE_RAW_SIZE 所表示。

壓縮碼緊接再實碼的後面,其體積依照所包含的程式多寡決定,編譯完成時,由載入器將其體積值填入實碼的 grub_compressed_size 欄位。 整個壓縮碼的內容可以分成二個部分,即核心映像和全部模組映像,其體積由 grub_kernel_image_size 和 grub_total_module_size 所表示,這兩個體積是未壓縮隻前的體積。 特別注意的是核心映像體積 grub_kernel_image_size 包含實碼部分的體積,壓縮核心映像時,是把核心映像從實碼後面開始進行壓縮,並非壓縮整個核心映像。

執行 GRUB 核心的方式有兩種,一種方法是使用多重啟動載入器,另一種方法是使用 GRUB 啟動程式。 GRUB 核心起始程式 startup.S 負責檢驗核心映像、解壓縮核心映像、執行 GRUB 核心。


1.1 多重啟動載入程式

多重啟動有多重啟動的規範,支援多重啟動的映像可由不同的多重啟動載入器載入並執行。 當多重起動載入器將核心映像載入到記憶體 0x100000的位址上,接著會跳到核心映像的多重啟動載入程式,由核心映像自己接手後面的載入動作。 其做法是將多重啟動表格和啟動碼放在多重啟動映像 0x730 的位址上,在這裡是位址 0x10200+0x730 ,在 GRUB 核心中是由在 startup.S 中的 multiboot_header 和 multiboot_entry 所表示。 multiboot_header 內放置關於映像的相關資訊,多重啟動載入器將映像載入到 0x100000的位址上,並跳入 multiboot_entry,將控制權交給映像本身。 GRUB 核心映像取得控制權後,multiboot_entry 取用 multiboot_header 內的映像資料,並將 GRUB 映像自己複製到 0000:8200 的位址上。 複製後的 GRUB 核心映像所在位址就和由 GRUB 啟動程式所載入的一樣。

之後,藉由跳躍到 multiboot_trampoline 的方式執行位於 0000:8200 上的映像。 multiboot_trampoline 會填入幾個啟動資訊到 startup.S 前端的變數中,並跳躍到 startup.S 的開頭附近的 LOCAL (codestart) 位址執行 GRUB 核心映像。 此後,GRUB 核心的啟動程序就和由 GRUB 啟動程式所執行的一樣。 多重啟動的程序就此結束,一切交由 GRUB 核心的起始程式 startup.S 處理。

Startup.S 執行到最後,會跳至 kern/main.c 內的函式 grub_main,繼續執行 GRUB2 的功能。這是 GRUB2 啟動時的大致行為。

步驟一 多重啟動載入器將 GRUB 核心載入記憶體 0x100000,並跳至 GRUB2 多重啟動程式處執行。
步驟二 GRUB2 多重啟動程式將GRUB核心映像複製到 0x8200 的位置上。
步驟三 起始程式將壓縮的核心部分解壓縮到 0x100000。
步驟四 起始程式將解壓縮後的核心映像複製到起始程式的實碼後面。

GRUB2

1.1.1 多重啟動程式碼

多重啟動程式碼是指當多重啟動載入器將 GRUB2 的核心載入到記憶體後,GRUB2 核心最開始執行的地方。 多重啟動程式碼是從 _start + GRUB_KERNEL_I386_PC_NO_REED_SOLOMON_PART 開始算起,也就是 0x8200+0x730 的位址。

   grub2\grub-core\kern\i386\pc\startup.S
00 . = _start + GRUB_KERNEL_I386_PC_NO_REED_SOLOMON_PART
00 .p2align 2 /* force 4-byte alignment */


1.1.2 多重啟動表

這是多重啟動的資料表格,內容包括表格特徵值等等。

   grub2\grub-core\kern\i386\pc\startup.S
00 multiboot_header:
00  .long	0x1BADB002
00  .long	(1 << 16)
00  .long	-0x1BADB002 - (1 << 16)
00  .long	multiboot_header - _start + 0x100000 + 0x200
00  .long	0x100000
00  .long	0
00  .long	0
00  .long	multiboot_entry - _start + 0x100000 + 0x200


1.1.3 複製核心壓縮映像

GRUB 多重啟動的核心壓縮載入程式,負責將放在 0x100200 後面的核心壓縮映像複製到 0x8200+RAW_DATA 的位址上。 這個壓縮映像之後會被啟動程式解壓縮,再放回到 0x100000的 位址上,到時候核心映像就不再是壓縮的映像了。

   grub2\grub-core\kern\i386\pc\startup.S
00 multiboot_entry:
00  .code32
00  movl	12(%ebx), %edx
00  movl	$GRUB_MEMORY_MACHINE_PROT_STACK, %ebp
00  movl	%ebp, %esp
00  movl	$(GRUB_KERNEL_MACHINE_RAW_SIZE + 0x200), %ecx
00  addl	EXT_C(grub_compressed_size) - _start + 0x100000 + 0x200, %ecx
00  movl	$0x100000, %esi
00  movl	$GRUB_BOOT_MACHINE_KERNEL_ADDR, %edi
00  cld
00  rep
00  movsb
00  movl	$multiboot_trampoline, %eax
00 	jmp	*%eax


1.1.4 處理安裝資料

填寫 DOS 和 BSD 的起始參數,之後就會從保護模式轉換成真實模式,回到起始程式的開頭,執行起始程式的核心起始程序。

   grub2\grub-core\kern\i386\pc\startup.S
00 multiboot_trampoline:
00  movl	%edx, %eax
00  shrl	$8, %eax
00  xorl	%ebx, %ebx
00  cmpb	$0xFF, %ah
00  je	1f
00  movb	%ah, %bl
00  movl	%ebx, EXT_C(grub_install_dos_part)
00 1:
00  cmpb	$0xFF, %al
00  je	2f
00  movb	%al, %bl
00  movl	%ebx, EXT_C(grub_install_bsd_part)
00 2:
00  shrl	$24, %edx
00  movb    $0xFF, %dh
00  call	prot_to_real
00  jmp     LOCAL (codestart)


1.2 GRUB 啟動程式

GRUB2 啟動程式的做法是使用 /boot/i386/pc/boot.s 做為啟動磁區的啟動程式。 boot.S (/grub-core/boot/i386/pc/boot.S) 接著會將在第二個磁區的 diskboot.S 載入到 0000:8000 並執行之。 diskboot.S (/grub-core/boot/i386/pc/diskboot.S) 會依據 blocklist_default_start、blocklist_default_len、blocklist_default_seg 這三個參數,將整個 GRUB 核心映像載入到 0000:8200 位址上。
參數 blocklist_default_start 是指 GRUB 核心映像在磁碟內的起始磁區位址。
參數 blocklist_default_len 是指 GRUB 核心映像體積在磁碟內所佔用的磁區數目。
參數 blocklist_default_seg 是指 GRUB 核心映像要被載入到記憶體內的段位址(SEGMENT),其實際位址由 SEGMENT:0000 所表示,應該是 0820:0000 或 0000:8200。
按照程式碼的寫法,其所採用的是 0000:8200 的位址表示方式。

diskboot.S 將 GRUB 核心載入到 0000:8200 ,之後,以 ljmp 0:8200 (原始程式不是這樣寫,但意思就是這樣。) 跳躍到 startup.S 的開頭 _start,緊接著在跳躍到 LOCAL (codestart)。 而 LOCAL (codestart) 也是多重啟動程序的完成點。多重啟動程序和 GRUB 啟動程序在此交會,之後就會共同以 start.S 的啟始程序,執行 GRUB 核心程序。 Startup.S 執行到最後,會跳至 kern/main.c 內的函式 grub_main,繼續執行 GRUB2 的功能。這是 GRUB2 啟動時的大致行為。

步驟一 BIOS 將啟動程式載入 0x7c00。
步驟二 啟動程式將磁碟啟動程式載入 0x8000。
步驟三 磁碟啟動程式將 GRUB2 核心映像載入 0x8200,並跳至 0x8200 執行核心起始程式。
步驟四 GRUB2 核心起始程式將核心壓縮部分解壓縮到 0x100000。
步驟五 GRUB2 核心起始程式將核心解壓縮後的程式碼複製到核心起始程式的實碼後面,準備執行 GRUB 核心。

GRUB2

1.2.1 啟始程式開頭

起始程式的開頭只是一個簡單的跳躍,越過變數區域,執行起始程序。 變數區域的內容包括全部模組體積、核心映像體積、壓縮映像體積、DOS安裝資訊、BSD安裝資訊、錯誤保護沉餘碼、未初始化變數記憶體、結束位址啟動磁碟碼。

   grub2\grub-core\kern\i386\pc\startup.S
00 .file "startup.S"
00 .text
00 .code16
00 .globl start, _start
00 start:
00 _start:
00 LOCAL (base):
00 ljmp $0, $ABS(LOCAL (codestart))
00 . = _start + 0x6
00 .byte	GRUB_BOOT_VERSION_MAJOR, GRUB_BOOT_VERSION_MINOR
00 . = _start + 0x8
00 VARIABLE(grub_total_module_size) .long 0
00 VARIABLE(grub_kernel_image_size) .long 0
00 VARIABLE(grub_compressed_size)   .long 0
00 VARIABLE(grub_install_dos_part)  .long 0xFFFFFFFF
00 VARIABLE(grub_install_bsd_part)  .long 0xFFFFFFFF
00 reed_solomon_redundancy:         .long 0
00 bss_end:                         .long 0
00 VARIABLE(grub_boot_drive)        .byte 0


1.2.2 起始程式開始點

起始程式開始點是一般啟動程序和多重啟動程序動作匯合的地方,從這裡開始,它們的起始程序都一樣。 這裡的動作是重新設定程式堆疊、檢查啟動磁碟、從真實模式切換到保護模式、啟用 A20 以上的位址線。

   grub2\grub-core\kern\i386\pc\startup.S
00 LOCAL (codestart):
00  cli
00  xorw %ax, %ax
00  movw %ax, %ds
00  movw %ax, %ss
00  movw %ax, %es
00  movl $GRUB_MEMORY_MACHINE_REAL_STACK, %ebp
00  movl %ebp, %esp
00  sti
00  ADDR32	movb %dl, EXT_C(grub_boot_drive)
00  int $0x13
00  DATA32	call real_to_prot
00  .code32
00  incl %eax
00  call grub_gate_a20


1.2.3 檢驗啟始程式

啟始程式有受到 REED SOLOMON 錯誤檢查碼的保護,檢驗起始程式以確定程式內容的正確,再執行起始程序。

   grub2\grub-core\kern\i386\pc\startup.S
00 LOCAL (codestart):
00  movl EXT_C(grub_compressed_size), %edx
00  addl $(GRUB_KERNEL_MACHINE_RAW_SIZE - GRUB_KERNEL_I386_PC_NO_REED_SOLOMON_PART), %edx
00  movl reed_solomon_redundancy, %ecx
00  leal _start + GRUB_KERNEL_I386_PC_NO_REED_SOLOMON_PART, %eax
00  call EXT_C (grub_reed_solomon_recover)
00  jmp post_reed_solomon


1.3 檢驗核心映像

GRUB2 使用 reed solomon 錯誤檢查演算法保護 GRUB 核心映像的前 0x730 位元組。 這是觀察程式碼的結果,其範圍是由 _start 到 _start+GRUB_KERNEL_I386_PC_NO_REED_SOLOMON_PART 所表示。 所以 reed solomon 只保護前面 0x730 位元組,而未保護到整個實碼範圍,更別說是整個 GRUB 映像,有點象徵性的保護而已。
當開始執行 startup.S 時,會先測試 A20 以確定保護模式的正常工作,再進入 reed solomon 驗證程序。 執行 reed solomon 驗證程序時,映像本身也必須存在一份 reed solomon 的驗證碼, 由 reed_solomon_redundancy 所表示,其字義是 reed solomon 沉餘碼,是一個 32 位元的數字。 當驗證並修復完成後,程式跳躍到 post_reed_solomon,其字義表示已經過錯誤檢查,接下來執行核心映像的解壓縮程序。


1.4 解壓縮核心映像

GRUB2 使用 LZMA (Lempel-Ziv-Markov chain-Algorithm) 壓縮演算法,壓縮核心映像。 在 0x8200 加上實碼體積之後的位址所存放的 GRUB 核心映像都會被解壓縮到 0x100000。 之後,再複製回到 0x8200 加上實碼體積的位址上,以完成映像解壓縮的程序。 完成解壓縮之後,整個 GRUB 核心映像就都是實碼,不再有壓縮的部分,是全部都可以執行的程式碼。

解壓縮函式 _LzmaDecodeA 是解壓縮程序的執行者,解壓縮時需要三個參數,即 GRUB 已壓縮的核心映像起始位址、GRUB 已壓縮的核心映像長度、解壓縮起始位址。 GRUB 已壓縮的核心映像起始位址是以 _start+GRUB_KERNEL_MACHINE_RAW_SIZE 表示。 GRUB 已壓縮的核心映像長度是以 grub_kernel_image_size+grub_total_module_size-GRUB_KERNEL_MACHINE_RAW_SIZE 表示。 解壓縮位址是直接指定的位址,其位址值為 0x100000。函式 _LzmaDecodeA 依據上述三個參數,將已壓縮的核心映像解壓縮到 0x100000 的位址上。 函式 _LzmaDecodeA 回返後,將核心部分複製到 0x8200 加上實碼體積的位址上,但模組部份不複製。

完成解壓縮的動作,並複製核心完成後,將核心內未初使化的記憶體區域 (bss) 全部設定為 0。之後呼叫 GRUB 核心主程式 grub_main,執行 GRUB。

   grub2\grub-core\kern\i386\pc\startup.S
00 post_reed_solomon:
00 movl $GRUB_MEMORY_MACHINE_DECOMPRESSION_ADDR, %edi
00 movl $(_start + GRUB_KERNEL_MACHINE_RAW_SIZE), %esi
00 pushl %edi
00 pushl %esi
00 movl EXT_C(grub_kernel_image_size), %ecx
00 addl EXT_C(grub_total_module_size), %ecx
00 subl $GRUB_KERNEL_MACHINE_RAW_SIZE, %ecx
00 pushl %ecx
00 leal (%edi, %ecx), %ebx
00 call _LzmaDecodeA
00 popl %ecx
00 popl %edi
00 popl %esi
00 subl EXT_C(grub_total_module_size), %ecx
00 rep
00 movsb
stosb


1.4 初始化記憶體

BSS_START_SYMBOL 是指未初始化的變數的記憶體,所有在程式中有宣告但沒有給初始值的變數都會安置在這個記憶體。 將未初使化的變數的記憶體內容清除為 0 是這段程式碼的目的。

00 movl $BSS_START_SYMBOL, %edi
00 movl $END_SYMBOL, %ecx
00 subl %edi, %ecx
00 xorl %eax, %eax
00 cld
00 rep
00 stosb


1.5 執行 GRUB 核心

完成 GRUB 核心解壓縮程序後,呼叫 GRUB 核心主程式,程式碼為 call EXT_C(grub_main)。 GRUB 核心主程式 grub_main 位於 grub-core/kern/main.c,其功能為初始化機器、繪製圖形介面、載入模組群、執行命令列指令等。 GRUB 核心主程式會載入模組群,執行 normal 模組。 此時會有一個使用者操作介面,供操作 GRUB 使用。

00 call EXT_C(grub_main)

核心主程式是 GRUB 核心執行的開始,會執行機器初始化、模組群載入,啟動 NORMAL 模組。 如果沒有 NORMAL 模組,就執行救援模式。

    grub2/grub-core/kern/main.c
01  grub_main (void){
02      grub_machine_init(); 
03      grub_setcolorstate(GRUB_TERM_COLOR_HIGHLIGHT);
04      grub_printf("Welcome to GRUB!\n\n");
05      grub_setcolorstate(GRUB_TERM_COLOR_STANDARD);
06      grub_register_exported_symbols ();
07      grub_load_modules();
08      grub_machine_set_prefix();
09      grub_set_root_dev();
10      grub_env_export("root");
11      grub_env_export("prefix");
12      grub_register_core_commands();
13      grub_load_config();
14      grub_load_normal_mode();
15      grub_rescue_run();
16  }

行號說明
02 建立操控台 (console)、記憶體管理 (mmap)、時間戳號時脈(tsc)。
03 設定終端機顏色屬性為高亮度。
04 顯示歡迎訊息。
05 設定終端機顏色屬性為標準值。
06 註冊所有引出的符號,但這個函式是空的,所以應該沒有註冊符號的動作。
07 載入模組群。這些模組是位在 GRUB 核心映像後面的模組群映像。模組載入時也會紀錄模組的相依性和符號群。
08 取得裝置路徑。
09 取得根目錄的裝置路徑,於取得裝置路徑後執行此動作。
10 設定環境變數 root 為外部變數,即可被其他程式取得。
11 設定環境變數 prefix 為外部變數,即可被其他程式取得。
12 註冊 GRUB 核心支援的命令集,這些命令在執行後面的組態敘述時應該會用到(我猜的)。
13 載入組態檔群。這些組態檔是位在 GRUB 核心映像後面的模組群映像,也就是說模組群映像也有可能是組態檔映像。
14 載入 NORMAL 模組,並執行 GRUB 的 grub_cmd_normal 函式,產生使用者介面環境。
15 執行救援程序。