Bootloader概述
一個嵌入式Linux系統從軟件的角度看通常分為四個層次:引導加載程序、Linux內核、文件系統、用戶應用程序。
在linux中,引導加載程序即等效為Bootloader。Bootloader就是在操作系統內核運行前執行的一段小程序。通過這段小程序,可以初始化硬件設備、建立內存空間的映射圖,從而將系統的軟硬件環境帶到一個合適的狀態,以便為最終調用操作系統內核準備好正確的環境。
Bootloader的安裝位置
系統加電或復位后,所有的CPU通常都從某個特定的地址(該地址由CPU製造商預先安排)上取指令。比如,基於ARM7TDMI core的CPU在復位時通常都從地址0x00000000取它的第一條指令,而基於CPU 構建的嵌入式系統通常都有某種類型的固態存儲設備(比如:ROM、EEPROM或FLASH等)被映射到這個預先安排的地址上。因此在系統加電后,CPU將首先執行Boot Loader程序。
Bootloader的操作模式
大多數bootloader都包含兩種不同的操作模式。啟動加載(Boot loading)模式和下載(Down loading)模式。
啟動加載模式:這種模式也稱為自主(Autonomous)模式,即bootloader從目標機上的某個固態存儲設備上將操縱系統加載到RAM中運行,整個過程并沒有用戶的介入,這種模式是bootloader的正常工作模式。在嵌入式產品發佈的時候,Boot Loader必須工作在這種模式下。
下載模式:在這種模式下目標機上的bootloader將通過串口連接或網絡連接等通信手段從主機上下載文件。比如:下載應用程序、數據文件、內核映像等。從主機下載的文件通常首先被bootloader保存到目標機的RAM中然後再被bootloader寫到目標機上的FLASH類固態存儲設備中。Bootloader的這種模式通常在第一次安裝內核與根文件系統時被使用;此外,以後的系統更新也會使用bootloader的這種工作模式。
Bootloader的基本架構
Bootloader的啟動過程可以是單階段的,也可以是多階段的。從固態存儲設備上啟動的Bootloader大多數是兩階段的啟動過程,也就是啟動過程可以分為stage1和stage2兩部份。依賴于CPU體系結構的代碼,比如設備初始化代碼等,通常都放在stage1中,而且通常都是用滙編語言來實現,已達到短小精悍的目的。而stage2則通常用C語言來實現,這樣可以實現更複雜的功能,而且代碼會具有更好的可讀性和可移植性。
Boot Loader的stage1通常包括以下步驟(以執行的先後順序):
●硬件設備初始化
●為加載Boot Loader 的stage2準備ARM空間。
●拷貝Boot Loader 的stage2到RAM空間中。
●設置好堆棧。
●跳轉到stage2 的C入口點。
Boot Loader的stage2通常包括以下步驟(以執行的先後順序):
●初始化本階段要使用到的硬件設備。
●檢測系統內核映射(memory map)。
●將kernel映像和根文件系統映像從flash上讀到RAM空間中。
●為內核設置啟動參數。
●調用內核。
在stage1中bootloader主要完成的5方面工作,依次為:
1. 基本的硬件初始化
這是bootloader一開始就執行的操作,其目的是為stage2的執行以及隨後的kernel的執行準備好一些基本的硬件環境。它通常包括以下步驟(以執行的先後順序):
1) 屏蔽所有的中斷。為中斷提供服務通常是OS設備驅動程序的責任,因此在Bootloader的執行全過程中可以不必響應任何中斷。中斷屏蔽可以通過寫CPU的中斷屏蔽寄存器或狀態寄存器(比如RAM的CPSR寄存器)來完成。
2) 設置CPU的速率和時鐘頻率。
3) RAM初始化。包括正確的設置系統的內存控制器的功能寄存器以及各內存庫控制寄存器等。
4) 初始化串口。典型的,初始化UART向串口打印Boot Loader的字符信息,以輸出各種調試信息。也可以初始化LED,通過IO來驅動LED,其目的是表明系統的狀態是OK還是Error。
5) 關閉CPU內部指令/數據cache。
2. 為加載stage2準備RAM空間
為了獲得更快的執行速度,通常把stage2加載到RAM空間中來執行,因此必須為加載Boot Loader的stage2準備好一段可用的RAM空間範圍。由於stage2通常是C語言執行代碼,因此在考慮空間大小時,除了stage2可執行映像的大小外,還必須把堆棧空間也考慮進來。此外,空間大小最好是memorypage大小(通常是4KB)的倍數。一般而言,1M的RAM空間已經足夠了。具體的地址範圍可以任意安排。
在此把所安排的RAM空間範圍的大小記為:stage2_size(字節),把起始地址和終止地址分別記為:stage2_start和stage2_end(這兩個地址均以4字節邊界對齊)。因此:
stage2_end = stage2_start + stage2_size
另外,必須確保所安排的地址範圍的的確確是可讀寫的RAM空間,因此,必須對所安排的地址範圍進行測試。具體測試方法可以以memorypage為被測試單位,其具體步驟如下:
1) 先保存memory page一開始兩個字的內容。
2) 向這兩個字中寫入任意的數字。比如,向第一個字寫入0x55,第二個字寫入0xaa。
3) 然後,立即將這兩個字的內容讀回。如果讀到的不是0x55和0xaa,則說明這個memorypage所佔據的地址範圍不是一段有效的RAM空間。
4) 再向這兩個字中寫入任意的數字。比如,向第一個字寫入0xaa,第二個字寫入0x55。
5) 然後,立即將這兩個字的內容讀回。如果讀到的不是0xaa和0x55,則說明這個memorypage所佔據的地址範圍不是一段有效的RAM空間。
6) 恢復這兩個字的原始內容。測試完畢。
爲了得到一段乾淨的RAM空間範圍,我們可以將所安排的RAM範圍空間進行清零操作。
3. 拷貝stage2到RAM中
拷貝時要確定兩點:
1) stage2的可執行映像在固態存儲設備的存放起始地址和終止地址;
2) RAM空間的起始地址。
4. 設置堆棧指針sp
堆棧指針的設置時爲了執行C語言代碼做好準備。通常把sp的值設置為(stage2_end -4),在上節所安排的那個1M的RAM空間的最頂端(堆棧向下生長)。
此外,在設置堆棧指針sp之前,也可以關閉led燈,以提示用戶準備跳轉到stage2。
經過上述執行步驟后,系統的物理內存佈局應該如下圖所示:
5. 跳轉到stage2的C入口點
在一切就緒后,就可以跳轉到bootloader的stage2去執行。
@ get read to call C functions
ldr sp, DW_STACK_START @ setup stack pointer
mov fp, #0 @ no previous frame, so fp=0
mov a2, #0 @ set argv to NULL
bl main @ call main
mov pc, #FLASH_BASE @ otherwise, reboot
Bootloader的stage2
Stage2的代碼通常用C語言來實現。但在編譯和鏈接bootloader程序時,是不能使用glibc庫中的任何支持函數。所以我們利用trampoline(彈簧床)的概念實現跳轉main()函數。即用滙編語言寫一段trampoline小程序,并將這段trampoline小程序作為stage2可執行映像的執行入口點。然後我們可以在trampoline滙編小程序中用CPU跳轉指令跳入main()函數中去執行,而當main()函數返回時,CPU執行路徑再次回到trampoline程序。
1. 初始化本階段要使用到的硬件設備
通常包括:
1) 初始化至少一個串口,以便和終端用戶進行I/O輸出信息;
2) 初始化計時器等。
在初始化這種之前,也可以重新把LED燈點亮,以表明我們已經進入main()函數執行。設備初始化完成后,可以輸出一些打印信息,程序名字字符串、版本號等。
2. 檢測系統的內存映射(memory map)
所謂內存映射就是指在整個4GB物理地址空間中有哪些地址範圍被分配用來尋址系統的RAM單元。
3. 加載內核映像和跟文件系統映像
這一步要完成兩個工作:
1) 規劃內存佔用的佈局:包括兩個方面:一是內核映像所佔用的內存範圍;二是根文件系統所佔用的內存範圍。
在規劃內存佔用的佈局時,主要考慮基地址和映像的大小兩個方面。
對於內核映像,一般將其拷貝到從(MEM_START+0X8000)這個基地址開始的大約1MB大小的內存範圍內。爲什麽要把MEM_START到MEM_START+0X8000這段32KB大小的內存空出來?這是因為內核要在這段內存中放置一些全局數據結構,如:啟動參數和內核頁表等信息。
而對應跟文件系統映像,則一般將其拷貝到MEM_START+0X0010 0000開始的地方。
2) 從flash上拷貝
由於像ARM這樣的嵌入式CPU通常都是在統一的內存地址空間中尋址flash等固態存儲設備的,因此從flash上讀取數據與從RAM單元中讀取數據并沒有什麽不同。用一個簡單的循環就可以完成從flash設備上拷貝映像的工作:
while(count){
*dest++= *src++; /* they are all aligned with word boundary */
count-= 4; /* byte number */
};
4. 設置內核的啟動參數
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等。其中BOOT_PARAMS表示內核啟動參數在內存中的起始地址,指針params是一個struct tag類型的指針。宏tag_next()將以指向當前標記的指針為參數,計算緊臨當前標記的下一個標記的起始地址。
5. 調用內核
Boot Loader調用Linux內核的方法是直接跳轉到內核的第一條指令處,也即直接跳轉到MEM_START+0x8000地址處。在跳轉時,下列條件要滿足:
●CPU寄存器的設置:
R0 = 0;
R1= 機器類型ID;
R2= 啟動參數標記列表在RAM中起始基地址;
●CPU模式:
必須禁止中斷(IRQs和FIQs);
CPU必須SVC模式;
●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()函數調用應該永遠不返回的。如果這個調用返回,則說明出錯。
小結: