Linux启动(1)

ARM 162浏览

            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 指针

r1r2中分别存放着由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 需要在板子上有 驻留程序,然后通过 串口就可以调试了 。用过的AXDtrace调试环境的话,对此应该比较熟悉。  

not_angel:  //若不是通过angel调试进入内核

              mrs  r2, cpsr          @ turn off interrupts to

              orr   r2, r2, #0xc0         @ prevent angel from running

              msr  cpsr_c, r2   //这里将cpsrIF位分别置“1,关闭IRQFIQ

#else

              teqp pc, #0x0c000003          @ turn off interrupts

常用 TEQP PC,#(新模式编号) 来改变模式

#endif

  另外链接器会把一些处理器相关的代码链接到这个位置,也就是arch/arm/boot/compressed/head-xxx.S文件中的代码。在高通平台下,这个文件是head-msm.S连接脚是compress/vmlinux.lds,其中部分内容大致如下,在连接时,连接器根据每个文件中的段名将相同的段合在一起,比如将head.Shead-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 r0LC0的值实际上按下面公式计算:

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 bufferI/DcacheTLB.接着打开i-cache,设置为Round-robin replacement。调用__common_mmu_cache_on,打开mmud-cache.把页表基地址和域访问控制写入协处理器寄存器c2c3. __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空间,并把对应的描述符的CB位均置”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,所以这里先对页表进行初始化,然后打开mmucache

上面这段就是对一级描述符表(页表)的初始化,首先比较这个描述符所描述的地址是否在那个256M的空间中,如果在则这个描述符对应的内存区域是cacheable ,bufferable。如果不在则noncacheable, nonbufferable.然后将描述符写入一个一级描述符表的入口,并将一级描述符表入口地址加4,而指向下一个1M section的基地址。如果页表入口未初始化完,则继续初始化。

 

页表大小为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//lc0SP进行了定义   .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

 

缓冲区空间的起始地址和结束地址分别存放在r1r2中。然后判断最终内核地址,也就是解压后内核的起始地址,是否大于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跳入内核,开始进行下个阶段的处理。

 

 

整个代码流程如下: