Linux內核啓動流程分析(一)

             S3C2410 Linux 2.6.35.7啓動分析(第一階段)


arm 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 解壓縮階段已經執行完。


原文出處:http://blog.chinaunix.net/uid-25909619-id-3380535.html

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