Android系统启动流程 二--bootloader启动

无论是PC还是嵌入式设备,系统上电后都会从一个固有地址开始运行程序。在PC上,这个程序就是BIOS,存储于主板的flash上。
在嵌入式设备上,处理器会调用reset向量从flash/ROM上的固定地址开始执行。

 
高通平台的Bootloader资料较少,需要与底软同事一起完成。


System startup
On an embedded platform, a bootstrap environment is used when the system is powered on, or reset. Examples include U-Boot, RedBoot, and MicroMonitor from Lucent. Embedded platforms are commonly shipped with a boot monitor. These programs reside in special region of flash memory on the target hardware and provide the means to download a Linux kernel image into flash memory and subsequently execute it. In addition to having the ability to store and boot a Linux image, these boot monitors perform some level of system test and hardware initialization. In an embedded target, these boot monitors commonly cover both the first- and second-stage boot loaders.

 
具体代码如下:
 
Major functions flow for the Linux kernel i386 boot
 

Linux启动之 /kernel/arch/arm/boot/compressed/ head.S分析

 
这段代码是linux boot后执行的第一个程序,完成的主要工作是解压内核,然后跳转到相关执行地址。这部分代码在做驱动开发时不需要改动,但分析其执行流程对是理解android的第一步
 
 
 
开头有一段宏定义这是gnu arm汇编的宏定义。关于GUN的汇编和其他编译器,在指令语法上有很大差别,具体可查询相关GUN汇编语法了解
 
另外此段代码必须不能包括重定位部分。因为这时一开始必须要立即运行的。所谓重定位,比如当编译时某个文件用到外部符号是用动态链接库的方式,那么该文件生成的目标文件将包含重定位信息,在加载时需要重定位该符号,否则执行时将因找不到地址而出错
 
#ifdef DEBUG//开始是调试用,主要是一些打印输出函数,不用关心
 
 
 
#if defined(CONFIG_DEBUG_ICEDCC)
 
……具体代码略
 
#endif
 
 
 
宏定义结束之后定义了一个段,
 
.section ".start", #alloc, #execinstr
 
 
 
这个段的段名是 .start,#alloc表示Section contains allocated data, #execinstr表示 Section contains executable instructions.
 
生成最终映像时,这段代码会放在最开头
 
              .align
 
start:
 
              .type       start,# function /*.type指定start这个符号是函数类型 */
 
              .rept 8
 
              mov r0, r0 // 将此命令重复8次,相当于nop,这里是为中断向量保存空间
 
.endr
 
 
 
              b     1f
 
              .word      0x016f2818           @ Magic numbers to help the loader
 
              .word      start               @ absolute load/run zImage
 
// 此处保存了内核加载和运行的地址,实质上也是本函数的运行地址
 
address
 
              .word      _edata                   @  内核结束地址
 
//注意这些地址在顶层vmlixu.lds(具体在/kernel文件夹里)里进行了定义,是链接的地址,加载内核后可能会进行重定位
 
1:            mov r7, r1                    @  保存architecture ID,这里是从bootload传递进来的
 
              mov r8, r2                    @  保存参数列表 atags指针
 
r1和r2中分别存放着由bootloader传递过来的architecture ID和指向标记列表的指针。这里将这两个参数先保存。
 
 
 
#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.
 
               */
 
读取cpsr并判断是否处理器处于supervisor模式——从bootload进入kernel,系统已经处于SVC32模式;而利用angel进入则处于user模式,还需要额外两条指令。之后是再次确认中断关闭,并完成cpsr写入
 
 
 
Angel 是 ARM的调试协议,一般用的是MULTI-ICE。ANGLE需要在板子上有驻留程序,然后通过串口就可以调试了。用过的AXD或trace调试环境的话,对此应该比较熟悉。  
 
not_angel:  // 若不是通过angel调试进入内核
 
              mrs  r2, cpsr          @ turn off interrupts to
 
              orr   r2, r2, #0xc0         @ prevent angel from running
 
              msr  cpsr_c, r2   // 这里将cpsr中I、F位分别置“1”,关闭IRQ和FIQ
 
#else
 
              teqp pc, #0x0c000003          @ turn off interrupts
 
常用 TEQP PC,#(新模式编号)来改变模式
 
