1. 程式人生 > >嵌入式系統 Boot Loader 技術內幕

嵌入式系統 Boot Loader 技術內幕

1. 引言

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

1. 引導載入程式。包括固化在韌體(firmware)中的 boot 程式碼(可選),和 Boot Loader 兩大部分。

2. Linux 核心。特定於嵌入式板子的定製核心以及核心的啟動引數。

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

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 設計與實現。

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 位元組邊界對齊)。因此:

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):

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

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 已經成功地轉起來了!"。 

致謝:感覺很棒給個好評呀!

相關推薦

嵌入式系統 Boot Loader 技術內幕

1. 引言 在專用的嵌入式板子執行 GNU/Linux 系統已經變得越來越流行。一個嵌入式 Linux 系統從軟體的角度看通常可以分為四個層次: 1. 引導載入程式。包括固化在韌體(firmware)中的 boot 程式碼(可選),和 Boot Loader 兩大部分。

嵌入式系統可靠性設計技術及案例解析》讀書筆記(七)

lcd 適合 實現 電源線 寬度 減少 狀態 面膜 平面 電磁兼容(Electro Magnetic Compatibility,EMC)是指設備或系統在電磁環境中運行時,不會因為其他設備的合理電磁幹擾而影響本機的功能和安全性,也不會對其環境中的任何設備產生不合理的電磁幹擾

嵌入式系統的BootLoader技術淺析_啟動過程

論文導讀::嵌入式系統是以應用為中心、以計算機為基礎、軟硬體可裁剪。即啟動過程可分為階段1和階段2兩個部分。技術概述。論文關鍵詞:嵌入式系統,啟動過程,BootLoader技術  一 BootLoader技術概述  嵌入式系統是以應用為中心、以計算機為基礎、軟硬體可裁剪,適

中國(北京)國際智能工業與信息安全技術發展論壇暨2014年中國嵌入式系統年會邀請函

row 中國 主持人 管理 工控機 views 協作 可信計算 ati ? 中國軟件行業協會嵌入式系統分會中國(北京)國際智能工業與信息安全技術發展論壇暨2014年

嵌入式系統中的目標識別技術

corn 基本 ast 數據 vpd 機器人 同時 質量 關鍵點 在人們生活中大部分信息都是以眼睛來觀察到的。因此對視覺的研究,人們一直沒有停止前進的步伐。就像很多年前人類根據鳥的飛行特征而發明了飛機。實現了曾經遨遊天空的夢想。如今對視覺的研究,也希望有朝一日由機器人來代替

《深入分析JavaWeb技術內幕》之 11-Tomcat系統架構與設計模式

1、 分發請求 2 、同時請求 3、 多級容器 4、 設計模式 Tomcat的組織結構 https://www.cnblogs.com/zhouyuqin/p/5143121.html   Tomcat Server處理一個HTTP請求的

《深入分析JavaWeb技術內幕》之 15-iBatis系統架構與對映原理

關鍵詞: 對映、 反射                              &

如何學習嵌入式系統?嵌入式技術學習路線大綱分享

嵌入式系統是當前熱門、具發展前景的IT應用領域之一,很多數字包括手機、電子字典、可視電話、數字相機、數字攝像機、機頂盒、智慧玩具醫療儀器和航空航天裝置等都是典型的嵌入式系統。嵌入式發展前景可觀,越來越多的年輕人選擇進入嵌入式行列,那麼今天就給大家分享嵌入式學習路線大綱。希望對大家的學習有所幫助。

嵌入式系統技術——實現對GPIO晶片的相關控制

嵌入式系統技術 實現對GPIO晶片的相關控制 一、實驗目的 1、LED燈的輪詢除錯 2、模擬串列埠收發資料的過程 3、模擬生活中的按鍵控制操作 二、實驗環境 1、硬體:通用節點或任意感測器節點一個、USB模擬器、USB電纜、PC機; 2、軟體:IAR Embedded Workbench f

ZooKeeper技術內幕-系統模型

系統模型 資料模型 ZooKeeper檢視機構和標準Unix 檔案系統非常相似,使用了資料節點(ZNode) ZNode是ZooKeeper資料的最小單元 ZNode可以掛在子節點,儲存資料資訊 ZNode層次化,樹結構儲存

有贊搜尋系統技術內幕

上文說到有贊搜尋系統的架構演進,為了支撐不斷演進的技術架構,除了 Elasticsearch 的維護優化之外,我們也開發了上層的中介軟體來應對不斷提高的穩定性和效能要求。 Elasticsearch 的檢索執行效率可以表示為:O(num_of_files logN

系統技術非業餘研究 » init_debug檢視系統boot過程

erl啟動的時候有個引數 -init_debug 作用是 Makes init write some debug information while interpreting the boot script. 參見erlang system_principles的1.3節:Boot Scripts

Hadoop技術內幕-Hadoop檔案系統

第5章 Hadoop 檔案系統 1.檔案及檔案系統 檔案的本質是儲存在裝置上的線性位元組流 檔案系統用於檔案的管理包括檔案的結構以及命名、存取、使用、保護和實現 檔案系統支援的系統呼叫: 1) 建立檔案 2) 刪除檔案 3) 開啟檔案 4) 關閉檔案 5) 讀檔案 6) 寫

2014025628《嵌入式系統設計》第四周總結

linu 學習 32位 本周 環境 支持包 系統 img path 本周學習內容 一、學會了安裝arm-linux-gcc交叉編譯工具 解壓了arm-linux-gcc交叉編譯工具的壓縮包,測試arm-linux-gcc是否可運行,解決了64位系統下安裝32位軟件的問題

2014025665《嵌入式系統程序設計》第三、四周學習總結

-i 源代碼 nss gcc 一個 usr 交叉開發 64位 操作系統 第三周1.我們在帶三周的嵌入式程序設計中學到了如何搭建嵌入式Linux交叉開發環境。其實主要是解決64位系統下如何安裝32位程序的問題。①若聯網:Syum install Id-Linux.so.2②若

2014025670《嵌入式系統程序設計》第三,四周學習總結

進行 中學 linu 搭建 學習總結 64位 系統 問題 com 第三周我們在帶三周的嵌入式程序設計中學到了如何搭建嵌入式Linux交叉開發環境。其實主要是解決64位系統下如何安裝32位程序的問題。 進行實驗樓實驗的時候真的一定要特別細致,環環相扣,做了兩次實驗到最後都

2014025654《嵌入式系統程序設計》第五周學習總結

判斷 輸出 父進程 文件流 嵌入式系統 結束 可執行文件 size_t 進程組 1.fread 在文件被打開後,可對文件進行讀寫等操作,其中讀操作的函數為fread() 函數原型 size_t fread (voidptr,size_t size,size_t nmemb,

2014025674(16) 《嵌入式系統程序設計》第五周學習總結

阻塞 成功 fread kernel 版本 use 緩沖區 con ons 一、標準I/O的編程 fread與fwrite 1.fread是一個函數,是計算機程序中從一個文件流中讀數據,最多讀取count個元素,每個元素size字節,如果調用成功返回實際讀取到的元

你知道嵌入式系統與單片機的區別是什麽嗎?

淩陽教育 嵌入式系統 單片機 當下據淩陽教育的老師根據各個地方去做宣講,得出了一個問題,就是很多的大家生初學嵌入式的同學大家都會問及到一個問題,就是嵌入式系統與單片機的區別是什麽?下面小編就帶大家來了解我所知道的吧! 第一者 硬件組成的區別: