分享

【转】Android kernel启动流程

 灬木木的花灬 2015-08-19
转自:http://blog.csdn.net/yili_xie/article/details/5716837

http://blog.csdn.net/yili_xie/article/details/5735235

http://blog.csdn.net/yili_xie/article/details/5750664


 
Android arm linux kernel启动流程(一)
分类: Android2010-07-06 19:19 14542人阅读 评论(7) 收藏 举报
linuxandroidmakefileimagecachealignment

    虽然这里的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项是必须的,最后一项是可选的。

     
[c-sharp] view plaincopyprint?

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

     
[c-sharp] view plaincopyprint?

    #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在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 ~

     
[c-sharp] view plaincopyprint?

    $(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(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:

     
[c-sharp] view plaincopyprint?

    $(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/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./binutils/docs-2.12/as.info/Pseudo-Ops.html#Pseudo%20Ops)

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

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

     
[c-sharp] view plaincopyprint?

    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出来的汇编的流程就比较清晰了。

     
[c-sharp] view plaincopyprint?

    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的值,然后进入超级用户模式,并关闭中断。

       
[c-sharp] view plaincopyprint?

    .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以后的一个值,adr r0, LC0编译之后实际上为add r0, pc, #208,这个208就是LC0到.text段头部的偏移。

     
[c-sharp] view plaincopyprint?

    add r5, r5, r0 
    add r6, r6, r0 
    add ip, ip, r0 

 


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

     
[c-sharp] view plaincopyprint?

    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。

     
[c-sharp] view plaincopyprint?

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

 


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

     
[c-sharp] view plaincopyprint?

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

     
[c-sharp] view plaincopyprint?

    * 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后面去了,然后跳转到重定位代码开始执行。

     
[c-sharp] view plaincopyprint?

    /*
     * 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上都是差不多一样的。
 
Android arm linux kernel启动流程(二)
分类: Linux Android2010-07-14 18:24 10847人阅读 评论(3) 收藏 举报

     写这个总结的时候咱的心情是沉重的,因为还有好多东西没弄明白。。。感叹自己的知识还是浅薄得很,前途钱途漫漫阿~~不过基本脉络是清楚的,具体的细节只能留在以后有时间再啃了。这里的第二部分启动流程指的是解压后kernel开始执行的一部分代码,这部分代码和ARM体系结构是紧密联系在一起的,所以最好是将ARM ARCHITECTURE REFERENCE MANUL仔细读读,尤其里面关于控制寄存器啊,MMU方面的内容~

      前面说过解压以后,代码会跳到解压完成以后的vmlinux开始执行,具体从什么地方开始执行我们可以看看生成的vmlinux.lds(arch/arm/kernel/)这个文件:

     
[c-sharp] view plaincopyprint?

    OUTPUT_ARCH(arm) 
    ENTRY(stext) 
    jiffies = jiffies_64; 
    SECTIONS 
    { 
     . = 0x80000000 + 0x00008000; 
     .text.head : {  
      _stext = .; 
      _sinittext = .; 
      *(.text.h 


      很明显我们的vmlinx最开头的section是.text.head,这里我们不能看ENTRY的内容,以为这时候我们没有操作系统,根本不知道如何来解析这里的入口地址,我们只能来分析他的section(不过一般来说这里的ENTRY和我们从seciton分析的结果是一样的),这里的.text.head section我们很容易就能在arch/arm/kernel/head.S里面找到,而且它里面的第一个符号就是我们的stext:

     
[c-sharp] view plaincopyprint?

    .section ".text.head", "ax" 
    Y(stext) 
    msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode 
                        @ and irqs disabled 
    mrc p15, 0, r9, c0, c0      @ get processor id 
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid 

 


      这里的ENTRY这个宏实际我们可以在include/linux/linkage.h里面找到,可以看到他实际上就是声明一个GLOBAL Symbol,后面的ENDPROC和END唯一的区别是前面的声明了一个函数,可以在c里面被调用。

     
[c-sharp] view plaincopyprint?

    #ifndef ENTRY 
    #define ENTRY(name) / 
      .globl name; / 
      ALIGN; / 
      name: 
    #endif 
    #ifndef WEAK 
    #define WEAK(name)     / 
        .weak name;    / 
        name: 
    #endif 
    #ifndef END 
    #define END(name) / 
      .size name, .-name 
    #endif 
    /* If symbol 'name' is treated as a subroutine (gets called, and returns)
     * then please use ENDPROC to mark 'name' as STT_FUNC for the benefit of
     * static analysis tools such as stack depth analyzer.
     */ 
    #ifndef ENDPROC 
    #define ENDPROC(name) / 
      .type name, @function; / 
      END(name) 
    #endif 

 


 

      找到了vmlinux的起始代码我们就来进行分析了,先总体概括一下这部分代码所完成的功能,head.S会首先检查proc和arch以及atag的有效性,然后会建立初始化页表,并进行CPU必要的处理以后打开MMU,并跳转到start_kernel这个symbol开始执行后面的C代码。这里有很多变量都是我们进行kernel移植时需要特别注意的,下面会一一讲到。

      在这里我们首先看看这段汇编开始跑的时候的寄存器信息,这里的寄存器内容实际上是同bootloader跳转到解压代码是一样的,就是r1=arch  r2=atag addr。下面我们就具体来看看这个head.S跑的过程:

     
[c-sharp] view plaincopyprint?

    msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode 
                        @ and irqs disabled 
    mrc p15, 0, r9, c0, c0      @ get processor id 

 


      首先进入SVC模式并关闭所有中断,并从arm协处理器里面读到CPU ID,这里的CPU主要是指arm架构相关的CPU型号,比如ARM9,ARM11等等。

     
[c-sharp] view plaincopyprint?

     

 


       然后跳转到__lookup_processor_type,这个函数定义在head-common.S里面,这里的bl指令会保存当前的pc在lr里面,最后__lookup_processor_type会从这个函数返回,我们具体看看这个函数:     

      
[c-sharp] view plaincopyprint?

    __lookup_processor_type: 
        adr r3, 3f 
        ldmda   r3, {r5 - r7} 
        sub r3, r3, r7          @ get offset between virt&phys 
        add r5, r5, r3          @ convert virt addresses to 
        add r6, r6, r3          @ physical address space 
    1:  ldmia   r5, {r3, r4}            @ value, mask 
        and r4, r4, r9          @ mask wanted bits 
        teq r3, r4 
        beq 2f 
        add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list) 
        cmp r5, r6 
        blo 1b 
        mov r5, #0              @ unknown processor 
    2:  mov pc, lr 
    ENDPROC(__lookup_processor_type) 

 


       他这里的执行过程其实比较简单就是在__proc_info_begin和__proc_info_end这个段里面里面去读取我们注册在里面的proc_info_list这个结构体,这个结构体的定义在arch/arm/include/asm/procinfo.h,具体实现根据你使用的cpu的架构在arch/arm/mm/里面找到具体的实现,这里我们使用的ARM11是proc-v6.S,我们可以看看这个结构体:

     
[c-sharp] view plaincopyprint?

    .section ".proc.info.init", #alloc, #execinstr 
    /* 
     * Match any ARMv6 processor core.
     */ 
    .type   __v6_proc_info, #object 
    _proc_info: 
    .long   0x0007b000 
    .long   0x0007f000 
    .long   PMD_TYPE_SECT | / 
        PMD_SECT_BUFFERABLE | /         
        PMD_SECT_CACHEABLE | / 
        PMD_SECT_AP_WRITE | / 
        PMD_SECT_AP_READ 
    .long   PMD_TYPE_SECT | / 
        PMD_SECT_XN | / 
        PMD_SECT_AP_WRITE | / 
        PMD_SECT_AP_READ 
    b   __v6_setup 
    .long   cpu_arch_name 
    .long   cpu_elf_name 
    .long   HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA 
    .long   cpu_v6_name 
    .long   v6_processor_functions  
    .long   v6wbi_tlb_fns 
    .long   v6_user_fns 
    .long   v6_cache_fns 
    .size   __v6_proc_info, . - __v6_proc_info 

 


       对着.h我们就知道各个成员变量的含义了,他这里lookup的过程实际上是先求出这个proc_info_list的实际物理地址,并将其内容读出,然后将其中的mask也就是我们这里的0x007f000与寄存器与之后与0x007b00进行比较,如果一样的话呢就校验成功了,如果不一样呢就会读下一个proc_info的信息,因为proc一般都是只有一个的,所以这里一般不会循环,如果检测正确寄存器就会将正确的proc_info_list的物理地址赋给寄存器,如果检测不到就会将寄存器值赋0,然后通过LR返回。

       
[c-sharp] view plaincopyprint?

    bl  __lookup_machine_type       @ r5=machinfo 
    movs    r8, r5              @ invalid machine (r5=0)?       
    beq __error_a           @ yes, error 'a' 

 


       检测完proc_info_list以后就开始检测machine_type了,这个函数的实现也在head-common.S里面,我们看看它具体的实现:

       
[c-sharp] view plaincopyprint?

    __lookup_machine_type: 
        adr r3, 3b 
        ldmia   r3, {r4, r5, r6} 
        sub r3, r3, r4          @ get offset between virt&phys  
        add r5, r5, r3          @ convert virt addresses to     
        add r6, r6, r3          @ physical address space        
    1:  ldr r3, [r5, #MACHINFO_TYPE]    @ get machine type 
        teq r3, r1              @ matches loader number?        
        beq 2f              @ found     
        add r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc 
        cmp r5, r6 
        blo 1b 
        mov r5, #0              @ unknown machine 
    2:  mov pc, lr 
    ENDPROC(__lookup_machine_type) 

 


        这里的过程基本上是同proc的检查是一样的,这里主要检查芯片的类型,比如我们现在的芯片是MSM7X27FFA,这也是一个结构体,它的头文件在arch/arm/include/asm/arch/arch.h里面(machine_desc),它具体的实现根据你对芯片类型的选择而不同,这里我们使用的是高通的7x27,具体实现在arch/arm/mach-msm/board-msm7x27.c里面,这些结构体最后都会注册到_arch_info_begin和_arch_info_end段里面,具体的大家可以看看vmlinux.lds或者system.map,这里的lookup会根据bootloader传过来的nr来在__arch_info里面的相匹配的类型,没有的话就寻找下一个machin_desk结构体,直到找到相应的结构体,并会将结构体的地址赋值给寄存器,如果没有的话就会赋值为0的。一般来说这里的machine_type会有好几个,因为不同的芯片类型可能使用的都是同一个cpu架构。

       对processor和machine的检查完以后就会检查atags parameter的有效性,关于这个atag具体的定义我们可以在./include/asm/setup.h里面看到,它实际是一个结构体和一个联合体构成的结合体,里面的size都是以字来计算的。这里的atags param是bootloader创建的,里面包含了ramdisk以及其他memory分配的一些信息,存储在boot.img头部结构体定义的地址中,具体的大家可以看咱以后对bootloader的分析~

     
[c-sharp] view plaincopyprint?

    __vet_atags: 
        tst r2, #0x3            @ aligned?  
        bne 1f 
        ldr r5, [r2, #0]            @ is first tag ATAG_CORE?       
        cmp r5, #ATAG_CORE_SIZE 
        cmpne   r5, #ATAG_CORE_SIZE_EMPTY 
        bne 1f 
        ldr r5, [r2, #4] 
        ldr r6, =ATAG_CORE 
        cmp r5, r6 
        bne 1f 
        mov pc, lr              @ atag pointer is ok            
    1:  mov r2, #0 
        mov pc, lr 
    ENDPROC(__vet_atags) 

 


       这里对atag的检查主要检查其是不是以ATAG_CORE开头,size对不对,基本没什么好分析的,代码也比较好看~ 下面我们来看后面一个重头戏,就是创建初始化页表,说实话这段内容我没弄清楚,它需要对ARM VIRT MMU具有相当的理解,这里我没有太多的时间去分析spec,只是粗略了翻了ARM V7的manu,知道这里建立的页表是arm的secition页表,完成内存开始1m内存的映射,这个页表建立在kernel和atag paramert之间,一般是4000-8000之间~具体的代码和过程我这里就不贴了,大家可以看看参考的链接,看看其他大虾的分析,我还没怎么看明白,等以后仔细研究ARM MMU的时候再回头来仔细研究了,不过代码虽然不分析,这里有几个重要的地址需要特别分析下~

      这几个地址都定义在arch/arm/include/asm/memory.h,我们来稍微分析下这个头文件,首先它包含了arch/memory.h,我们来看看arch/arm/mach-msm/include/mach/memory.h,在这个里面定义了#define PHYS_OFFSET     UL(0x00200000) 这个实际上是memory的物理内存初始地址,这个地址和我们以前在boardconfig.h里面定义的是一致的。然后我们再看asm/memory.h,他里面定义了我们的memory虚拟地址的首地址#define PAGE_OFFSET     UL(CONFIG_PAGE_OFFSET)。     

      另外我们在head.S里面看到kernel的物理或者虚拟地址的定义都有一个偏移,这个偏移又是从哪来的呢,实际我们可以从arch/arm/Makefile里面找到:textofs-y   := 0x00008000     TEXT_OFFSET := $(textofs-y) 这样我们再看kernel启动时候的物理地址和链接地址,实际上它和我们前面在boardconfig.h和Makefile.boot里面定义的都是一致的~

      建立初始化页表以后,会首先将__switch_data这个symbol的链接地址放在sp里面,然后获得__enable_mmu的物理地址,然后会跳到__proc_info_list里面的INITFUNC执行,这个偏移是定义在arch/arm/kernel/asm-offset.c里面,实际上就是取得__proc_info_list里面的__cpu_flush这个函数执行。

     
[c-sharp] view plaincopyprint?

    ldr r13, __switch_data      @ address to jump to after      
                        @ mmu has been enabled          
    adr lr, __enable_mmu        @ return (PIC) address 
    add pc, r10, #PROCINFO_INITFUNC 

 


      这个__cpu_flush在这里就是我们proc-v6.S里面的__v6_setup函数了,具体它的实现我就不分析了,都是对arm控制寄存器的操作,这里转一下它对这部分操作的注释,看完之后就基本知道它完成的功能了。

 /*

 *  __v6_setup

 *

 *  Initialise TLB, Caches, and MMU state ready to switch the MMU

 *  on.  Return in r0 the new CP15 C1 control register setting.

 *

 *  We automatically detect if we have a Harvard cache, and use the

 *  Harvard cache control instructions insead of the unified cache

 *  control instructions.

 *

 *  This should be able to cover all ARMv6 cores.

 *

 *  It is assumed that:      

 *  - cache type register is implemented

 */   

        完成这部分关于CPU的操作以后,下面就是打开MMU了,这部分内容也没什么好说的,也是对arm控制寄存器的操作,打开MMU以后我们就可以使用虚拟地址了,而不需要我们自己来进行地址的重定位,ARM硬件会完成这部分的工作。打开MMU以后,会将SP的值赋给PC,这样代码就会跳到__switch_data来运行,这个__switch_data是一个定义在head-common.S里面的结构体,我们实际上是跳到它地一个函数指针__mmap_switched处执行的。

        这个switch的执行过程我们只是简单看一下,前面的copy data_loc段以及清空.bss段就不用说了,它后面会将proc的信息和machine的信息保存在__switch_data这个结构体里面,而这个结构体将来会在start_kernel的setup_arch里面被使用到。这个在后面的对start_kernel的详细分析中会讲到。另外这个switch还涉及到控制寄存器的一些操作,这里我不没仔细研究spec,不懂也就不说了~

        好啦,switch操作完成以后就会b start_kernel了~ 这样就进入了c代码的运行了,下一篇文章仔细研究这个start_kernel的函数~~

 

 

 

 

Ref:

http://linux./bbs/thread-1021226-1-1.html

http://blog.csdn.net/yhmhappy2006/archive/2008/08/06/2775239.aspx

http://blog.csdn.net/sustzombie/archive/2010/06/12/5667607.aspx


 
Arm linux kernel 启动之start_kernel (一)
分类: Linux2010-07-20 19:53 6074人阅读 评论(3) 收藏 举报

      了解完kernel启动以前的汇编之后我们来看看正式的c语言启动代码,也就是我们的start_kernel函数了。start_kernel相当大,里面每一个调用到的函数都足够我们伤脑筋了,我这里只是浅尝辄止的描述一下函数的功能,从而对kernel启动的过程有一个比较直观的了解。很多函数真正理解需要对linux相关体系有很深的了解,暂时没有时间深入,留待以后了。

       说实话启动的代码看到现在唯一的感觉就是kernel的全局变量实在太多了,要了解一个过程跟踪一个变量的值的变化相当痛苦啊,不过耐心看下来,收获还是比较丰富的,对很多概念都有了一个比较直观的理解。闲话就不多说了,直接来上代码~~

       smp_setup_processor_id();

       //这个函数现在是空的;

        lockdep_init();

        //Runtime  locking correctness validator, see Documentation/lockdep_design.txt

        debug_objects_early_init();

        cgroup_init_early();

        //Control group, read Documentation/cgroup.txt

        local_irq_disable();

        //使用arm cpsid i指令来禁止IRQ

        early_boot_irqs_off();

        early_init_irq_lock_class();

        /* 基本上面几个函数就是初始化lockdep和cgroup,然后禁止IRQ,为后续的运行创造条件 */

       lock_kernel();

       /* 看这个函数的之前我们首先来了解一段知识,linux kernel默认是支持preemption(抢占)的。在SMP环境下为了实现kernel的锁定,kernel使用了一个BKL(big kernel lock)的概念,在初始化的过程中先锁定这个BKL,然后再继续进行其他启动或者初始化过程,这样就可以防止启动过程中被中断,执行到res_init以后,kernel会释放这个锁,这样就确保了整个start_kernel过程都不会被抢占或者中断。由此我们可以看到这主要是针对多处理器而言的,实际上如果只有一个处理器的话它也不会被中断或者被抢占。 */

       /* 下面我们来看看这个函数的执行过程,在这里面能学到很多东西的:

            int depth = current->lock_depth+1;

            这个current实际上是一个宏,定义在arch/arm/include/asm/current.h里面,它实际是一个task_struct的结构体,这个结构体描述了这个task的一系列信息(task应该是linux进程调度的一个基本单位?)。linux下每个进程都有一个相对应的task_struct,这个task_struct有几个我们经常能看到的信息,一个就是PID,然后就是comm进程的名字,然后就是mm_struct,它定义了跟这个进程相关的所有申请的memory的管理。这个curren_thread_info()也是个很有意思的函数,直接读取SP来获得当前线程的结构体信息thread_info。thread_info和task_struct这两个结构体应该就代表了当前线程的所有信息。

             初始化的lock_depth是-1,只有init task的lock_depth是0。

              if (likely(!depth))

                  __lock_kernel();

              这里判断是不是init task,如果是就会执行__lock_kernel();这个__lock_kernel首先禁止抢占,然后试着获得BKL,如果成功则直接返回,如果不成功首先判断是否禁止抢占成功了,如果成功了就用自旋锁去锁BKL。如果禁止抢占没有成功则在抢占有效的情况下去等待BKL,直到获得BKL。因为QC的片子不是SMP,所有这里第一次try的时候就直接成功了。

              current->lock_depth = depth;

              这个就没什么好说的了 */

        /* 基本上来说这个lock_kernel就是禁止抢占,然后获得BKL,干了这么件事 */

 

         tick_init();

         //和时钟相关的初始化,好像是注册notify事件,没有仔细研究过

         boot_cpu_init();

         //这个实际上是在SMP环境下选择CPU,这里直接CPUID选择的是0号cpu

         page_address_init();

         //初始化high memory,在arm环境下实际上这个函数是空的,也就是说arm不支持high memory

         printk(KERN_NOTICE);

         printk(linux_banner);

         //这里的KER_NOTICE是字符串<5>,不太明白它的意思。。。后面的linux_banner定义在kernel/init/version.c里面,这里的printk是门高深的学问,以后看console的时候会仔细分析

        setup_arch(&command_line);

         /* 这是一个重量级的函数了,会比较仔细地分析一下,主要完成了4个方面的工作,一个就是取得MACHINE和PROCESSOR的信息然或将他们赋值给kernel相应的全局变量,然后呢是对boot_command_line和tags接行解析,再然后呢就是memory、cach的初始化,最后是为kernel的后续运行请求资源。 */

         /* 我们来仔细看看这个函数的实现:

              setup_processor();

              这个函数首先从arm寄存器里面取得cpu ID,然后调用lookup_processor_type来取得proc_info_list这个结构体。这个过程实际上和我们在head-common.S里面的过程是一样的,不知道这里为什么不直接去读switch_data里面已经保存好的数据反而又查询一遍是为什么?取得proc_info_list以后,将里面的内容一个个赋值给相应的全局变量,然后将CPU的信息打印出来。然后它会从arm寄存器里面获得cache的信息,并将cache的信息打印出来。最后它会调用cpu_proc_init()的函数,这个函数实际上定义在proc-v6.S里面,没有做任何事情。

               mdesc = setup_machine(machine_arch_type);

               首先这个machine_arch_type定义在生成的./include/asm-arm/mach-types.h里面,这个setup_macine实际上也是和上面的processor类似,都是调用head-common.S里面的函数,根据machine ID来获得Machine的信息,并将Machine name打印出来。

               if (mdesc->soft_reboot)

                    reboot_setup("s");

                设置reboot方式,默认是硬启动;

               if (__atags_pointer)                                                              

                      tags = phys_to_virt(__atags_pointer);                                          

               else if (mdesc->boot_params)                                                      

                       tags = phys_to_virt(mdesc->boot_params);

              这里首先判断head-common.S里面定义的__atags_pointer是不是为空,不为空的话说明bootloader设置了初始化参数,将参数的物理地址转化为虚拟地址,这里有一个覆盖,就是说可以在Machine desc里面对初始化参数的物理地址重新定位。

               if (tags->hdr.tag != ATAG_CORE)                                                    

                            convert_to_tag_list(tags);

               if (tags->hdr.tag != ATAG_CORE)                                                    

                             tags = (struct tag *)&init_tags;

               首先判断是不是正确的atag格式,如果是以前老版本的param_struct格式会首先将其转换成tag格式,如果转换以后还是不对,则使用默认的init_tags,这里判断的过程都是根据结构体第一个值是不是ATAG_CORE.

               if (mdesc->fixup)

                      mdesc->fixup(mdesc, tags, &from, &meminfo);                                    

                if (tags->hdr.tag == ATAG_CORE) {

                       if (meminfo.nr_banks != 0)                                                     

                             squash_mem_tags(tags);

                 save_atags(tags);                                                              

                 parse_tags(tags);

                 这里首先判断fixup函数指针,这里一般为空,如果不为空就会地用fixup来重新修改memory map,meminfo这个结构体定义在arch/arm/include/asm/setup.h里面,描述了内存块的信息,内存块的个数,每个内存块的起始地址和大小,如果修改了memory map则需要从atag参数里面去掉bootloader传过来的的memory map信息,然后是保存一下atag,这个保存函数在这里实际上是空的,没有做任何操作,最后是对atag参数进行解析。这里要说明一下这里的tags实际上是一个tag的数组或者说队列,里面有多个tag结构体,每一个结构体都是一个header加一个参数,具体的结构我们可以看看setup.h。对ATAG参数的解析全部定义在arch/arm/kernel/setup.c里面,首先在setup.c里面定义了一个类似于这样__tagtable(ATAG_CORE, parse_tag_core)的宏,这个宏实际上是声明了一个放在__tagtable_begin和__tagtable_end段之间结构体,这个结构体定义了这个一个参数类型,和对这个参数类型进行解析的函数。所有的参数解析我们都可以从setup.c里面找到相对应的函数,比如说对boot_commad_line的解析,从config文件得到的default_commad_line就会被ATAG里面获得commad_line给替换掉;再比如ramdisk,就会将ATAG里面的ramdisk的信息赋值给rd_image_start, rd_size等系统的全局变量。

                 init_mm.start_code = (unsigned long) _text;              

                 init_mm.end_code   = (unsigned long) _etext;                     

                 init_mm.end_data   = (unsigned long) _edata;                     

                 init_mm.brk    = (unsigned long) _end;

                 这就就是对init_mm结构体进行赋值,具体不了解这些东西咋用的,但是就是将text和data段给赋值了。

                 memcpy(boot_command_line, from, COMMAND_LINE_SIZE);                                

                 boot_command_line[COMMAND_LINE_SIZE-1] = '/0';                                     

                 parse_cmdline(cmdline_p, from);

                 这里的boot_command_line来自于与config文件里面的CONFIG_CMDLINE,也有可能被ATAG里面的boot参数给覆盖,获得command_line以后对command_line进行解析。这个解析的过程也是在setup.c里面进行的,它首先查找.early_param.init段里面注册的结构体,通过__early_param将early_param结构体注册到这些段里面,实际这个early_param很简单一个就是类似于"initrd="的字符串,用来与command_line里面的字符进行匹配,如果匹配到了就执行early_param里面的那个函数,并将匹配到的字符作为参数传给函数。举个例子比如说我们现在的commadline里面有一句initrd=0x11000000,然后我们首先在early_param.init段里面搜索时候有没有early_param->arg和这个initrd=匹配,找到了就执行它里面的func然后将这个initrd=的值作为参数传进去。

                 paging_init(mdesc);

                 这个函数是个大函数,具体内容没有仔细看,需要对arm MMU了解比较深,这里只贴一下source里面关于这个函数的注释:

 

                 /*

                  * paging_init() sets up the page tables, initialises the zone memory

                  * maps, and sets up the zero page, bad page and bad page tables.

                  */

                 request_standard_resources(&meminfo, mdesc);

                 这个函数用来申请一些应该是内存资源,具体的内容没有仔细研究,看不大懂。。

                 cpu_init();

                 初始化CPU,这里主要是对arm寄存器cpsr的操作

                 init_arch_irq = mdesc->init_irq;

                 system_timer = mdesc->timer;

                 init_machine = mdesc->init_machine;

                 这里将体系结构相关的几个函数,中断,初始化,定时器之类的赋值给kernel全局变量;

                 conswitchp = &vga_con;

                 这里设置了关于console的一个变量,具体不知道怎么用的,以后看console的时候再仔细分析

                 early_trap_init();

                 不知道这个函数具体做什么用的。。。 */

         /* 基本上我们可以总结出setup_arch主要将一些体系结构的相关信息来赋值给kernel的全局变量,包括cpu啊,machine啊,memory,cahce啊,然后kernel再根据这些函数或者变量来做相应的工作,而且很明显地可以看出来这个setup_arch和前面的head.S,head-common.S,proc-v6.S,board-msm7x27.c是紧密联系在一起的 */

 

 

 

        差不多这边就讲到这里,下一篇将start_kernel的剩余部分讲完~~

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多