嵌入式系統 Boot Loader 技術內幕

https://www.ibm.com/developerworks/cn/linux/l-btloader/

1. 引言

在專用的嵌入式板子運行 GNU/Linux 系統已經變得愈來愈流行。一個嵌入式 Linux 系統從軟件的角度看一般能夠分爲四個層次:html

1. 引導加載程序。包括固化在固件(firmware)中的 boot 代碼(可選),和 Boot Loader 兩大部分。node

2. Linux 內核。特定於嵌入式板子的定製內核以及內核的啓動參數。linux

3. 文件系統。包括根文件系統和創建於 Flash 內存設備之上文件系統。一般用 ram disk 來做爲 root fs。c++

4. 用戶應用程序。特定於用戶的應用程序。有時在用戶應用程序和內核層之間可能還會包括一個嵌入式圖形用戶界面。經常使用的嵌入式 GUI 有:MicroWindows 和 MiniGUI 懂。算法

引導加載程序是系統加電後運行的第一段軟件代碼。回憶一下 PC 的體系結構咱們能夠知道,PC 機中的引導加載程序由 BIOS(其本質就是一段固件程序)和位於硬盤 MBR 中的 OS Boot Loader(好比,LILO 和 GRUB 等)一塊兒組成。BIOS 在完成硬件檢測和資源分配後,將硬盤 MBR 中的 Boot Loader 讀到系統的 RAM 中,而後將控制權交給 OS Boot Loader。Boot Loader 的主要運行任務就是將內核映象從硬盤上讀到 RAM 中,而後跳轉到內核的入口點去運行,也即開始啓動操做系統。小程序

而在嵌入式系統中,一般並無像 BIOS 那樣的固件程序(注,有的嵌入式 CPU 也會內嵌一段短小的啓動程序),所以整個系統的加載啓動任務就徹底由 Boot Loader 來完成。好比在一個基於 ARM7TDMI core 的嵌入式系統中,系統在上電或復位時一般都從地址 0x00000000 處開始執行,而在這個地址處安排的一般就是系統的 Boot Loader 程序。數組

本文將從 Boot Loader 的概念、Boot Loader 的主要任務、Boot Loader 的框架結構以及 Boot Loader 的安裝等四個方面來討論嵌入式系統的 Boot Loader。網絡

2. Boot Loader 的概念

簡單地說,Boot Loader 就是在操做系統內核運行以前運行的一段小程序。經過這段小程序,咱們能夠初始化硬件設備、創建內存空間的映射圖,從而將系統的軟硬件環境帶到一個合適的狀態,以便爲最終調用操做系統內核準備好正確的環境。數據結構

一般,Boot Loader 是嚴重地依賴於硬件而實現的,特別是在嵌入式世界。所以,在嵌入式世界裏創建一個通用的 Boot Loader 幾乎是不可能的。儘管如此,咱們仍然能夠對 Boot Loader 概括出一些通用的概念來,以指導用戶特定的 Boot Loader 設計與實現。app

1. Boot Loader 所支持的 CPU 和嵌入式板

每種不一樣的 CPU 體系結構都有不一樣的 Boot Loader。有些 Boot Loader 也支持多種體系結構的 CPU,好比 U-Boot 就同時支持 ARM 體系結構和MIPS 體系結構。除了依賴於 CPU 的體系結構外,Boot Loader 實際上也依賴於具體的嵌入式板級設備的配置。這也就是說,對於兩塊不一樣的嵌入式板而言,即便它們是基於同一種 CPU 而構建的,要想讓運行在一塊板子上的 Boot Loader 程序也能運行在另外一塊板子上,一般也都須要修改 Boot Loader 的源程序。

2. Boot Loader 的安裝媒介(Installation Medium)

系統加電或復位後,全部的 CPU 一般都從某個由 CPU 製造商預先安排的地址上取指令。好比,基於 ARM7TDMI core 的 CPU 在復位時一般都從地址 0x00000000 取它的第一條指令。而基於 CPU 構建的嵌入式系統一般都有某種類型的固態存儲設備(好比:ROM、EEPROM 或 FLASH 等)被映射到這個預先安排的地址上。所以在系統加電後,CPU 將首先執行 Boot Loader 程序。

下圖1就是一個同時裝有 Boot Loader、內核的啓動參數、內核映像和根文件系統映像的固態存儲設備的典型空間分配結構圖。

