聲明本文轉載http://blog.csdn.net/zhou1232006/article/details/6215361的文章,由於這段時間要深入的分析linux各個環節想收集點資料
首先,需要理解加載域與運行域的概念。加載域是代碼存放的地址,運行域是代碼運行時的地址。爲什麼會產生這2個概念?這2個概念的實質意義又是什麼呢?
在一些場合,一些代碼並不在儲存這部分代碼的地址上執行地址,比如說,放在norflash中的代碼可能最終是放在RAM中運行,那麼中norflash中的地址就是加載域,而在RAM中的地址就是運行域。
在彙編代碼中我們常常會看到一些跳轉指令,比如說b、bl等,這些指令後面是一個相對地址而不是絕對地址,比如說b main,這個指令應該怎麼理解呢?main這裏究竟是一個什麼東西呢?這時候就需要涉及到鏈接地址的概念了,鏈接地址實際上就是鏈接器對代碼中的變量名、函數名等東西進行一個地址的編排,賦予這些抽象的東西一個地址,然後在程序中訪問這些變量名、函數名就是在訪問一些地址。一般所說的鏈接地址都是指鏈接這些代碼的起始地址,代碼必須放在這個地址開始的地方纔可以正常運行,否則的話當代碼去訪問、執行某個變量名、函數名對應地址上的代碼時就會找不到,接着程序無疑就是跑飛。但是上面說的那個b main的情形有點特殊,b、bl等跳轉指令並不是一個絕對跳轉指令,而是一個相對跳轉指令,什麼意思呢?就是說,這個main標籤最後得到的只並不是main被鏈接器編排後的絕對地址,而是main的絕對地址減去當前的這個指令的絕對地址所得到的值,也就是說b、bl訪問到的是一個相對地址,不是絕對地址,因此,包括這個語句和main在內的代碼段無論是否放在它的運行域這段代碼都能正常運行。這就是所謂的位置無關代碼。
由上面的論述可以得知,如果你的這段代碼需要實現位置無關,那麼你就不能使用絕對尋址指令,否則的話就是位置有關了。
接着,將結合uboot、vivi、linux中的PIC(position independent code)代碼進行分析。
另外需要指出的是本文的分析基於mini2440的板子及配套代碼,對於其他板子或者源代碼代碼或者會有差別。
Uboot部分:
在/u-boot-1.1.2/cpu/arm920t/start.S截取部分相關代碼如下:
1 .globl _start
2 _start: b reset
3 ldr pc, _undefined_instruction
4 ldr pc, _software_interrupt
5 ldr pc, _irq
6 ldr pc, _fiq
7
8 reset:
9 mrs r0,cpsr
10 bic r0,r0,#0x1f
11 orr r0,r0,#0xd3
12 bl cpu_init_crit
12 relocate: /* relocate U-Boot to RAM */
13 adr r0, _start /* r0 <- current position of code */
14 ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
15 cmp r0, r1 /* don't reloc during debug */
16 beq stack_setup
17 ldr r2, _armboot_start
18 ldr r3, _bss_start
19 sub r2, r3, r2 /* r2 <- size of armboot */
20 add r2, r0, r2 /* r2 <- source end address */
21
22 copy_loop:
23 ldmia r0!, {r3-r10} /* copy from source address [r0] */
24 stmia r1!, {r3-r10} /* copy to target address [r1] */
25 cmp r0, r2 /* until source end addreee [r2] */
26 ble copy_loop
27
28 /* Set up the stack */
29 stack_setup:
30 ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */
31 sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
32 sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */
33 #ifdef CONFIG_USE_IRQ
34 sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
35 #endif
36 sub sp, r0, #12 /* leave 3 words for abort-stack */
37 ldr pc, _start_armboot
假設(其實不是假設,一般都是)這段代碼當前是在起始地址爲0的norflash中儲存,上電覆位後CPU首先跳轉到2運行,這句使用的是一個相對跳轉指令b,這個指令將跳轉到本地儲存的reset例程中運行。接下來的所有語句中都沒有通過絕對尋址來尋找某個變量或者函數,因此即使目前的加載域與運行域不一致也沒有問題,因爲運行域主要是設計絕對尋址的正確性,如果沒有進行絕對尋址,不一致又能奈他何?
這裏其實還有個細節需要點出,就是3~6這幾句異常處理都是使用ldr進行絕對跳轉的,因此使用的undefined_instruction等都是這些名稱對應的鏈接地址,但是由下面的分析可知,這幾個中斷處理函數可能根本沒有被複制到RAM中((uboot只是把第一個C語言開始的代碼複製到RAM,但是這個結論尚未通過反彙編來驗證),也就是這些函數沒有被加載到運行域所以一旦發生reset以外的異常情況(比如說硬件中斷)程序就會跑飛,由此也可以得到另一個結論:uboot不支持中斷(至少當前分析的這個版本1.1.2是這樣的,其他版本可能不一樣)。
下一個重點是12開始的這段重定位代碼,13中使用了一個很特別的指令adr, adr r0, _start 作用是獲得 _start 的實際運行所在的地址值,其中adr r0, _start翻譯成 add r0,(PC+#offset),offset 就是 adr r0, _start 指令到_start 的偏移量,值爲負數,在鏈接時確定,這個偏移量是地址無關的,而pc爲當前指令的地址的下一個地址值,由於CPU復位後pc的值從0開始增加,因此到這這裏的值剛剛好能將offset抵消,所以最後r0的值就是0。
對於14,ldr r1, _TEXT_BASE 指令表示以程序相對偏移的方式加載數據,是索引偏移加載的另外一種形式,等同於ldr r1,[PC+#offset],offset 是 ldr r1, _TEXT_BASE 到 _TEXT_BASE 的偏移量, 因此最後r1得到的值就是TEXT_BASE,這個值在其他文件中已經定義(比如對smdk2410平臺而言就是在~/board/smdk2410/config.mk文件中)
15的就是比較上面的r0、r1是否相等,如果相等那麼證明程序現在的位置正是想要去的運行域,因此不需要做重定位就可以通過16語句跳轉stack_setup,否則就需要繼續執行17開始的代碼做重定位。17是將start_armboot這個地址給r2,start_armboot是誰?就是uboot的第一個C語言函數,這個函數名在鏈接的時候已經被安排爲某個地址,也就是運行域。18是將bss_start這個值付給r3,那bss_start又是誰呢?說到這裏先看看u-boot/board/smdk2410下的u-boot.lds鏈接文本的內容:
1 ENTRY(_start)
2 SECTIONS
3 {
4 . = 0x00000000;
5 . = ALIGN(4);
6 .text :
7 {
8 cpu/arm920t/start.o (.text)
9 *(.text)
10 }
11 . = ALIGN(4);
12 .rodata : { *(.rodata) }
13 . = ALIGN(4);
14 .data : { *(.data) }
15 . = ALIGN(4);
16 .got : { *(.got) }
17 __u_boot_cmd_start = .;
18 .u_boot_cmd : { *(.u_boot_cmd) }
19 __u_boot_cmd_end = .;
20 . = ALIGN(4);
21 __bss_start = .;
22 .bss : { *(.bss) }
23 _end = .;
24 }
鏈接文本是鏈接器用來鏈接可執行文件所使用的腳本文件,鏈接器根據這個來決定編譯器編譯出來的一大堆.o文件如何組織成爲一個可執行文件。其中ENTRY(_start)指定了這個可執行文件的入口點(或者通俗點說第一個執行的函數)。SECTIONS描述了代碼在內存中的佈局情況,4指定了內存的分佈從0開始,但是實際上在uboot鏈接過程中使用了一個-Ttext選項,這個選項最後就替代了0而將.text的鏈接地址修改爲0x33f80000,這個鏈接命令如下:
arm-linux-ld –Tu-boot-1.1.4/board/smdk2410/u-boot.lds –Ttext 0x33f80000
因此這個鏈接腳本實際上並不是最終的內存佈局。另外需要額外說明的是,這個鏈接地址是一個虛擬地址,如果mmu沒有打開的話(對於uboot就是這種情況,mmu一直都是處於關閉狀態)那麼這個虛地址就是物理地址。在linux中,對於ARM平臺內核一般從0xC0004000開始鏈接,這個地址明顯就是虛擬地址,在2410中一般把內核解壓在0x30008000這個物理地址上,所以在內核打開mmu之前的代碼必須是位置無關的,否則沒有辦法啓動內核;在打開mmu後,可以使用mmu將0x30000000開始的64M的物理空間映射爲0xC0000000開始的64M虛擬空間。
說了這麼多的題外話後回到之前的話題,bss_start就是在鏈接腳本中指出的一個符號,用以得到一個地址給外部的程序使用,這個地址實際上就是uboot鏈接後的最高地址,後面的bss雖然也還是uboot可執行文件的範疇,但是這個段在鏈接的時候並不分配任何內存空間,因此可以認爲可執行文件到bss_start就結束了。由此可以理解/u-boot-1.1.2/cpu/arm920t/start.S的17、18實際上是分別得到了uboot的第一個C語言函數地址和這個可執行文件的最後一個鏈接地址,之前運行的那段位置無關的彙編代碼直接被忽略。
22~26這段就是實現將uboot從norflash複製到RAM,地址爲TEXT_BASE。
29~36是設置堆棧空間和sp指針的值,爲執行C語言程序做準備。
37是重點,作用是跳轉到C語言的入口點start_armboot函數。
ldr pc, _start_armboot
這個語句使用的是ldr指令,這是一個絕對尋址指令,將start_armboot這個絕對地址加載到pc寄存器中,從而CPU轉到start_armboot這個地址上開始運行,實現了從flash到RAM的跳轉。
Uboot啓動內核的過程:
1、 uboot對需要啓動的映像文件有一定的格式要求,對於一個需要使用uboot引導的映像文件需要先使用uboot自帶的mkimage工具對這個映像進行修改,添加結構爲image_header頭部。具體指令如下:
./mkimage -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image
這樣映像文件就增加了sizeof(image_header_t)個字節。爲什麼要增加一個這樣的頭部呢?關於這個文件頭還是很有研究分量的,但是由於這裏並不把這個作爲重點,因此把這部分的分析寫在了另一篇文章《uboot中關於mkimage指令的矛盾分析.doc》中。這裏僅僅強調一下addr、ep這2個參數,前者是映像的下載地址,就是把這個映像複製到哪裏(不包括上面說到的那個文件頭,這部分在複製的時候被跳過),後者就是內核的入口,uboot就是從這點來找到並啓動內核的。
2、按照文件頭的參數將映像從flash或者RAM的其他位置複製到addr這裏,其中文件頭被直接忽略。如果是使用mkimage的時候使用了壓縮選項那麼就不是單純的複製,而是解壓到這個addr。
3、設置啓動參數表。與vivi不一樣,uboot對向內核傳遞參數時使用的是struct tag(標記列表,tagged list)方式。設置方式大概遵循如下步驟:
1 setup_start_tag (bd);
2 setup_memory_tags (bd);
3 setup_commandline_tag (bd, commandline);
4 setup_initrd_tag (bd, initrd_start, initrd_end);
5 setup_end_tag (bd);
其中,1是標記列表的開始,這個設置選項主要是指定這個啓動參數表的存放位置,比如說一般會指定在SDRAM_BASE+0x100這個地方。2是設置內存標記,告訴內核這個硬件平臺的RAM有多大、起始地址是多少等。3是設置命令行參數。4是設置initrd標記,如果並不使用initrd的話那麼這個設置是不需要的。
4、關閉cache,禁止mmu,禁止中斷,調用kernel函數跳轉到內核,thekernel函數是如何定義的呢?這個過程是怎麼實現的呢?
1 void (*theKernel)(int zero, int arch, uint params);
2 theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
3 theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
這是在armlinux.c文件截取do_bootm_linux函數(這是啓動內核時調用的最後一個函數)的的幾個相關語句,1是定義一個函數指針,2是把這個指針指向實際執行的函數地址,這個地址是誰呢?就是上面第一點說到的那個ep,就是內核的入口地址,或者說內核的第一個語句,這裏還使用了(void (*)(int, int, uint))將這個地址強制轉換爲有3個參數的函數,爲什麼要這樣做呢?因爲在跳轉到內核前需要設置好r0、r1、r2這3個寄存器的值,而在一般的C函數調用過程中,入口參數就是使用r0、r1、r2.。。。。。。。等來傳遞的,換言之,在3語句中調用theKernel函數時提供的3個入口參數最後將分別傳給r0、r1、r2,這樣就非常巧妙地通過C語言的特性來實現了r0、r1、r2的設置。
其中值得一提的是,r2的值在vivi中是設置爲內核在RAM中的地址,但是在uboot中是設置爲了uboot傳遞給linux的啓動參數表在RAM中的位置。究竟誰是對的呢?從邏輯上看,vivi向內核傳遞內核所在的地址沒有意義,因爲內核要這個沒用,但是內核需要啓動參數表,因此設置這個參數表的位置有意義,由此可以認定vivi那樣做是不對的。再者,vivi如果要將參數正確傳遞給linux,必須事先知道linux默認會去哪裏找啓動參數表,然後在這個位置上填好這個參數表,否則如果隨便找個地方放這個表linux是沒有辦法找到的,這樣的話內核將啓動失敗。
另外需要再次強調的是,uboot沒有打開mmu,因此一直使用的地址都是直接對應在物理地址上,但是vivi爲了實現更好的性能打開了cache和mmu,因此情況就相當不一樣了,下面將說明vivi的情況。
Vivi部分:
Vivi的入口點在vivi/arch/s3c2440/head.S的start,在這個文件中截取部分相關代碼進行分析:
1 ENTRY(_start)
2 b Reset
3 b HandleUndef
4 b HandleIRQ
5 b HandleFIQ
6 Reset:
7 @ disable watch dog timer
8 mov r1, #0x53000000
9 mov r2, #0x0
10 str r2, [r1]
11 @ disable all interrupts
12 mov r1, #INT_CTL_BASE
13 mov r2, #0xffffffff
14 str r2, [r1, #oINTMSK]
15 ldr r2, =0x7ff
16 str r2, [r1, #oINTSUBMSK]
17 @ initialise system clocks
18 mov r1, #CLK_CTL_BASE
19 bl memsetup
20 bl InitUART
21 # ifdef CONFIG_S3C2440_NAND_BOOT
22 bl copy_myself
23 @ jump to ram
24 ldr r1, =on_the_ram
25 add pc, r1, #0
26 nop
27 nop
28 1: b 1b @ infinite loop
29 on_the_ram:
30 #endif
31 @ get read to call C functions
32 ldr sp, DW_STACK_START @ setup stack pointer
33 mov fp, #0 @ no previous frame, so fp=0
34 mov a2, #0 @ set argv to NULL
35 bl main @ call main
36 mov pc, #FLASH_BASE @ otherwise, reboot
2~5都是使用b進行跳轉,就是說對異常情況vivi能正常處理,但是在vivi的中還沒發現有哪個驅動程序使用硬件中斷。而在11也看到了vivi將硬件中斷屏蔽掉,至於有沒有在後面重新打開我沒有進一步去求證。
7~18都是位置無關的,因爲都沒有對某個變量名、函數名進行絕對尋址,要注意的是CLK_CTL_BASE受僱一個直接數而不是一個變量名。
19、20、22都使用了一個bl進行相對跳轉,跳到儲存在本地的函數名中運行,這些函數的實現都放在了head.S文件中。但是這應該不是必須的,因爲在copy_myself函數中就調用了一個C文件中的函數。下面分析一下這個copy_myself函數中重要的幾個語句。
下面是從copy_myself截取出來的幾個重要的語句:
1 @ get read to call C functions (for nand_read())
2 ldr sp, DW_STACK_START @ setup stack pointer
3 mov fp, #0 @ no previous frame, so fp=0
4
5 @ copy vivi to RAM
6 ldr r0, =VIVI_RAM_BASE
7 mov r1, #0x0
8 mov r2, #0x20000
9 bl nand_read_ll
上面已經說到,copy_myself將調用一個C語言函數nand_read_ll,C函數在運行之前必須設置好堆棧指針sp,1~3就是這樣來的。
6~8是9中nand_read_ll函數的入口參數,分別代表了目標地址、源地址、複製的size,執行這個nand_read_ll以後vivi就玩玩整整地被複制一個副本到了VIVI_RAM_BASE開始的地方。爲跳轉到RAM中運行提供了前提條件。
說完了copy_myself函數繼續回到之前的reset函數進行分析。23~25意圖很明顯,先是通過24將on_the_ram這個鏈接地址給r1,接着就將r1的值賦給pc從而跳轉到這個地址上運行,當然也可以直接把on_the_ram賦給pc。這裏用的指令是ldr,是一個絕對尋址指令,on_the_ram對應的鏈接地址被直接送進了r1,這一步已經是位置相關了,由於已經將vivi複製到RAM中的運行域所以這時on_the_ram對應的鏈接地址上已經真的放着相應的代碼,所以這樣執行是不會出任何問題的。
之後,32語句被執行,但是這個語句已經是在RAM中,和上一條語句的位置已經有天壤之別。這句就是設置堆棧指針sp以便35調用C語言函數main,但是在copy_myself中不是已經設置了嗎?我也覺得在這裏應該不需要再設置一次也能使程序正常運行。
35使用bl來相對尋址到main,這時的main也是在RAM中的那個,當然這裏也可以使用絕對跳轉指令。到了main後就進入了C語言環境了。
再返回上面提到的VIVI_RAM_BASE這個數值來展開討論。Mini2440提供的vivi中,這個數值在vivi/include/platform/smdk440.h中定義,VIVI_RAM_BASE=0x33f00000。這個地址按道理應該和vivi的鏈接地址是一致的,那麼到底是不是呢?查看鏈接文本vivi/arch/vivi.lds.in文件找到TEXTADDR,這個值在vivi/arch/makefile中相應地也定義爲0x33f00000,因此是符合之前的猜測的。
說到這裏本來就可以結束了,但是由於在vivi的main函數裏面使用了mmu,因此情況一下子變得複雜起來。首先爲什麼vivi要打開MMU呢?在uboot裏面這個一直是關閉的。原因也是比較簡單的,就是追求系統運行的高效。因爲s3c2410的Icache不受MMU的影響,而Dcache和write buffer則必須開啓了MMU功能之後,才能使用。而使用Dcache和write buffer後,對系統運行速度的提高是非常明顯的,後面還將通過實驗來驗證這一點。也就是說,在nand flash啓動時,vivi使用了MMU,主要是爲了獲得Dcache和write buffer的使用權,藉此提高系統運行的性能。(在《vivi開發筆記: MMU分析》一文中有關於這點的說明)。
接下來需要討論一下mmu被開啓後的代碼運行情況。
在vivi/init/main.c的main函數中截取以下語句:
mem_map_init();
mmu_init();
第一句實際上是建立一個內存映射表,對於nand啓動的內存映射完全是線性映射,就是將虛擬地址的0~4G映射爲物理地址的0~4G,虛擬地址等於物理地址,實現語句如下:
static inline void mem_mapping_linear(void)
{
unsigned long pageoffset, sectionNumber;
for (sectionNumber = 0; sectionNumber < 4096; sectionNumber++)
{
pageoffset = (sectionNumber << 20);
*(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC;
}
for (pageoffset = DRAM_BASE; pageoffset < (DRAM_BASE+DRAM_SIZE); pageoffset += SZ_1M)
{
*(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC | MMU_CACHEABLE;
}
}
對於norflash則是先將norflash的物理地址映射到一個空閒的虛擬地址,把norflash原先佔用的0開始的虛擬地址空間讓出來;接着,將虛擬地址爲0開始的1M空間映射到DRAM對應的物理地址上。這樣的結果就是,虛擬地址爲0開始的1M空間被映射到DRAM的物理地址,同時與DRAM物理地址相同的虛擬地址也映射到DRAM的物理地址上;物理地址爲0開始的空間被映射到另一端空閒的虛擬地址空間;發生中斷、尋址的時候CPU用的都是虛擬地址,CPU把虛擬地址發給MMU,由MMU通過映射表得到相應的虛擬地址並對物理地址進行尋址。在CPU地址總線上出現的就是物理地址。具體的實現語句這裏不打算貼出來,可以看看源代碼。
回到main函數中的mmu_init(),這個函數主要是使能MMU,並將上一步建立好的映射表地址告訴MMU,這樣MMU就能使用已經建立好的映射表來進行內存映射。這個函數都是一些彙編指令,具體功能沒有一條條語句去分許。
由上面的分析還可以得到一些額外的結論。中2410/2440在nand方式啓動下,物理地址0開始的4K空間被映射爲內部的4K SRAM,接着nand開始的4K代碼(實際上vivi的start)就被複制到這個4K SRAM中開始運行,並在4K代碼結束前就把自身複製到DRAM中並跳轉到DRAM運行,發生異常的時候就跳轉到SRAM中的異常入口表中執行;而vivi的中斷跳轉命令都用的是b,因此都是跳轉到SRAM本地的相應處理例程。如果是以nor啓動,那麼物理地址0開始的地方映射在norflash,發生異常的時候都跳轉到norflash中運行。還有一種情況就是上面所說的把物理地址0映射到了DRAM,這樣在發生異常的情況的時候實際就跳轉到DRAM的地址中運行。前2種情況對與uboot也是一樣的,但是由於uboot沒有啓用MMU因此沒有第三種情況。
Vivi啓動內核的過程:
1、 把內核複製到合適的地址上。一般都是將linux內核從flash中複製到DRAM,具體將內核從哪個地址複製到哪個地址都是自定義的。
2、 設置傳遞給linux的啓動參數。Bootloader在執行過程中必須設置和初始化 Linux 的內核啓動參數。目前傳遞啓動參數主要採用兩種方式:即通過 struct
param_struct 和struct tag(標記列表,tagged list)兩種結構傳遞。struct param_struct 是一種比較老的參數傳遞方式,在 2.4 版本以前的內核中使用較多。從 2.4 版本以後 Linux 內核基本上採用標記列表的方式。但爲了保持和以前版本的兼容性,它仍支持 struct param_struct 參數傳遞方式,只不過在內核啓動過程中它將被轉換成標記列表方式。
標記列表方式是種比較新的參數傳遞方式,它必須以 ATAG_CORE 開始,並以ATAG_NONE 結尾。中間可以根據需要加入其他列表。Linux內核在啓動過程中會根據該啓動參數進行相應的初始化工作。(參考《ARM Linux啓動過程分析》一文)。Vivi中使用的就是param_struct方式,先是創建一個param_struct結構並進行填充,主要是填充以下幾個域:
params->u1.s.page_size = LINUX_PAGE_SIZE;
params->u1.s.nr_pages = (DRAM_SIZE >> LINUX_PAGE_SHIFT);
memcpy(params->commandline, linux_cmd, strlen(linux_cmd) + 1);
第一個是指定linux內核的分頁大小,第二個指定DRAM總共有多少個頁。第三個就是指定命令行參數。填充完之後就將這個結構的內容複製到一個特定的地址上,這個地址在vivi中定義爲param_base=DRAM+0x100。
3、關閉cache,禁止mmu,設置r0=0,r1=處理器類型,r2=內核在RAM中的地址,這個地址實際上就是bootloader複製內核時使用的那個地址。直接就使用mov pc, r2或者類似的指令將r2的值賦給pc,CPU就轉到了linux內核中運行。
Linux部分:
Bootloader跳轉到內核運行的入口點在linux/arch/arm/boot/compressed/Head.S文件中的start函數。怎麼知道這裏是內核的入口點呢?
首先需要了解linux內核的產生過程。首先是通過ld命令將編譯好的各個.o文件鏈接成爲linux/vmlinux可執行文件,同時產生system.map文件,這是一個非壓縮的可執行文件,鏈接腳本爲linux/arch/arm/kernel/vmlinux.lds.S,鏈接地址爲TEXTADDR=0xC0008000,可以稱之爲真正的linux內核;接着通過objcopy工具將vmlinux複製爲linux/arch/arm /boot/image,這時大小有變化,估計是格式已經發生了變化吧^_^;接下來在通過linux/arch/arm/boot/compressed/
makefile將image複製到本目錄下並壓縮成爲piggy.gz,體積大大減少,這個是壓縮後的內核;通過linux/arch/arm/boot/compressed/piggy.S文件將piggy.gz轉換爲數據段;通過linux/
arch/arm/boot/compressed下的makefile、vmlinux.lds.in將piggy.o、head.o、misc.o鏈接成vmlinux。Head.o、misc.o是什麼呢?這是爲了使用壓縮內核而設計的一段解壓縮代碼,因爲壓縮內核沒辦法直接運行,只能解壓後運行,所有在內核運轉起來前必須先有一段代碼將這個內核解壓縮,這就是head.o、misc.o的作用了。在鏈接的時候piggy.o作爲一個數據段鏈接到內核映像中,而不是作爲一個程序的目標文件進行鏈接,head.o、misc.o則視爲一般的目標文件進行鏈接,鏈接地址從0x00000000開始,這個鏈接後的vmlinux就是一個加上壓縮代碼後的壓縮後內核;鏈接完vmlinux後就將這個文件使用objcopy轉換爲zImage等我們熟知的映像文件。
從上面的分析可知bootloader轉移到內核運行的時候第一個運行的實際上還不是真內核的內容,而只是內核前面的那段解壓縮代碼,由linux/arch/arm/boot/compressed/vmlinux.lds.in的內容可知head.S中的start函數就是bootloader轉到內核映像時第一個要被執行的函數。這部分代碼是鏈接在0x00000000這個地方的(注意解壓縮代碼運行期間一直沒有打開mmu),但是實際上卻被複制在DRAM上運行,因此這部分代碼必須是位置無關的。事實上通過查看這部分代碼可以發現的確是這樣的,在調用函數的時候也是通過b、bl等指令而不是一些絕對尋址指令。那麼到底到什麼時候PIC代碼才結束呢?答案是,壓縮代碼解壓完畢並跳轉到真正內核入口運行,並在執行完一些初始化的工作後打開mmu,這時候纔是PIC代碼結束的時候。那爲什麼是這樣呢?上面已經說到,內核是從0xC0008000開始鏈接的,但是代碼實際上是放在0x30008000的地方,0xC0008000這段地址在s3c2410中又是保留的地址,不是實際的RAM,因此沒有辦法使用像uboot、vivi那樣的重定位方式來解決運行域與加載域不一致的矛盾,只能通過虛擬內存映射的方法,將物理上的0x30008000映射爲虛擬的0xC0008000,這樣運行域就與加載域一致了。因此在使用mmu之前的代碼必須是位置無關的,否則代碼將無法運行。
這裏還有一個細節需要提到一下,就是在查看system.map的時候會發現內核映像並不是從0xC0008000開始,而是從0xC0004000開始,那麼是不是說鏈接地址應該是0xC0004000呢?不是的,0xC0004000開始的16K空間放在一個叫做初始化頁表的東西,這個東西是在鏈接的時候靜態添加到鏈接地址之前的16K開始的地方去的,實際上的鏈接地址就是0xC0008000。這個值可以在linux/arch/arm/mach-s3c2410/Makefile.boot中找到。