1. 程式人生 > >Linux核心啟動流程分析(一)

Linux核心啟動流程分析(一)

1. 依據arch/arm/kernel/vmlinux.lds 生成linux核心原始碼根目錄下的vmlinux,這個vmlinux屬於未壓縮,帶除錯資訊、符號表的最初的核心,大小約23MB;  命令:arm-linux-gnu-ld -o vmlinux -T arch/arm/kernel/vmlinux.lds   arch/arm/kernel/head.o   init/built-in.o   --start-group    arch/arm/mach-s3c2410/built-in.o    kernel/built-in.o           mm/built-in.o    fs/built-in.o    ipc/built-in.o    drivers/built-in.o    net/built-in.o   --end-group .tmp_kallsyms2.o 

2將上面的vmlinux去除除錯資訊、註釋、符號表等內容,生成arch/arm/boot/Image,這是不帶多餘資訊的linux核心,Image的大小約3.2MB;    命令:arm-linux-gnu-objcopy -O binary -S  vmlinux arch/arm/boot/Image 

3.將 arch/arm/boot/Image 用gzip -9 壓縮生成arch/arm/boot/compressed/piggy.gz大小約1.5MB;          命令:gzip -f -9 < arch/arm/boot/compressed/../Image > arch/arm/boot/compressed/piggy.gz 

4編譯arch/arm/boot/compressed/piggy.S 生成arch/arm/boot/compressed/piggy.o大小約1.5MB,這裡實際上是將piggy.gz通過piggy.S編譯進piggy.o檔案中。而piggy.S檔案僅有6行,只是包含了檔案piggy.gz;   命令:arm-linux-gnu-gcc -o arch/arm/boot/compressed/piggy.o arch/arm/boot/compressed/piggy.S 

5. 依據arch/arm/boot/compressed/vmlinux.lds 將arch/arm/boot/compressed/目錄下的檔案head.o 、piggy.o 、misc.o連結生成 arch/arm/boot/compressed/vmlinux,這個vmlinux是經過壓縮且含有自解壓程式碼的核心,大小約1.5MB; 

命令:arm-linux-gnu-ld zreladdr=0x30008000 params_phys=0x30000100 -T arch/arm/boot/compressed/vmlinux.lds arch/arm/boot/compressed/head.o arch/arm/boot/compressed/piggy.o arch/arm/boot/compressed/misc.o -o arch/arm/boot/compressed/vmlinux 

6. 將arch/arm/boot/compressed/vmlinux去除除錯資訊、註釋、符號表等內容,生成arch/arm/boot/zImage大小約1.5MB;這已經是一個可以使用的linux核心映像檔案了;  命令:arm-linux-gnu-objcopy -O binary -S  arch/arm/boot/compressed/vmlinux  arch/arm/boot/zImage 

7. 將arch/arm/boot/zImage新增64Bytes的相關資訊打包為arch/arm/boot/uImage大小約1.5MB;  命令: ./mkimage -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008000 -n 'Linux-2.6.35.7' -d arch/arm/boot/zImage arch/arm/boot/uImage

核心啟動分析:

本文著重分析S3C2410 linux-2.6.35.7 核心啟動的詳細過程,主要包括: zImage 解壓縮階段、 vmlinux 啟動彙編階段、 startkernel 到建立第一個程序階段三個部分,一般將其稱為 linux 核心啟動一、二、三階段,本文也將採用這種表達方式。對於 zImage 之前的啟動過程,本文不做表述,可參考前面正亮講得  u-boot的啟動過程分析”。

本文中涉及到的術語約定如下:

基本核心映像:即核心編譯過程中最終在核心原始碼根目錄下生成的 vmlinux 映像檔案,並不包含任何核心解壓縮和重定位程式碼;

zImage 核心映像:包含了核心piggy.o及解壓縮和重定位程式碼,通常是目標板 bootloader 載入的物件;

zImage 下載地址: bootloader  zImage 下載到目標板記憶體的某個地址或者 nand read  zImage 讀到記憶體的某個地址;

zImage 載入地址: Linux  bootloader 完成的將 zImage 搬移到目標板記憶體的某個位置所對應的地址值,預設值 0x30008000 

