1. 程式人生 > >GRUB原始碼概述及簡單應用

GRUB原始碼概述及簡單應用

 

前言

第七章之後的是針對一些小應用的修改。

[編輯]

概述

以下是我畢業論文的一部分:)對grub原始碼進行了一些分析,其實也沒有什麼新的東西,基本上是對斑竹的一個總結,然後細化了一些內容。可能有些不對的地方還請各位指正。

[編輯]

GRUB整體分析

總體上我們可以把GRUB看成一個微型的作業系統,他有Shell,支援Script,有檔案系統……我們可以把Stage1和Stage1.5看 成一個載入程式,而Stage2則是一個作業系統,只不過這個作業系統是專門用來引導其他作業系統的作業系統,為此,Stage2支援像kernel, initrd,chainloader等等為此目的而設定的內部“命令”。

[編輯]

GRUB引導作業系統的兩種方式

[編輯]

直接引導方式

GRUB同時支援 Linux, FreeBSD, NetBSD 和OpenBSD。如果想要啟動其他的作業系統,你必須使用鏈式啟動方式來啟動他們[6]。

通常,GRUB直接引導作業系統的步驟如下:

(1) 通過'root'指令來設定GRUB的主裝置指向作業系統映像檔案所儲存的地方。
(2) 通過'kernel'命令來載入該作業系統的核心映像。
(3) 如果需要模組,通過'module'命令來載入模組。
(4) 執行命令'boot'。

Linux, FreeBSD, NetBSD 和 OpenBSD使用相同的方式啟動。你可以通過'kernel'命令來裝載核心映像,然後執行'boot'命令。如果核心需要一些引數的話,只要在'kernal'命令以後追加就可以了。

[編輯]

鏈式引導方式

如果你要啟動一個不被GRUB直接支援的作業系統(例如: Windows 95),可以通過鏈式引導啟動一個作業系統。通常來說,那個載入程式和所要啟動的作業系統是安裝在一個分割槽中的。

主要步驟如下:

(1) 通過'rootnoverify'命令設定GRUB的主裝置指向一個扇區。
grub> rootnoverify (hd0,0)
(2) 通過'makeactive'命令來設定在扇區上的'active’標誌位。
grub> makeactive
(3) 通過'chainloader'命令來載入載入程式。
grub> chainloader +1
'+1'表明GRUB需要從起始分割槽讀一個扇區。
(4) 執行命令'boot'。
[編輯]

GRUB引導作業系統的簡要流程分析

[編輯]

從計算機啟動到GRUB啟動作業系統

(1) BIOS執行INT 0x19,載入MBR至0x7c00並跳轉執行。如果你安裝GRUB到MBR,GRUB的安裝程式會把Stage1(512B)拷貝到MBR。視 stage2的大小,安裝程式會在Stage1中嵌入Stage1_5或者Stage2的磁碟位置資訊。
(2) Stage1開始執行,它在進行直接載入Stage1_5或者Stage2並跳轉執行。不論是哪種情況,這一步的結果都是Stage2開始運行了。
(3) Stage2這個小型的作業系統終於開始正式運行了!它會把系統切入保護模式,設定好C執行環境(主要是BSS)。他會先找Config檔案(就是我們的 Menulist),如果沒有的話就執行一個Shell,等待我們輸入命令。然後Grub的工作就是輸入命令-解析命令-執行命令的迴圈,當然 Stage2本身是為載入其他作業系統而存在的,所以如果情況允許,在他執行Boot命令以後就會把控制權轉交出去。
[編輯]

GRUB的主要啟動模組

GRUB 包含如下幾個啟動模組:兩個必須的場景檔案,一個叫"Stage 1.5"的可選的場景檔案以及2個網路啟動的映像檔案。首先對他們有一個大致的瞭解。

Stage1

這是一個基本必須的用來啟動GRUB的映像檔案。通常,這個檔案是被裝載到MBR或者啟動扇區所在的分割槽。由於PC的啟動扇區的大小為512位元組,所以這個映像檔案編譯以後也必須為512位元組。
Stage1的全部的工作是從本地磁碟把Stage 2或者Stage 1.5裝載進來。由於對stage1大小的限制,它通過分程式表的形式來編碼Stage 2或者Stage 1.5的位置,所以在stage1是不能識別任何檔案系統的。

Stage2

這是GRUB的核心映像。它幾乎做了除啟動它本身以外的所有事情。通常,它被存放為某一種檔案系統下,但並非是必須的。
e2fs_stage1_5
fat_stage1_5
ffs_stage1_5
jfs_stage1_5
minix_stage1_5
reiserfs_stage1_5
vstafs_stage1_5
xfs_stage1_5
這些檔案被稱為stage 1.5,它存在的目的是做為stage1與stage2之間的橋樑,也就是說,stage1載入stage1.5,然後stage1.5載入stage2。
stage1與stage1.5之間的區別是,前者是不識別任何檔案系統的但後者識別檔案系統(例如 'e2fs_stage1_5' 識別 ext2fs)。所以你可以安全的移動stage2的位置,即使是在GRUB安裝完以後。

nbgrub

這是一個網路啟動的映像檔案,被類似於乙太網啟動裝載器所使用。它很類似於stage2,但它還要建立網路,然後通過網路來載入配置檔案[7]。

pxegrub

這是另一個網路啟動的映像檔案。除了格式以外,它和'nbgrub'是一致的。
[編輯]

STAGE1模組分析

Stage1模組是整個載入程式的引導模組,是從開機過渡到GRUB的第一個模組。Stage1的程式碼檔案,是原始碼目錄下Stage1/Stage1.S,彙編後便成了一個512位元組的Img,被寫在硬碟的0面0道第1扇區,作為硬碟的主引導扇區。

[編輯]

Stage1.h檔案分析

在此檔案中主要是定義了一些在Stage1.S檔案中使用到的一些常量。 關於這些常量的分析如下:

/* 定義了grub的版本號,在stage1中可以識別他們.*/
#define COMPAT_VERSION_MAJOR 3
#define COMPAT_VERSION_MINOR 2
#define COMPAT_VERSION ((COMPAT_VERSION_MINOR << 8) /
| COMPAT_VERSION_MAJOR)

/* MBR最後兩個位元組的標誌*/
#define STAGE1_SIGNATURE 0xaa55

/* BPB (BIOS 引數塊BIOS Parameter Block)的結束標記的偏移,他含有對驅動器的低階引數的說明. */
#define STAGE1_BPBEND 0x3e

/* 主版本號的標記的偏移*/
#define STAGE1_VER_MAJ_OFFS 0x3e

/* Stage1啟動驅動器的標記的偏移*/
#define STAGE1_BOOT_DRIVE 0x40

/* 強迫使用LBA方式的標記的偏移*/
#define STAGE1_FORCE_LBA 0x41

/* Stage2地址標記的偏移*/
#define STAGE1_STAGE2_ADDRESS 0x42

/* STAGE2扇區的標記的偏移*/
#define STAGE1_STAGE2_SECTOR 0x44

/* STAGE2_段的標記的偏移*/
#define STAGE1_STAGE2_SEGMENT 0x48

/* 使用Windows NT的魔術頭標識的偏移*/
#define STAGE1_WINDOWS_NT_MAGIC 0x1b8

/* 分割槽表起始地址的標記的偏移*/
#define STAGE1_PARTSTART 0x1be

/* 分割槽表結束地址的標記的偏移*/
#define STAGE1_PARTEND 0x1fe

/* Stage1堆疊段的起始地址*/
#define STAGE1_STACKSEG 0x2000

/* 磁碟緩衝段。磁碟緩衝必須是32K長而且不能跨越64K的邊界。*/
#define STAGE1_BUFFERSEG 0x7000

/* 驅動器引數的地址*/
#define STAGE1_DRP_ADDR 0x7f00

/* 驅動器引數的大小*/
#define STAGE1_DRP_SIZE 0x42

/*在BOIS中軟盤的驅動器號標誌*/
#define STAGE1_BIOS_HD_FLAG 0x80

[編輯]

Stage1.s檔案分析

首先在這個檔案的開始部分定義了一些巨集。

#define ABS(x) (x-_start+0x7c00)

這個巨集計算了直接地址。由於MBR是被載入到0x7c00的位置,所以通過計算可以直接得到x引數的直接地址。這樣就可以不依賴於Linker程式。

#define MSG(x) movw $ABS(x), %si;

這個巨集用於處理對字串的載入和響應。

然後程式從_start程式入口開始執行,此入口在記憶體中的位置為CS:IP 0:0x7c00。隨後對一系列的變數進行了初始化。設定了起始的扇區、磁軌和柱面,並設定了他們的起始位置。同時還設定了stage1的版本號。通過設 置boot_drive變數,來設定從那個盤來載入stage2。如果此變數設定成0xff則從預設的啟動驅動器中來載入stage2。然後指定了 stage2的起始地址是0x8000,起始段是0x800,起始的扇區號是1。也就是說stage2起始位置是被存放在0柱面,0磁軌,第2扇區上的 [8]。

程式從real_start入口開始真正執行。首先設定了資料段以及堆疊段的偏移為0,然後設定stage1的堆疊的起始地址為 STAGE1_STACKSEG即0x2000。隨後開啟中斷。然後檢查是否設定了啟動的磁碟。即boot_drive變數是否為0xff,如果非 0xff則儲存設定的磁碟號到dl暫存器中,並壓入堆疊儲存。同時在螢幕上顯示GRUB字樣。然後檢查此啟動磁碟是否是軟盤,如果是軟盤則直接跳轉到 CHS模式不用檢測是否支援LBA模式。然後檢測所啟動的磁碟是否支援LBA模式。接著程式分成兩塊,一塊是LBA模式,一塊是CHS模式。

LBA英文全名為Logical Block Addressing,中文名稱為邏輯區塊定址[9]。LBA所指的是一種磁碟裝置的定址技術,它是利用邏輯映對的方式來指定磁碟驅動器的扇區,目前個人 計算機所使用的傳輸介面中,增強型 IDE (Enhanced IDE) 和 SCSI 均使用邏輯區塊定址方式。傳統的硬碟定址技術是採取實體定址(physical mapping、physical addressing)的方式,以磁碟上的實際結構,直接作為資料區塊地址的結構。但由於初期在設計實體定址方式時,硬碟容量只有5、10、20 MB等等小容量機種,所以設計出來的最大的定址能力,只能到1024個磁柱(cylinder)、16個磁頭(head)、63個扇區(sector)。 以每個扇區(sector)512位元組(bytes)計算,實體定址的方式最多隻能使用512×63×1024×16=528482304位元組 (528MB)的硬碟空間。但是由於磁性儲存技術不斷的提升,硬碟容量大幅增加的情況之下,這樣的限制讓使用者必須將硬碟畫分為多個區塊,使用上非常的不 方便。

因此硬體廠商研究出了LBA邏輯定址方式,也就是計算機系統並沒有將資料存放地點的相關記錄,應對到硬碟上資料實際存放的位置。而是由 IDE控制電路和 BIOS負責轉換定址(mapping)資料的記錄位置表。經過轉換後的記錄方式,是將第1個磁柱上的第1條磁軌的第1個扇區編號為0,第二個扇區編號為 1,以此類推……,假設1條磁軌有2000個扇區,那麼第2000個扇區的編號就是1999。第2條磁軌上的第1個扇區就是2000,如此一直線性排列下 去。以邏輯區塊的方式來定址的硬碟,最多可達16383磁柱,最大磁頭數為16個,每軌扇區有63區,扇區大小為512位元組,所支援之硬碟空間為512× 63×16383×16=8455200768位元組(8.4GB)[10]。

在ATA的介面規格中,定義了使用28位來定址,因此計算出來,它可以支援到224×512=137GB的容量。不過不幸的,BIOS並 無法配合,它使用 24位來定址(也就是LBA模式)。所以根本之道,就是改變BIOS對中斷13h的支援,因此後來的BIOS就設計了加強版的中斷13h。一口氣使用了 64位來對硬碟做定址,因此可以支援到264×512=9.4TB,相當於3萬億倍的8.4GB[11]。

如果是LBA模式下讀取,首先對先前定義的磁碟的一些引數進行了定義,為以後呼叫INT 13做準備。使用INT13的0x42功能,把磁碟內容讀到記憶體中。設定ah為0x42為功能號,設定dl暫存器來設定磁碟號,si為記錄磁碟一系列資訊 的地址偏移量,磁碟資訊中包括了要讀入的柱面號、磁軌號以及扇區號。程式然後呼叫BOIS INT 13中斷將啟動磁碟上的第二扇區上的內容讀到記憶體中的STAGE1_BUFFERSEG處,在Stage1.h中定義STAGE1_BUFFERSEG為 0x7000。即將第二扇區上的內容讀到記憶體中的0x7000處。讀入成功的話跳轉到COPY_BUFFER處,如果讀取失敗則嘗試使用CHS模式讀入。

與LBA模式不同的是,呼叫BIOS INT 0x13中斷中的0x2號功能,設定ah暫存器為0x2,al為扇區數,cl的位6,位7和ch組合為磁軌號,cl的0-5位為扇區號,dh為磁頭號, dl為驅動器號(其中0x80為硬碟,0x0為軟碟機)。es:bx為資料緩衝區的地址。但所起的功能與前面提到的LBA模式是類似的,也是將第二扇區中的 內容讀到記憶體中的0x7000處,作為快取。然後跳轉到COPY_BUFFER處。

最後呼叫COPY_BUFFER將剛剛讀入的扇區轉移到stage2_address。即轉移到0x8000處。

[編輯]

Stage1模組功能綜述

由於對Stage1檔案容量的限制,所以Stgae1所做的工作相對來說比較有限。它首先被BOIS裝載到記憶體中的0x7c00處,然後通過呼叫 BOIS INT13中斷,把第啟動驅動器中第二扇區上的內容讀到記憶體中的0x7000處,然後通過呼叫COPY_BUFFER將其轉移到了記憶體中0x8000的位 置上。這個被讀入的第二扇區上的內容,就是下面將要分析的Start.s功能模組。

[編輯]

START模組分析

從上一章節的分析中我們看到,Stage1的是完成了一個MBR所需要完成的任務,但GRUB並沒有直接就通過Stage1直接載入GRUB的內 核,而是通過Stage1載入了另一個模組到0x8000處。根據對原始碼的分析,發現被載入的這個模組就是下面需要分析的第二個模組,即Start.S 模組。

[編輯]

Start.s 模組功能分析

在程式的開始部分,仍然是對程式定義了一些巨集。

#ifdef STAGE1_5
# define ABS(x) (x-_start+0x2000)
#else
# define ABS(x) (x-_start+0x8000)
#endif

可以發現,如果定義了STAGE1_5則程式的起始地址是0x2000,而如果沒有定義STAGE1_5程式起始的地址正好是0x8000。所以我 判斷,在Stage1後載入記憶體的程式部分就是Start.s所編譯以後的512位元組的映象檔案。關於STAGE1_5的部分暫時先不進行分析,這裡暫且 跳過。

巨集 “#define MSG(x) movw $ABS(x), %si;”的作用是在螢幕上顯示字串。

接著就是程式的入口_start。由於是緊接著Stage1被載入記憶體中的,所以它的起始地址就是0x8000,並且它仍然將使用 Stage1模組留下來的暫存器以及變數等資訊。如果設定STAGE1_5變數則在螢幕上顯示“Loading stage1.5”,如果沒有設定這個變數則顯示“Loading stage2”。然後讀入需要讀入的扇區的數目。接著進入一個bootloop的迴圈,如果需要讀入的扇區不為0,則繼續迴圈,直到當需要讀入的扇區數目 為0時,迴圈結束。在這個迴圈中,使用與Stage1中的方法相同,判斷了驅動器磁碟所支援的讀寫模式,根據不同的磁碟所支援的不同模式,跳轉到相應的部 分去讀取磁碟上的扇區到記憶體中去。如果磁碟支援的是LBA模式則跳轉到lba_mode部分讀取相應的扇區,如果磁碟不支援LBA模式,則跳轉到 chs_mode部分,通過CHS模式來讀入把磁碟中的扇區讀入到記憶體中。首先是把讀到的扇區讀到記憶體中的0x7000處快取起來,然後通過呼叫 copy_buffer子程式,把快取中的內容複製到目標地址,即0x8200開始的地方。與Stage1中的一樣,在Start.s中也有一個記錄地址 的資料結構,不同的是在Stage1中只有一項,而Start.S記錄的是一個地址的連結串列,稱為Blocklist,該連結串列的結點都記錄了一個連續 sectors的集合。

lastlist:
.word 0
.word 0
. = _start + 0x200 - BOOTSEC_LISTSIZE
/*加0x200是由於Start.s編譯完以後也是一個512位元組的映象檔案。*/

