Android arm linux kernel启动流程(一)

ARM 151浏览

虽然这里的Arm Linux kernel前面加上了Android,但实际上还是和普遍Arm linux kernel启动的过程一样的,这里只是结合一下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  --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

      根据上面的命令我们可以首先看看mkbootimg-msm7627ffa这个工具的源文件:system/core/mkbootimg.c。看完之后我们就能很清晰地看到boot.img的内部构造,它是由boot header /kernel  /ramdisk /second stage构成的,其中前3项是必须的,最后一项是可选的。

      

  1. /* 
  2. ** +-----------------+  
  3. ** | boot header     | 1 page 
  4. ** +-----------------+ 
  5. ** | kernel          | n pages   
  6. ** +-----------------+ 
  7. ** | ramdisk         | m pages   
  8. ** +-----------------+ 
  9. ** | second stage    | o pages 
  10. ** +-----------------+ 
  11. ** 
  12. ** n = (kernel_size + page_size - 1) / page_size 
  13. ** m = (ramdisk_size + page_size - 1) / page_size 
  14. ** o = (second_size + page_size - 1) / page_size 
  15. ** 
  16. ** 0. all entities are page_size aligned in flash 
  17. ** 1. kernel and ramdisk are required (size != 0) 
  18. ** 2. second is optional (second_size == 0 -> no second) 
  19. ** 3. load each element (kernel, ramdisk, second) at 
  20. **    the specified physical address (kernel_addr, etc) 
  21. ** 4. prepare tags at tag_addr.  kernel_args[] is 
  22. **    appended to the kernel commandline in the tags. 
  23. ** 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr 
  24. ** 6. if second_size != 0: jump to second_addr 
  25. **    else: jump to kernel_addr 
  26. */  

 

      关于boot header这个数据结构我们需要重点注意,在这里我们关注其中几个比较重要的值,这些值定义在boot/boardconfig.h里面,不同的芯片对应vendor下不同的boardconfig,在这里我们的值分别是(分别是kernel/ramdis/tags载入ram的物理地址):

      

  1. #define PHYSICAL_DRAM_BASE   0x00200000  
  2. #define KERNEL_ADDR          (PHYSICAL_DRAM_BASE + 0x00008000)  
  3. #define RAMDISK_ADDR         (PHYSICAL_DRAM_BASE + 0x01000000)  
  4. #define TAGS_ADDR            (PHYSICAL_DRAM_BASE + 0x00000100)  
  5. #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在arm linux中的作用。它在kernel启动过程中充当着第一阶段的文件系统,是一个CPIO格式打成的包。通俗上来讲他就是我们将生成的root目录,用CPIO方式进行了打包,然后在kernel启动过程中会被mount作为文件系统,当kernel启动完成以后会执行init,然后将system.img再mount进来作为Android的文件系统。在这里稍微解释下这个mount的概念,所谓mount实际上就是告诉linux虚拟文件系统它的根目录在哪,就是说我这个虚拟文件系统需要操作的那块区域在哪,比如说ramdisk实际上是我们在内存中的一块区域,把它作为文件系统的意思实际上就是告诉虚拟文件系统你的根目录就在我这里,我的起始地址赋给你,你以后就能对我进行操作了。实际上我们也可以使用rom上的一块区域作为根文件系统,但是rom相对ram慢,所以这里使用ramdisk。然后我们在把system.img