圖1 固態存儲設備的典型空間分配結構

圖1 固態存儲設備的典型空間分配結構

3. 用來控制 Boot Loader 的設備或機制

主機和目標機之間通常經過串口創建鏈接,Boot Loader 軟件在執行時一般會經過串口來進行 I/O,好比:輸出打印信息到串口,從串口讀取用戶控制字符等。

4. Boot Loader 的啓動過程是單階段(Single Stage)仍是多階段(Multi-Stage)

一般多階段的 Boot Loader 能提供更爲複雜的功能,以及更好的可移植性。從固態存儲設備上啓動的 Boot Loader 大多都是 2 階段的啓動過程,也即啓動過程能夠分爲 stage 1 和 stage 2 兩部分。而至於在 stage 1 和 stage 2 具體完成哪些任務將在下面討論。

5. Boot Loader 的操做模式 (Operation Mode)

大多數 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 內核。

6. BootLoader 與主機之間進行文件傳輸所用的通訊設備及協議

最多見的狀況就是,目標機上的 Boot Loader 經過串口與主機之間進行文件傳輸,傳輸協議一般是 xmodem/ymodem/zmodem 協議中的一種。可是,串口傳輸的速度是有限的,所以經過以太網鏈接並藉助 TFTP 協議來下載文件是個更好的選擇。

此外,在論及這個話題時,主機方所用的軟件也要考慮。好比,在經過以太網鏈接和 TFTP 協議來下載文件時,主機方必須有一個軟件用來的提供 TFTP 服務。

在討論了 BootLoader 的上述概念後,下面咱們來具體看看 BootLoader 的應該完成哪些任務。

3. Boot Loader 的主要任務與典型結構框架

在繼續本節的討論以前,首先咱們作一個假定,那就是:假定內核映像與根文件系統映像都被加載到 RAM 中運行。之因此提出這樣一個假設前提是由於,在嵌入式系統中內核映像與根文件系統映像也能夠直接在 ROM 或 Flash 這樣的固態存儲設備中直接運行。但這種作法無疑是以運行速度的犧牲爲代價的。

從操做系統的角度看,Boot Loader 的總目標就是正確地調用內核來執行。

另外,因爲 Boot Loader 的實現依賴於 CPU 的體系結構,所以大多數 Boot Loader 都分爲 stage1 和 stage2 兩大部分。依賴於 CPU 體系結構的代碼,好比設備初始化代碼等,一般都放在 stage1 中,並且一般都用匯編語言來實現,以達到短小精悍的目的。而 stage2 則一般用C語言來實現,這樣能夠實現給複雜的功能,並且代碼會具備更好的可讀性和可移植性。

Boot Loader 的 stage1 一般包括如下步驟(以執行的前後順序):

  • 硬件設備初始化。
  • 爲加載 Boot Loader 的 stage2 準備 RAM 空間。
  • 拷貝 Boot Loader 的 stage2 到 RAM 空間中。
  • 設置好堆棧。
  • 跳轉到 stage2 的 C 入口點。

Boot Loader 的 stage2 一般包括如下步驟(以執行的前後順序):

  • 初始化本階段要使用到的硬件設備。
  • 檢測系統內存映射(memory map)。
  • 將 kernel 映像和根文件系統映像從 flash 上讀到 RAM 空間中。
  • 爲內核設置啓動參數。
  • 調用內核。

3.1 Boot Loader 的 stage1

3.1.1 基本的硬件初始化

這是 Boot Loader 一開始就執行的操做,其目的是爲 stage2 的執行以及隨後的 kernel 的執行準備好一些基本的硬件環境。它一般包括如下步驟(以執行的前後順序):

1. 屏蔽全部的中斷。爲中斷提供服務一般是 OS 設備驅動程序的責任,所以在 Boot Loader 的執行全過程當中能夠沒必要響應任何中斷。中斷屏蔽能夠經過寫 CPU 的中斷屏蔽寄存器或狀態寄存器(好比 ARM 的 CPSR 寄存器)來完成。

2. 設置 CPU 的速度和時鐘頻率。

3. RAM 初始化。包括正確地設置系統的內存控制器的功能寄存器以及各內存庫控制寄存器等。

4. 初始化 LED。典型地,經過 GPIO 來驅動 LED,其目的是代表系統的狀態是 OK 仍是 Error。若是板子上沒有 LED,那麼也能夠經過初始化 UART 向串口打印 Boot Loader 的 Logo 字符信息來完成這一點。

