- UID
- 852722
|
Android arm linux kernel启动流程(1)
虽然这里的Arm Linux kernel前面加上了Android,但实际上还是和普遍Arm linuxkernel启动的过程一样的,这里只是结合一下Android的Makefile,讲一下bootimage生成的一个过程。这篇文档主要描述bootimage的构造,以及kernel真正执行前的解压过程。 在了解这些之前我们首先需要了解几个名词,这些名词定义在/Documentation/arm/Porting里面,这里首先提到其中的几个,其余几个会在后面kernel的执行过程中讲述:
1)ZTEXTADDR boot.img运行时候zImage的起始地址,即kernel解压代码的地址。这里没有虚拟地址的概念,因为没有开启MMU,所以这个地址是物理内存的地址。解压代码不一定需要载入RAM才能运行,在FLASH或者其他可寻址的媒体上都可以运行。
2)ZBSSADDR 解压代码的BSS段的地址,这里也是物理地址。
3)ZRELADDR 这个是kernel解压以后存放的内存物理地址,解压代码执行完成以后会跳到这个地址执行kernel的启动,这个地址和后面kernel运行时候的虚拟地址满足:__virt_to_phys(TEXTADDR) = ZRELADDR。
4)INITRD_PHYS Initial Ram Disk存放在内存中的物理地址,这里就是我们的ramdisk.img。
5)INITRD_VIRT Initial Ram Disk运行时候虚拟地址。
6)PARAMS_PHYS 内核启动的初始化参数在内存上的物理地址。
下面我们首先来看看boot.img的构造,了解其中的内容对我们了解kernel的启动过程是很有帮助的。首先来看看Makefile是如何产生我们的boot.img的:
out/host/linux-x86/bin/mkbootimg-msm7627_ffa --kernelout/target/product/msm7627_ffa/kernel --ramdiskout/target/product/msm7627_ffa/ramdisk.img --cmdline "mem=203Mconsole=ttyMSM2,115200n8 androidboot.hardware=qcom" --outputout/target/product/msm7627_ffa/boot.img
根据上面的命令我们可以首先看看mkbootimg-msm7627ffa这个工具的源文件:system/core/mkbootimg.c。看完之后我们就能很清晰地看到boot.img的内部构造,它是由boot header /kernel /ramdisk /secondstage构成的,其中前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
- */
关于bootheader这个数据结构我们需要重点注意,在这里我们关注其中几个比较重要的值,这些值定义在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)
上面这些值分别和我们开篇时候提到的那几个名词相对应,比如kernel_addr就是ZTEXTADDR,RAMDISK_ADDR就是INITRD_PHYS,而TAGS_ADDR就是PARAMS_PHYS。bootloader会从boot.img的分区中将kernel和ramdisk分别读入RAM上面定义的地址中,然后就会跳到ZTEXTADDR开始执行。
基本了解boot.img的内容之后我们来分别看看里面的ramdisk.img和kernel又是如何产生的,以及其包含的内容。从简单的说起,我们先看看ramdisk.img,这里首先要强调一下这个ramdisk.img在armlinux中的作用。它在kernel启动过程中充当着第一阶段的文件系统,是一个CPIO格式打成的包。通俗上来讲他就是我们将生成的root目录,用CPIO方式进行了打包,然后在kernel启动过程中会被mount作为文件系统,当kernel启动完成以后会执行init,然后将system.img再mount进来作为Android的文件系统。我们可以看看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'
我们分解地来看各个步骤,第一个是将vmlinux经过objcopy后生成一个未经压缩的raw binary(Image4M左右),这里的vmlinux是我们编译链接以后生成的vmlinx,大概60多M。这里稍微说一下这个objcopy,在启动的时候ELF格式是没法执行的,ELF格式的解析是在kernel启动以后有了操作系统之后才能进行的。因为虽然我们编出的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
从这里我们就可以看出来实际上这个vmlinux就是将Image压缩以后根据vmlinux.lds与解压代码head.o和misc.o链接以后生成的一个elf,而且用readelf或者objdump可以很明显地看到解压代码是PIC的,所有的虚拟地址都是相对的,没有绝对地址。这里的vmlinx.lds可以对照着后面的head.s稍微看一下~得到压缩以后的vmlinx以后再将这个vmlinx经过objcopy以后就得到我们的zImage了,然后拷贝到out目录下就是我们的kernel了~~
在这里要强调几个地址,这些地址定义在arch/arm/mach-msm/makefile.boot里面,被arch/arm/boot/compressed/Makefile调用,其中zreladdr-y就是我们的kernel被解压以后要释放的地址了,解压代码跑完以后就会跳到这个地址来执行kernel的启动。不过这里还有其他两个PHYS,跟前面定义在boardconfig.h里面的值重复了,不知道这两个值在这里定义跟前面的值是一种什么关系???
好啦,讲到这里我们基本就知道boot.img的构成了,下面我们就从解压的代码开始看看arm linuxkernel启动的一个过程,这个解压的source就是/arch/arm/boot/compressed/head.S。要看懂这个汇编需要了解GNU ASM以及ARM汇编指令,ARM指令就不说了,ARM RVCT里面的文档有得下,至于GNUASM,不需要消息了解的话主要是看一下一些伪指令的含义(http://sources.redhat.com/binuti ... s.html#Pseudo%20Ops)
那么我们现在就开始分析这个解压的过程:
1)bootloader会传递2个参数过来,分别是r1=architecture ID, r2=atags pointer。head.S从哪部分开始执行呢,这个我们可以看看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);
- }
可以看到我们最开始的section就是.start,所以我们是从start段开始执行的。ELF对程序的入口地址是有定义的,这可以参照*.lds的语法规则里面有描述,分别是GNU LD的-E ---> *.lds里面的ENTRY定义 ---> start Symbol ---> .text section--->0。在这里是没有这些判断的,因为还没有操作系统,bootloader会直接跳到这个start的地址开始执行。
在这里稍微带一句,如果觉得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
上面首先保存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.
这里首先判断LC0当前的运行地址和链接地址是否一样,如果一样就不需要重定位,如果不一样则需要进行重定位。这里肯定是不相等的,因为我们可以通过objdump看到LC0的地址是0x00000138,是一个相对地址,然后adr r0, LC0实际上就是将LC0当前的运行地址,而我们直接跳到ZTEXTADDR跑的,实际上PC里面现在的地址肯定是0x00208000以后的一个值,adrr0, LC0编译之后实际上为add
r0, pc, #208,这个208就是LC0到.text段头部的偏移。
view plaincopy to clipboardprint?
- add r5, r5, r0
- add r6, r6, r0
- add ip, ip, r0
然后就是重定位了,即都加上一个偏移,经过重定位以后就都是绝对地址了。
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
重定位完成以后打开cache,具体这个打开cache的过程咱没仔细研究过,大致过程是先从C0里面读到processor ID,然后根据ID来进行cache_on。
view plaincopy to clipboardprint?
- 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
这个过程是判断我们解压出的vmlinx会不会覆盖原来的zImage,这里的final kerneladdress就是解压后的kernel要存放的地址,而start of thisimage则是zImage在内存中的地址。根据我们前面的分析,现在这两个地址是重复的,即都是0x00208000。同样r2是我们申请的一段内存空间,因为他是在sp上申请的,而根据vmlinx.lds我们知道stack实际上处与vmlinx的最上面,所以r4>=r2是不可能的,这里首先计算zImage的大小,然后判断r4+r3是不是比r5小,很明显r4和r5的值是一样的,所以这里先将r2的值赋给r0,经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
因为没有将kernel解压在要求的地址,所以必须重定向,说穿了就是要将解压的kernel拷贝到正确的地址,因为正确的地址与zImage的地址是重合的,而要拷贝我们又要执行zImage的重定位代码,所以这里首先将重定位代码reloc_start拷贝到vmlinx上面,然后再将vmlinx拷贝到正确的地址并覆盖掉zImage。这里首先计算出解压后的vmlinux的高地址放在r1里面,r2存放着重定位代码的首地址,r3存放着重定位代码的size,这样通过拷贝就将reloc_start移动到vmlinx后面去了,然后跳转到重定位代码开始执行。
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
这里就是将vmlinx拷贝到正确的地址了,拷贝到正确的位置以后,就将kernel的首地址赋给PC,然后就跳转到真正kernel启动的过程~~
最后我们来总结一下一个基本的过程:
1)当bootloader要从分区中数据读到内存中来的时候,这里涉及最重要的两个地址,一个就是ZTEXTADDR还有一个是INITRD_PHYS。不管用什么方式来生成IMG都要让bootloader有方法知道这些参数,不然就不知道应该将数据从FLASH读入以后放在什么地方,下一步也不知道从哪个地方开始执行了;
2)bootloader将IMG载入RAM以后,并跳到zImage的地址开始解压的时候,这里就涉及到另外一个重要的参数,那就是ZRELADDR,就是解压后的kernel应该放在哪。这个参数一般都是arch/arm/mach-xxx下面的Makefile.boot来提供的;
3)另外现在解压的代码head.S和misc.c一般都会以PIC的方式来编译,这样载入RAM在任何地方都可以运行,这里涉及到两次冲定位的过程,基本上这个重定位的过程在ARM上都是差不多一样的。 |
|