天天看點

嵌入式系統 Boot Loader 技術内幕

來源:http://www.ibm.com/developerworks/cn/linux/l-btloader/index.html

級别: 初級

2003 年 12 月 01 日

本文詳細地介紹了基于嵌入式系統中的 OS 啟動加載程式 ―― Boot Loader 的概念、軟體設計的主要任務以及結構架構等内容。

<a>1. 引言</a>

在專用的嵌入式闆子運作 GNU/Linux 系統已經變得越來越流行。一個嵌入式 Linux 系統從軟體的角度看通常可以分為四個層次:

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

2. <b>Linux 核心。</b>特定于嵌入式闆子的定制核心以及核心的啟動參數。

3. <b>檔案系統。</b>包括根檔案系統和建立于 Flash 記憶體裝置之上檔案系統。通常用 ram disk 來作為 root fs。

4. <b>使用者應用程式。</b>特定于使用者的應用程式。有時在使用者應用程式和核心層之間可能還會包括一個嵌入式圖形使用者界面。常用的嵌入式 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。

<a>2. Boot Loader 的概念</a>

簡單地說,Boot Loader 就是在作業系統核心運作之前運作的一段小程式。通過這段小程式,我們可以初始化硬體裝置、建立記憶體空間的映射圖,進而将系統的軟硬體環境帶到一個合适的狀态,以便為最終調用作業系統核心準備好正确的環境。

通常,Boot Loader 是嚴重地依賴于硬體而實作的,特别是在嵌入式世界。是以,在嵌入式世界裡建立一個通用的 Boot Loader 幾乎是不可能的。盡管如此,我們仍然可以對 Boot Loader 歸納出一些通用的概念來,以指導使用者特定的 Boot Loader 設計與實作。

<a>1. Boot Loader 所支援的 CPU 和嵌入式闆</a>

每 種不同的 CPU 體系結構都有不同的 Boot Loader。有些 Boot Loader 也支援多種體系結構的 CPU,比如 U-Boot 就同時支援 ARM 體系結構和MIPS 體系結構。除了依賴于 CPU 的體系結構外,Boot Loader 實際上也依賴于具體的嵌入式闆級裝置的配置。這也就是說,對于兩塊不同的嵌入式闆而言,即使它們是基于同一種 CPU 而建構的,要想讓運作在一塊闆子上的 Boot Loader 程式也能運作在另一塊闆子上,通常也都需要修改 Boot Loader 的源程式。

<a>2. Boot Loader 的安裝媒介(Installation Medium)</a>

系 統加電或複位後,所有的 CPU 通常都從某個由 CPU 制造商預先安排的位址上取指令。比如,基于 ARM7TDMI core 的 CPU 在複位時通常都從位址 0x00000000 取它的第一條指令。而基于 CPU 建構的嵌入式系統通常都有某種類型的固态儲存設備(比如:ROM、EEPROM 或 FLASH 等)被映射到這個預先安排的位址上。是以在系統加電後,CPU 将首先執行 Boot Loader 程式。

下圖1就是一個同時裝有 Boot Loader、核心的啟動參數、核心映像和根檔案系統映像的固态儲存設備的典型空間配置設定結構圖。

<a><b>圖1 固态儲存設備的典型空間配置設定結構</b></a>

<a>3. 用來控制 Boot Loader 的裝置或機制</a>

主機和目标機之間一般通過序列槽建立連接配接,Boot Loader 軟體在執行時通常會通過序列槽來進行 I/O,比如:輸出列印資訊到序列槽,從序列槽讀取使用者控制字元等。

<a>4. Boot Loader 的啟動過程是單階段(Single Stage)還是多階段(Multi-Stage)</a>

通 常多階段的 Boot Loader 能提供更為複雜的功能,以及更好的可移植性。從固态儲存設備上啟動的 Boot Loader 大多都是 2 階段的啟動過程,也即啟動過程可以分為 stage 1 和 stage 2 兩部分。而至于在 stage 1 和 stage 2 具體完成哪些任務将在下面讨論。

<a>5. Boot Loader 的操作模式 (Operation Mode)</a>

大多數 Boot Loader 都包含兩種不同的操作模式:"啟動加載"模式和"下載下傳"模式,這種差別僅對于開發人員才有意義。但從最終使用者的角度看,Boot Loader 的作用就是用來加載作業系統,而并不存在所謂的啟動加載模式與下載下傳工作模式的差別。

