嵌入式BootLoader技術(shù)內幕
本文詳細地介紹了基于嵌入式系統中的 OS 啟動(dòng)加載程序 ―― Boot Loader 的概念、軟件設計的主要任務(wù)以及結構框架等內容。
1. 引言
在專(zhuān)用的嵌入式板子運行 GNU/Linux 系統已經(jīng)變得越來(lái)越流行。一個(gè)嵌入式 Linux 系統從軟件的角度看通??梢苑譃樗膫€(gè)層次:
1. 引導加載程序。包括固化在固件(firmware)中的 boot 代碼(可選),和 Boot Loader 兩大部分。
2. Linux 內核。特定于嵌入式板子的定制內核以及內核的啟動(dòng)參數。
3. 文件系統。包括根文件系統和建立于 Flash 內存設備之上文件系統。通常用 ram disk 來(lái)作為 root fs。
4. 用戶(hù)應用程序。特定于用戶(hù)的應用程序。有時(shí)在用戶(hù)應用程序和內核層之間可能還會(huì )包括一個(gè)嵌入式圖形用戶(hù)界面。常用的嵌入式 GUI 有:MicroWindows 和 MiniGUI 懂。
引導加載程序是系統加電后運行的第一段軟件代碼?;貞浺幌?PC 的體系結構我們可以知道,PC 機中的引導加載程序由 BIOS(其本質(zhì)就是一段固件程序)和位于硬盤(pán) MBR 中的 OS Boot Loader(比如,LILO 和 GRUB 等)一起組成。BIOS 在完成硬件檢測和資源分配后,將硬盤(pán) MBR 中的 Boot Loader 讀到系統的 RAM 中,然后將控制權交給 OS Boot Loader。Boot Loader 的主要運行任務(wù)就是將內核映象從硬盤(pán)上讀到 RAM 中,然后跳轉到內核的入口點(diǎn)去運行,也即開(kāi)始啟動(dòng)操作系統。
而在嵌入式系統中,通常并沒(méi)有像 BIOS 那樣的固件程序(注,有的嵌入式 CPU 也會(huì )內嵌一段短小的啟動(dòng)程序),因此整個(gè)系統的加載啟動(dòng)任務(wù)就完全由 Boot Loader 來(lái)完成。比如在一個(gè)基于 ARM7TDMI core 的嵌入式系統中,系統在上電或復位時(shí)通常都從地址 0x00000000 處開(kāi)始執行,而在這個(gè)地址處安排的通常就是系統的 Boot Loader 程序。
本文將從 Boot Loader 的概念、Boot Loader 的主要任務(wù)、Boot Loader 的框架結構以及 Boot Loader 的安裝等四個(gè)方面來(lái)討論嵌入式系統的 Boot Loader。
![]() ![]() |
![]() |
2. Boot Loader 的概念
簡(jiǎn)單地說(shuō),Boot Loader 就是在操作系統內核運行之前運行的一段小程序。通過(guò)這段小程序,我們可以初始化硬件設備、建立內存空間的映射圖,從而將系統的軟硬件環(huán)境帶到一個(gè)合適的狀態(tài),以便為最終調用操作系統內核準備好正確的環(huán)境。
通常,Boot Loader 是嚴重地依賴(lài)于硬件而實(shí)現的,特別是在嵌入式世界。因此,在嵌入式世界里建立一個(gè)通用的 Boot Loader 幾乎是不可能的。盡管如此,我們仍然可以對 Boot Loader 歸納出一些通用的概念來(lái),以指導用戶(hù)特定的 Boot Loader 設計與實(shí)現。
1. Boot Loader 所支持的 CPU 和嵌入式板
每種不同的 CPU 體系結構都有不同的 Boot Loader。有些 Boot Loader 也支持多種體系結構的 CPU,比如 U-Boot 就同時(shí)支持 ARM 體系結構和MIPS 體系結構。除了依賴(lài)于 CPU 的體系結構外,Boot Loader 實(shí)際上也依賴(lài)于具體的嵌入式板級設備的配置。這也就是說(shuō),對于兩塊不同的嵌入式板而言,即使它們是基于同一種 CPU 而構建的,要想讓運行在一塊板子上的 Boot Loader 程序也能運行在另一塊板子上,通常也都需要修改 Boot Loader 的源程序。
2. Boot Loader 的安裝媒介(Installation Medium)
系統加電或復位后,所有的 CPU 通常都從某個(gè)由 CPU 制造商預先安排的地址上取指令。比如,基于 ARM7TDMI core 的 CPU 在復位時(shí)通常都從地址 0x00000000 取它的第一條指令。而基于 CPU 構建的嵌入式系統通常都有某種類(lèi)型的固態(tài)存儲設備(比如:ROM、EEPROM 或 FLASH 等)被映射到這個(gè)預先安排的地址上。因此在系統加電后,CPU 將首先執行 Boot Loader 程序。
下圖1就是一個(gè)同時(shí)裝有 Boot Loader、內核的啟動(dòng)參數、內核映像和根文件系統映像的固態(tài)存儲設備的典型空間分配結構圖。
圖1 固態(tài)存儲設備的典型空間分配結構
3. 用來(lái)控制 Boot Loader 的設備或機制
主機和目標機之間一般通過(guò)串口建立連接,Boot Loader 軟件在執行時(shí)通常會(huì )通過(guò)串口來(lái)進(jìn)行 I/O,比如:輸出打印信息到串口,從串口讀取用戶(hù)控制字符等。
4. Boot Loader 的啟動(dòng)過(guò)程是單階段(Single Stage)還是多階段(Multi-Stage)
通常多階段的 Boot Loader 能提供更為復雜的功能,以及更好的可移植性。從固態(tài)存儲設備上啟動(dòng)的 Boot Loader 大多都是 2 階段的啟動(dòng)過(guò)程,也即啟動(dòng)過(guò)程可以分為 stage 1 和 stage 2 兩部分。而至于在 stage 1 和 stage 2 具體完成哪些任務(wù)將在下面討論。
5. Boot Loader 的操作模式 (Operation Mode)
大多數 Boot Loader 都包含兩種不同的操作模式:"啟動(dòng)加載"模式和"下載"模式,這種區別僅對于開(kāi)發(fā)人員才有意義。但從最終用戶(hù)的角度看,Boot Loader 的作用就是用來(lái)加載操作系統,而并不存在所謂的啟動(dòng)加載模式與下載工作模式的區別。
啟動(dòng)加載(Boot loading)模式:這種模式也稱(chēng)為"自主"(Autonomous)模式。也即 Boot Loader 從目標機上的某個(gè)固態(tài)存儲設備上將操作系統加載到 RAM 中運行,整個(gè)過(guò)程并沒(méi)有用戶(hù)的介入。這種模式是 Boot Loader 的正常工作模式,因此在嵌入式產(chǎn)品發(fā)布的時(shí)侯,Boot Loader 顯然必須工作在這種模式下。
下載(Downloading)模式:在這種模式下,目標機上的 Boot Loader 將通過(guò)串口連接或網(wǎng)絡(luò )連接等通信手段從主機(Host)下載文件,比如:下載內核映像和根文件系統映像等。從主機下載的文件通常首先被 Boot Loader 保存到目標機的 RAM 中,然后再被 Boot Loader 寫(xiě)到目標機上的FLASH 類(lèi)固態(tài)存儲設備中。Boot Loader 的這種模式通常在第一次安裝內核與根文件系統時(shí)被使用;此外,以后的系統更新也會(huì )使用 Boot Loader 的這種工作模式。工作于這種模式下的 Boot Loader 通常都會(huì )向它的終端用戶(hù)提供一個(gè)簡(jiǎn)單的命令行接口。
像 Blob 或 U-Boot 等這樣功能強大的 Boot Loader 通常同時(shí)支持這兩種工作模式,而且允許用戶(hù)在這兩種工作模式之間進(jìn)行切換。比如,Blob 在啟動(dòng)時(shí)處于正常的啟動(dòng)加載模式,但是它會(huì )延時(shí) 10 秒等待終端用戶(hù)按下任意鍵而將 blob 切換到下載模式。如果在 10 秒內沒(méi)有用戶(hù)按鍵,則 blob 繼續啟動(dòng) Linux 內核。
6. BootLoader 與主機之間進(jìn)行文件傳輸所用的通信設備及協(xié)議
最常見(jiàn)的情況就是,目標機上的 Boot Loader 通過(guò)串口與主機之間進(jìn)行文件傳輸,傳輸協(xié)議通常是 xmodem/ymodem/zmodem 協(xié)議中的一種。但是,串口傳輸的速度是有限的,因此通過(guò)以太網(wǎng)連接并借助 TFTP 協(xié)議來(lái)下載文件是個(gè)更好的選擇。
此外,在論及這個(gè)話(huà)題時(shí),主機方所用的軟件也要考慮。比如,在通過(guò)以太網(wǎng)連接和 TFTP 協(xié)議來(lái)下載文件時(shí),主機方必須有一個(gè)軟件用來(lái)的提供 TFTP 服務(wù)。
在討論了 BootLoader 的上述概念后,下面我們來(lái)具體看看 BootLoader 的應該完成哪些任務(wù)。
![]() ![]() |
![]() |
3. Boot Loader 的主要任務(wù)與典型結構框架
在繼續本節的討論之前,首先我們做一個(gè)假定,那就是:假定內核映像與根文件系統映像都被加載到 RAM 中運行。之所以提出這樣一個(gè)假設前提是因為,在嵌入式系統中內核映像與根文件系統映像也可以直接在 ROM 或 Flash 這樣的固態(tài)存儲設備中直接運行。但這種做法無(wú)疑是以運行速度的犧牲為代價(jià)的。
從操作系統的角度看,Boot Loader 的總目標就是正確地調用內核來(lái)執行。
另外,由于 Boot Loader 的實(shí)現依賴(lài)于 CPU 的體系結構,因此大多數 Boot Loader 都分為 stage1 和 stage2 兩大部分。依賴(lài)于 CPU 體系結構的代碼,比如設備初始化代碼等,通常都放在 stage1 中,而且通常都用匯編語(yǔ)言來(lái)實(shí)現,以達到短小精悍的目的。而 stage2 則通常用C語(yǔ)言來(lái)實(shí)現,這樣可以實(shí)現給復雜的功能,而且代碼會(huì )具有更好的可讀性和可移植性。
Boot Loader 的 stage1 通常包括以下步驟(以執行的先后順序):
- 硬件設備初始化。
- 為加載 Boot Loader 的 stage2 準備 RAM 空間。
- 拷貝 Boot Loader 的 stage2 到 RAM 空間中。
- 設置好堆棧。
- 跳轉到 stage2 的 C 入口點(diǎn)。
Boot Loader 的 stage2 通常包括以下步驟(以執行的先后順序):
- 初始化本階段要使用到的硬件設備。
- 檢測系統內存映射(memory map)。
- 將 kernel 映像和根文件系統映像從 flash 上讀到 RAM 空間中。
- 為內核設置啟動(dòng)參數。
- 調用內核。
3.1 Boot Loader 的 stage1
3.1.1 基本的硬件初始化
這是 Boot Loader 一開(kāi)始就執行的操作,其目的是為 stage2 的執行以及隨后的 kernel 的執行準備好一些基本的硬件環(huán)境。它通常包括以下步驟(以執行的先后順序):
1. 屏蔽所有的中斷。為中斷提供服務(wù)通常是 OS 設備驅動(dòng)程序的責任,因此在 Boot Loader 的執行全過(guò)程中可以不必響應任何中斷。中斷屏蔽可以通過(guò)寫(xiě) CPU 的中斷屏蔽寄存器或狀態(tài)寄存器(比如 ARM 的 CPSR 寄存器)來(lái)完成。
2. 設置 CPU 的速度和時(shí)鐘頻率。
3. RAM 初始化。包括正確地設置系統的內存控制器的功能寄存器以及各內存庫控制寄存器等。
4. 初始化 LED。典型地,通過(guò) GPIO 來(lái)驅動(dòng) LED,其目的是表明系統的狀態(tài)是 OK 還是 Error。如果板子上沒(méi)有 LED,那么也可以通過(guò)初始化 UART 向串口打印 Boot Loader 的 Logo 字符信息來(lái)完成這一點(diǎn)。
5. 關(guān)閉 CPU 內部指令/數據 cache。
3.1.2 為加載 stage2 準備 RAM 空間
為了獲得更快的執行速度,通常把 stage2 加載到 RAM 空間中來(lái)執行,因此必須為加載 Boot Loader 的 stage2 準備好一段可用的 RAM 空間范圍。
由于 stage2 通常是 C 語(yǔ)言執行代碼,因此在考慮空間大小時(shí),除了 stage2 可執行映象的大小外,還必須把堆??臻g也考慮進(jìn)來(lái)。此外,空間大小最好是 memory page 大小(通常是 4KB)的倍數。一般而言,1M 的 RAM 空間已經(jīng)足夠了。具體的地址范圍可以任意安排,比如 blob 就將它的 stage2 可執行映像安排到從系統 RAM 起始地址 0xc0200000 開(kāi)始的 1M 空間內執行。但是,將 stage2 安排到整個(gè) RAM 空間的最頂 1MB(也即(RamEnd-1MB) - RamEnd)是一種值得推薦的方法。
為了后面的敘述方便,這里把所安排的 RAM 空間范圍的大小記為:stage2_size(字節),把起始地址和終止地址分別記為:stage2_start 和 stage2_end(這兩個(gè)地址均以 4 字節邊界對齊)。因此:
|
另外,還必須確保所安排的地址范圍的的確確是可讀寫(xiě)的 RAM 空間,因此,必須對你所安排的地址范圍進(jìn)行測試。具體的測試方法可以采用類(lèi)似于 blob 的方法,也即:以 memory page 為被測試單位,測試每個(gè) memory page 開(kāi)始的兩個(gè)字是否是可讀寫(xiě)的。為了后面敘述的方便,我們記這個(gè)檢測算法為:test_mempage,其具體步驟如下:
1. 先保存 memory page 一開(kāi)始兩個(gè)字的內容。
2. 向這兩個(gè)字中寫(xiě)入任意的數字。比如:向第一個(gè)字寫(xiě)入 0x55,第 2 個(gè)字寫(xiě)入 0xaa。
3. 然后,立即將這兩個(gè)字的內容讀回。顯然,我們讀到的內容應該分別是 0x55 和 0xaa。如果不是,則說(shuō)明這個(gè) memory page 所占據的地址范圍不是一段有效的 RAM 空間。
4. 再向這兩個(gè)字中寫(xiě)入任意的數字。比如:向第一個(gè)字寫(xiě)入 0xaa,第 2 個(gè)字中寫(xiě)入 0x55。
5. 然后,立即將這兩個(gè)字的內容立即讀回。顯然,我們讀到的內容應該分別是 0xaa 和 0x55。如果不是,則說(shuō)明這個(gè) memory page 所占據的地址范圍不是一段有效的 RAM 空間。
6. 恢復這兩個(gè)字的原始內容。測試完畢。
為了得到一段干凈的 RAM 空間范圍,我們也可以將所安排的 RAM 空間范圍進(jìn)行清零操作。
3.1.3 拷貝 stage2 到 RAM 中
拷貝時(shí)要確定兩點(diǎn):(1) stage2 的可執行映象在固態(tài)存儲設備的存放起始地址和終止地址;(2) RAM 空間的起始地址。
3.1.4 設置堆棧指針 sp
堆棧指針的設置是為了執行 C 語(yǔ)言代碼作好準備。通常我們可以把 sp 的值設置為(stage2_end-4),也即在 3.1.2 節所安排的那個(gè) 1MB 的 RAM 空間的最頂端(堆棧向下生長(cháng))。
此外,在設置堆棧指針 sp 之前,也可以關(guān)閉 led 燈,以提示用戶(hù)我們準備跳轉到 stage2。
經(jīng)過(guò)上述這些執行步驟后,系統的物理內存布局應該如下圖2所示。
3.1.5 跳轉到 stage2 的 C 入口點(diǎn)
在上述一切都就緒后,就可以跳轉到 Boot Loader 的 stage2 去執行了。比如,在 ARM 系統中,這可以通過(guò)修改 PC 寄存器為合適的地址來(lái)實(shí)現。
圖2 bootloader 的 stage2 可執行映象剛被拷貝到 RAM 空間時(shí)的系統內存布局
3.2 Boot Loader 的 stage2
正如前面所說(shuō),stage2 的代碼通常用 C 語(yǔ)言來(lái)實(shí)現,以便于實(shí)現更復雜的功能和取得更好的代碼可讀性和可移植性。但是與普通 C 語(yǔ)言應用程序不同的是,在編譯和鏈接 boot loader 這樣的程序時(shí),我們不能使用 glibc 庫中的任何支持函數。其原因是顯而易見(jiàn)的。這就給我們帶來(lái)一個(gè)問(wèn)題,那就是從那里跳轉進(jìn) main() 函數呢?直接把 main() 函數的起始地址作為整個(gè) stage2 執行映像的入口點(diǎn)或許是最直接的想法。但是這樣做有兩個(gè)缺點(diǎn):1)無(wú)法通過(guò)main() 函數傳遞函數參數;2)無(wú)法處理 main() 函數返回的情況。一種更為巧妙的方法是利用 trampoline(彈簧床)的概念。也即,用匯編語(yǔ)言寫(xiě)一段trampoline 小程序,并將這段 trampoline 小程序來(lái)作為 stage2 可執行映象的執行入口點(diǎn)。然后我們可以在 trampoline 匯編小程序中用 CPU 跳轉指令跳入 main() 函數中去執行;而當 main() 函數返回時(shí),CPU 執行路徑顯然再次回到我們的 trampoline 程序。簡(jiǎn)而言之,這種方法的思想就是:用這段 trampoline 小程序來(lái)作為 main() 函數的外部包裹(external wrapper)。
下面給出一個(gè)簡(jiǎn)單的 trampoline 程序示例(來(lái)自blob):
|
可以看出,當 main() 函數返回后,我們又用一條跳轉指令重新執行 trampoline 程序――當然也就重新執行 main() 函數,這也就是 trampoline(彈簧床)一詞的意思所在。
3.2.1初始化本階段要使用到的硬件設備
這通常包括:(1)初始化至少一個(gè)串口,以便和終端用戶(hù)進(jìn)行 I/O 輸出信息;(2)初始化計時(shí)器等。
在初始化這些設備之前,也可以重新把 LED 燈點(diǎn)亮,以表明我們已經(jīng)進(jìn)入 main() 函數執行。
設備初始化完成后,可以輸出一些打印信息,程序名字字符串、版本號等。
3.2.2 檢測系統的內存映射(memory map)
所謂內存映射就是指在整個(gè) 4GB 物理地址空間中有哪些地址范圍被分配用來(lái)尋址系統的 RAM 單元。比如,在 SA-1100 CPU 中,從 0xC000,0000 開(kāi)始的 512M 地址空間被用作系統的 RAM 地址空間,而在 Samsung S3C44B0X CPU 中,從 0x0c00,0000 到 0x1000,0000 之間的 64M 地址空間被用作系統的 RAM 地址空間。雖然 CPU 通常預留出一大段足夠的地址空間給系統 RAM,但是在搭建具體的嵌入式系統時(shí)卻不一定會(huì )實(shí)現 CPU 預留的全部 RAM 地址空間。也就是說(shuō),具體的嵌入式系統往往只把 CPU 預留的全部 RAM 地址空間中的一部分映射到 RAM 單元上,而讓剩下的那部分預留 RAM 地址空間處于未使用狀態(tài)。 由于上述這個(gè)事實(shí),因此 Boot Loader 的 stage2 必須在它想干點(diǎn)什么 (比如,將存儲在 flash 上的內核映像讀到 RAM 空間中) 之前檢測整個(gè)系統的內存映射情況,也即它必須知道 CPU 預留的全部 RAM 地址空間中的哪些被真正映射到 RAM 地址單元,哪些是處于 "unused" 狀態(tài)的。
(1) 內存映射的描述
可以用如下數據結構來(lái)描述 RAM 地址空間中的一段連續(continuous)的地址范圍:
|
這段 RAM 地址空間中的連續地址范圍可以處于兩種狀態(tài)之一:(1)used=1,則說(shuō)明這段連續的地址范圍已被實(shí)現,也即真正地被映射到 RAM 單元上。(2)used=0,則說(shuō)明這段連續的地址范圍并未被系統所實(shí)現,而是處于未使用狀態(tài)。
基于上述 memory_area_t 數據結構,整個(gè) CPU 預留的 RAM 地址空間可以用一個(gè) memory_area_t 類(lèi)型的數組來(lái)表示,如下所示:
|
(2) 內存映射的檢測
下面我們給出一個(gè)可用來(lái)檢測整個(gè) RAM 地址空間內存映射情況的簡(jiǎn)單而有效的算法:
|
在用上述算法檢測完系統的內存映射情況后,Boot Loader 也可以將內存映射的詳細信息打印到串口。
3.2.3 加載內核映像和根文件系統映像
(1) 規劃內存占用的布局
這里包括兩個(gè)方面:(1)內核映像所占用的內存范圍;(2)根文件系統所占用的內存范圍。在規劃內存占用的布局時(shí),主要考慮基地址和映像的大小兩個(gè)方面。
對于內核映像,一般將其拷貝到從(MEM_START+0x8000) 這個(gè)基地址開(kāi)始的大約1MB大小的內存范圍內(嵌入式 Linux 的內核一般都不操過(guò) 1MB)。為什么要把從 MEM_START 到 MEM_START+0x8000 這段 32KB 大小的內存空出來(lái)呢?這是因為 Linux 內核要在這段內存中放置一些全局數據結構,如:?jiǎn)?dòng)參數和內核頁(yè)表等信息。
而對于根文件系統映像,則一般將其拷貝到 MEM_START 0x0010,0000 開(kāi)始的地方。如果用 Ramdisk 作為根文件系統映像,則其解壓后的大小一般是1MB。
(2)從 Flash 上拷貝
由于像 ARM 這樣的嵌入式 CPU 通常都是在統一的內存地址空間中尋址 Flash 等固態(tài)存儲設備的,因此從 Flash 上讀取數據與從 RAM 單元中讀取數據并沒(méi)有什么不同。用一個(gè)簡(jiǎn)單的循環(huán)就可以完成從 Flash 設備上拷貝映像的工作:
|
3.2.4 設置內核的啟動(dòng)參數
應該說(shuō),在將內核映像和根文件系統映像拷貝到 RAM 空間中后,就可以準備啟動(dòng) Linux 內核了。但是在調用內核之前,應該作一步準備工作,即:設置 Linux 內核的啟動(dòng)參數。
Linux 2.4.x 以后的內核都期望以標記列表(tagged list)的形式來(lái)傳遞啟動(dòng)參數。啟動(dòng)參數標記列表以標記 ATAG_CORE 開(kāi)始,以標記 ATAG_NONE 結束。每個(gè)標記由標識被傳遞參數的 tag_header 結構以及隨后的參數值數據結構來(lái)組成。數據結構 tag 和 tag_header 定義在 Linux 內核源碼的include/asm/setup.h 頭文件中:
|
在嵌入式 Linux 系統中,通常需要由 Boot Loader 設置的常見(jiàn)啟動(dòng)參數有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。
比如,設置 ATAG_CORE 的代碼如下:
|
其中,BOOT_PARAMS 表示內核啟動(dòng)參數在內存中的起始基地址,指針 params 是一個(gè) struct tag 類(lèi)型的指針。宏 tag_next() 將以指向當前標記的指針為參數,計算緊臨當前標記的下一個(gè)標記的起始地址。注意,內核的根文件系統所在的設備ID就是在這里設置的。
下面是設置內存映射情況的示例代碼:
|
可以看出,在 memory_map[]數組中,每一個(gè)有效的內存段都對應一個(gè) ATAG_MEM 參數標記。
Linux 內核在啟動(dòng)時(shí)可以以命令行參數的形式來(lái)接收信息,利用這一點(diǎn)我們可以向內核提供那些內核不能自己檢測的硬件參數信息,或者重載(override)內核自己檢測到的信息。比如,我們用這樣一個(gè)命令行參數字符串"console=ttyS0,115200n8"來(lái)通知內核以 ttyS0 作為控制臺,且串口采用 "115200bps、無(wú)奇偶校驗、8位數據位"這樣的設置。下面是一段設置調用內核命令行參數字符串的示例代碼:
|