加載內核映像和根文件系統映像

加載內核映像和根文件系統映像

(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 == '')

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 的大小時,必須包括字符串的終止符'',此外還要將字節數向上圓整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 已經成功地轉起來了!"。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章