/* 初始化了第一個資料列表*/
blocklist_default_start:
.long 2 /* 記錄了從第3個扇區開始*/
blocklist_default_len:
/* 這個引數記錄了需要讀取多少個扇區 */
#ifdef STAGE1_5
.word 0 /* 如果設定了STAGE1_5標誌,則不讀入*/
#else
.word (STAGE2_SIZE + 511) >> 9 /*讀入Stage2所佔的所有扇區*/
#endif
blocklist_default_seg:
#ifdef STAGE1_5
.word 0x220 /*如果設定STAGE1_5則從0x220開始讀入*/
#else
.word 0x820 /*如果沒有設定STAGE1_5則從0x820開始讀入*/
#endif

firstlist:

當把所有需要讀入的扇區都讀入以後,程式進入bootit子程式塊。然後程式進行跳轉,如果設定了STAGE1_5標誌,則跳轉到0x2200執行,如果沒有設定STAGE1_5標誌,則跳轉到0x8200處繼續執行。

[編輯]

Start 模組功能綜述

通過對Start.s檔案的分析,我們可以看到。Start模組主要是做了一件事情,就是把Stage2或者Stage1_5模組從磁碟裝載到記憶體 中。如果是直接裝載Stage2的話,是裝載在記憶體的0x8200處,如果裝載Stage1_5的話,是裝載在記憶體的0x2200處。

[編輯]

GRUB Kernel模組分析

由於我分析的是GRUB2的原始碼,從GRUB2開始,從Start模組載入的是Grub的整個kernel。從官方的說明可以看到,與Grub相 比,最大的差異在於GRUB2將Stage1.5以及Stage2的功能歸併為GRUB2的kernel,並提高了壓縮效能;編譯生成的 kernel──core.img只有24KB左右,即使對於最普遍的CHS讀寫模式所支援的0面0道的64個扇區(摺合32K左右)而言,空間是足夠放 置GRUB2的kernel的,Grub的每個Stage1.5都至少在11K左右,而stage2則為110K左右。

[編輯]

Asm.s 檔案分析

在分析了Start模組以後,發現如果沒有設定Stage1_5引數,那麼系統已經把Grub kernel從磁碟完全裝載到了起始地址為0x8200開始的記憶體中。於是我便在原始碼中尋找起始地址從0x8200開始執行的程式碼。發現Asm.s檔案 就是這樣一個符合條件的模組。

首先在這個檔案的開始,仍然定義了這樣一個巨集:

#ifdef STAGE1_5
# define ABS(x) ((x) - EXT_C(main) + 0x2200)
#else
# define ABS(x) ((x) - EXT_C(main) + 0x8200)
#endif

從這個巨集中可以看出,從Start模組以後從磁碟轉載的應該就是這個檔案編譯以後的模組。程式的入口是EXT_C(main)。如果沒有定義 STAGE1_5那麼程式的起始地址正是0x8200完全符合前面所做的分析。同時由於設定了.code16,整個程式開始仍然時工作在真實模式下的。

接著分析ENTRY(main)這個函式。首先為了保證main這個函式如果是Stage2的話被裝載在0x8200,如果是 Stage1.5的話被裝載在0x2200。然後程式執行了一個長跳轉。ljmp $0, $ABS(codestart)。 在執行codestart程式碼之前,它對一些變數進行了初始化。設定瞭如版本號、install_partition、saved_entryno、 stage2_id、force_lba和config_file等。如果是Stgae1.5則config_file為 “/boot/grub/stage2”,如果是Stage2則為“boot/grub/menu.lst”。

然後進入codestart程式碼,首先關中斷,對斷暫存器進行了一些初始化,然後設定了堆疊的起始地址為STACKOFF即(0x2000 - 0x10)。

接著程式呼叫了real_to_prot這樣一個子功能模組。從真實模式轉換把程式轉換到保護模式下。分析ENTRY (real_to_prot)子功能,主要的轉換步驟如下:首先程式仍然在真實模式下,關中斷。接著載入了GDT表。然後通過.code32轉到保護模式 下,跳轉到protcseg子功能下,重新裝載所有的段暫存器。同時把返回的地址放到STACKOFF中,然後獲得保護模式下的堆疊地址,把 STACKOFF壓入堆疊中,進行保護。然後返回。

然後程式繼續,清空了bss段,呼叫了init_bios_info函式,這個函式體是整個C語言程式碼的入口,是Cmain函式前的初始化程式碼。

在asm.s檔案中,主要是一些彙編程式碼的函式塊,沒有C語言的程式碼,於是我在share.h中找到了init_bios_info的函式定義,從而在common.c中找到了init_bios_info的程式碼。

同時在這個檔案中還定義了非常多的彙編程式碼寫的函式,這些函式將來會被C檔案呼叫。這裡先對這些檔案進行一下說明,如表6.1所示。

表6.1 asm.s檔案中的彙編函式列表

