Android arm linux kernel啟動流程一
在了解這些之前我們首先需要了解幾個名詞,這些名詞定義在/Documentation/arm/Porting里面,這里首先提到其中的幾個,其余幾個會在后面kernel的執(zhí)行過程中講述:
本文引用地址:http://butianyuan.cn/article/201611/317680.htm1)ZTEXTADDR boot.img運行時候zImage的起始地址,即kernel解壓代碼的地址。這里沒有虛擬地址的概念,因為沒有開啟MMU,所以這個地址是物理內存的地址。解壓代碼不一定需要載入RAM才能運行,在FLASH或者其他可尋址的媒體上都可以運行。
2)ZBSSADDR 解壓代碼的BSS段的地址,這里也是物理地址。
3)ZRELADDR 這個是kernel解壓以后存放的內存物理地址,解壓代碼執(zhí)行完成以后會跳到這個地址執(zhí)行kernel的啟動,這個地址和后面kernel運行時候的虛擬地址滿足:__virt_to_phys(TEXTADDR) = ZRELADDR。
4)INITRD_PHYS Initial Ram Disk存放在內存中的物理地址,這里就是我們的ramdisk.img。
5)INITRD_VIRT Initial Ram Disk運行時候虛擬地址。
6)PARAMS_PHYS 內核啟動的初始化參數(shù)在內存上的物理地址。
下面我們首先來看看boot.img的構造,了解其中的內容對我們了解kernel的啟動過程是很有幫助的。首先來看看Makefile是如何產生我們的boot.img的:
out/host/linux-x86/bin/mkbootimg-msm7627_ffa --kernel out/target/product/msm7627_ffa/kernel --ramdisk out/target/product/msm7627_ffa/ramdisk.img --cmdline "mem=203M console=ttyMSM2,115200n8 androidboot.hardware=qcom" --output out/target/product/msm7627_ffa/boot.img
根據(jù)上面的命令我們可以首先看看mkbootimg-msm7627ffa這個工具的源文件:system/core/mkbootimg.c??赐曛笪覀兙湍芎芮逦乜吹絙oot.img的內部構造,它是由boot header /kernel /ramdisk /second stage構成的,其中前3項是必須的,最后一項是可選的。
view plaincopy to clipboardprint?
/*
+-----------------+
| boot header | 1 page
+-----------------+
| kernel | n pages
+-----------------+
| ramdisk | m pages
+-----------------+
| second stage | o pages
+-----------------+
n = (kernel_size + page_size - 1) / page_size
m = (ramdisk_size + page_size - 1) / page_size
o = (second_size + page_size - 1) / page_size
0. all entities are page_size aligned in flash
1. kernel and ramdisk are required (size != 0)
2. second is optional (second_size == 0 -> no second)
3. load each element (kernel, ramdisk, second) at
the specified physical address (kernel_addr, etc)
4. prepare tags at tag_addr. kernel_args[] is
appended to the kernel commandline in the tags.
5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
6. if second_size != 0: jump to second_addr
else: jump to kernel_addr
*/
/*
+-----------------+
| boot header | 1 page
+-----------------+
| kernel | n pages
+-----------------+
| ramdisk | m pages
+-----------------+
| second stage | o pages
+-----------------+
n = (kernel_size + page_size - 1) / page_size
m = (ramdisk_size + page_size - 1) / page_size
o = (second_size + page_size - 1) / page_size
0. all entities are page_size aligned in flash
1. kernel and ramdisk are required (size != 0)
2. second is optional (second_size == 0 -> no second)
3. load each element (kernel, ramdisk, second) at
the specified physical address (kernel_addr, etc)
4. prepare tags at tag_addr. kernel_args[] is
appended to the kernel commandline in the tags.
5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
6. if second_size != 0: jump to second_addr
else: jump to kernel_addr
*/
關于boot header這個數(shù)據(jù)結構我們需要重點注意,在這里我們關注其中幾個比較重要的值,這些值定義在boot/boardconfig.h里面,不同的芯片對應vendor下不同的boardconfig,在這里我們的值分別是(分別是kernel/ramdis/tags載入ram的物理地址):
view plaincopy to clipboardprint?
#define PHYSICAL_DRAM_BASE 0x00200000
#define KERNEL_ADDR (PHYSICAL_DRAM_BASE + 0x00008000)
#define RAMDISK_ADDR (PHYSICAL_DRAM_BASE + 0x01000000)
#define TAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00000100)
#define NEWTAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00004000)
#define PHYSICAL_DRAM_BASE 0x00200000
#define KERNEL_ADDR (PHYSICAL_DRAM_BASE + 0x00008000)
#define RAMDISK_ADDR (PHYSICAL_DRAM_BASE + 0x01000000)
#define TAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00000100)
#define NEWTAGS_ADDR (PHYSICAL_DRAM_BASE + 0x00004000)
上面這些值分別和我們開篇時候提到的那幾個名詞相對應,比如kernel_addr就是ZTEXTADDR,RAMDISK_ADDR就是INITRD_PHYS,而TAGS_ADDR就是PARAMS_PHYS。bootloader會從boot.img的分區(qū)中將kernel和ramdisk分別讀入RAM上面定義的地址中,然后就會跳到ZTEXTADDR開始執(zhí)行。
基本了解boot.img的內容之后我們來分別看看里面的ramdisk.img和kernel又是如何產生的,以及其包含的內容。從簡單的說起,我們先看看ramdisk.img,這里首先要強調一下這個ramdisk.img在arm linux中的作用。它在kernel啟動過程中充當著第一階段的文件系統(tǒng),是一個CPIO格式打成的包。通俗上來講他就是我們將生成的root目錄,用CPIO方式進行了打包,然后在kernel啟動過程中會被mount作為文件系統(tǒng),當kernel啟動完成以后會執(zhí)行init,然后將system.img再mount進來作為Android的文件系統(tǒng)。在這里稍微解釋下這個mount的概念,所謂mount實際上就是告訴linux虛擬文件系統(tǒng)它的根目錄在哪,就是說我這個虛擬文件系統(tǒng)需要操作的那塊區(qū)域在哪,比如說ramdisk實際上是我們在內存中的一塊區(qū)域,把它作為文件系統(tǒng)的意思實際上就是告訴虛擬文件系統(tǒng)你的根目錄就在我這里,我的起始地址賦給你,你以后就能對我進行操作了。實際上我們也可以使用rom上的一塊區(qū)域作為根文件系統(tǒng),但是rom相對ram慢,所以這里使用ramdisk。然后我們在把system.img mount到ramdisk的system目錄,實際上就是將system.img的地址給了虛擬文件系統(tǒng),然后虛擬文件系統(tǒng)訪問system目錄的時候會重新定位到對system.img的訪問。我們可以看看makefile是如何生成它的:
out/host/linux-x86/bin/mkbootfs out/target/product/msm7627_ffa/root | out/host/linux-x86/bin/minigzip > out/target/product/msm7627_ffa/ramdisk.img
下面我們來看看kernel產生的過程,老方法,從Makefile開始/arch/arm/boot/Makefile ~
view plaincopy to clipboardprint?
$(obj)/Image: vmlinux FORCE
$(call if_changed,objcopy)
@echo Kernel: $@ is ready
$(obj)/compressed/vmlinux: $(obj)/Image FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@
$(obj)/zImage: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
@echo Kernel: $@ is ready
$(obj)/Image: vmlinux FORCE
$(call if_changed,objcopy)
@echo Kernel: $@ is ready
$(obj)/compressed/vmlinux: $(obj)/Image FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@
$(obj)/zImage: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
@echo Kernel: $@ is ready
我們分解地來看各個步驟,第一個是將vmlinux經(jīng)過objcopy后生成一個未經(jīng)壓縮的raw binary(Image 4M左右),這里的vmlinux是我們編譯鏈接以后生成的vmlinx,大概60多M。這里稍微說一下這個objcopy,在啟動的時候ELF格式是沒法執(zhí)行的,ELF格式的解析是在kernel啟動以后有了操作系統(tǒng)之后才能進行的。因為雖然我們編出的img雖然被編成ELF格式,但要想啟動起來必須將其轉化成原始的二進制格式,我們可以多照著man objcopy和OBJCOPYFLAGS :=-O binary -R .note -R .note.gnu.build-id -R .comment -S(arch/arm/Makefile)來看看這些objcopy具體做了什么事情 ~
得到Image以后,再將這個Image跟解壓代碼合成一個vmlinux,具體的我們可以看看arch/arm/boot/compressed/Makefile:
view plaincopy to clipboardprint?
$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o /
$(addprefix $(obj)/, $(OBJS)) FORCE
$(call if_changed,ld)
@:
$(obj)/piggy.gz: $(obj)/../Image FORCE
$(call if_changed,gzip)
$(obj)/piggy.o: $(obj)/piggy.gz FORCE
$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o /
$(addprefix $(obj)/, $(OBJS)) FORCE
$(call if_changed,ld)
@:
$(obj)/piggy.gz: $(obj)/../Image FORCE
$(call if_changed,gzip)
$(obj)/piggy.o: $(obj)/piggy.gz FORCE
從這里我們就可以看出來實際上這個vmlinux就是將Image壓縮以后根據(jù)vmlinux.lds與解壓代碼head.o和misc.o鏈接以后生成的一個elf,而且用readelf或者objdump可以很明顯地看到解壓代碼是PIC的,所有的虛擬地址都是相對的,沒有絕對地址。這里的vmlinx.lds可以對照著后面的head.s稍微看一下~得到壓縮以后的vmlinx以后再將這個vmlinx經(jīng)過objcopy以后就得到我們的zImage了,然后拷貝到out目錄下就是我們的kernel了~~
在這里要強調幾個地址,這些地址定義在arch/arm/mach-msm/makefile.boot里面,被arch/arm/boot/Makefile調用,其中zreladdr-y就是我們的kernel被解壓以后要釋放的地址了,解壓代碼跑完以后就會跳到這個地址來執(zhí)行kernel的啟動。不過這里還有其他兩個PHYS,跟前面定義在boardconfig.h里面的值重復了,不知道這兩個值在這里定義跟前面的值是一種什么關系???
好啦,講到這里我們基本就知道boot.img的構成了,下面我們就從解壓的代碼開始看看arm linux kernel啟動的一個過程,這個解壓的source就是/arch/arm/boot/compressed/head.S。要看懂這個匯編需要了解GNU ASM以及ARM匯編指令,ARM指令就不說了,ARM RVCT里面的文檔有得下,至于GNU ASM,不需要消息了解的話主要是看一下一些偽指令的含義(http://sources.redhat.com/binutils/docs-2.12/as.info/Pseudo-Ops.html#Pseudo%20Ops)
那么我們現(xiàn)在就開始分析這個解壓的過程:
1)bootloader會傳遞2個參數(shù)過來,分別是r1=architecture ID, r2=atags pointer。head.S從哪部分開始執(zhí)行呢,這個我們可以看看vmlinx.lds:
view plaincopy to clipboardprint?
ENTRY(_start)
SECTIONS
{
. = 0;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
*(.piggydata)
. = ALIGN(4);
}
ENTRY(_start)
SECTIONS
{
. = 0;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
*(.piggydata)
. = ALIGN(4);
}
可以看到我們最開始的section就是.start,所以我們是從start段開始執(zhí)行的。ELF對程序的入口地址是有定義的,這可以參照*.lds的語法規(guī)則里面有描述,分別是GNU LD的-E ---> *.lds里面的ENTRY定義 ---> start Symbol ---> .text section --->0。在這里是沒有這些判斷的,因為還沒有操作系統(tǒng),bootloader會直接跳到這個start的地址開始執(zhí)行。
在這里稍微帶一句,如果覺得head.S看的不太舒服的話,比如有些跳轉并不知道意思,可以直接objdump vmlinx來看,dump出來的匯編的流程就比較清晰了。
view plaincopy to clipboardprint?
1: mov r7, r1 @ save architecture ID
mov r8, r2 @ save atags pointer
#ifndef __ARM_ARCH_2__
/*
* Booting from Angel - need to enter SVC mode and disable
* FIQs/IRQs (numeric definitions from angel arm.h source).
* We only do this if we were in user mode on entry.
*/
mrs r2, cpsr @ get current mode
tst r2, #3 @ not user?
bne not_angel @ 如果不是
mov r0, #0x17 @ angel_SWIreason_EnterSVC
swi 0x123456 @ angel_SWI_ARM
not_angel:
mrs r2, cpsr @ turn off interrupts to
orr r2, r2, #0xc0 @ prevent angel from running
msr cpsr_c, r2
1: mov r7, r1 @ save architecture ID
mov r8, r2 @ save atags pointer
#ifndef __ARM_ARCH_2__
/*
* Booting from Angel - need to enter SVC mode and disable
* FIQs/IRQs (numeric definitions from angel arm.h source).
* We only do this if we were in user mode on entry.
*/
mrs r2, cpsr @ get current mode
tst r2, #3 @ not user?
bne not_angel @ 如果不是
mov r0, #0x17 @ angel_SWIreason_EnterSVC
swi 0x123456 @ angel_SWI_ARM
not_angel:
mrs r2, cpsr @ turn off interrupts to
orr r2, r2, #0xc0 @ prevent angel from running
msr cpsr_c, r2
上面首先保存r1和r2的值,然后進入超級用戶模式,并關閉中斷。
view plaincopy to clipboardprint?
.text
adr r0, LC0
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}
subs r0, r0, r1 @ calculate the delta offset
@ if delta is zero, we are
beq not_relocated @ running at the address we
@ were linked at.
.text
adr r0, LC0
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}
subs r0, r0, r1 @ calculate the delta offset
@ if delta is zero, we are
beq not_relocated @ running at the address we
@ were linked at.
這里首先判斷LC0當前的運行地址和鏈接地址是否一樣,如果一樣就不需要重定位,如果不一樣則需要進行重定位。這里肯定是不相等的,因為我們可以通過objdump看到LC0的地址是0x00000138,是一個相對地址,然后adr r0, LC0 實際上就是將LC0當前的運行地址,而我們直接跳到ZTEXTADDR跑的,實際上PC里面現(xiàn)在的地址肯定是0x00208000以后的一個值,adr r0, LC0編譯之后實際上為add r0, pc, #208,這個208就是LC0到.text段頭部的偏移。
view plaincopy to clipboardprint?
add r5, r5, r0
add r6, r6, r0
add ip, ip, r0
add r5, r5, r0
add r6, r6, r0
add ip, ip, r0
然后就是重定位了,即都加上一個偏移,經(jīng)過重定位以后就都是絕對地址了。
view plaincopy to clipboardprint?
not_relocated: mov r0, #0
1: str r0, [r2], #4 @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b
/*
* The C runtime environment should now be setup
* sufficiently. Turn the cache on, set up some
* pointers, and start decompressing.
*/
bl cache_on
not_relocated: mov r0, #0
1: str r0, [r2], #4 @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b
/*
* The C runtime environment should now be setup
* sufficiently. Turn the cache on, set up some
* pointers, and start decompressing.
*/
bl cache_on
重定位完成以后打開cache,具體這個打開cache的過程咱沒仔細研究過,大致過程是先從C0里面讀到processor ID,然后根據(jù)ID來進行cache_on。
view plaincopy to clipboardprint?
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
解壓的過程首先是在堆棧之上申請一個空間
view plaincopy to clipboardprint?
/*
* Check to see if we will overwrite ourselves.
* r4 = final kernel address
* r5 = start of this image
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
cmp r4, r2
bhs wont_overwrite
sub r3, sp, r5 @ > compressed kernel size
add r0, r4, r3, lsl #2 @ allow for 4x expansion
cmp r0, r5
bls wont_overwrite
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
add r0, r0, #127 + 128 @ alignment + stack
bic r0, r0, #127 @ align the kernel length
/*
* Check to see if we will overwrite ourselves.
* r4 = final kernel address
* r5 = start of this image
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
cmp r4, r2
bhs wont_overwrite
sub r3, sp, r5 @ > compressed kernel size
add r0, r4, r3, lsl #2 @ allow for 4x expansion
cmp r0, r5
bls wont_overwrite
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
add r0, r0, #127 + 128 @ alignment + stack
bic r0, r0, #127 @ align the kernel length
這個過程是判斷我們解壓出的vmlinx會不會覆蓋原來的zImage,這里的final kernel address就是解壓后的kernel要存放的地址,而start of this image則是zImage在內存中的地址。根據(jù)我們前面的分析,現(xiàn)在這兩個地址是重復的,即都是0x00208000。同樣r2是我們申請的一段內存空間,因為他是在sp上申請的,而根據(jù)vmlinx.lds我們知道stack實際上處與vmlinx的最上面,所以r4>=r2是不可能的,這里首先計算zImage的大小,然后判斷r4+r3是不是比r5小,很明顯r4和r5的值是一樣的,所以這里先將r2的值賦給r0,經(jīng)kernel先解壓到s申請的內存空間上面,具體的解壓過程就不描述了,定義在misc.c里面。(這里我所說的上面是指內存地址的高地址,默認載入的時候從低地址往高地址寫,所以從內存低地址開始運行,stack處于最后面,所以成說是最上面)
view plaincopy to clipboardprint?
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
add r1, r5, r0 @ end of decompressed kernel
adr r2, reloc_start
ldr r3, LC1
add r3, r2, r3
: ldmia r2!, {r9 - r14} @ copy relocation code
stmia r1!, {r9 - r14}
ldmia r2!, {r9 - r14}
stmia r1!, {r9 - r14}
cmp r2, r3
blo 1b
add sp, r1, #128 @ relocate the stack
bl cache_clean_flush
add pc, r5, r0 @ call relocation code
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
add r1, r5, r0 @ end of decompressed kernel
adr r2, reloc_start
ldr r3, LC1
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
add sp, r1, #128 @ relocate the stack
bl cache_clean_flush
add pc, r5, r0 @ call relocation code
因為沒有將kernel解壓在要求的地址,所以必須重定向,說穿了就是要將解壓的kernel拷貝到正確的地址,因為正確的地址與zImage的地址是重合的,而要拷貝我們又要執(zhí)行zImage的重定位代碼,所以這里首先將重定位代碼reloc_start拷貝到vmlinx上面,然后再將vmlinx拷貝到正確的地址并覆蓋掉zImage。這里首先計算出解壓后的vmlinux的高地址放在r1里面,r2存放著重定位代碼的首地址,r3存放著重定位代碼的size,這樣通過拷貝就將reloc_start移動到vmlinx后面去了,然后跳轉到重定位代碼開始執(zhí)行。
view plaincopy to clipboardprint?
/*
* All code following this line is relocatable. It is relocated by
* the above code to the end of the decompressed kernel image and
* executed there. During this time, we have no stacks.
*
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
.align 5
reloc_start: add r9, r5, r0
sub r9, r9, #128 @ do not copy the stack
debug_reloc_start
mov r1, r4
1:
.rept 4
ldmia r5!, {r0, r2, r3, r10 - r14} @ relocate kernel
stmia r1!, {r0, r2, r3, r10 - r14}
.endr
cmp r5, r9
blo 1b
add sp, r1, #128 @ relocate the stack
debug_reloc_end
call_kernel: bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
/*
* All code following this line is relocatable. It is relocated by
* the above code to the end of the decompressed kernel image and
* executed there. During this time, we have no stacks.
*
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
.align 5
reloc_start: add r9, r5, r0
sub r9, r9, #128 @ do not copy the stack
debug_reloc_start
mov r1, r4
1:
.rept 4
ldmia r5!, {r0, r2, r3, r10 - r14} @ relocate kernel
stmia r1!, {r0, r2, r3, r10 - r14}
.endr
cmp r5, r9
blo 1b
add sp, r1, #128 @ relocate the stack
debug_reloc_end
call_kernel: bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
這里就是將vmlinx拷貝到正確的地址了,拷貝到正確的位置以后,就將kernel的首地址賦給PC,然后就跳轉到真正kernel啟動的過程~~
最后我們來總結一下一個基本的過程:
1)當bootloader要從分區(qū)中數(shù)據(jù)讀到內存中來的時候,這里涉及最重要的兩個地址,一個就是ZTEXTADDR還有一個是INITRD_PHYS。不管用什么方式來生成IMG都要讓bootloader有方法知道這些參數(shù),不然就不知道應該將數(shù)據(jù)從FLASH讀入以后放在什么地方,下一步也不知道從哪個地方開始執(zhí)行了;
2)bootloader將IMG載入RAM以后,并跳到zImage的地址開始解壓的時候,這里就涉及到另外一個重要的參數(shù),那就是ZRELADDR,就是解壓后的kernel應該放在哪。這個參數(shù)一般都是arch/arm/mach-xxx下面的Makefile.boot來提供的;
3)另外現(xiàn)在解壓的代碼head.S和misc.c一般都會以PIC的方式來編譯,這樣載入RAM在任何地方都可以運行,這里涉及到兩次沖定位的過程,基本上這個重定位的過程在ARM上都是差不多一樣的。
評論