Chinaunix首页 | 论坛 | 博客
  • 博客访问: 250247
  • 博文数量: 101
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 95
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-12 12:35
文章分类

全部博文(101)

文章存档

2016年(5)

2015年(16)

2014年(37)

2013年(32)

2012年(8)

2011年(3)

我的朋友

分类: LINUX

2014-04-06 10:48:16

 虽然这里的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的文件系统。我们可以看看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 /compressed/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,不需要消息了解的话主要是看一下一些伪指令的含义( /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上都是差不多一样的。

阅读(919) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~