函式名稱                 函式作用
stop() 呼叫prot_to_real子函式,從保護模式轉換成真實模式
hard_stop() 通過反覆呼叫自身,形成一個死迴圈,起到一個暫停的作用
grub_reboot() 重新啟動系統
grub_halt(int no_apm) 暫停系統,利用時鐘計時,如果設定NO_APM將不使用時鐘計時
track_int13(int drive) 追蹤INT13來操作I/O的地址空間
set_int15_handler(void) 建立INT15的控制代碼
unset_int15_handler(void) 重新恢復INT15的控制代碼
set_int13_handler(map) 複製一塊資料到驅動器並且建立INT13的控制代碼
chain_stage1(segment,offset,part_table_addr) 啟動另一個stage1的載入程式
chain_stage2(segment, offset, second_sector) 啟動另一個stage2的載入程式
real_to_prot () 真實模式轉換成保護模式
prot_to_rea l() 保護模式轉換成真實模式
int biosdisk_int13_extensions (int ah, int drive, void *dap) 呼叫IBM/MS 擴充套件INT13 的功能。
int biosdisk_standard (int ah, int drive, int coff,int hoff, int soff,int nsec, int segment) 呼叫標準的INT13功能
int check_int13_extensions (int drive) 檢查磁碟是否支援LBA模式
get_diskinfo_int13_extensions (int drive, void *drp) 從引數*drp返回磁碟驅動的具體結構
int get_diskinfo_standard (int drive, unsigned long *cylinders,unsigned long *heads, unsigned long *sectors) 返回指定磁碟的柱面,磁頭以及扇區資訊
int get_diskinfo_floppy (int drive, unsigned long *cylinders,unsigned long *heads, unsigned long *sectors) 返回軟盤的磁碟的柱面,磁頭以及扇區資訊
get_code_end() 返回程式碼末端的地址
get_memsize(i) 返回記憶體大小,如果I為0返回常規記憶體,I為1返回擴充套件記憶體
get_eisamemsize() 返回EISA的記憶體分佈圖
get_rom_config_table() 獲得Rom配置表的線性地址
int get_vbe_controller_info (struct vbe_controller *controller_ptr) 獲得VBE控制器 的資訊
int get_vbe_mode_info (int mode_number, struct vbe_mode *mode_ptr) 獲得VBE模式資訊
int set_vbe_mode (int mode_number) 設定VBE模式
linux_boot() 做一些危險的設定,然後跳轉到Linxu安裝的入口程式碼
multi_boot(int start, int mb_info) 這個函式啟動一個核心使用多重啟動的標準方法
void console_putchar (int c) 通過這個函式在終端上顯示字元
int console_getkey (void) 呼叫INT16從鍵盤上讀取字元
int console_checkkey (void) 檢查是否某個鍵被一直按下去
int console_getxy (void) 呼叫INT10獲得游標的位置
void console_gotoxy(int x, int y) 呼叫INT10設定游標的位置
void console_cls (void) 呼叫INT10 清空螢幕
int console_setcursor (int on) 呼叫INT10設定游標的型別
getrtsecs() 如果第二個值能被讀取,則返回這個值
currticks() 用Ticks為單位返回當前時間,一秒約為18-20個Ticks
[編輯]

Common.c 檔案分析

在common.c檔案中我找到了函式init_bios_info的實現的程式碼。分析發現,整個init_bios_info檔案主要是對multiboot_info這個結構進行初始化以及填充。

整個結構體分析如下:

struct multiboot_info
{
/* 多重啟動資訊的版本號*/
unsigned long flags;

/* 可以使用的記憶體 */
unsigned long mem_lower;
unsigned long mem_upper;

/* 主分割槽 */
unsigned long boot_device;

/*核心的命令列*/
unsigned long cmdline;

/*啟動模組的列表*/
unsigned long mods_count;
unsigned long mods_addr;

union
{
struct
{
/* (a.out) 核心標識表的資訊 */
unsigned long tabsize;
unsigned long strsize;
unsigned long addr;
unsigned long pad;
}
a;

struct
{
/* (ELF) 核心標識表的資訊*/
unsigned long num;
unsigned long size;
unsigned long addr;
unsigned long shndx;
}
e;
}
syms;

/* 記憶體分佈圖的快取 */
unsigned long mmap_length;
unsigned long mmap_addr;

/* 驅動器資訊快取 */
unsigned long drives_length;
unsigned long drives_addr;

/* ROM 配置表 */
unsigned long config_table;

/* 啟動裝載器的名稱 */
unsigned long boot_loader_name;

/* APM 表 */
unsigned long apm_table;

/* 視訊 */
unsigned long vbe_control_info;
unsigned long vbe_mode_info;
unsigned short vbe_mode;
unsigned short vbe_interface_seg;
unsigned short vbe_interface_off;
unsigned short vbe_interface_len;
};

通過呼叫asm.s中的底層功能模組,對這個結構進行初始化以後,直接呼叫cmain函式。

[編輯]

Stage2.c 檔案分析

通過查詢,在Stage2.c中找到了cmain函式,這個應該就是Stage2這個小型作業系統的入口了。然後程式就進入一個死迴圈,整個 Stage2 就在這個死迴圈中執行。接著呼叫reset()函式對stage2的內部變數進行初始化。通過open_preset_menu()函式嘗試開啟已經設定 好的選單。如果使用者沒有設定好選單,那麼將返回0,如果已經設定好了選單則不返回0。如果沒有成功開啟選單,那麼將通過grub_open()函式嘗試打 開config_file。Grub使用內部的檔案格式來開啟這樣一個配置檔案,如果仍然開啟失敗,則跳出整個迴圈。如果開啟成功,則根據開啟的情況,即 is_preset變數的值的情況來判斷是從預設選單讀入還是從配置檔案讀入命令。然後通過把is_preset傳入 get_line_from_config()函式,將命令讀入cmline中。然後通過find_command()函式查詢有沒有這條命令。

在Grub中,儲存命令的格式是儲存在一個builtin的結構體中的。這個結構體在shared.h標頭檔案中進行了定義。

struct builtin
{
/* 命令名稱,重要,是搜尋命令時的依據 */
char *name;
/* 命令函式,重要,是搜尋匹配後呼叫的函式 */
int (*func) (char *, int);
/* 功能標識 */
int flags;
/* 簡短幫助資訊 */
char *short_doc;
/* 完整幫助資訊 */
char *long_doc;
};

整個命令的表的定義如下

extern struct builtin *builtin_table[];

find_command()函式在cmdline.c中定義,它對整個builtin表進行遍歷,然後比較名稱。如果在表中發現了這個命令,則返 回指向當前builtin結構的指標。如果沒有發現這個命令則返回0同時返回一個errnum。如果成功的找到了一條指令,然後通過呼叫在 cmdline.c中的skip_to ()函式,獲得當前builtin指標所指向結構的命令的引數。然後通過(builtin->func) (arg, BUILTIN_MENU)直接呼叫此命令。最後一直迴圈,直到沒有命令可以取為止。