#endif
 
   另外链接器会把一些处理器相关的代码链接到这个位置,也就是arch/arm/boot/compressed/head-xxx.S文件中的代码。在高通平台下,这个文件是head-msm.S连接脚是compress/vmlinux.lds,其中部分内容大致如下,在连接时,连接器根据每个文件中的段名将相同的段合在一起,比如将head.S和head-msm.S的.start段合在一起
 
SECTIONS
 
{
 
  . = TEXT_START;
 
  _text = .;
 
 
 
  .text : {
 
    _start = .;
 
    *(.start)
 
    *(.text)
 
    *(.text.*)
 
    *(.fixup)
 
    *(.gnu.warning)
 
    *(.rodata)
 
    *(.rodata.*)
 
    *(.glue_7)
 
    *(.glue_7t)
 
    *(.piggydata)
 
    . = ALIGN(4);
 
  }
 
 
 
  _etext = .;
 
 
 
 
下面即进入.text段
 
   .text
 
    adr    r0, LC0 //当前运行时LC0符号所在地址位置,注意,这里用的是adr指令,这个指令会根据目前PC的值,计算符号相对于PC的位置,是个相对地址。之所以这样做,是因为下面指令用到了绝对地址加载ldmia指令,必须要调整确定目前LC0的真实位置,这个位置也就是用adr来计算
 
    ldmia       r0, {r1, r2, r3, r4, r5, r6, ip, sp}
 
   subs r0, r0, r1        @ // 这里获得当前LCD0实际地址与链接地址差值
 
//r1即是LC0的连接地址,也即由vmlinux.lds定位的地址
 
//差值存入r0中。
 
               beq  not_relocated // 如果相等不需要重定位,因为已经在正确的//地址运行了。重定位的原因是,MMU单元未使能,不能进行地址映射,必须要手工重定位。
 
下面举个简单例子说明:
 
如果连接地址是0xc0000000,那么LC0的连接地址假如连接为0xc0000010,那么LC0相对于连接起始地址的差为0x10,当此段代码是从0xc0000000运行的话,那么执行adr r0,LC0的值实际上按下面公式计算:
 
R0=PC+0x10,由于PC=连接处的值,可知,此时是在ram中运行,同理如果是在不是在连接处运行,则假设是在0x00000000处运行,则R0=0x00000000+0x10,可知,此时不是在ram的连接处运行。
 
上面这几行代码用于判断代码是否已经重定位到内存中,LC0这个符号在head.S中定义如下,实质上相当于c语言的全局数据结构,结构的每个域存储的是一个指针。指针本身的值代表不同的代码段,已经在顶层连接脚本vmlinux.lds里进行了赋值,比如_start是内核开始的地址
 
              .type       LC0, #object
 
LC0:              .word      LC0               @ r1 // 这个要加载到r1中的LC0是链接时LC0的地址
 
              .word      __bss_start            @ r2
 
              .word      _end                     @ r3
 
              .word      zreladdr          @ r4
 
              .word      _start                    @ r5
 
              .word      _got_start              @ r6
 
              .word      _got_end        @ ip
 
              .word      user_stack+4096           @ sp
 
通过当前运行时LC0的地址与链接器所链接的地址进行比较判断。若相等则是运行在链接的地址上。
 
 
 
如果不是运行在链接的地址上,则下面的代码必须修改相关地址,进行重新运行
 
              /*
 
               *   r5 - zImage base address
 
               *   r6 - GOT start
 
               *   ip - GOT end
 
               */
 
                            // 修正实际运行的位置,否则跳转指令就找不到相关代码
 
              add  r5, r5, r0 // 修改内核映像基地址
 
              add  r6, r6, r0
 
              add  ip, ip, r0 // 修改got表的起始和结束位置
 
 
 
#ifndef CONFIG_ZBOOT_ROM
 
              /*若 没有定义CONFIG_ZBOOT_ROM,此时运行的是完全位置无关代码
 
位置无关代码,也就是不能有绝对地址寻址。所以为了保持相对地址正确,
 
需要将bss段以及堆栈的地址都进行调整
 
            
 
               *   r2 - BSS start
 
               *   r3 - BSS end
 
               *   sp - stack pointer
 
               */
 
              add  r2, r2, r0
 
              add  r3, r3, r0
 
              add  sp, sp, r0
 
// 全局符号表的地址也需要更改,否则,对全局变量引用将会出错
 
1:            ldr   r1, [r6, #0]            @ relocate entries in the GOT
 
              add  r1, r1, r0        @ table.  This fixes up the
 
              str   r1, [r6], #4            @ C references.
 
              cmp r6, ip
 
              blo   1b
 
#else // 若定义了CONFIG_ZBOOT_ROM,只对got表中在bss段以外的符号进行重定位
 
            
 
1:            ldr   r1, [r6, #0]            @ relocate entries in the GOT
 
              cmp r1, r2                    @ entry < bss_start ||
 
              cmphs     r3, r1                    @ _end < entry
 
              addlo       r1, r1, r0        @ table.  This fixes up the
 
              str   r1, [r6], #4            @ C references.
 
              cmp r6, ip
 
              blo   1b
 
#endif
 
 
 
 
 
 
 
 
 
 
 
如果运行当前运行地址和链接地址相等,则不需进行重定位。直接清除bss段
 
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
 
 
 
之后跳转到cache_on处
 
         
 
              bl     cache_on
 
 
 
cache_on定义
 
              .align       5
 
cache_on:       mov r3, #8                    @ cache_on function
 
              b     call_cache_fn
 
 
 
把r3的值设为8。这是一个偏移量,也就是索引proc_types中的操作函数。
 
然后跳转到call_cache_fn。这个函数的定义如下:
 
 
 
call_cache_fn:
 
adr   r12, proc_types  // 把proc_types的相对地址加载到r12中
 
#ifdef CONFIG_CPU_CP15
 
              mrc p15, 0, r6, c0, c0   @ get processor ID
 
#else
 
              ldr   r6, =CONFIG_PROCESSOR_ID
 
#endif
 
1:            ldr   r1, [r12, #0]          @ get value
 
              ldr   r2, [r12, #4]          @ get mask
 
              eor   r1, r1, r6        @ (real ^ match)
 
              tst    r1, r2           @是 否和CPU ID匹配?
 
              addeq      pc, r12, r3             @  用刚才的偏移量,查找//到cache操作函数,找到后就执行相关操作,比如执行b     __armv7_mmu_cache_on
 
 // 
 
 
 
  add  r12, r12, #4*5 // 如果不相等,则偏移到下个proc_types结构处
 
   b     1b
 
 
 
addeq      pc, r12, r3             @ call cache function
 
 
 
 
 
proc_type的定义如下  , 实质上还是一张数据结构表
 
              .type       proc_types,#object
 
proc_types:
 
              .word      0x41560600           @ ARM6/610
 
              .word      0xffffffe0
 
              b     __arm6_mmu_cache_off      @ works, but slow
 
              b     __arm6_mmu_cache_off
 
              mov pc, lr
 
@           b     __arm6_mmu_cache_on              @ untested
 
@           b     __arm6_mmu_cache_off
 
@           b     __armv3_mmu_cache_flush
 
 
 
              .word      0x00000000           @ old ARM ID
 
              .word      0x0000f000
 
              mov pc, lr
 
              mov pc, lr
 
              mov pc, lr
 
 
 
              .word      0x41007000           @ ARM7/710
 
              .word      0xfff8fe00
 
              b     __arm7_mmu_cache_off
 
              b     __arm7_mmu_cache_off
 
              mov pc, lr
 
 
 
              .word      0x41807200           @ ARM720T (writethrough)
 
              .word      0xffffff00
 
              b     __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_off
 
              mov pc, lr
 
 
 
              .word      0x41007400           @ ARM74x
 
              .word      0xff00ff00
 
              b     __armv3_mpu_cache_on
 
              b     __armv3_mpu_cache_off
 
              b     __armv3_mpu_cache_flush
 
             
 
              .word      0x41009400           @ ARM94x
 
              .word      0xff00ff00
 
              b     __armv4_mpu_cache_on
 
              b     __armv4_mpu_cache_off
 
              b     __armv4_mpu_cache_flush
 
 
 
              .word      0x00007000           @ ARM7 IDs
 
              .word      0x0000f000
 
              mov pc, lr
 
              mov pc, lr
 
              mov pc, lr
 
 
 
              @ Everything from here on will be the new ID system.
 
 
 
              .word      0x4401a100           @ sa110 / sa1100
 
              .word      0xffffffe0
 
              b     __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_off
 
              b     __armv4_mmu_cache_flush
 
 
 
              .word      0x6901b110           @ sa1110
 
              .word      0xfffffff0
 
              b     __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_off
 
              b     __armv4_mmu_cache_flush
 
 
 
              @ These match on the architecture ID
 
 
 
              .word      0x00020000    @
 
              .word      0x000f0000        //
 
b   __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_on  //指令的地址
 
              b     __armv4_mmu_cache_off
 
              b     __armv4_mmu_cache_flush
 
 
 
              .word      0x00050000           @ ARMv5TE
 
              .word      0x000f0000
 
              b     __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_off
 
              b     __armv4_mmu_cache_flush
 
 
 
              .word      0x00060000           @ ARMv5TEJ
 
              .word      0x000f0000
 
              b     __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_off
 
              b     __armv4_mmu_cache_flush
 
 
 
              .word      0x0007b000           @ ARMv6
 
              .word      0x0007f000
 
              b     __armv4_mmu_cache_on
 
              b     __armv4_mmu_cache_off
 
              b     __armv6_mmu_cache_flush
 
 
 
              .word      0                   @ unrecognised type
 
              .word      0
 
              mov pc, lr
 
              mov pc, lr
 
              mov pc, lr
 
 
 
              .size proc_types, . - proc_types
 
    找到执行的cache函数后,就用上面的 addeq   pc, r12, r3 直接跳转,例如执行下面这个处理器结构的cache函数
 
__armv7_mmu_cache_on:
 
         mov r12, lr // 注意,这里需要手工保存返回地址!!这样做的原因是下面的bl指令会覆盖掉原来的lr,为保证程序正确返回,需要保存原来lr的值
 
              bl     __setup_mmu
 
              mov r0, #0
 
              mcr p15, 0, r0, c7, c10, 4     @ drain write buffer
 
              mcr p15, 0, r0, c8, c7, 0      @ flush I,D TLBs
 
              mrc p15, 0, r0, c1, c0, 0      @ read control reg
 
              orr   r0, r0, #0x5000             @ I-cache enable, RR cache replacement
 
              orr   r0, r0, #0x0030   
 
              bl     __common_mmu_cache_on
 
              mov r0, #0
 
              mcr p15, 0, r0, c8, c7, 0      @ flush I,D TLBs
 
              mov pc, r12  //返回到cache_on
 
这个函数首先执行__setup_mmu,然后清空write buffer、I/Dcache、TLB.接着打开i-cache,设置为Round-robin replacement。调用__common_mmu_cache_on,打开mmu和d-cache.把页表基地址和域访问控制写入协处理器寄存器c2、c3. __common_mmu_cache_on函数数定义如下:
 
__common_mmu_cache_on:
 
#ifndef DEBUG
 
              orr   r0, r0, #0x000d             @ Write buffer, mmu
 
#endif
 
              mov r1, #-1 //-1的补码是ffff ffff,
 
              mcr p15, 0, r3, c2, c0, 0      @  把页表地址存于协处理器寄存 器中
 
              mcr p15, 0, r1, c3, c0, 0   @ 设置domain access control寄存器
 
              b     1f                                
 
              .align       5                   @ cache line aligned
 
1:            mcr p15, 0, r0, c1, c0, 0      @ load control register
 
              mrc p15, 0, r0, c1, c0, 0      @ and read it back to
 
              sub  pc, lr, r0, lsr #32    @ properly flush pipeline
 
 
 
重点来看一下__setup_mmu这个函数,定义如下:
 
 
 
__setup_mmu:       sub  r3, r4, #16384        @ Page directory size
 
              bic   r3, r3, #0xff          @ Align the pointer
 
              bic   r3, r3, #0x3f00
 
这里r4中存放着内核执行地址,将16K的一级页表放在这个内核执行地址下面的16K空间里,上面通过 sub   r3, r4, #16384   获得16K空间后,又将页表的起始地址进行16K对齐放在r3中。即ttb的低14位清零。
 
 
 
// 初始化页表,并在RAM空间里打开cacheable和bufferable位
 
              mov r0, r3
 
              mov r9, r0, lsr #18
 
              mov r9, r9, lsl #18         @ start of RAM
 
              add  r10, r9, #0x10000000    @ a reasonable RAM size
 
 
 
上面这几行把一级页表的起始地址保存在r0中,并通过r0获得一个ram起始地址(每个页面大小为1M)然后映射256M ram空间,并把对应的描述符的C和B位均置”1”   
 
               mov r1, #0x12 // 一级描述符的bit[1:0]为10,表示这是一个section描述符。也即分页方式为段式分页 
 
              orr   r1, r1, #3 << 10 // 一级描述符的access permission bits bit[11:10]为11.即
 
 
 
              add  r2, r3, #16384  // 一级描述符表的结束地址存放在r2中。
 
 
 
 
 
1:            cmp r1, r9                    @ if virt > start of RAM
 
              orrhs       r1, r1, #0x0c         @ set cacheable, bufferable
 
              cmp r1, r10                  @ if virt > end of RAM
 
              bichs       r1, r1, #0x0c         @ clear cacheable, bufferable
 
              str   r1, [r0], #4            @ 1:1 mapping
 
              add  r1, r1, # 1048576//下个1M物理空间,每个页框1M。
 
              teq   r0, r2
 
              bne  1b
 
 
 
因为打开cache前必须打开mmu,所以这里先对页表进行初始化,然后打开mmu和cache。
 
上面这段就是对一级描述符表(页表)的初始化,首先比较这个描述符所描述的地址是否在那个256M的空间中,如果在则这个描述符对应的内存区域是cacheable ,bufferable。如果不在则noncacheable, nonbufferable.然后将描述符写入一个一级描述符表的入口,并将一级描述符表入口地址加4,而指向下一个1M s ection的基地址。如果页表入口未初始化完,则继续初始化。
 
 
 
页表大小为16K,每个描述符4字节,刚好可以容纳4096个描述符,每个描述符映射1M      ,那么4096*所以这里就映射了4096*1M = 4G的空间。因此16K的页完全可以把256M地址空间全部映射
 
 
 
              mov r1, #0x1e
 
              orr   r1, r1, #3 << 10 // 这两行将描述的bit[11:10] bit[4:1]置位,
 
//具体置位的原因,在ARM11的页表项描述符里有说明,由于没找到完整的文档,这里只给出图示:
 
 
 
 
 
              mov r2, pc, lsr #20
 
              orr   r1, r1, r2, lsl #20  / /将当前地址进1M对齐,并与r1中的内容结合形成一个描述当前指令所在section的描述符。
 
 
 
              add  r0, r3, r2, lsl #2   // r3为刚才建立的一级描述符表的起始地址。通过将当前地
 
//址(pc)的高12位左移两位(形成14位索引)与r3中的地址
 
                             // ( 低14位为0)相加形成一个4字节对齐的地址,这个
 
                             // 地址也在16K的一级描述符表内。当前地址对应的
 
                             // 描述符在一级页表中的位置
 
                          
 
              str   r1, [r0], #4
 
              add  r1, r1, #1048576
 
              str   r1, [r0]           //这里将上面形成的描述符及其连续的下一个section描述
 
//写入上面4字节对齐地址处(一级页表中索引为r2左移
 
//2位)
 
 
 
              mov pc, lr       // 返回,调用此函数时,调用指令的下一语句 mov   r0, #0 的地址保存在lr中
 
                      
 
 
 
这里进行的是一致性的映射,物理地址和虚拟地址是一样。
 
 
 
__common_mmu_cache_on最后执行mov pc, r12返回cache_on,为何返回到的是cache_on呢?这就是上面解释保存lr的原因,因为原来的lr保存了执行
 
bl     cache_on 语句的下条指令,因此能正确返回!
 
下一条指令也即是下面开始
 
              mov r1, sp                    @ 栈空间大小是4096字节,那//么在栈空间地址上面再分配64K字节空间
 
              add  r2, sp, #0x10000    @  分配64k字节。
 
  栈的分配如下:
 
       .align
 
              .section ".stack", "w"
 
user_stack:    .space    4096/ /lc0对SP进行了定义   .word     user_stack+4096  @ sp
 
由此可见sp是往下增长的
 
分配了解压缩用的缓冲区,那么接下来就判断这个数据区是否和我们目前运行的代码空间重叠,如果重叠则需调整
 
/*
 
 * 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
 
 
 
缓冲区空间的起始地址和结束地址分别存放在r1、r2中。然后判断最终内核地址,也就是解压后内核的起始地址,是否大于malloc空间的结束地址,如果大于就跳到wont_overwrite执行,wont_overwrite函数后面会讲到。否则,检查最终内核地址加解压后内核大小,也就是解压后内核的结束地址,是否小于现在未解压内核映像的起始地址。小于也会跳到wont_owerwrite执行。如两这两个条件都不满足,则继续往下执行。
 
 
 
              mov r5, r2                    @ decompress after malloc space
 
              mov r0, r5
 
              mov r3, r7
 
              bl     decompress_kernel
 
 
 
这里将解压后内核的起始地址设为malloc空间的结束地址。然后后把处理器id(开始时保存在r7中)保存到r3中,调用decompress_kernel开始解压内核。这个函数的四个参数分别存放在r0-r3中,它在arch/arm/boot/compressed/misc.c中定义。解压的过程为先把解压代码放到缓冲区,然后从缓冲区在拷贝到最终执行空间。
 
 
 
              add  r0, r0, #127
 
              bic   r0, r0, #127           @ align the kernel length
 
/*
 
 * 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
 
1:            ldmia       r2!, {r9 - r14}              @ copy relocation code
 
              stmia       r1!, {r9 - r14}
 
              ldmia       r2!, {r9 - r14}
 
              stmia       r1!, {r9 - r14}
 
              cmp r2, r3
 
              blo   1b
 
这里首先计算出重定位段,也即reloc_start段,然后对它的进行重定位
 
 
 
              bl     cache_clean_flush
 
              add  pc, r5, r0        @ call relocation code
 
重定位结束后跳到解压后执行  b       call_kernel, 不再返回。call_kernel定义如下:
 
 
 
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
 
在运行解压后内核之前,先调用了
 
cache_clean_flush这个函数。这个函数的定义如下
 
 
 
cache_clean_flush:
 
              mov r3, #16
 
              b     call_cache_fn
 
其实这里又调用了call_cache_fn这个函数,注意,这里r3的值为16,上面对cache操作已经比较详细,不再讨论。
 
刷新cache后,则执行mov   pc, r4 跳入内核,开始进行下个阶段的处理。
 
====================================================================================
 
第二部分:汇编部分
 
Linux启动之linux-rk3288-tchip/kernel/arch/arm/kernel/head.S
 
整个代码流程如下:
 
当解压缩部分的head.S执行完后,就开始执行kernel/目录下真正的linux内核代码。在内核连接文件/kernel/vmlinux/lds里定义了这部分开始所处的段空间为.text.head,也即内核代码段的头
 
关键代码如下:
 
       mrc p15, 0, r9, c0, c0        @ get processor id// 读出CPUid
 
       bl    __lookup_processor_type         @ r5=procinfo r9=cpuid
 
       movs      r10, r5                         @ invalid processor (r5=0)?
 
       beq __error_p                    @ yes, error 'p'
 
       bl    __lookup_machine_type            @ r5=machinfo
 
       movs      r8, r5                           @ invalid machine (r5=0)?
 
       beq __error_a                    @ yes, error 'a'
 
       bl    __vet_atags
 
       bl    __create_page_tables
 
大致流程为,寻找CPU类型查找机器信息,解析内核参数列表,创建内存分页机制
 
__lookup_processor_type,__lookup_machine_type,__vet_atags函数都在kernel/head-comm.S内,这个文件实际上是被包含在head.S内
 
Linux之所以把搜索机器类型和CPU类型独立出来,就是为了让内核尽可能的和bootload独立,增强移植性,把不同CPU的差异性处理减到最小。比如不同ARM架构的CPU处理中断的,打开MMU,cach操作是不同的,因此,在内核开始执行前需要定位CPU架构,比如高通利用的ARM11,Ti用的cortex-8架构
 
__lookup_machine_type 寻找的机器类型结构定义在arch/arm/include/asm/mach.h中
 
查询方法比较简单,利用bootloa传进来的参数依次查询上述结构表项
 
这个表项是在编译阶段将#define MACHINE_START(_type,_name)宏定义的结构体struct machine_desc连接到
 
__arch_info段,那么结构体开始和结束地址用__arch_info_begin和__arch_info_end符号引用
 
3:    .long       .
 
       .long       __arch_info_begin
 
       .long       __arch_info_end
 
// r1 = 机器架构代码 number,由bootload最后阶段传进来
 
       .type       __lookup_machine_type, %function
 
__lookup_machine_type:
 
       adr  r3, 3b
 
       ldmia      r3, {r4, r5, r6}
 
       sub  r3, r3, r4                     @  此时没有开MMU,因此需要确定放置__arch_info_begin的实际物理地址
 
       add r5, r5, r3                     @  调整地址,找到__arch_info的实际地址(连接地址和物理地址不一定一样,因此需要调整)
 
       add r6, r6, r3                     @
 
1:    ldr   r3, [r5, #MACHINFO_TYPE] @  MACHINFO_TYPE=机器类型域的偏移量
 
       teq  r3, r1                           @  是否和bootload传进来的参数相同?
 
       beq 2f                         @  找到则跳出循环
 
       add r5, r5, #SIZEOF_MACHINE_DESC     @  地址偏移至下个__arch_inf表项
 
       cmp r5, r6
 
       blo  1b
 
       mov r5, #0                          @  未知的类型
 
2:    mov pc, lr// 返回
 
__lookup_processor_type的查询的结构为struct proc_info_list
 
机器类型确定后即开始解析(__vet_atags)内核参数列表,判断第一个参数类型是不是ATAG_CORE。
 
内核参数列表一般放在内核前面16K地址空间处。列表的表项由struct tag构成,每个struct tag有常见的以下类型:
 
:ATAG_CORE 、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。
 
这些类型是宏定义,比如#define ATAG_CORE    0x54410001
 
arch/arm/include/asm/setup.h
 
struct tag_header {
 
       __u32 size;
 
       __u32 tag;
 
};
 
struct tag {
 
       struct tag_header hdr;
 
       union {
 
              struct tag_core             core;// 有效的内核
 
              struct tag_mem32 mem;
 
              struct tag_videotext      videotext;
 
              struct tag_ramdisk       ramdisk;// 文件系统
 
              struct tag_initrd     initrd;// 临时根文件系统
 
              struct tag_serialnr  serialnr;
 
              struct tag_revision revision;
 
              struct tag_videolfb       videolfb;
 
              struct tag_cmdline cmdline;// 命令行
 
       } u;
 
};
 
接下来就是创建页表,因为要使能MMU进行虚拟内存管理,因此必须创建映射用的页表。页表就像一个函数发生器,保证访问虚拟地址时能从物理地址里取到正确代码
 
       pgtbl       r4                         @ page table address
 
// 页表放置的位置可由下面的宏确定,即在内核所在空间的前16K处
 
.macro    pgtbl, rd
 
       ldr   /rd, =(KERNEL_RAM_PADDR - 0x4000)
 
       .endm
 
 
 
       mov r0, r4
 
       mov r3, #0
 
       add r6, r0, # 0x4000//16K的空间,r6即是页表结束处
 
1:    str   r3, [r0], #4/ /清空页表项,页表项共有16K/4项
 
       str   r3, [r0], #4
 
       str   r3, [r0], #4
 
       str   r3, [r0], #4
 
       teq  r0, r6
 
       bne  1b
 
 
 
       ldr   r7, [r10, #PROCINFO_MM_MMUFLAGS]
 
// 从从差得的proc_info_list结构PROCINFO_MM_MMUFLAGS处获取MMU的信息
 
       /*
 
为内核创建1M的映射空间,这里是按照1:1一致映射,即代码的基地址(高12bit)对应相同的物理块地址。这种映射关系只是在启动阶段,在跳进start_kernel后会被paging_init().移除。这种映射可以直接利用当前地址的高12bit作为基地址,这种方式很巧妙,因为当前的PC(加颜色处的地址)依然在1M空间内,因此,高12bit(段基地址)在1M空间内都是相同的。
 
        */
 
       mov r6, pc, lsr #20                       @ 内核映像的基地址
 
       orr   r3, r7, r6, lsl #20         @  基地址偏移后再加上标示符,即可得一个页表项的值
 
       str   r3, [r4, r6, lsl #2]         @ 将此表项按照页表项的索引存入对应的表项中。比如,若//基地址是0xc0001000,那么存入页表的第0xc00项中
 
//目前的映射依然是1:1的映射
 
         // 然后移到下个段基地址处,开始映射此KERNEL_START对应的空间
 
//这个空间映射的物理地址与上面的相同,也就是两个虚拟地址映射到了同一个物理地址空间
 
         //r0+ 基地址组成//在第一级页表中索引到相关的项
 
       add r0, r4,  #(KERNEL_START & 0xff000000) >> 18
 
       str   r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
 
       ldr   r6, =(KERNEL_END - 1)
 
       add r0, r0, #4// 移到下个表项
 
       add r6, r4, r6, lsr #18// 结束的基地址
 
1:    cmp r0, r6
 
       add r3, r3, #1 << 20// 下个1M物理地址空间
 
       strls r3, [r0], #4// 建立映射表项,开始创建所有的内核空间页表项
 
       bls   1b//
 
#ifdef CONFIG_XIP_KERNEL
 
       /*
 
        * Map some ram to cover our .data and .bss areas.
 
        */
 
       orr   r3, r7, #(KERNEL_RAM_PADDR & 0xff000000)
 
       .if    (KERNEL_RAM_PADDR & 0x00f00000)
 
       orr   r3, r3, #(KERNEL_RAM_PADDR & 0x00f00000)
 
       .endif
 
       add r0, r4,  #(KERNEL_RAM_VADDR & 0xff000000) >> 18
 
       str   r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> 18]!
 
       ldr   r6, =(_end - 1)
 
       add r0, r0, #4
 
       add r6, r4, r6, lsr #18
 
1:    cmp r0, r6
 
       add r3, r3, #1 << 20
 
       strls r3, [r0], #4
 
       bls   1b
 
#endif
 
 
 
       /*
 
        * Then map first 1MB of ram in case it contains our boot params.
 
        */
 
// 虚拟ram地址的第一个1M空间包含了参数列表,也需要映射
 
       add r0, r4, #PAGE_OFFSET >> 18
 
       orr   r6, r7, #(PHYS_OFFSET & 0xff000000)
 
       .if    (PHYS_OFFSET & 0x00f00000)
 
       orr   r6, r6, #(PHYS_OFFSET & 0x00f00000)
 
       .endif
 
       str   r6, [r0]
 
       mov pc, lr// 页表建立完成,返回
 
 
 
页表创建后,具体的映射空间如下图:
 
执行完上述页表创建,开始执行内核跳转:
 
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
 
 
 
__switch_data       是一个数据结构,如下
 
       .type       __switch_data, %object
 
__switch_data:
 
       .long       __mmap_switched
 
       .long       __data_loc                  @ r4
 
       .long       __data_start                @ r5
 
       .long       __bss_start                  @ r6
 
       .long       _end                            @ r7
 
       .long       processor_id               @ r4
 
       .long       __machine_arch_type         @ r5
 
       .long       __atags_pointer                  @ r6
 
       .long       cr_alignment                @ r7
 
       .long       init_thread_union + THREAD_START_SP @ sp
 
 
 
语句“add pc, r10, #PROCINFO_INITFUNC”通过查表调用proc-v7.s中__v7_setup函数,该函数末尾通过将lr寄存器赋给pc,导致对__enable_mmu的调用,完成使能mmu的操作,之后将r13寄存器值赋给pc,调用__switch_data数据结构中的第一个函数__mmap_switched,
 
.type       __mmap_switched, %function
 
__mmap_switched:
 
       adr  r3, __switch_data + 4
 
 
 
       ldmia      r3!, {r4, r5, r6, r7}
 
       cmp r4, r5                           @  拷贝数据段
 
1:    cmpne    r5, r6
 
       ldrne       fp, [r4], #4
 
       strne       fp, [r5], #4
 
       bne  1b
 
 
 
       mov fp, #0                          @  清除BSS段
 
1:    cmp r6, r7
 
       strcc       fp, [r6],#4
 
       bcc  1b
 
 
 
       ldmia      r3, {r4, r5, r6, r7, sp}// 然后调整指针到processor_id      域
 
       str   r9, [r4]                 @  保存CPU ID
 
       str   r1, [r5]                 @ 保存机器类型
 
       str   r2, [r6]                 @  保存参数列表指针
 
       bic   r4, r0, #CR_A                    @ Clear 'A' bit
 
       stmia      r7, {r0, r4}                  @  保存控制信息
 
       b     start_kernel
 
最终调用init/main.c文件中的start_kernel函数。
 
这个start_kernel正是kernel/init/main.c的内核起始函数

你可能感兴趣的:(Android)