bootloader與linux中位置無關代碼的分析理解

聲明本文轉載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中找到。

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