如果由於前面沒有成功的開啟預先配置的檔案而跳出迴圈,則通過在cmdline.c檔案中定義的enter_cmdline()函式呼叫 來啟動命令列。在 enter_cmdline()函式中,進入另一個迴圈等待接受命令。當通過get_cmdline()函式接收到命令以後,仍然通過 find_command()函式呼叫來遍歷builtin表,如果沒有在表中找到輸入的指令則返回一個errnum= ERR_UNRECONGNIZED。如果成功找到了這條指令,同樣首先呼叫在cmdline.c中的skip_to ()函式,獲得當前builtin指標所指向結構的命令的引數。然後(builtin->func) (arg, BUILTIN_MENU)直接呼叫此命令。

如果成功的打開了選單則跳轉到run_menu()函式,這裡是grub中整個menu使用者介面的主迴圈。首先有一個計時器 grub_timout進行計時,如果grub_timeout<0或者沒有設定,那麼就強行顯示選單。如果選單沒有顯示,則在螢幕上顯示 “Press `ESC' to enter the menu...”,並進入一個死迴圈中,當用戶按下ESC按鍵,則馬上顯示選單。如果超時,那麼就直接進入第一個,也就時預設的那個啟動專案。如果顯示菜 單,則顯示所有可以選擇的入口。

不論是否顯示選單,最後程式都將跳轉到boot_entry。首先程式先清空了螢幕,然後把游標定於第一行的位置。然後再次進入一個循 環。然後如果沒有設定入口則通過呼叫get_entry()函式來獲取一個預設的入口。然後呼叫在cmdline中的run_script()函式解釋這 個入口。 Run_script()函式對這個入口以後的指令指令碼,進行了解析。解析的方式仍然是利用find_command()函式呼叫。

[編輯]

GRUB部分指令說明

Grub中所有的預先設定的指令都是在builtins.c檔案中實現的。比如啟動一個FreeBSD作業系統,可以輸入以下的指令:

grub> root (hd0,a)
grub> kernel /boot/loader
grub> boot
[編輯]

Root指令

呼叫root指令的函式是在builtins.c中的root_func (char *arg, int flags)函式。第一個引數指定了哪個磁碟驅動器,如hd0是指第一塊硬碟。第二個引數是分割槽號。然後在root_func()中它有呼叫了 real_root_func (char *arg, int attempt_mount)這個函式,並把引數arg傳入real_root_func中並把attempt_mount設定為1。如果傳入的arg是 空的,那麼就直接使用預設的驅動器。然後呼叫set_device()函式,從字串中提取出驅動器號和分割槽號。測試如果所填寫的驅動器號以及分割槽號讀寫 沒有問題,那麼就在變數saved_partition和saved_drive中儲存讀取的這兩個資料。然後返回。

這個函式主要的作用是為GRUB指定一個根分割槽。

[編輯]

Kernel指令

呼叫kernel指令的函式是在builtins.c檔案中kernel_func (char *arg, int flags)函式。在這個函式中,首先進入一個迴圈,對傳進來的引數進行解析。如果“--type=TYPE”引數被設定了,根據傳入的引數設定 suggested_type變數賦予不用的作業系統的值。當沒有別的引數被設定以後,則跳出迴圈。然後從引數中獲得核心的檔案路徑,賦值給 mb_cmdline變數,然後通過load_image()函式載入核心,並且返回核心的型別。如果返回的核心型別是grub不支援得型別,即 kernel_type == KERNEL_TYPE_NONE返回1,成功則返回0。 這個函式主要的作用是,載入作業系統的核心。

[編輯]

Boot指令

呼叫boot指令的函式是在builtins.c檔案中的boot_func (char *arg, int flags)函式。如果被載入的核心型別不是未知的,那麼呼叫unset_int15_handler()函式,清除int15 handler。接著根據grub支援的不同的作業系統呼叫相應的啟動程式。當啟動的核心為BSD時呼叫bsd_boot ()函式,當啟動的核心為LINUX時呼叫的函式時linux_boot()函式,當啟動方式是鏈式啟動方式時,呼叫chain_stage1()函式, 當啟動方式是多重啟動時,呼叫multi_boot()函式。

這個函式主要的作用是,根據不同的核心型別呼叫相應的啟動函式。

[編輯]

GRUB Kernel分析總結

通過分析,這個核心模組主要的工作是完成了GRUB這個微型作業系統的從磁碟到記憶體的裝載和執行。在asm.s這個檔案中提供了從彙編程式碼到C程式碼 轉換的介面,也是從這裡開始正式載入了GRUB這個微型作業系統,可以說是GRUB執行的一個入口。同時,在asm.s檔案中,對底層的方法用匯編語言進 行了封裝,方便在以後的C程式碼中呼叫。然後經過對BIOS進行一些初始化以後,正式進入了GRUB的主程式,即在stage2中的cmain入口。從此這 個微型的作業系統開始正式執行。然後值得注意的是buildin這個資料結構,這個結構就是GRUB所有支援命令的資料結構。結構包括了一個用來識別的名 字和一個用來呼叫的方法。GRUB通過接收外部輸入的指令的方式,來間接的啟動和裝載其他的作業系統。

[編輯]

總結

[編輯]

GRUB原始碼分析總結

通過對整個原始碼的分析,大致上整個GRUB啟動到引導其他作業系統分為如下幾個步驟。

第一步 開機後,通過BIOS裝載Stage1模組

第二步 通過Stage1模組裝載Start模組

第三步 通過Start模組將整個GRUB的核心載入記憶體

第四步 通過GRUB的一個Shell的機制,作為一個小型的作業系統,來通過指令的方式裝載不同的其他作業系統。

整個過程中GRUB啟動的記憶體映象圖如圖7.1所示。

總體分析下來,首先感覺到GRUB整個程式碼在編碼方面是非常嚴謹的,特別是整個程式的構架體現出了它靈活容易擴充套件的特性。主要體現在,它有 別於普通的作業系統載入程式,在BIOS啟動時就直接去裝載特定作業系統的模組或者核心,而是通過BIOS的功能首先裝載了一個屬於自己的載入程式,也可 以理解為 GRUB這個作業系統的載入程式。也就是說在引導任何使用者的作業系統之前GRUB首先引導的是它的本身。這樣為將來的擴充套件性打下了非常好的基礎。

由於GRUB採用了類似SHELL的方式來解釋並執行使用者設計好的指令碼或者接受使用者輸入的指令,並且為使用者提供了非常好的底層的方法接 口,所以使用者可以非常靈活的組合指令來引導不同的作業系統,同時並不要求使用者對底層的物理結構有非常高的瞭解,只要能使用提供的指令就可以來操作配置多重 啟動的多個作業系統。並且,GRUB提供了非常好的人機互動介面,可以通過預先設定的指令,或者選單來顯示讓使用者選擇作業系統,相對來說就比較易用。

同時GRUB也提供了非常好的擴充套件性,這個也是由於GRUB特殊的結構保證的。首先GRUB是一個開源的專案,它的原始碼是向所有的使用者 和開發著公開的,這樣無論是使用者的需求發生了變化,還是硬體的標準得到了提升,都能很快的在GRUB中得到實現。其次,GRUB的指令是非常容易新增的, 使用者只要瞭解了 GRUB指令的格式,就能非常容易的在GRUB原始指令的基礎上新增屬於自己的指令,這樣的設計相當程度上提高了程式的模組化,耦合度比較低。

圖7.1 GRUB啟動記憶體映像圖

[編輯]

grub顯示時間

./stage2/asm.S中的getrtsecs處,新增獲取年月日時分的方法。

/*
* getrtsecs()
* if a seconds value can be read, read it and return it (BCD),
* otherwise return 0xFF
* BIOS call "INT 1AH Function 02H" to check whether a character is pending
* Call with %ah = 0x2
* Return:
* If RT Clock can give correct values
* %ch = hour (BCD)
* %cl = minutes (BCD)
* %dh = seconds (BCD)
* %dl = daylight savings time (00h std, 01h daylight)
* Carry flag = clear
* else
* Carry flag = set
* (this indicates that the clock is updating, or
* that it isn't running)
*/
ENTRY(getrtsecs)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x2, %ah
int $0x1a

DATA32 jnc gottime
movb $0xff, %dh

gottime:
DATA32 call EXT_C(real_to_prot)
.code32

movb %dh, %al

pop %ebp
ret

改為如下內容:

ENTRY(getrtsecs)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x2, %ah
int $0x1a

DATA32 jnc gottime
movb $0xff, %dh

gottime:
DATA32 call EXT_C(real_to_prot)
.code32

movb %dh, %al

pop %ebp
ret
ENTRY(getrtmins)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x2, %ah
int $0x1a

DATA32 jnc gotmin
movb $0xff, %dh

gotmin:
DATA32 call EXT_C(real_to_prot)
.code32

movb %cl, %al

pop %ebp
ret
ENTRY(getrthous)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x2, %ah
int $0x1a

DATA32 jnc gothou
movb $0xff, %dh

gothou:
DATA32 call EXT_C(real_to_prot)
.code32

movb %ch, %al

pop %ebp
ret

ENTRY(getrtday)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x4, %ah
int $0x1a

DATA32 jnc gotday
movb $0xff, %dh

gotday:
DATA32 call EXT_C(real_to_prot)
.code32

movb %dh, %al

pop %ebp
ret

ENTRY(getrtmon)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x4, %ah
int $0x1a

DATA32 jnc gotmon
movb $0xff, %dh

gotmon:
DATA32 call EXT_C(real_to_prot)
.code32

movb %dh, %al

pop %ebp
ret

ENTRY(getrtyea)
push %ebp

call EXT_C(prot_to_real) /* enter real mode */
.code16

movb $0x4, %ah
int $0x1a

DATA32 jnc gotyea
movb $0xff, %dh

gotyea:
DATA32 call EXT_C(real_to_prot)
.code32

movb %dh, %al

pop %ebp
ret

./stage2/shared.h中的如下位置中

/* low-level timing info */
int getrtsecs (void);
int currticks (void);

新增為如下程式碼,宣告我們新增的方法

/* low-level timing info */
int getrtsecs (void);
int getrtmins (void);
int getrthous (void);
int getrtday (void);
int getrtmon (void);
int getrtyea (void);
int currticks (void);

./grub/asmstub.c中的如下位置中

/* low-level timing info */
int
getrtsecs (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

新增為如下程式碼,宣告我們新增的方法

/* low-level timing info */
int
getrtsecs (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

int
getrtmins (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

int
getrthous (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

int
getrtday (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

int
getrtmon (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

int
getrtyea (void)
{
/* FIXME: exact value is not important, so just return time_t for now. */
return time (0);
}

./stage2/stage2.c中的320行,我們修改為

printf ("/
Press enter to boot the selected OS, /'e/' to edit the/n/
commands before booting, or /'c/' for a command-line.year %d month
%d day %d hour %d min %d second %d",getrtyea(),getrtmon(),
getrtday(),getrthous(),getrtmins(),getrtsecs());

相關推薦

GRUB原始碼概述簡單應用

  前言 第七章之後的是針對一些小應用的修改。 [編輯] 概述 以下是我畢業論文的一部分:)對grub原始碼進行了一些分析,其實也沒有什麼新的東西,基本上是對斑竹的一個總結,然後細化了一些內容。可能有些不對的地方還請各位指正。 [編輯] GRUB整體分析 總體上我們

SpringMVC配置簡單應用

新的 bat pan start rop jstl cast 版本 ans 1.在web.xml中配置前端控制器 <servlet>   <servlet-name>springmvc</servlet-name>   <servl

達內科技NTD1712華為vlan定義,簡單應用

system blog interface config ethernet 地址 col tag efault Vlan的定義:是物理設備上連接的不受物理限制的用戶的一個邏輯組。 Vlan的作用:交換機可以分割沖突域,但不能分割廣播域,為了解決這個問題,

自動化運維之SaltStack(概述簡單配置實例)

ati 遠程 int lock 狀態 工具 保持 restart x86 在生產環境中,服務器往往不止一臺,有可能是成千上萬臺。對於運維人員來說,如果單獨對每臺服務器進行管理,工作難度實在是太大了。SaltStack是一個服務器基礎設施管理工具,它具有配置管理、遠程執行、監

Tesseract 在 windows 下的安裝簡單應用

打開 版本信息 文本 否則 選擇 分享 16px alt 運行 Tesseract 是一個開源的 OCR 引擎,可以識別多種格式的圖像文件並將其轉換成文本,最初由 HP 公司開發,後來由 Google 維護。下載地址:https://digi.bib.uni-mannhei

ETCD叢集安裝配置簡單應用

環境配置 CentOS Linux release 7.3.1611 (Core)  etcd-v3.2.6 192.168.108.128 節點1 192.168.108.129 節點2 192.168.108.130 節點3 ETCD

Eureka概念簡單應用

1、為什麼使用Eureka?  在Spring Cloud中我們經常使用Eureka,是因為SpringCloud對Eureka支援力度非常大 ,Eureka的社群活躍多較高,版本更新的速度快。 Eureka簡介: Eureka是Netflix開發的服務發現元件,

使用python操作redis簡單應用

redis 連線例項是執行緒安全的,可以直接將redis連線例項設定為一個全域性變數,直接使用. pip install redis import redis >> r = redis.Redis(host='localhost',port=6379,password='', db=0

串:串的基本定義簡單應用魔王語言

串 串,於我的理解就是字串 一般認為有三種儲存方式:定長順序串,堆串,塊鏈串(個人認為比較雞肋)。定長順序串類似於普通字串,同陣列的大小一樣最長長度固定。堆串儲存在堆中,空間大小可重新分配。塊鏈串類似於連結串列,是極端節省空間的堆串。 定長順序串與堆串應用較多,定長順序串的一些操作存

JxBrowser概述簡單應用

Q:JxBrowser是什麼? JxBrowser是一個跨平臺的Java庫,允許將基於Google Chromium的Web瀏覽器元件整合到Java Swing / AWT / JavaFX應用程式中。使用JxBrowser,您可以將輕量級Swing / JavaFX元件嵌入到Java應用程式中,以顯

Weka安裝簡單應用

因為前段時間上課有接觸WEKA這個軟體 ,寫了一個實驗報告,特此把它貼出來,希望能對大家有所幫助~ 一、Weka介紹 1、Weka簡介 Weka是懷卡託智慧分析環境(Waikato Environment for Knowledge Analysis)的英文字首縮寫,在

vs 2017新增Report Viewer控制元件簡單應用

安裝完vs2017之後我們進行新增Report Viewer控制元件: 1. 點選Tools -> Extensions and Updates... 2. 在新視窗搜尋欄中輸入rdlc後

【Spring訊息】RabbitMq安裝簡單應用(二)

前言: 埋頭苦寫。先把官方文件翻譯過來。整個流程跑一遍。上一篇文章,【Spring訊息】RabbitMq安裝及簡單應用(一),把點對點發送訊息寫完了。之前雖然也可以一個生產者多個消費者,但是一條訊息只能被一個消費者處理,所以是點對點。這篇文章來講講釋出訂閱,一對多。一條訊息

深度學習介紹簡單應用

引言   深度學習背後的主要原理是從大腦中汲取靈感。,這種觀點產生了“神經網路”術語,大腦包含數十億個神經元,它們之間有數萬個連線。 在許多情況下,深度學習演算法類似於大腦,因為大腦和深度學習模型都涉及大量的計算單元(神經元),這些單元在未啟用時並不是活躍的,它們彼此互動時會變得智慧化。 神經元   神經網路

Java單元測試工具:JUnit4(一)——概述簡單例子

(一)JUnit概述及一個簡單例子         看了慕課網的JUnit視訊教程: http://www.imooc.com/learn/356,總結筆記。         這篇筆記記錄JUnit的

【舊文章搬運】ZwQuerySystemInformation枚舉內核模塊簡單應用

接下來 smo and obj 基址 add dwr 調用 mit 原文發表於百度空間,2008-10-24========================================================================== 簡單說,即調用

jquery datatables各引數詳細說明簡單應用

v1.9.0下載後,將media資料夾裡面的css,images,js資料夾拷貝到你的網站即可。接下來引入以下內容: <style type="text/css" title="currentStyle"> @import "./style/datatable/css/de

POI入門簡單應用

        由於工作的需要,學習了一下POI,  Apache POI是Apache軟體基金會的開放原始碼函式庫,POI提供API給Java程式對Microsoft Office格式檔案讀和寫的功能。         那麼我們怎麼使用POI呢?POI的使用其實非常簡單,

人工智慧學習筆記-Theano介紹簡單應用

1 Theano介紹和安裝 1.1 什麼是Theano Theano是一個較為老牌和穩定的機器學習python庫之一。Theano基於Python擅長處理多維陣列(緊密集成了Numpy),屬於比較底層的框架,theano起初也是為了深

InternetExplorer.Application物件的Documen屬性簡單應用

InternetExplorer.Application物件的Documen屬性(轉帖)  document 文擋物件 - JavaScript指令碼語言描述  --------------------------------------------------------