嵌入式 arm平臺kernel啟動第一階段彙編head.s分析
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;
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
核心啟動分析:
本文著重分析S3C2410linux-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 解壓縮階段已經執行完。
第二階段的在另外一篇中分析。