5. 關閉 CPU 內部指令/數據 cache。

3.1.2 爲加載 stage2 準備 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 字節邊界對齊)。所以:

1
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 空間範圍進行清零操做。

3.1.3 拷貝 stage2 到 RAM 中

拷貝時要肯定兩點:(1) stage2 的可執行映象在固態存儲設備的存放起始地址和終止地址;(2) RAM 空間的起始地址。

3.1.4 設置堆棧指針 sp

堆棧指針的設置是爲了執行 C 語言代碼做好準備。一般咱們能夠把 sp 的值設置爲(stage2_end-4),也即在 3.1.2 節所安排的那個 1MB 的 RAM 空間的最頂端(堆棧向下生長)。

此外,在設置堆棧指針 sp 以前,也能夠關閉 led 燈,以提示用戶咱們準備跳轉到 stage2。

通過上述這些執行步驟後,系統的物理內存佈局應該以下圖2所示。

3.1.5 跳轉到 stage2 的 C 入口點

在上述一切都就緒後,就能夠跳轉到 Boot Loader 的 stage2 去執行了。好比,在 ARM 系統中,這能夠經過修改 PC 寄存器爲合適的地址來實現。

圖2 bootloader 的 stage2 可執行映象剛被拷貝到 RAM 空間時的系統內存佈局

圖2 bootloader 的 stage2 可執行映象剛被拷貝到 RAM 空間時的系統內存佈局

3.2 Boot Loader 的 stage2

正如前面所說,stage2 的代碼一般用 C 語言來實現,以便於實現更復雜的功能和取得更好的代碼可讀性和可移植性。可是與普通 C 語言應用程序不一樣的是,在編譯和連接 boot loader 這樣的程序時,咱們不能使用 glibc 庫中的任何支持函數。其緣由是顯而易見的。這就給咱們帶來一個問題,那就是從那裏跳轉進 main() 函數呢?直接把 main() 函數的起始地址做爲整個 stage2 執行映像的入口點或許是最直接的想法。可是這樣作有兩個缺點:1)沒法經過main() 函數傳遞函數參數;2)沒法處理 main() 函數返回的狀況。一種更爲巧妙的方法是利用 trampoline(彈簧牀)的概念。也即,用匯編語言寫一段trampoline 小程序,並將這段 trampoline 小程序來做爲 stage2 可執行映象的執行入口點。而後咱們能夠在 trampoline 彙編小程序中用 CPU 跳轉指令跳入 main() 函數中去執行;而當 main() 函數返回時,CPU 執行路徑顯然再次回到咱們的 trampoline 程序。簡而言之,這種方法的思想就是:用這段 trampoline 小程序來做爲 main() 函數的外部包裹(external wrapper)。

下面給出一個簡單的 trampoline 程序示例(來自blob):

1
2
3
4
5
6
.text
.globl _trampoline
_trampoline:
     bl  main
     /* if main ever returns we just call it again */
     b   _trampoline

能夠看出,當 main() 函數返回後,咱們又用一條跳轉指令從新執行 trampoline 程序――固然也就從新執行 main() 函數,這也就是 trampoline(彈簧牀)一詞的意思所在。

3.2.1初始化本階段要使用到的硬件設備

這一般包括:(1)初始化至少一個串口,以便和終端用戶進行 I/O 輸出信息;(2)初始化計時器等。

在初始化這些設備以前,也能夠從新把 LED 燈點亮,以代表咱們已經進入 main() 函數執行。

設備初始化完成後,能夠輸出一些打印信息,程序名字字符串、版本號等。

3.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" 狀態的。

(1) 內存映射的描述

能夠用以下數據結構來描述 RAM 地址空間中的一段連續(continuous)的地址範圍:

1
2
3
4
5
typedef struct memory_area_struct {
     u32 start; /* the base address of the memory region */
     u32 size; /* the byte number of the memory region */
     int used;
} memory_area_t;

這段 RAM 地址空間中的連續地址範圍能夠處於兩種狀態之一:(1)used=1,則說明這段連續的地址範圍已被實現,也即真正地被映射到 RAM 單元上。(2)used=0,則說明這段連續的地址範圍並未被系統所實現,而是處於未使用狀態。

基於上述 memory_area_t 數據結構,整個 CPU 預留的 RAM 地址空間能夠用一個 memory_area_t 類型的數組來表示,以下所示:

