bootloader 啟動過程
一、 Boot Loader的概念和功能
1、嵌入式Linux軟體結構與分佈在一般情況下嵌入式Linux系統中的軟體主要分為以下及部分:
(1)引導載入程式:其中包括內部ROM中的固化啟動程式碼和Boot Loader兩部分。而這個內部固化ROM是廠家在晶片生產時候固化的,作用基本上是引導Boot Loader。有的晶片比較複雜,比如Omap3,他在flash中沒有程式碼的時候有許多啟動方式:USB、UART或乙太網等等。而S3C24x0則很簡單,只有Norboot和Nandboot。
(2)Linux kernel 和drivers。
(3)檔案系統。包括根檔案系統和建立於Flash記憶體裝置之上的檔案系統(EXT4、UBI、CRAMFS等等)。它是提供管理系統的各種配置檔案以及系統執行使用者應用程式的良好執行環境的載體。
(4)應用程式。使用者自定義的應用程式,存放於檔案系統之中。
在Flash 儲存器中,他們的 一般分佈如下:
但是以上只是大部分情況下的分佈,也有一些可能根檔案系統是initramfs,被一起壓縮到了核心映像裡,或者沒有Bootloader引數區,等等。
2、在嵌入式Linux中為什麼要有BootLoader
在linux核心的啟動執行除了核心映像必須在主存的適當位置,CPU還必須具備一定的條件:
【1】CPU暫存器設定:
R0=0;
R1=Machine ID(即Machine Type Number,定義在linux/arch/arm/tools/mach-types);
R2=核心啟動引數在RAM中起始基地址;
【2】CPU模式:
必須禁止中斷(IRQs和FIQs);
CPU 必須工作在是超級保護模式(SVC) 模式;
【3】Cache和MMU的設定:
MMU 必須關閉;
指令Cache可以開啟也可以關閉;
資料Cache必須關閉;
但是在CPU剛上電啟動的時候,一般連記憶體控制器都沒有配置過,根本無法在記憶體中執行程式,更不可能處在Linux核心的啟動環境中。為了初始化CPU及其他外設,使得Linux核心可以在系統主存中跑起來,並讓系統符合Linux核心啟動的必備條件,必須要有一個先於核心執行的程式,他就是所謂的引導載入程式(Boot Loader)。
而Boot Loader並不是Linux才需要,是幾乎所有的執行作業系統的裝置都具備的。我們的PC的BOIS就是Boot Loader的一部分(只是前期引導,後面一般還有外存中的各種Boot Loader),對於Linux PC來說,Boot Loader = BIOS + GRUB/LILO。
正如前面所述,Boot Loader是在作業系統核心執行之前執行的一段小程式。通過這段小程式,我們可以初始化硬體裝置,從而將系統的軟硬體環境帶到一個合適的狀態,以便為最終呼叫作業系統核心準備好正確的環境,最後從別處(Flash、乙太網、UART)載入核心映像並跳到入口地址。
由於BootLoader直接操作硬體,所以她嚴重依賴於硬體,而且依據所引導的作業系統的不同。
二、Boot Loader的工作模式
大多數 Boot Loader 都包含兩種不同的操作模式:“啟動載入”模式和“下載”模式,這種區別僅對於開發人員才有意義。但從終端使用者的角度看,Boot Loader 的作用就是用來載入作業系統,而並不存在所謂的啟動載入模式與下載工作模式的區別。
啟動載入(Boot loading)模式:
這種模式也稱為"自主"(Autonomous)模式。也即 Boot Loader 從目標機上的某個固態儲存裝置上將作業系統載入到 RAM 中執行,整個過程並沒有使用者的介入。這種模式是 Boot Loader 的正常工作模式,因此在嵌入式產品釋出的時侯,Boot Loader 顯然必須工作在這種模式下。
下載(Downloading)模式:
在這種模式下,目標機上的 Boot Loader 將通過串列埠連線或網路連線等通訊手段從主機(Host)下載檔案,比如:下載核心映像和根檔案系統映像等。從主機下載的檔案通常首先被 Boot Loader 儲存到目標機的 RAM 中,然後再被 Boot Loader 寫到目標機上的FLASH 類固態儲存裝置中。Boot Loader 的這種模式通常在第一次安裝核心與根檔案系統時被使用;此外,以後的系統更新也會使用 Boot Loader 的這種工作模式。工作於這種模式下的 Boot Loader 通常都會向它的終端使用者提供一個簡單的命令列介面。
象Blob 或U-Boot 等這樣功能強大的Boot Loader 通常同時支援這兩種工作模式,而且允許使用者在這兩種工作模式之間進行切換。比如,Blob 在啟動時處於正常的啟動載入模式,但是它會延時10 秒等待終端使用者按下任意鍵而將 blob 切換到下載模式。如果在 10 秒內沒有使用者按鍵,則 blob 繼續啟動 Linux 核心。
三、Boot Loader 與主機之間進行檔案傳輸協議
最常見的情況就是,目標機上的 Boot Loader 通過串列埠與主機之間進行檔案傳輸,傳輸協議通常是 xmodem/ymodem/zmodem 協議中的一種。但是,串列埠傳輸的速度是有限的,因此通過乙太網連線並藉助 TFTP 協議來下載檔案是個更好的選擇。
此外,在論及這個話題時,主機方所用的軟體也要考慮。比如,在通過乙太網連線和TFTP 協議來下載檔案時,主機方必須有一個軟體用來的提供 TFTP 服務。
四、Bootloader的工作流程
由於Boot Loader的實現依賴與CPU的體系結構,因此大多數的Boot Loader都分為stage1和stage2兩個階段:
1,Bootloader 的第一階段(Stage1),工作流程
· 硬體裝置初始化
· 程式碼重定位,為載入 Boot Loader 的 stage2 準備 RAM 空間
· 載入t第二階段程式碼到RAM空間
· 設定堆疊跳轉到第二階段程式碼入口
1.1,硬體裝置初始化通常包括如下步驟:(按先後順序執行):
【1】復位(reset)
【2】設定CPU為超級保護模式(SVC) 即特權模式(Supervisor)
【3】關閉看門狗,不必附加喂狗程式碼。
【4】遮蔽所有中斷,為中斷提供服務通常是OS裝置驅動程式的責任,因此在 Boot Loader 的執行全過程中可以不必響應任何中斷。中斷遮蔽可以通過寫CPU的中斷遮蔽暫存器或狀態暫存器(比如 ARM 的 CPSR 暫存器)來完成。
【5】設定系統時鐘頻率。
【6】初始化記憶體控制器,包括正確地設定系統的記憶體控制器的功能暫存器以及各記憶體庫控制暫存器等。
【7】初始化串列埠等,典型地,初始化UART並向串列埠列印相關字元資訊。
【8】初始化LED。典型地,通過GPIO 來驅動LED,其目的是表明系統的狀態是 OK 還是 Error。如果板子上沒有 LED,那麼也可以通過初始化 UART 向串列埠列印 Boot Loader 的Logo 字元資訊來完成這一點。
【9】關閉 CPU 內部指令/資料 cache。
1.2,程式碼重定位主要檢查自己是否在記憶體中。如果是跳到堆疊段(stack_setup程式碼段)設定堆疊,不是就載入自己到RAM空間。
為了獲得更快的執行速度,通常把 stage2 載入到 RAM 空間中來執行,因此必須為載入 Boot Loader 的 stage2 準備好一段可用的 RAM 空間範圍。
由於 stage2 通常是 C 語言執行程式碼,因此在考慮空間大小時,除了 stage2 可執行映象的大小外,還必須把堆疊空間也考慮進來。此外,空間大小最好是 memory page 大小(通常是 4KB)的倍數。一般而言,1M 的 RAM 空間已經足夠了。具體的地址範圍可以任意安排,比如 blob 就將它的 stage2 可執行映像安排到從系統 RAM 起始地址 0xc0200000 開始的 1M 空間內執行。但是,將 stage2 安排到整個 RAM 空間的最頂 1MB(也即(RamEnd-1MB) - RamEnd)是一種值得推薦的方法。
為了後面的敘述方便,這裡把所安排的RAM 空間範圍的大小記為:stage2_size(位元組),把起始地址和終止地址分別記為:stage2_start 和 stage2_end(這兩個地址均以 4 位元組邊界對齊)。因此:
stage2_end = stage2_start +stage2_size
另外,還必須確保所安排的地址範圍的的確確是可讀寫的RAM 空間,因此,必須對你所安排的地址範圍進行測試。具體的測試方法可以採用類似於blob 的方法,也即:以 memory page 為被測試單位,測試每個 memory page 開始的兩個字是否是可讀寫的。為了後面敘述的方便,我們記這個檢測演算法為:test_mempage,其具體步驟如下:
【1】先儲存 memory page 一開始兩個字的內容。
【2】向這兩個字中寫入任意的數字。比如:向第一個字寫入 0x55,第 2 個字寫入 0xaa。
【3】 然後,立即將這兩個字的內容讀回。顯然,我們讀到的內容應該分別是 0x55 和 0xaa。如果不是,則說明這個 memory page 所佔據的地址範圍不是一段有效的 RAM 空間。
【4】再向這兩個字中寫入任意的數字。比如:向第一個字寫入 0xaa,第 2 個字中寫入 0x55。
【5】然後,立即將這兩個字的內容立即讀回。顯然,我們讀到的內容應該分別是 0xaa 和 0x55。如果不是,則說明這個 memory page 所佔據的地址範圍不是一段有效的 RAM 空間。
【6】恢復這兩個字的原始內容。測試完畢。
為了得到一段乾淨的RAM 空間範圍,我們也可以將所安排的 RAM 空間範圍進行清零操作。
1.3,載入Bootloader第二階段程式碼到RAM空間,拷貝時要確定兩點:(1) stage2 的可執行映象在固態儲存裝置的存放起始地址和終止地址;(2) RAM 空間的起始地址。
1.4,設定好堆疊,強調下堆和棧的區別:棧區(stack) 由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧;堆區(heap) 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。程式的區域性變數存在於(棧)中,全域性變數存在於(靜態區 )中,動態申請資料存在於( 堆)中全域性變數實際上是存在一個(一般來說正常的編譯器)可讀可寫的記憶體空間,這個空間是在你寫程式編譯好的空間地址(由編譯器決定),是固定的。
堆疊指標的設定是為了執行 C 語言程式碼作好準備。通常我們可以把 sp 的值設定為(stage2_end-4),因為棧是向下生長的,所以通常把棧指標設在1MB空間的最頂端。此外,在設定堆疊之前,也可以把指示用的LED燈關閉,以提示使用者跳轉到Stage2。經過以上步驟設定以後,系統的實體記憶體佈局應該如圖所示。
1.5,跳轉到第二階段(Stage2)程式碼入口,在上述一切就緒後,就可以跳轉到Boot Loader的Stage2執行了,在ARM系統中是通過修改PC暫存器為合適的地址來實現的。如U-Boot中是這樣實現的:
ldr pc, _start_armboot
start_armboot是第二階段(Stage2)的C程式的入口點。start_armboot是U-Boot執行的第一個C語言函式,完成系統初始化工作,進入主迴圈,處理使用者輸入的命令。
2,Bootloader的第二階段(Stage2)工作流程
· 初始化本階段要使用到的硬體裝置
· 檢測系統記憶體對映
· 載入核心映像和根檔案系統映像
· 設定核心的啟動引數
· 啟動核心
2.1,初始化本階段要使用到的硬體裝置,這通常包括:
(1)設定時鐘、初始化至少一個串列埠,以便和終端使用者進行 I/O 輸出資訊;(2)初始化計時器等。在初始化這些裝置之前,也可以重新把 LED 燈點亮,以表明我們已經進入 main_loop() 函式執行。
board_init函式設定MPLL、改變系統時鐘,它是開發板相關的函式,在board/samsung/smdk2440/smdk2440.c中實現。值得注意的是board_init函式還儲存了機器型別ID,這將在呼叫核心的時候傳遞給核心。程式碼如下:
gd->bd->bi_arch_number = MACH_TYPE_S3C2440; //值為362
串列埠的初始化函式主要是serial_init,它設定UART控制器,是CPU相關的函式。
2.2,檢測系統記憶體對映(memory map)
所謂記憶體對映就是指在整個4GB 實體地址空間中有哪些地址範圍被分配用來定址系統的RAM 單元。比如,在SA-1100 CPU 中,從0xC000,0000 開始的512M 地址空間被用作系統的RAM 地址空間,而在Samsung S3C44B0X CPU 中,從 0x0c00,0000 到 0x1000,0000 之間的 64M 地址空間被用作系統的 RAM 地址空間。雖然CPU 通常預留出一大段足夠的地址空間給系統 RAM,但是在搭建具體的嵌入式系統時卻不一定會實現 CPU 預留的全部 RAM 地址空間。也就是說,具體的嵌入式系統往往只把 CPU 預留的全部 RAM 地址空間中的一部分對映到 RAM 單元上,而讓剩下的那部分預留 RAM 地址空間處於未使用狀態。由於上述這個事實,因此 Boot Loader 的 stage2 必須在它想幹點什麼 (比如,將儲存在 flash 上的核心映像讀到 RAM 空間中) 之前檢測整個系統的記憶體對映情況,也即它必須知道 CPU 預留的全部 RAM 地址空間中的哪些被真正對映到 RAM 地址單元,哪些是處於 "unused" 狀態的。
對於smdk2440的開發板,其記憶體分佈是明確的,一般記憶體起始地址為0x3000 0000,大小為64M = 0x0400 0000。程式碼如下:
int dram_init(void)
{
gd->bd->bi_dram[0] . start = PHYS_SDRAM_1; //即0x3000 0000
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE; //即0x0400 0000
//這兩個值都定義在include/configs/smdk2440.h中
}
2.3,將核心映像和根檔案系統映像從Flash上讀到RAM空間中。
【1】 規劃記憶體佔用的佈局
這裡包括兩個方面:(1)核心映像所佔用的記憶體範圍;(2)根檔案系統所佔用的記憶體範圍。在規劃記憶體佔用的佈局時,主要考慮基地址和映像的大小兩個方面。
對於核心映像,一般將其拷貝到從(MEM_START+0x8000) 這個基地址開始的大約1MB大小的記憶體範圍內(嵌入式 Linux 的核心一般都不操過 1MB)。為什麼要把從 MEM_START 到 MEM_START+0x8000 這段 32KB 大小的記憶體空出來呢?這是因為 Linux 核心要在這段記憶體中放置一些全域性資料結構,如:啟動引數和核心頁表等資訊。
而對於根檔案系統映像,則一般將其拷貝到 MEM_START+0x0010,0000 開始的地方。如果用 Ramdisk 作為根檔案系統映像,則其解壓後的大小一般是1MB。
【2】從 Flash 上拷貝
由於像 ARM 這樣的嵌入式 CPU 通常都是在統一的記憶體地址空間中定址 Flash 等固態儲存裝置的,因此從 Flash 上讀取資料與從 RAM 單元中讀取資料並沒有什麼不同。用一個簡單的迴圈就可以完成從 Flash 裝置上拷貝映像的工作:
while(count) {
*dest++ = *src++; /* they are all aligned with word boundary*/
count -= 4; /* byte number */
};
2.4,為核心設定啟動引數。
應該說,在將核心映像和根檔案系統映像拷貝到 RAM 空間中後,就可以準備啟動 Linux 核心了。但是在呼叫核心之前,應該作一步準備工作,即:設定 Linux 核心的啟動引數。
U-Boot 是通過標記列表向核心傳遞引數。
setup_memory_tags
setup_commandline_tag
這兩個標記列表定義在arch/arm/lib/bootm.c中,需要在定義命令的檔案include/configs/smdk2440.h中定義兩個命令
#define CONFIG_SETUP_MEMORY_TAGS 1
#define CONFIG_CMDLINE_TAG 1
Linux 2.4.x 以後的核心都期望以標記列表(tagged list)的形式來傳遞啟動引數。啟動引數標記列表以標記ATAG_CORE 開始,以標記 ATAG_NONE 結束。每個標記由標識被傳遞引數的 tag_header 結構以及隨後的引數值資料結構來組成。資料結構 tag 和 tag_header 定義在 Linux 核心原始碼的/arch/arm/include/asm/setup.h標頭檔案中:
/* The list ends with an ATAG_NONE node. */
#define ATAG_NONE 0x00000000
struct tag_header {
u32 size;/* 注意,這裡size是字數為單位的 */
u32 tag;
};
……
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
在嵌入式 Linux 系統中,通常需要由 Boot Loader 設定的常見啟動引數有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。比如,設定 ATAG_CORE 的程式碼如下:
params = (struct tag *)BOOT_PARAMS;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);
其中,BOOT_PARAMS 表示核心啟動引數在記憶體中的起始基地址,指標 params 是一個 struct tag 型別的指標。巨集 tag_next() 將以指向當前標記的指標為引數,計算緊臨當前標記的下一個標記的起始地址。注意,核心的根檔案系統所在的裝置ID就是在這裡設定的。
下面是設定記憶體對映情況的示例程式碼:
for(i = 0; i < NUM_MEM_AREAS; i++) {
if(memory_map[i].used) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size(tag_mem32);
params->u.mem.start = memory_map[i].start;
params->u.mem.size = memory_map[i].size;
params = tag_next(params);
}
}
可以看出,在 memory_map[]陣列中,每一個有效的記憶體段都對應一個 ATAG_MEM 引數標記。 Linux 核心在啟動時可以以命令列引數的形式來接收資訊,利用這一點我們可以向核心提供那些核心不能自己檢測的硬體引數資訊,或者過載(override)核心自己檢測到的資訊。比如,我們用這樣一個命令列引數字串"console=ttyS0,115200n8"來通知核心以 ttyS0 作為控制檯,且串列埠採用 "115200bps、無奇偶校驗、8位資料位"這樣的設定。下面是一段設定呼叫核心命令列引數字串的示例程式碼:
char *p;
/* eat leading whitespace */
for(p = commandline;*p == ' '; p++)
;
/* skip non-existentcommand lines so the kernel will still
* use its defaultcommand line.
*/
if(*p == '\0')
return;
params->hdr.tag =ATAG_CMDLINE;
params->hdr.size =(sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
strcpy(params->u.cmdline.cmdline, p);
params =tag_next(params);
請注意在上述程式碼中,設定 tag_header 的大小時,必須包括字串的終止符'\0',此外還要將位元組數向上圓整4個位元組,因為 tag_header 結構中的size 成員表示的是字數。
下面是設定 ATAG_INITRD 的示例程式碼,它告訴核心在 RAM 中的什麼地方可以找到 initrd 映象(壓縮格式)以及它的大小:
params->hdr.tag = ATAG_INITRD2;
params->hdr.size = tag_size(tag_initrd);
params->u.initrd.start = RAMDISK_RAM_BASE;
params->u.initrd.size = INITRD_LEN;
params = tag_next(params);
下面是設定 ATAG_RAMDISK 的示例程式碼,它告訴核心解壓後的 Ramdisk 有多大(單位是KB):
params->hdr.tag = ATAG_RAMDISK;
params->hdr.size = tag_size(tag_ramdisk);
params->u.ramdisk.start = 0;
params->u.ramdisk.size = RAMDISK_SIZE;/* 請注意,單位是KB */
params->u.ramdisk.flags = 1; /* automatically loadramdisk */
params = tag_next(params);
最後,設定 ATAG_NONE 標記,結束整個啟動引數列表:
static void setup_end_tag(void)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
2.5,啟動核心
Boot Loader 呼叫 Linux 核心的方法是直接跳轉到核心的第一條指令處,也即直接跳轉到 MEM_START+0x8000 地址處。在跳轉時,下列條件要滿足:
【1】CPU 暫存器的設定:
R0=0;
注:
@R1=機器型別 ID;關於 Machine Type Number,可以參見 linux/arch/arm/tools/mach-types。
@R2=啟動引數標記列表在 RAM 中起始基地址;
【2】CPU 模式:
必須禁止中斷(IRQs和FIQs);
CPU 必須 SVC 模式;
【3】Cache 和 MMU 的設定:
MMU 必須關閉;
指令 Cache 可以開啟也可以關閉;
資料 Cache 必須關閉;
如果用 C 語言,可以像下列示例程式碼這樣來呼叫核心:
void (*theKernel)(int zero, int arch, u32 params_addr)
= (void (*)(int, int, u32))KERNEL_RAM_BASE;
……
theKernel(0, ARCH_NUMBER, (u32) kernel_params_start);
注意,theKernel()函式呼叫應該永遠不返回的。如果這個呼叫返回,則說明出錯。
對於ARM構架的CPU來說,都是通過../lib_arm/bootm.c中的do_bootm_linux函式來啟動核心的。這個函式中,設定標記列表,最後通過
theKernel = (void (*)(int, int, uint))images->ep;
呼叫核心。其中,theKernel 指向核心存放的地址(對於ARM構架的CPU,通常這個地址是0x3000 8000)。傳遞的3個引數如下:
void (*theKernel)(int zero, int arch, uint params);
R0: 0
R1: 機器型別ID -- gd->bd->bi_arch_number = MACH_TYPE_S3C2440; //值為362
R2: 啟動引數標記列表在RAM中的起始地址 0x3000 0100
五、 關於串列埠終端
在 boot loader 程式的設計與實現中,沒有什麼能夠比從串列埠終端正確地收到列印資訊能更令人激動了。此外,向串列埠終端列印資訊也是一個非常重要而又有效的除錯手段。但是,我們經常會碰到串列埠終端顯示亂碼或根本沒有顯示的問題。造成這個問題主要有兩種原因:(1) boot loader 對串列埠的初始化設定不正確。(2) 執行在 host 端的終端模擬程式對串列埠的設定不正確,這包括:波特率、奇偶校驗、資料位和停止位等方面的設定。
此外,有時也會碰到這樣的問題,那就是:在 boot loader 的執行過程中我們可以正確地向串列埠終端輸出資訊,但當 boot loader 啟動核心後卻無法看到核心的啟動輸出資訊。對這一問題的原因可以從以下幾個方面來考慮:
(1) 首先請確認你的核心在編譯時配置了對串列埠終端的支援,並配置了正確的串列埠驅動程式。
(2) 你的 boot loader 對串列埠的初始化設定可能會和核心對串列埠的初始化設定不一致。此外,對於諸如 s3c44b0x 這樣的 CPU,CPU 時鐘頻率的設定也會影響串列埠,因此如果 boot loader 和核心對其 CPU 時鐘頻率的設定不一致,也會使串列埠終端無法正確顯示資訊。
(3) 最後,還要確認 boot loader 所用的核心基地址必須和核心映像在編譯時所用的執行基地址一致,尤其是對於 uClinux 而言。假設你的核心映像在編譯時用的基地址是 0xc0008000,但你的 boot loader 卻將它載入到 0xc0010000 處去執行,那麼核心映像當然不能正確地執行了。
六、 結束語
Boot Loader 的設計與實現是一個非常複雜的過程。如果能從串列埠收到那激動人心的
"uncompressing linux
.................. done,
booting the kernel……"
核心啟動資訊,說明boot loader 已經成功地轉起來了!。