首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Android arm linux kernel启动流程(1)

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上都是差不多一样的。
返回列表