1、 Linux 核心啟動第一階段:核心解壓縮和重定位

該階段是從 u-boot 引導進入核心執行的第一階段,我們知道 u-boot 引導核心啟動的最後一步是:通過一個函式指標 thekernel()帶三個引數跳轉到核心( zImage )入口點開始執行,此時, u-boot 的任務已經完成,控制權完全交給核心( zImage )。 

稍作解釋,在 u-boot 的檔案arch\arm\lib\bootm.c(uboot-2010.9)中定義了 thekernel, 並在 do_bootm_linux 的最後執行 thekernel.

定義如下:void (*theKernel)(int zero, int arch, uint params); 

theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep); 

//hdr->ih_ep----Entry Point Address uImage 中指定的核心入口點,這裡是 0x30008000  

theKernel (0, bd->bi_arch_number, bd->bi_boot_params); 

其中第二個引數為機器 ID, 第三引數為 u-boot 傳遞給核心引數存放在記憶體中的首地址,此處是 0x30000100  

由上述 zImage 的生成過程我們可以知道,第一階段執行的核心映像實際就是arch/arm/boot/compressed/vmlinux,而這一階段所涉及的檔案也只有三個:   

(1)arch/arm/boot/compressed/vmlinux.lds

(2)arch/arm/boot/compressed/head.S      

(3)arch/arm/boot/compressed/misc.c 

下面的圖是使用64MRAM時,通常的記憶體分佈圖:

下面我們的分析集中在 arch/arm/boot/compressed/head.S, 適當參考 vmlinux.lds 

從linux/arch/arm/boot/compressed/vmlinux.lds檔案可以看出head.S的入口地址為ENTRY(_start),也就是head.S彙編檔案的_start標號開始的第一條指令。

下面從head.S中得_start 標號開始分析。(有些指令不影響初始化,暫時略去不分析)

程式碼位置在/arch/arm/boot/compressed/head.S中: 

start:

.type start,#function   /*uboot跳轉到核心後執行的第一條程式碼*/

.rept 8            /*重複定義8次下面的指令,也就是空出中斷向量表的位置*/

 mov r0, r0            /*就是nop指令*/

.endr

b 1f                   @ 跳轉到後面的標號1處

.word 0x016f2818 @ 輔助載入程式的幻數,用來判斷映象是否是zImage

.word start @ 載入執行zImage的絕對地址,start表示賦的初值

.word _edata @ zImage結尾地址,_edata是在vmlinux.lds.S中定義的,表示init,text,data三個段的結束位置

1: mov r7, r1 @ save architecture ID 儲存體系結構ID 用r1儲存

mov r8, r2 @ save atags pointer 儲存r2暫存器 引數列表,r0始終為0

mrs r2, cpsr @ get current mode  得到當前模式

tst r2, #3 @ not user?,tst實際上是相與,判斷是否處於使用者模式

bne not_angel            @ 如果不是處於使用者模式,就跳轉到not_angel標號處

/*如果是普通使用者模式,則通過軟中斷進入超級使用者許可權模式*/

mov r0, #0x17 @ angel_SWIreason_EnterSVC,向SWI中傳遞引數

swi 0x123456 @ angel_SWI_ARM這個是讓使用者空間進入SVC空間

not_angel:                                /*表示非使用者模式,可以直接關閉中斷*/

mrs r2, cpsr @ turn off interrupts to 讀出cpsr暫存器的值放到r2中

orr r2, r2, #0xc0 @ prevent angel from running關閉中斷

msr cpsr_c, r2           @ 把r2的值從新寫回到cpsr中