<b>啟動加載(Boot loading)模式:</b>這種模式也稱為"自主" (Autonomous)模式。也即 Boot Loader 從目标機上的某個固态儲存設備上将作業系統加載到 RAM 中運作,整個過程并沒有使用者的介入。這種模式是 Boot Loader 的正常工作模式,是以在嵌入式産品釋出的時侯,Boot Loader 顯然必須工作在這種模式下。

<b>下載下傳(Downloading)模式:</b>在這種模式下,目标機上的 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 核心。

<a>6. BootLoader 與主機之間進行檔案傳輸所用的通信裝置及協定</a>

最常見的情況就是,目标機上的 Boot Loader 通過序列槽與主機之間進行檔案傳輸,傳輸協定通常是 xmodem/ymodem/zmodem 協定中的一種。但是,序列槽傳輸的速度是有限的,是以通過以太網連接配接并借助 TFTP 協定來下載下傳檔案是個更好的選擇。

此外,在論及這個話題時,主機方所用的軟體也要考慮。比如,在通過以太網連接配接和 TFTP 協定來下載下傳檔案時,主機方必須有一個軟體用來的提供 TFTP 服務。

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

<a>3. Boot Loader 的主要任務與典型結構架構</a>

在 繼續本節的讨論之前,首先我們做一個假定,那就是:假定核心映像與根檔案系統映像都被加載到 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 空間中。

為核心設定啟動參數。

調用核心。

<a>3.1 Boot Loader 的 stage1</a>

<b>3.1.1 基本的硬體初始化</b>

這是 Boot Loader 一開始就執行的操作,其目的是為 stage2 的執行以及随後的 kernel 的執行準備好一些基本的硬體環境。它通常包括以下步驟(以執行的先後順序):

1. <b>屏蔽所有的中斷。</b>為中斷提供服務通常是 OS 裝置驅動程式的責任,是以在 Boot Loader 的執行全過程中可以不必響應任何中斷。中斷屏蔽可以通過寫 CPU 的中斷屏蔽寄存器或狀态寄存器(比如 ARM 的 CPSR 寄存器)來完成。

2. <b>設定 CPU 的速度和時鐘頻率。</b>

3. <b>RAM 初始化。</b>包括正确地設定系統的記憶體控制器的功能寄存器以及各記憶體庫控制寄存器等。

4. <b>初始化 LED。</b>典型地,通過 GPIO 來驅動 LED,其目的是表明系統的狀态是 OK 還是 Error。如果闆子上沒有 LED,那麼也可以通過初始化 UART 向序列槽列印 Boot Loader 的 Logo 字元資訊來完成這一點。

5. <b>關閉 CPU 内部指令/資料 cache。</b>

<b>3.1.2 為加載 stage2 準備 RAM 空間</b>

為了獲得更快的執行速度,通常把 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 位元組邊界對齊)。是以:

另外,還必須確定所安排的位址範圍的的确确是可讀寫 的 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 空間範圍進行清零操作。

<b>3.1.3 拷貝 stage2 到 RAM 中</b>

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

<b>3.1.4 設定堆棧指針 sp</b>

堆棧指針的設定是為了執行 C 語言代碼作好準備。通常我們可以把 sp 的值設定為(stage2_end-4),也即在 3.1.2 節所安排的那個 1MB 的 RAM 空間的最頂端(堆棧向下生長)。

此外,在設定堆棧指針 sp 之前,也可以關閉 led 燈,以提示使用者我們準備跳轉到 stage2。

經過上述這些執行步驟後,系統的實體記憶體布局應該如下圖2所示。

<b>3.1.5 跳轉到 stage2 的 C 入口點</b>

在上述一切都就緒後,就可以跳轉到 Boot Loader 的 stage2 去執行了。比如,在 ARM 系統中,這可以通過修改 PC 寄存器為合适的位址來實作。

<a><b>圖2 bootloader 的 stage2 可執行映象剛被拷貝到 RAM 空間時的系統記憶體布局</b></a>

<a>3.2 Boot Loader 的 stage2</a>

正如前面所說,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):

可以看出,當 main() 函數傳回後,我們又用一條跳轉指令重新執行 trampoline 程式――當然也就重新執行 main() 函數,這也就是 trampoline(彈簧床)一詞的意思所在。

<b>3.2.1初始化本階段要使用到的硬體裝置</b>

