1. 程式人生 > >U-Boot完美解讀(前篇)——說說bootloader那些事兒

U-Boot完美解讀(前篇)——說說bootloader那些事兒

1、官方說bootloader

BootLoader是在作業系統核心執行之前執行。可以初始化硬體裝置、建立記憶體空間對映圖,從而將系統的軟硬體環境帶到一個合適狀態,以便為最終呼叫作業系統核心準備好正確的環境。在嵌入式系統中,通常並沒有像BIOS那樣的韌體程式(注,有的嵌入式CPU也會內嵌一段短小的啟動程式),因此整個系統的載入啟動任務就完全由BootLoader來完成。在一個基於ARM7TDMI core的嵌入式系統中,系統在上電或復位時通常都從地址0x00000000處開始執行,而在這個地址處安排的通常就是系統的BootLoader程式。

1.1、bootloader那些哥們

雖然現在晶片是越來越豐富了,想選個開發板都得糾結半個月,其實都是“挨踢苦逼”手上的米有限呀,不過常見bootloader可就沒那麼容易糾結了,常用的也就那麼幾個,下面就簡單的介紹幾兄弟:

1.1.1、Redboot

Redboot是Redhat公司隨eCos釋出的一個BOOT方案,是一個開源專案。Redboot支援的處理器構架有ARM,MIPS,MN10300,PowerPC, Renesas SHx,v850,x86等,是一個完善的嵌入式系統Boot Loader。   Redboot是在ECOS的基礎上剝離出來的,繼承了ECOS的簡潔、輕巧、可靈活配置、穩定可靠等品質優點。當前支援單板機的移植版特性有:
- 支援ECOS,Linux作業系統引導
- 線上讀寫Flash
- 支援序列口kermit,S-record下載程式碼
- 監控(minitor)命令集:讀寫I/O,記憶體,暫存器、 記憶體、外設測試功能等
Redboot是標準的嵌入式除錯和引導解決方案,支援幾乎所有的處理器構架以及大量的外圍硬體介面,並且還在不斷地完善過程中。

1.1.2、ARMboot

ARMboot是一個ARM平臺的開源韌體專案,它特別基於PPCBoot,一個為PowerPC平臺上的系統提供類似功能的姊妹專案。鑑於對PPCBoot的嚴重依賴性,已經與PPCBoot專案合併,新的專案為U-Boot(這傢伙的前身,嘿嘿,用的時候不要忘了本喲)。ARMboot釋出的最後版本為ARMboot-1.1.0,2002年ARMboot終止了維護。ARMboot的目標是成為通用的、容易使用和移植的載入程式,非常輕便地運用於新的平臺上。ARMboot是GPL下的ARM韌體專案中唯一支援Flash快閃記憶體,BOOTP、DHCP、TFTP網路下載,PCMCLA尋線機等多種型別來引導系統的。特性為:
-支援多種型別的FLASH   
-允許映像檔案經由BOOTP、DHCP、TFTP從網路傳輸;   
-支援序列口下載S-record或者binary檔案   
-允許記憶體的顯示及修改   
-支援jffs2檔案系統等


1.1.3、U-Boot

此處略去三千字,不然後面的章節就沒寫的了......

1.1.4、Blob
  Blob(Boot Loader Object)是由Jan-Derk Bakker and Erik Mouw釋出的,是專門為StrongARM 構架下的LART設計的Boot Loader。Blob的最後版本是blob-2.0.5。Blob支援SA1100的LART主機板,但使用者也可以自行修改移植。Blob也提供兩種工作模式,在啟動時處於正常的啟動載入模式,但是它會延時 10 秒等待終端使用者按下任意鍵而將 Blob 切換到下載模式。如果在 10 秒內沒有使用者按鍵,則 Blob 繼續啟動 Linux 核心。其基本功能為:   
初始化硬體(CPU速度,儲存器,中斷,RS232串列埠)   
-引導Linux核心並提供ramdisk   
- 給LART下載一個核心或者ramdisk   
-給FLASH片更新核心或者ramdisk   
-測定儲存配置並通知核心   
-給核心提供一個命令列

1.1.5、Bios-lt
  Bios-lt是專門支援三星(Samsung)公司ARM構架處理器S3C4510B的Loader,可以設定CPU/ROM/SDRAM/EXTIO,管理並燒寫FLASH,裝載引導uClinux核心。這是國內工程師申請GNU通用公共許可釋出的。Bios-lt的最新版本是Bios-lt-0.74,另外還提供了S3C4510B的一些外圍驅動。

1.1.6、Bootldr
  Bootldr是康柏(Compaq)公司釋出的,類似於compaq iPAQ Pocket PC,支援SA1100晶片。它被推薦用來引導Llinux,支援串列埠Y-modem協議以及jffs檔案系統。Bootldr的最後版本為Bootldr-2.19。

1.1.7、vivi
  vivi是韓國mizi 公司開發的bootloader, 適用於ARM9處理器。Vivi有兩種工作模式:啟動載入模式和下載模式。啟動載入模式可以在一段時間後(這個時間可更改)自行啟動linux核心,這是vivi的預設模式。在下載模式下,vivi為使用者提供一個命令列介面,通過介面可以使用vivi提供的一些命令。不過現在這支軍團也慚慚消失了,samsung官方也提供u-boot的開發包供使用者使用。