/*讀入地址表。因為我們的程式碼可以在任何地址執行,也就是位置無關程式碼(PIC),所以我們需要加上一個偏移量。下面有每一個列表項的具體意義。

LC0是表的首項,它本身就是在此head.s中定義的

.type LC0, #object

LC0: .word LC0 @ r1 LC0表的起始位置

.word __bss_start @ r2 bss段的起始地址在vmlinux.lds.S中定義

.word _end @ r3 zImage(bss)連線的結束地址在vmlinux.lds.S中定義

.word zreladdr @ r4 zImage的連線地址,我們在arch/arm/mach-s3c2410/makefile.boot中定義的

.word _start @ r5 zImage的基地址,bootp/init.S中的_start函式,主要起傳遞引數作用

.word _got_start @ r6 GOT(全域性偏移表)起始地址,_got_start是在compressed/vmlinux.lds.in中定義的

.word _got_end @ ip GOT結束地址

.word user_stack+4096 @ sp 使用者棧底 user_stack是緊跟在bss段的後面的,在compressed/vmlinux.lds.in中定義的

在本head.S的末尾定義了zImag的臨時棧空間,在這裡分配了4K的空間用來做堆疊。

.section ".stack", "w"

user_stack: .space 4096

GOT表的初值是聯結器指定的,當時程式並不知道程式碼在哪個地址執行。如果當前執行的地址已經和表上的地址不一樣,還要修正GOT表。*/

.text

adr r0, LC0                              /*把地址表的起始地址放入r0中*/

ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp} /*載入地址表中的所有地址到相應的暫存器*/

@r0是執行時地址,而r1則是連結時地址,而它們兩都是表示LC0表的起始位置,這樣他們兩的差則是執行和連結的偏移量,糾正了這個偏移量才可以執行與”地址相關的程式碼“

subs r0, r0, r1 @ calculate the delta offset 計算偏移量,並放入r0中

beq not_relocated @ if delta is zero, we are running at the address we  were linked at.

@ 如果為0,則不用重定位了,直接跳轉到標號not_relocated處執行

/*

  *   偏移量不為零,說明執行在不同的地址,那麼需要修正幾個指標 

         *   r5 – zImage基地址 

         *   r6 – GOT(全域性偏移表)起始地址 

         *   ip – GOT結束地址 

*/

add r5, r5, r0 /*加上偏移量修正zImage基地址*/

add r6, r6, r0 /*加上偏移量修正GOT(全域性偏移表)起始地址*/

add ip, ip, r0 /*加上偏移量修正GOT(全域性偏移表)結束地址*/

              /*

  * 這時需要修正BSS區域的指標,我們平臺適用。 

          *   r2 – BSS 起始地址 

            *   r3 – BSS 結束地址 

            *   sp – 堆疊指標 

*/

add r2, r2, r0 /*加上偏移量修正BSS 起始地址*/

add r3, r3, r0 /*加上偏移量修正BSS 結束地址*/

add sp, sp, r0 /*加上偏移量修正堆疊指標*/

/*

* 重新定位GOT表中所有的項.

*/

1: ldr r1, [r6, #0] @ relocate entries in the GOT

add r1, r1, r0 @ table.  This fixes up the

str r1, [r6], #4 @ C references.

cmp r6, ip

blo 1b

not_relocated: mov r0, #0 

1: str r0, [r2], #4 @ clear bss 清除bss段

str r0, [r2], #4

str r0, [r2], #4

str r0, [r2], #4

cmp r2, r3

blo 1b

bl cache_on        /* 開啟指令和資料Cache ,為了加快解壓速度*/

@ 這裡的 r1,r2 之間的空間為解壓縮核心程式所使用,也是傳遞給 decompress_kernel 的第二和第三的引數

mov r1, sp @ malloc space above stack

add r2, sp, #0x10000 @ 64k max解壓縮的緩衝區

@下面程式的意義就是保證解壓地址和當前程式的地址不重疊。上面分配了64KB的空間來做解壓時的資料快取。

/*

 *   檢查是否會覆蓋核心映像本身 

 *   r4 = 最終解壓後的核心首地址 

 *   r5 = zImage 的執行時首地址,一般為 0x30008000

 *   r2 = end of malloc space分配空間的結束地址(並且處於本映像的前面) 

 * 基本要求:r4 >= r2 或者 r4 + 映像長度 <= r5 

(1)vmlinux 的起始地址大於 zImage 執行時所需的最大地址( r2  , 那麼直接將 zImage 解壓到 vmlinux 的目標地址

cmp r4, r2

bhs wont_overwrite /*如果r4大於或等於r2的話*/

(2)zImage 的起始地址大於 vmlinux 的目標起始地址加上 vmlinux 大小( 4M )的地址,所以將 zImage 直接解壓到 vmlinux 的目標地址

add r0, r4, #4096*1024 @ 4MB largest kernel size

cmp r0, r5

bls wont_overwrite /*如果r4 + 映像長度 <= r5 的話*/

前兩種方案通常都不成立,不會跳轉到wont_overwrite標號處,會繼續走如下分支,其解壓後的記憶體分配示意圖如下:

mov r5, r2 @ decompress after malloc space

mov r0, r5          /*解壓程式從分配空間後面存放 */

mov r3, r7

bl decompress_kernel

/******************************進入decompress_kernel***************************************************/

@ decompress_kernel共有4個引數,解壓的核心地址、快取區首地址、快取區尾地址、和晶片ID,返回解壓縮程式碼的長度。

decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,

  int arch_id)

{

output_data = (uch *)output_start;/* Points to kernel start */

free_mem_ptr = free_mem_ptr_p;     /*儲存快取區首地址*/

free_mem_ptr_end = free_mem_ptr_end_p;/*儲存緩衝區結束地址*/

__machine_arch_type = arch_id;           

arch_decomp_setup();  

makecrc();                             /*映象校驗*/

putstr("Uncompressing Linux...");

gunzip();                            /*通過free_mem_ptr來解壓縮*/

putstr(" done, booting the kernel.\n");

return output_ptr;                     /*返回映象的大小*/

}

/******************************從decompress_kernel函式返回*************************************************/

add r0, r0, #127 + 128

bic r0, r0, #127 @ align the kernel length對齊核心長度

/*

 * r0     = 解壓後核心長度

 * r1-r3  = 未使用 

 * r4     = 真正核心執行地址  0x30008000

 * r5     = 臨時解壓核心Image的起始地址 

 * r6     = 處理器ID         

 * r7     = 體系結構ID         

 * r8     = 引數列表               0x30000100

 * r9-r14 = 未使用

 */

@ 完成了解壓縮之後,由於核心沒有解壓到正確的地址,最後必須通過程式碼搬移來搬到指定的地址0x30008000。搬運過程中有

@ 可能會覆蓋掉現在執行的重定位程式碼,所以必須將這段程式碼搬運到安全的地方,

@ 這裡運到的地址是解壓縮了的程式碼的後面r5+r0的位置。

add r1, r5, r0 @ end of decompressed kernel 解壓核心的結束地址

adr r2, reloc_start

ldr r3, LC1             @ LC1: .word reloc_end - reloc_start 表示reloc_start段程式碼的大小

add r3, r2, r3

1: ldmia r2!, {r9 - r14}     @ copy relocation code

stmia r1!, {r9 - r14}

ldmia r2!, {r9 - r14}

stmia r1!, {r9 - r14}

cmp r2, r3

blo 1b

bl cache_clean_flush  @清 cache

ARM(add pc, r5, r0)                     @ call relocation code 跳轉到重定位程式碼開始執行

@ 在此處會呼叫重定位程式碼reloc_start來將Image 的程式碼從緩衝區r5幫運到最終的目的地r4:0x30008000處

reloc_start: add r9, r5, r0         @r9中存放的是臨時解壓核心的末尾地址

sub r9, r9, #128      @ 不拷貝堆疊

mov r1, r4      @r1中存放的是目的地址0x30008000

1:

.rept 4

ldmia r5!, {r0, r2, r3, r10 - r14} @ relocate kernel

stmia r1!, {r0, r2, r3, r10 - r14} /*搬運核心Image的過程*/

.endr

cmp r5, r9

blo 1b

mov sp, r1                            /*留出堆疊的位置*/

add sp, sp, #128              @ relocate the stack

call_kernel: bl cache_clean_flush    @清除cache             

bl cache_off            @關閉cache

mov r0, #0 @ must be zero

mov r1, r7 @ restore architecture number

mov r2, r8 @ restore atags pointer

@ 這就是最終我們從zImage跳轉到Image的偉大一跳了,跳之前準備好r0,r1,r2

mov pc, r4 @ call kernel

到此kernel的第一階段zImage 解壓縮階段已經執行完。

第二階段的在另外一篇中分析。