這通常包括:(1)初始化至少一個序列槽,以便和終端使用者進行 I/O 輸出資訊;(2)初始化計時器等。

在初始化這些裝置之前,也可以重新把 LED 燈點亮,以表明我們已經進入 main() 函數執行。

裝置初始化完成後,可以輸出一些列印資訊,程式名字字元串、版本号等。

<b>3.2.2 檢測系統的記憶體映射(memory map)</b>

所 謂記憶體映射就是指在整個 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 位址空間處于未使用狀态。 <b>由于上述這個事實,是以 Boot Loader 的 stage2 必須在它想幹點什麼 (比如,将存儲在 flash 上的核心映像讀到 RAM 空間中) 之前檢測整個系統的記憶體映射情況,也即它必須知道 CPU 預留的全部 RAM 位址空間中的哪些被真正映射到 RAM 位址單元,哪些是處于 "unused" 狀态的。</b>

<b>(1) 記憶體映射的描述</b>

可以用如下資料結構來描述 RAM 位址空間中的一段連續(continuous)的位址範圍:

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

基于上述 memory_area_t 資料結構,整個 CPU 預留的 RAM 位址空間可以用一個 memory_area_t 類型的數組來表示,如下所示:

(2) 記憶體映射的檢測

下面我們給出一個可用來檢測整個 RAM 位址空間記憶體映射情況的簡單而有效的算法:

在用上述算法檢測完系統的記憶體映射情況後,Boot Loader 也可以将記憶體映射的詳細資訊列印到序列槽。

<b>3.2.3 加載核心映像和根檔案系統映像</b>

<b>(1) 規劃記憶體占用的布局</b>

這裡包括兩個方面:(1)核心映像所占用的記憶體範圍;(2)根檔案系統所占用的記憶體範圍。在規劃記憶體占用的布局時,主要考慮基位址和映像的大小兩個方面。

對于核心映像,一般将其拷貝到從(MEM_START+0x8000) 這個基位址開始的大約1MB大小的記憶體範圍内(嵌入式 Linux 的核心一般都不操過 1MB)。為什麼要把從 MEM_START 到 MEM_START+0x8000 這段 32KB 大小的記憶體空出來呢?這是因為 Linux 核心要在這段記憶體中放置一些全局資料結構,如:啟動參數和核心頁表等資訊。

而對于根檔案系統映像,則一般将其拷貝到 MEM_START+0x0010,0000 開始的地方。如果用 Ramdisk 作為根檔案系統映像,則其解壓後的大小一般是1MB。

<b>(2)從 Flash 上拷貝</b>

由于像 ARM 這樣的嵌入式 CPU 通常都是在統一的記憶體位址空間中尋址 Flash 等固态儲存設備的,是以從 Flash 上讀取資料與從 RAM 單元中讀取資料并沒有什麼不同。用一個簡單的循環就可以完成從 Flash 裝置上拷貝映像的工作:

<b>3.2.4 設定核心的啟動參數</b>

應該說,在将核心映像和根檔案系統映像拷貝到 RAM 空間中後,就可以準備啟動 Linux 核心了。但是在調用核心之前,應該作一步準備工作,即:設定 Linux 核心的啟動參數。

Linux 2.4.x 以後的核心都期望以标記清單(tagged list)的形式來傳遞啟動參數。啟動參數标記清單以标記 ATAG_CORE 開始,以标記 ATAG_NONE 結束。每個标記由辨別被傳遞參數的 tag_header 結構以及随後的參數值資料結構來組成。資料結構 tag 和 tag_header 定義在 Linux 核心源碼的include/asm/setup.h 頭檔案中:

在嵌入式 Linux 系統中,通常需要由 Boot Loader 設定的常見啟動參數有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。

比如,設定 ATAG_CORE 的代碼如下:

其中,BOOT_PARAMS 表示核心啟動參數在記憶體中的起始基位址,指針 params 是一個 struct tag 類型的指針。宏 tag_next() 将以指向目前标記的指針為參數,計算緊臨目前标記的下一個标記的起始位址。注意,核心的根檔案系統所在的裝置ID就是在這裡設定的。

下面是設定記憶體映射情況的示例代碼:

可以看出,在 memory_map[]數組中,每一個有效的記憶體段都對應一 ...

本文轉自feisky部落格園部落格,原文連結:http://www.cnblogs.com/feisky/archive/2009/03/18/1586305.html,如需轉載請自行聯系原作者

繼續閱讀