mount到ramdisk的system目录,实际上就是将system.img的地址给了虚拟文件系统,然后虚拟文件系统访问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 ~

      

  1. $(obj)/Image: vmlinux FORCE  
  2.     $(call if_changed,objcopy)  
  3.     @echo '  Kernel: $@ is ready'  
  4. $(obj)/compressed/vmlinux: $(obj)/Image FORCE  
  5.     $(Q)$(MAKE) $(build)=$(obj)/compressed $@  
  6. $(obj)/zImage:  $(obj)/compressed/vmlinux FORCE  
  7.     $(call if_changed,objcopy)  
  8.     @echo '  Kernel: $@ is ready'  

      

      我们分解地来看各个步骤,第一个是将vmlinux经过objcopy后生成一个未经压缩的raw binary(Image 4M左右),这里的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:

      

  1. $(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o /  
  2.         $(addprefix $(obj)/, $(OBJS)) FORCE  
  3.     $(call if_changed,ld)  
  4.     @:  
  5. $(obj)/piggy.gz: $(obj)/../Image FORCE  
  6.     $(call if_changed,gzip)  
  7. $(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/Makefile调用,其中zreladdr-y就是我们的kernel被解压以后要释放的地址了,解压代码跑完以后就会跳到这个地址来执行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)

       那么我们现在就开始分析这个解压的过程:

       1)bootloader会传递2个参数过来,分别是r1=architecture ID, r2=atags pointer。head.S从哪部分开始执行呢,这个我们可以看看vmlinx.lds:

      

  1. ENTRY(_start)  
  2. SECTIONS  
  3. {  
  4.   . = 0;  
  5.   _text = .;  
  6.   .text : {   
  7.     _start = .;  
  8.     *(.start)  
  9.     *(.text)  
  10.     *(.text.*)  
  11.     *(.fixup)  
  12.     *(.gnu.warning)  
  13.     *(.rodata)  
  14.     *(.rodata.*)  
  15.     *(.glue_7)  
  16.     *(.glue_7t)  
  17.     *(.piggydata)  
  18.     . = ALIGN(4);  
  19.   }  

 

      可以看到我们最开始的section就是.start,所以我们是从start段开始执行的。ELF对程序的入口地址是有定义的,这可以参照*.lds的语法规则里面有描述,分别是GNU LD的-E ---> *.lds里面的ENTRY定义  ---> start Symbol  ---> .text section --->0。在这里是没有这些判断的,因为还没有操作系统,bootloader会直接跳到这个start的地址开始执行。

       在这里稍微带一句,如果觉得head.S看的不太舒服的话,比如有些跳转并不知道意思,可以直接objdump vmlinx来看,dump出来的汇编的流程就比较清晰了。

      

  1. 1:      mov r7, r1          @ save architecture ID  
  2.         mov r8, r2          @ save atags pointer  
  3. #ifndef __ARM_ARCH_2__  
  4.         /* 
  5.          * Booting from Angel - need to enter SVC mode and disable 
  6.          * FIQs/IRQs (numeric definitions from angel arm.h source). 
  7.          * We only do this if we were in user mode on entry. 
  8.          */  
  9.         mrs r2, cpsr        @ get current mode  
  10.         tst r2, #3          @ not user?  
  11.         bne not_angel       @ 如果不是  
  12.         mov r0, #0x17       @ angel_SWIreason_EnterSVC  
  13.         swi 0x123456        @ angel_SWI_ARM  
  14. not_angel:  
  15.         mrs r2, cpsr        @ turn off interrupts to  
  16.         orr r2, r2, #0xc0       @ prevent angel from running  
  17.         msr cpsr_c, r2  

 

       上面首先保存r1和r2的值,然后进入超级用户模式,并关闭中断。

        

  1. .text  
  2. adr r0, LC0  
  3. ldmia   r0, {r1, r2, r3, r4, r5, r6, ip, sp}  
  4. subs    r0, r0, r1      @ calculate the delta offset  
  5.                 @ if delta is zero, we are  
  6. beq not_relocated       @ running at the address we  
  7.                 @ were linked at.  

 

      这里首先判断LC0当前的运行地址和链接地址是否一样,如果一样就不需要重定位,如果不一样则需要进行重定位。这里肯定是不相等的,因为我们可以通过objdump看到LC0的地址是0x00000138,是一个相对地址,然后adr r0, LC0 实际上就是将LC0当前的运行地址,而我们直接跳到ZTEXTADDR跑的,实际上PC里面现在的地址肯定是0x00208000以后的一个值,adr r0, LC0编译之后实际上为add
r0, pc, #208,这个208就是LC0到.text段头部的偏移。

      

  1. add r5, r5, r0  
  2. add r6, r6, r0  
  3. add ip, ip, r0  

 

      然后就是重定位了,即都加上一个偏移,经过重定位以后就都是绝对地址了。

      

  1. not_relocated:  mov r0, #0  
  2. 1:      str r0, [r2], #4        @ clear bss  
  3.         str r0, [r2], #4  
  4.         str r0, [r2], #4  
  5.         str r0, [r2], #4  
  6.         cmp r2, r3  
  7.         blo 1b  
  8.         /* 
  9.          * The C runtime environment should now be setup 
  10.          * sufficiently.  Turn the cache on, set up some 
  11.          * pointers, and start decompressing. 
  12.          */  
  13.         bl  cache_on  

 

      重定位完成以后打开cache,具体这个打开cache的过程咱没仔细研究过,大致过程是先从C0里面读到processor ID,然后根据ID来进行cache_on。

      

  1. mov r1, sp          @ malloc space above stack  
  2. add r2, sp, #0x10000    @ 64k max  

 

       解压的过程首先是在堆栈之上申请一个空间

      

  1. /* 
  2.  * Check to see if we will overwrite ourselves. 
  3.  *   r4 = final kernel address 
  4.  *   r5 = start of this image 
  5.  *   r2 = end of malloc space (and therefore this image) 
  6.  * We basically want: 
  7.  *   r4 >= r2 -> OK 
  8.  *   r4 + image length <= r5 -> OK 
  9.  */  
  10.         cmp r4, r2  
  11.         bhs wont_overwrite  
  12.         sub r3, sp, r5      @ > compressed kernel size  
  13.         add r0, r4, r3, lsl #2  @ allow for 4x expansion  
  14.         cmp r0, r5  
  15.         bls wont_overwrite  
  16.         mov r5, r2          @ decompress after malloc space  
  17.         mov r0, r5  
  18.         mov r3, r7  
  19.         bl  decompress_kernel  
  20.         add r0, r0, #127 + 128  @ alignment + stack  
  21.         bic r0, r0, #127        @ align the kernel length  

 

        这个过程是判断我们解压出的vmlinx会不会覆盖原来的zImage,这里的final kernel address就是解压后的kernel要存放的地址,而start of this image则是zImage在内存中的地址。根据我们前面的分析,现在这两个地址是重复的,即都是0x00208000。同样r2是我们申请的一段内存空间,因为他是在sp上申请的,而根据vmlinx.lds我们知道stack实际上处与vmlinx的最上面,所以r4>=r2是不可能的,这里首先计算zImage的大小,然后判断r4+r3是不是比r5小,很明显r4和r5的值是一样的,所以这里先将r2的值赋给r0,经kernel先解压到s申请的内存空间上面,具体的解压过程就不描述了,定义在misc.c里面。(这里我所说的上面是指内存地址的高地址,默认载入的时候从低地址往高地址写,所以从内存低地址开始运行,stack处于最后面,所以成说是最上面)

      

  1. * r0     = decompressed kernel length  
  2. * r1-r3  = unused  
  3. * r4     = kernel execution address  
  4. * r5     = decompressed kernel start  
  5. * r6     = processor ID  
  6. * r7     = architecture ID  
  7. * r8     = atags pointer  
  8. * r9-r14 = corrupted  
  9. */  
  10.        add r1, r5, r0      @ end of decompressed kernel  
  11.        adr r2, reloc_start  
  12.        ldr r3, LC1  
  13.        add r3, r2, r3  
  14. :      ldmia   r2!, {r9 - r14}     @ copy relocation code  
  15.        stmia   r1!, {r9 - r14}  
  16.        ldmia   r2!, {r9 - r14}  
  17.        stmia   r1!, {r9 - r14}  
  18.        cmp r2, r3  
  19.        blo 1b  
  20.        add sp, r1, #128        @ relocate the stack  
  21.        bl  cache_clean_flush  
  22.        add pc, r5, r0      @ call relocation code  

 

      因为没有将kernel解压在要求的地址,所以必须重定向,说穿了就是要将解压的kernel拷贝到正确的地址,因为正确的地址与zImage的地址是重合的,而要拷贝我们又要执行zImage的重定位代码,所以这里首先将重定位代码reloc_start拷贝到vmlinx上面,然后再将vmlinx拷贝到正确的地址并覆盖掉zImage。这里首先计算出解压后的vmlinux的高地址放在r1里面,r2存放着重定位代码的首地址,r3存放着重定位代码的size,这样通过拷贝就将reloc_start移动到vmlinx后面去了,然后跳转到重定位代码开始执行。

      

  1. /* 
  2.  * All code following this line is relocatable.  It is relocated by 
  3.  * the above code to the end of the decompressed kernel image and 
  4.  * executed there.  During this time, we have no stacks. 
  5.  * 
  6.  * r0     = decompressed kernel length 
  7.  * r1-r3  = unused 
  8.  * r4     = kernel execution address 
  9.  * r5     = decompressed kernel start 
  10.  * r6     = processor ID 
  11.  * r7     = architecture ID 
  12.  * r8     = atags pointer 
  13.  * r9-r14 = corrupted 
  14.  */  
  15.         .align  5  
  16. reloc_start:    add r9, r5, r0  
  17.         sub r9, r9, #128        @ do not copy the stack  
  18.         debug_reloc_start  
  19.         mov r1, r4  
  20. 1:  
  21.         .rept   4  
  22.         ldmia   r5!, {r0, r2, r3, r10 - r14}    @ relocate kernel  
  23.         stmia   r1!, {r0, r2, r3, r10 - r14}  
  24.         .endr  
  25.         cmp r5, r9  
  26.         blo 1b  
  27.         add sp, r1, #128        @ relocate the stack  
  28.         debug_reloc_end  
  29. call_kernel:    bl  cache_clean_flush  
  30.         bl  cache_off  
  31.         mov r0, #0          @ must be zero  
  32.         mov r1, r7          @ restore architecture number  
  33.         mov r2, r8          @ restore atags pointer  
  34.         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上都是差不多一样的。