1
2
3
4
5
6
7
memory_area_t memory_map[NUM_MEM_AREAS] = {
     [0 ... (NUM_MEM_AREAS - 1)] = {
         .start = 0,
         .size = 0,
         .used = 0
     },
};

(2) 內存映射的檢測

下面咱們給出一個可用來檢測整個 RAM 地址空間內存映射狀況的簡單而有效的算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* 數組初始化 */
for(i = 0; i < NUM_MEM_AREAS; i++)
     memory_map[i].used = 0;
/* first write a 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
     * (u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
      /*
       * 檢測從基地址 MEM_START+i*PAGE_SIZE 開始,大小爲
* PAGE_SIZE 的地址空間是不是有效的RAM地址空間。
       */
      調用3.1.2節中的算法test_mempage();
      if ( current memory page isnot a valid ram page) {
         /* no RAM here */
         if(memory_map[i].used )
             i++;
         continue;
     }
     
     /*
      * 當前頁已是一個被映射到 RAM 的有效地址範圍
      * 可是還要看看當前頁是否只是 4GB 地址空間中某個地址頁的別名?
      */
     if(* (u32 *)addr != 0) { /* alias? */
         /* 這個內存頁是 4GB 地址空間中某個地址頁的別名 */
         if ( memory_map[i].used )
             i++;
         continue;
     }
     
     /*
      * 當前頁已是一個被映射到 RAM 的有效地址範圍
      * 並且它也不是 4GB 地址空間中某個地址頁的別名。
      */
     if (memory_map[i].used == 0) {
         memory_map[i].start = addr;
         memory_map[i].size = PAGE_SIZE;
         memory_map[i].used = 1;
     } else {
         memory_map[i].size += PAGE_SIZE;
     }
} /* end of for (…) */

在用上述算法檢測完系統的內存映射狀況後,Boot Loader 也能夠將內存映射的詳細信息打印到串口。

3.2.3 加載內核映像和根文件系統映像

(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 設備上拷貝映像的工做:

1
2
3
4
while(count) {
     *dest++ = *src++; /* they are all aligned with word boundary */
     count -= 4; /* byte number */
};

3.2.4 設置內核的啓動參數

應該說,在將內核映像和根文件系統映像拷貝到 RAM 空間中後,就能夠準備啓動 Linux 內核了。可是在調用內核以前,應該做一步準備工做,即:設置 Linux 內核的啓動參數。

Linux 2.4.x 之後的內核都指望以標記列表(tagged list)的形式來傳遞啓動參數。啓動參數標記列表以標記 ATAG_CORE 開始,以標記 ATAG_NONE 結束。每一個標記由標識被傳遞參數的 tag_header 結構以及隨後的參數值數據結構來組成。數據結構 tag 和 tag_header 定義在 Linux 內核源碼的include/asm/setup.h 頭文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 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 的代碼以下:

1
2
3
4
5
6
7
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就是在這裏設置的。

下面是設置內存映射狀況的示例代碼:

1
2
3
4
5
6
7
8
9
10
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位數據位"這樣的設置。下面是一段設置調用內核命令行參數字符串的示例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
char *p;
     /* eat leading white space */
     for(p = commandline; *p == ' '; p++)
         ;
     /* skip non-existent command lines so the kernel will still
     * use its default command 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 映象(壓縮格式)以及它的大小:

1
2
3
4
5
6
7
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):

1
2
3
4
5
6
7
8
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 load ramdisk */
     
params = tag_next(params);

最後,設置 ATAG_NONE 標記,結束整個啓動參數列表:

1
2
3
4
5
static void setup_end_tag(void)
{
     params->hdr.tag = ATAG_NONE;
     params->hdr.size = 0;
}

3.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 語言,能夠像下列示例代碼這樣來調用內核:

1
2
3
4
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()函數調用應該永遠不返回的。若是這個調用返回,則說明出錯。

4. 關於串口終端

在 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 處去執行,那麼內核映像固然不能正確地執行了。

5. 結束語

Boot Loader 的設計與實現是一個很是複雜的過程。若是不能從串口收到那激動人心的"uncompressing linux.................. done, booting the kernel……"內核啓動信息,恐怕誰也不能說:"嗨,個人 boot loader 已經成功地轉起來了!"。

相關文章
相關標籤/搜索