1.2、主角登場u-boot

U-Boot,全稱 Universal Boot Loader,是遵循GPL條款的開放原始碼專案。從FADSROM、8xxROM、PPCBOOT逐步發展演化而來。其原始碼目錄、編譯形式與Linux核心很相似,事實上,不少U-Boot原始碼就是相應的Linux核心源程式的簡化,尤其是一些裝置的驅動程式,這從U-Boot原始碼的註釋中能體現這一點。

U-Boot是由開源專案PPCBoot發展起來的,ARMboot併入了PPCBoot,和其他一些arch的Loader合稱U-Boot。2002年12月17日第一個版本U-Boot-0.2.0釋出,同時PPCBoot和ARMboot停止維護。U-Boot支援的處理器構架包括PowerPC (MPC5xx,MPC8xx,MPC82xx,MPC7xx,MPC74xx,4xx), ARM (ARM7,ARM9,StrongARM,Xscale),MIPS (4Kc,5Kc),x86等等,U-Boot提供兩種操作模式:啟動載入(Boot loading)模式和下載(Downloading)模式,並具有大型Boot Loader的全部功能。主要特性為:   
-SCC/FEC乙太網支援   
-BOOTP/TFTP引導   
-IP,MAC預置功能   
-線上讀寫FLASH,DOC, IDE,IIC,EEROM,RTC  
-支援序列口kermit,S-record下載程式碼   
-識別二進位制、ELF32、pImage格式的Image,對Linux引導有特別的支援   
-監控(minitor)命令集:讀寫I/O,記憶體,暫存器、記憶體、外設測試功能等   
-指令碼語言支援(類似BASH指令碼)   
-支援WatchDog,LCD logo,狀態指示功能等

2、系統軟體結構

在一般情況下嵌入式Linux系統中(WinCE系統結構上略有差異,而且老比爾童鞋不開源,咱也不跟他計較了)的軟體主要分為以下及部分:
(1)引導載入程式:包括固化在韌體(firmware)中的 boot 程式碼(可選),和 Boot Loader 兩大部分。
而這個內部固化ROM是廠家在晶片生產時候固化的,作用基本上是引導Boot Loader。有的晶片比較複雜,比如Omap3,他在flash中沒有程式碼的時候有許多啟動方式:USB、UART或乙太網等等。而S3C24x0則很簡單,只有Norboot和Nandboot。
(2)Linux kernel 和drivers,特定於嵌入式板子的定製核心、核心的啟動引數以及外圍硬體裝置驅動程式。
(3)檔案系統。包括根檔案系統和建立於Flash記憶體裝置之上的檔案系統(EXT4、UBI、CRAMFS等等)。它是提供管理系統的各種配置檔案以及系統執行使用者應用程式的良好執行環境的載體。
(4)應用程式。特定於使用者的應用程式。有時在使用者應用程式和核心層之間可能還會包括一個嵌入式圖形使用者介面。常用的嵌入式 GUI如:MicroWindows、MiniGUI等,當然現在時下很流行的Android也有涉及這一層。

在Flash 儲存器中,他們的 一般分佈如下:

但是以上只是大部分情況下的分佈,也有一些可能根檔案系統是initramfs,被一起壓縮到了核心映像裡,或者沒有Bootloader引數區,等等。

3、該做點貢獻了

在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。 

3.1、說說你的能耐

從作業系統的角度看,Boot Loader 的總目標就是正確地呼叫核心來執行。 由於 Boot Loader 的實現依賴於 CPU 的體系結構,因此大多數 Boot Loader 都分為 stage1 和 stage2 兩大部分。依賴於 CPU 體系結構的程式碼,比如裝置初始化程式碼等,通常都放在 stage1 中,而且通常都用匯編語言來實現,以達到短小精悍的目的。而 stage2 則通常用C語言來實現,這樣可以實現給複雜的功能,而且程式碼會具有更好的可讀性和可移植性。


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 位元組邊界對齊)。因此:
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 暫存器為合適的地址來實現。

3.1.2、Boot Loader 的 stage2 通常包括以下步驟(以執行的先後順序):
初始化本階段要使用到的硬體裝置。
檢測系統記憶體對映(memory map)。
將 kernel 映像和根檔案系統映像從 flash 上讀到 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):
.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)的地址範圍:
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 型別的陣列來表示,如下所示:
memory_area_t memory_map[NUM_MEM_AREAS] = {
 [0 ... (NUM_MEM_AREAS - 1)] = {
  .start = 0,
  .size = 0,
  .used = 0
 },
};
(2) 記憶體對映的檢測
下面我們給出一個可用來檢測整個 RAM 地址空間記憶體對映情況的簡單而有效的演算法:
/* 陣列初始化 */
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 裝置上拷貝映像的工作:
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 標頭檔案中:
/* 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 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 映象(壓縮格式)以及它的大小:
 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 load ramdisk */
 
params = tag_next(params);

最後,設定 ATAG_NONE 標記,結束整個啟動引數列表:
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 語言,可以像下列示例程式碼這樣來呼叫核心:

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()函式呼叫應該永遠不返回的。如果這個呼叫返回,則說明出錯。  

【參考文獻】

雖然國內時下很流行山寨,但尊重原創是中華民族的優秀品德,下面列出本文涉及內容的來源: