arm-linux内核启动学习笔记(一)

ARM 221浏览


arm-linux 内核的启动

  • 这里的分析是从./arch/arm/boot/compressed/head.S:start开始的,这个start标签是zImage的入口代码。 ./arch/arm/kernel里也有个head.S,这个head.S中的stext标签,就是zImage中decompress_kernel之后要跳转的地址,即Image的入口代码(见 arm-linux内核编译过程小结)。
  • arm启动后执行的第一条指令并不是head.S:start,在此之前一般都会有一个平台相关的bootloader(如mt6582的代码就是平台相关的代码,在mtk平台上bootloader分为preloader和lk两部分)来初始化一些平台相关的信息,然后再跳转到start标签,来执行体系结构相关的代码(如arm的代码就是体系结构相关的代码)。
  • bootloader部分(如mtk平台中preloader和lk部分)的代码会完成对硬件的默认初始化,准备start的结构ID和atags两个参数。

Part1: [start ,cache_on]

这部分组要是找到平台相关的cache_on函数,然后调用之。cache_on函数的左右就是开启缓存,
一般需要通过初始化页表并使能mmu来实现。

//部分关系不大代码已省略
start:
//指定start这个符号是函数类型
        .type   start,#function
//.rept count .endr 把rept和endr之间的指令重复count次
//mov r0,r0 实际上就是nop指令,这里的意思应该是是清空指令流水线
        .rept   7
        mov r0, r0
        .endr
        ARM(mov r0, r0)
//跳到前面(下面的1)
        ARM(b   1f)

//这个魔数是在bootloader中用于判断zImage存在的,是内核和bootloader约定好的(mtk中参见lk代码)
        .word   0x016f2818
//这里存的是start的默认加载地址,这个地址是编译时确定的,与代码最终加载到哪里无关,
//但如果start最终加载的物理地址(zImage这会没开mmu,所以物理地址)
//不是这个编译地址的话,可能需要重定位。
        .word   start
//zImage的结束地址,也是编译地址,这个值是在连接的时候确定的,其定义在vmlinux.lds中
        .word   _edata          @ zImage end address
//THUMB宏在这里没有定义的,所以这里没啥用,如果定义了,.thumb表示往下都是用thumb指令集。
        THUMB(.thumb)

//r1, r2分别存着bootloader传递过来的结构ID和atags
1:      mov r7, r1          @ save architecture ID
        mov r8, r2          @ save atags pointer

//关中断
#ifndef __ARM_ARCH_2__
//__ARM_ARCH_2__下的关中断方式,goldfish中也是这种方式
        mrs r2, cpsr        @ get current mode
        tst r2, #3          @ not user?
        bne not_angel
        mov r0, #0x17       @ angel_SWIreason_EnterSVC
        ARM(swi 0x123456)   @ angel_SWI_ARM
        THUMB(svc 0xab)     @ angel_SWI_THUMB
not_angel:
        mrs r2, cpsr        @ turn off interrupts to
        orr r2, r2, #0xc0   @ prevent angel from runing
        msr cpsr_c, r2
#else
//arm 版本2,3的核心的关中断方式
        teqp    pc, #0x0c000003     @ turn off interrupts
#endif
        .text
#ifdef CONFIG_AUTO_ZRELADDR
        @ determine final kernel image address
        mov r4, pc
        and r4, r4, #0xf8000000
        add r4, r4, #TEXT_OFFSET
#else
//我的内核配置了zreladdr,将其加载到r4,这是Image的加载地址
        ldr r4, =zreladdr
#endif
        bl  cache_on

反汇编后的代码:

ROM:00000000                 AREA ROM, CODE, READWRITE, ALIGN=0
ROM:00000000                 CODE32
; mov r0, r0 被解释成了nop指令,这里应该是用来清空指令流水的
ROM:00000000                 NOP
ROM:00000004                 NOP
ROM:00000008                 NOP
ROM:0000000C                 NOP
ROM:00000010                 NOP
ROM:00000014                 NOP
ROM:00000018                 NOP
ROM:0000001C                 NOP
ROM:00000020                 B close_IRQ
ROM:00000020 ; ----------------------------------------
; zImage魔数
ROM:00000024                 DCD 0x16F2818
; start 标签的编译地址,这里编译地址就是0,也就是说zImage默认被加载到0地址     
ROM:00000028                 DCD 0
; 这个数与 ll zImage的大小完全一样,也是zImage编译的结束地址。
ROM:0000002C                 DCD 0x27A1F8
ROM:00000030 ; ----------------------------------------
ROM:00000030
ROM:00000030 close_IRQ      ; CODE XREF: ROM:00000020j
ROM:00000030                 MOV             R7, R1
ROM:00000034                 MOV             R8, R2
ROM:00000038                 MRS             R2, CPSR
ROM:0000003C                 TST             R2, #3
ROM:00000040                 BNE             not_angel
ROM:00000044                 MOV             R0, #0x17
ROM:00000048                 SVC             0x123456
ROM:0000004C
ROM:0000004C not_angel      ; CODE XREF: ROM:00000040j
ROM:0000004C                 MRS             R2, CPSR
ROM:00000050                 ORR             R2, R2, #0xC0
ROM:00000054                 MSR             CPSR_c, R2
ROM:00000058                 ANDEQ           R0, R0, R0
ROM:0000005C                 ANDEQ           R0, R0, R0
; 在goldfish上,r4 存入立即数 0x00008000,为Image的默认加载地址
ROM:00000060                 LDR         R4,=dword_8000
ROM:00000064                 BL              cache_on

cache_on

cache_on函数主要的功能是根据处理器ID,找到处理器对应的体系结构(armXXX)的cache_on函数并调用。kernel支持的所有arm型号的cache_on函数都会包含在zImage里面(这个head.S文件中),如果有新的arm处理器,内核想要支持的话,就需要在kernel.org提交这部分的patch。

//这个8 是proc_types数组中的第三个元素,代表某个处理器型号中cache_on函数的地址。
cache_on:   mov r3, #8          @ cache_on function
        b   call_cache_fn

cache_on->call_cache_fn

//call_cache_fn为一个循环,根据当前cpu的魔数(可能是从cp15中获取,或编译时指定的),找到并调用对应处理器的cache_on函数
call_cache_fn:  adr r12, proc_types

//获取处理器ID,goldfish中是通过cp15获取的处理器ID
#ifdef CONFIG_CPU_CP15
        mrc p15, 0, r9, c0, c0  @ get processor ID
#else
        ldr r9, =CONFIG_PROCESSOR_ID
#endif

//可以先到下面看下proc_types的结构,这里以第一个循环为例
//r1 = proc_types[0] = 0x41560600 代表ARM6/610
1:      ldr r1, [r12, #0]       @ get value
//r2位掩码 r2 = proc_types[1] = 0xffffffe0
        ldr r2, [r12, #4]       @ get mask
//EOR <Rd>,<Rn>, <shifter_operand>, 将shifter_operand与Rn做异或操作,结果存于Rd
        eor r1, r1, r9      @ (real ^ match)
//TST <Rn>, <shifter_operand>, Rn - shifter_operand,用结果更新标志位
        tst r1, r2          @       & mask
//如果r1 = r2,代表处理器匹配,跳转到对应的cache_on函数
        ARM(addeq   pc, r12, r3) @ call cache function
        THUMB(addeq r12, r3)
        THUMB(moveq pc, r12) @ call cache function
//如果不一致,则增加到下一个位置,继续测试
        add r12, r12, #PROC_ENTRY_SIZE
//如果不匹配,则往上跳,循环。
        b   1b

proc_types里面记录各个arm处理器的魔数,掩码,以及相应的cache函数,如下:

/*
 *   - CPU ID match
 *   - CPU ID mask
 *   - 'cache on' method instruction
 *   - 'cache off' method instruction
 *   - 'cache flush' method instruction
*/
        .align  2
        .type   proc_types,#object
proc_types:
        .word   0x41560600      @ ARM6/610
        .word   0xffffffe0
        W(b) __arm6_mmu_cache_off   @ works, but slow
        W(b)    __arm6_mmu_cache_off
        mov pc, lr
        .word   0x41007000      @ ARM7/710
        .word   0xfff8fe00
        W(b)    __arm7_mmu_cache_off
        W(b)    __arm7_mmu_cache_off
        mov pc, lr
        THUMB(      nop             )
        .word   0x41807200  @ ARM720T (writethrough)
        .word   0xffffff00
        W(b)    __armv4_mmu_cache_on
        W(b)    __armv4_mmu_cache_off
        mov pc, lr
        THUMB(      nop             )
        .word   0x41007400      @ ARM74x
        .word   0xff00ff00
        W(b)    __armv3_mpu_cache_on
        W(b)    __armv3_mpu_cache_off
        W(b)    __armv3_mpu_cache_flush

        .word   0x41009400      @ ARM94x
        .word   0xff00ff00
        W(b)    __armv4_mpu_cache_on
        W(b)    __armv4_mpu_cache_off
        W(b)    __armv4_mpu_cache_flush

        .word   0x41069260      @ ARM926EJ-S (v5TEJ)
        .word   0xff0ffff0
        W(b)    __arm926ejs_mmu_cache_on
        W(b)    __armv4_mmu_cache_off
        W(b)    __armv5tej_mmu_cache_flush

        ......

这里以__armv4_mmu_cache_on为例(goldfish 应该是armv7的处理器,看/proc/cpuinfo ):
cache_on->call_cache_fn->__armv4_mmu_cache_on

//cache_on函数做了两件事:初始化页表,开启mmu,因为只有在开启mmu的情况下才能使用cache
__armv4_mmu_cache_on:
//这个函数是通过addeq  pc, r12, r3指令进来的,
//这里保存的lr是start标签的那句bl cache_on之后的位置的
        mov r12, lr
#ifdef CONFIG_MMU
//开启缓存相关,缓存具体怎么开怎么用,这里先不管,
//主要先看__setup_mmu是如何启动分页机制的
        bl  __setup_mmu
//高速缓存相关操作
        mov r0, #0
        mcr p15, 0, r0, c7, c10, 4
        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     
        orr r0, r0, #0x0030
//这个函数中有一步是将页表基地址写入cp15.c2寄存器,就是开启mmu。
        bl  __common_mmu_cache_on
        mov r0, #0
        mcr p15, 0, r0, c8, c7, 0
//返回到start标签的bl cache_on后面那一句
        mov pc, r12

__setup_mmu

调用流程是:cache_on->call_cache_fn->__armv4_mmu_cache_on->__setup_mmu

/* 跳转到__setup_mmu之前:
 * r4 = zreladdr,为Image的加载地址(在goldfish里面为0x00008000,
 * 有的系统为0x3/5/7/0008000,这个值在./arch/arm/$(MACH)/
 * Makefile.boot中有定义,$(MACH)指的是具体的芯片)。
 * Image前的16KB内存(glodfish上默认为0x00004000 - 0x00008000)
 * 是内核的整个页表,大小为16K。
 *
 *  __setup_mmu函数的主要工作是初始化全局页表(Image加载地址向前16KB)
 * 将部分区域加上C/B属性(cache和write buffer),其他区域就直接默认属性了,
 * 这里将当前pc所在的两页和解压后的内核所在的256MB空间加上了C/B属性
 * 估计因为这一片可能为代码区域,加上C/B属性可以提升访问速度???
 */

//r3 = r4 - 0x4000(16K) 最终r3 = 0x00004000,为页表基地址(pgd) 
__setup_mmu:    sub r3, r4, #16384
//r3 = r3 & 0xffffc000 (清除r3的0x3fff位)
        bic r3, r3, #0xff       @ Align the pointer
        bic r3, r3, #0x3f00
//当前页表项指针 r0 = 0x00004000
        mov r0, r3
//要初始化为C/B属性的起始物理地址,这里r9 = 0x0
        mov r9, r0, lsr #18
        mov r9, r9, lsl #18     @ start of RAM
//要初始化为C/B属性的结束物理地址 r10 = r9 + 256MB
        add r10, r9, #0x10000000    
//r1 = 0xc12, 这是默认的页表属性
        mov r1, #0x12
        orr r1, r1, #3 << 10
//页表项的结束位置 r2 = 0x00008000
        add r2, r3, #16384
//到这里开始初始化页表了,当前的r1是0xc12,同时也能代表虚拟地址0x00000000所在的页
//因为0xc12是个很小的数。每次循环r1都会递增1MB,在每一次循环中都设定r1
//代表的这1MB虚拟地址如何映射。这里做的是一个恒等映射,唯一不同就是各个区段
//的映射属性不同
1:      cmp r1, r9          @ if virt > start of RAM
//如果r1 > r9 则执行 r1 = r1|0x0C = 0x0C1E
        orrhs   r1, r1, #0x0c   
//如果 r1 > r10(过了0x10004000),则清除缓存标记
        cmp r1, r10         @ if virt > end of RAM
        bichs   r1, r1, #0x0c       
//r1是当前页描述符,r0是当前页表项的地址,这里是向当前页表项写入页描述符: *r0 = r1; r0 += 4;
        str r1, [r0], #4        
//r1 += 0x100000 = 2^20 = 1MB,设置下一个页描述符的内容
        add r1, r1, #1048576
//如果r0 != r2(没到页表项结束位置)则向后(向上)跳转到1,实现循环。
//这里是将整个0x00004000- 0x00008000处的内存全部初始化作为页表。
        teq r0, r2
        bne 1b
//16KB页表代表内核4GB空间,其初始化到这里结束,后面修正当前pc所在的虚拟地址
//(等于物理地址)对应的页表的属性了,这是怕pc不在前面那个0xC1E的范围内。
//r1 = 0x0C1E
        mov r1, #0x1e
        orr r1, r1, #3 << 10
//要修正pc所在页的地址
        mov r2, pc
//段对齐
        mov r2, r2, lsr #20
//加上页属性,r1为要写入的页表项内容(恒等映射)
        orr r1, r1, r2, lsl #20
//获取pc所在页的虚拟地址在全局页表pgd中的页表项的地址 -> r0
        add r0, r3, r2, lsl #2
//修改pc所在页和其后面一页的页表属性为0xC1E
        str r1, [r0], #4
        add r1, r1, #1048576
        str r1, [r0]
//返回
        mov pc, lr
ENDPROC(__setup_mmu)

/* 最终一级页表的内容:
其中:
第一列是各个页描述符的物理地址
第二列是各个页描述符的内容,整个第二列就是物理内存中的一级页表
第三列是表示当前页描述符处于一级页表中的第几个页表项。
/物理地址/页描述符内容/当前是第几个页表项/
|0x00004000|0x00000C1E|0000| //最后单独修改的两项页表项
|0x00004004|0x00100C1E|0001| //页表项内容>0x00004000
|0x00004008|0x00200C1E|0002| //<0x10004000的属于C/B区
|0x0000400C|0x00300C1E|0003|
......
|0x00004400|0x10000C1E|0256| //页表项内容<0x10004000
|0x00004404|0x10100C12|0257| //页表项内容>0x10004000
|0x00004408|0x10200C12|0258|
......
|0x00007FFC|0xFFF00C12|4095|
一个一级页描述符作为段描述符时,可表示的地址范围为2^20 = 1MB(见后面一级页描述符), 32位cpu的寻址空间为4GB,所以一共需要4096个一级页描述符。前面可以看到16KB,刚好能存储4096个页描述符,所以内核初始化的时候,页表的大小为16KB. 16K/4096 = 4B 一个地址。

此恒等映射最终的结果就是,4GB的任意虚拟地址 = 物理地址。
*/

Part2:(cache_on,call_kernel]

vmlinux(小)的链接脚本:

//./arch/arm/boot/compressed/vmlinux.lds.in
//这个脚本是用来链接vmlinux(小)的,故zImage代码执行时
//内核各个段的分布也是在这个脚本中定义的(部分代码已省略)。
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
  . = TEXT_START;
  _text = .;
  .text : {
    _start = .;
    *(.start)
    *(.text)
    *(.text.*)
    *(.fixup)
    *(.gnu.warning)
    *(.glue_7t)
    *(.glue_7)
  }
  .rodata : {
    *(.rodata)
    *(.rodata.*)
  }
  //这里是piggy.gz的位置
  .piggydata : {
    *(.piggydata)
  }
  . = ALIGN(4);
  _etext = .;
  .got.plt      : { *(.got.plt) }
  _got_start = .;
  .got          : { *(.got) }
  _got_end = .;

  .pad          : { BYTE(0); . = ALIGN(8); }
  _edata = .;

  . = BSS_START;
  __bss_start = .;
  .bss          : { *(.bss) }
  _end = .;

  . = ALIGN(8);     /* the stack must be 64-bit aligned */
  .stack        : { *(.stack) }

  .stab 0       : { *(.stab) }
  .stabstr 0        : { *(.stabstr) }
  .stab.excl 0      : { *(.stab.excl) }
  .stab.exclstr 0   : { *(.stab.exclstr) }
  .stab.index 0     : { *(.stab.index) }
  .stab.indexstr 0  : { *(.stab.indexstr) }
  .comment 0        : { *(.comment) }
}

piggy.gzip.S

piggy.gzip.S是用来生成piggy.gzip.o的,其实际上相当于将一个piggy.gzip文件当
二进制打包到vmlinux(小)中了。

//./arch/arm/boot/compressed/piggy.gzip.S
    .section .piggydata,#alloc
    //global只是声明,并不分配空间
    .globl  input_data
input_data:
##INCBIN 指令在被汇编的文件内包含一个文件。 该文件按原样包含,没有进行汇编。
    .incbin "arch/arm/boot/compressed/piggy.gzip"
    .globl  input_data_end
input_data_end:

cache_on之后

cache_on之后段代的代码大体可认为做了三件事:
1. 检查内核是否需要移动,如果需要移动则将其移动并重定向.got的内容。
2. 调用decompress_kernel解压内核。
3. 调用call_kernel跳转到Image。

        //cache_on函数的实际作用是开启高速缓存,这样可以加快后续代码的执行速度
        //在arm体系结构中,高速缓存TLB必须依赖于mmu,所以cache_on函数内部通过
        //调用__setup_mmu来初始化页表(代码段的页表属性允许缓存)
        //调用__common_mmu_cache_on来使能mmu
        bl  cache_on
        //跳转到这里,mmu已经开启了,恒等映射!
/*
        LC0的代码本来是在后面的,这里为了方便解释,先在前面列出了
        各个代码段的分布见后(这些值都是编译地址!)。
LC0:        .word   LC0         @ r1
        //bss段的开始位置
        .word   __bss_start     @ r2
        //bss的结束位置
        .word   _end            @ r3
        //数据段的结束位置
        .word   _edata          @ r6
        //pizzy.gz的结束位置
        .word   input_data_end - 4  @ r10 (inflated size location)
        .word   _got_start      @ r11
        .word   _got_end        @ r12 (ip)
        //栈的结束位置
        .word   .L_user_stack_end   @ sp
        //这个应该是个伪指令,指定LC0大小的
        .size   LC0, . - LC0
*/
        //adr是相对于当前pc的相对寻址,一般用于获取指令的真是地址,而不是加载地址
restart:    adr r0, LC0
        //把上面LC0中的一堆变量载入到各个寄存器中
        ldmia   r0, {r1, r2, r3, r6, r10, r11, r12}
        //设置临时栈, sp = L_user_stack_end,目前sp还指向栈底
        ldr sp, [r0, #28]

        /*
            zImage的加载地址未必是编译地址,所以这里需要
            将各个变量由编译地址修正为真实地址。
        */
        //计算LC0的真实地址(既是物理地址,又是虚拟地址)和编译地址的差值
        sub r0, r0, r1      @ calculate the delta offset
        //将_edata的编译地址转为真实地址
        add r6, r6, r0      @ _edata
        //将input_data_end的编译地址转为真实地址
        add r10, r10, r0        @ inflated kernel size location

        //将sp的编译地址修正为真实地址,并向上增加64K,作为栈顶
        add sp, sp, r0
        add r10, sp, #0x10000

        /*
         * The kernel build system appends the size of the
         * decompressed kernel at the end of the compressed data
         * in little-endian form.
         */
        /*
             r9是piggy.gzip这个gzip文件的最后四个字节,这四个字节记录的
             是解压后的Image的大小存的.在我这里piggy.gzip最后: 30c1 b500 0a
             Image大小为0xb5c130, 最后那个0a估计最后会去掉的。
             这里用ldrb指令是考虑到这个存Image大小的位置可能不是4byte对齐的。
         */
        ldrb    r9, [r10, #0]
        ldrb    lr, [r10, #1]
        orr r9, r9, lr, lsl #8
        ldrb    lr, [r10, #2]
        ldrb    r10, [r10, #3]
        orr r9, r9, lr, lsl #16
        orr r9, r9, r10, lsl #24

/*
 *   这一段代码是来检测是否需要复制自身的
 *   r4是最终内核要解压到的地址,是通过zreladdr来指定的
 *   r9是Image镜像的大小,是从piggy.gz文件末尾获取的
 *   r10是piggy.gz的结束位置
 * 往下执行需要满足两个条件之一:
 *   1) 最终内核要解压到的地址(r4) - 16k页表 >= zImage的基本代码结束位置(r10)
 *   2) 最终内核解压后的结束地址(r4 + r9(image length)) <= 后续执行的代码(wont_overwrite的地址)
0x00000000 -----------------------------------------------------------------------------

                                                ------ Image(zreladdr)
                                                  |
                                                  |
     ----- zImage起始地址                          |
       |                                          |
       |                                          |
     ----- 当前代码(pc)                          ------ Image end 在此之上都可以
     ----- wont_overwrite的代码
       |
     ----- zImage基本代码结束(piggy.gz末尾,r10)
                                                ----- Image(zreladdr)
                                                      ;如果解压到这个位置往下都是可以的
                                                  |
                                                  |
                                                ----- Image end

0xffffffff -----------------------------------------------------------------------------
 */

        //这里r10 += 0x4000是预留给页表的
        add r10, r10, #16384
        cmp r4, r10
        //如果r4 >= r10则满足条件1,Image要解压的地址在zImage后面,
        //跳转到wont_overwrite,不需移动自身代码。
        bhs wont_overwrite
        //r10 = r4 + r9, r10为预计解压后的image的结束地址
        add r10, r4, r9
        //获取wont_overwrite的物理地址
        adr r9, wont_overwrite
        //如果否满足条件2,即image解压后没有覆盖wont_overwrite
        //之后的代码,则也无需移动自身代码。
        cmp r10, r9
        bls wont_overwrite
/*
 * Relocate ourselves past the end of the decompressed kernel.
 *   r6  = _edata
 *   r10 = end of the decompressed kernel
 * Because we always copy ahead, we need to do it from the end and go
 * backward in case the source and destination overlap.
 */
        /*
         * Bump to the next 256-byte boundary with the size of
         * the relocation code added. This avoids overwriting
         * ourself when the offset is small.
         */
        //否则就需要移动代码的位置了,这里的逻辑是把从restart到zImage结束的代码
        //移动到Image预计解压后的结束位置的后面,为什么从restart开始见下面。
        add r10, r10, #((reloc_code_end - restart + 256) & ~255)
        //这里的r10应该就是最终要移动到的地址了
        bic r10, r10, #255

        /* Get start of code we want to copy and align it down. */
        //要复制的代码的起始地址
        adr r5, restart
        //清除末尾位
        bic r5, r5, #31
        //要复制的数据大小
        sub r9, r6, r5      @ size to copy
        add r9, r9, #31     @ rounded up to a multiple
        bic r9, r9, #31     @ ... of 32 bytes
        add r6, r9, r5
        //要复制到的结束位置
        add r9, r9, r10

        //复制数据
1:      ldmdb   r6!, {r0 - r3, r10 - r12, lr}
        cmp r6, r5
        stmdb   r9!, {r0 - r3, r10 - r12, lr}
        bhi 1b

        /* Preserve offset to relocated code. */
        sub r6, r9, r6

        //代码被移动过了,所以清缓存
        bl  cache_clean_flush
        //跳转到restart重新执行,因为当前地址变了,所以LC0的当前地址和编译地址
        //的差值就变了,前面的好多变量都是根据这个差值算出来的,所以这里跳到
        //restart重新来过,所以前面检测的时候是检测wont_overwrite之前
        //的代码是否被覆盖,而移动得要从restart的代码开始移动。
        adr r0, BSYM(restart)
        add r0, r0, r6
        mov pc, r0
        //到这里

wont_overwrite:
/*
 * If delta is zero, we are running at the address we were linked at.
 *   r0  = delta (运行地址与链接地址的偏移量)
 *   r2  = BSS start
 *   r3  = BSS end
 *   r4  = kernel execution address
 *   r5  = appended dtb size (0 if not present)
 *   r7  = architecture ID
 *   r8  = atags pointer
 *   r11 = GOT start
 *   r12 = GOT end
 *   sp  = stack pointer
 */
        //r5在一开始被初始化为0
        orrs    r1, r0, r5
        //这里是如果运行地址与链接地址相等则跳转到not_relocated
        //不执行got段的重定位
        beq not_relocated

        //否则将r11,r12存的GOT表的编译地址修改为当前地址
        add r11, r11, r0
        add r12, r12, r0
        //修正bbs段的起始,结束地址
        add r2, r2, r0
        add r3, r3, r0
        //对GOT表中的所有元素做重定位
1:      ldr r1, [r11, #0]       @ relocate entries in the GOT
        add r1, r1, r0      @ This fixes up C references
        cmp r1, r2          @ if entry >= bss_start &&
        cmphs   r3, r1          @       bss_end > entry
        addhi   r1, r1, r5      @    entry += dtb size
        str r1, [r11], #4       @ next entry
        cmp r11, r12
        blo 1b
        /* bump our bss pointers too */
        add r2, r2, r5
        add r3, r3, r5

not_relocated:  mov r0, #0
        //初始化bss段的所有数据为空
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.
 * Set up some pointers, and start decompressing.
 *   r4  = kernel execution address
 *   r7  = architecture ID
 *   r8  = atags pointer
 */
        mov r0, r4
        mov r1, sp          @ malloc space above stack
        add r2, sp, #0x10000    @ 64k max
        mov r3, r7
        //这个即是将piggy.gz解压为Image的函数,这里是c代码
        bl  decompress_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
        //r4是Image的入口地址,也是Image的解压地址,Image
        //本身是个binary文件,第一个字节即为指令,这里跳转到Image
        //即./arch/arm/kernel/head.s:stext
 ARM(   mov pc, r4  )       @ call kernel

decompress_kernel

decompress_kernel是用c函数完成的,其代码如下

//./kernel/arch/arm/boot/compressed/Misc.c
extern char input_data[];
void decompress_kernel(
    unsigned long output_start,        //zImage的解压地址(r4)
    unsigned long free_mem_ptr_p,      //临时空间,解压用(这里用的是栈)
    unsigned long free_mem_ptr_end_p,  //临时空间结尾 
    int arch_id)                       // arch id
{
    int ret;
    output_data     = (unsigned char *)output_start;
    free_mem_ptr        = free_mem_ptr_p;
    free_mem_end_ptr    = free_mem_ptr_end_p;
    __machine_arch_type = arch_id;
    arch_decomp_setup();
    //putstr是内核启动早期的打印函数,一般都是直接向IO端口写的数据
    putstr("Uncompressing Linux...");
    //内核解压函数,这里用的是gzip解压,这个input_data和input_data_end
    //定义在piggy.gzip.S中,是piggy.gzip的起始和结束位置。
    ret = do_decompress(input_data, input_data_end - input_data, output_data, error);
    if (ret)
        error("decompressor returned an error");
    else
        putstr(" done, booting the kernel.n");
}

这里要注意一点的是:decompress_kernel函数中就已经开始调用打印函数了,这一句

putstr("Uncompressing Linux...")

在goldfish启动的时候是可以打印出来的,在真实设备上(如MT6582),通过串口也是可以打出来的,其实现如下:

./kernel/arch/arm/boot/compressed/Misc.c
static void putstr(const char *ptr)
{
    char c;
    while ((c = *ptr++) != '') {
        if (c == 'n')
            putc('r');
        putc(c);
    }
    flush();
}

而这个putc,具体平台实现的方式不同,在goldfish上:

//./arch/arm/mach-goldfish/include/mach/uncompress.h
#define GOLDFISH_TTY_PUT_CHAR (*(volatile unsigned int *)0xff002000)
static void putc(int c)
{
        //向IO端口0xff002000直接写入字符,这就是goldfish的uart端口
        GOLDFISH_TTY_PUT_CHAR = c;
}

在MT6582上,也是通过一个地址写入的,如下:

//./mediatek/platform/mt6582/kernel/core/include/mach/uncompress.h
#define MT_UART_PHY_BASE 0x11002000
#define MT_UART_THR *(volatile unsigned char *)(MT_UART_PHY_BASE+0x0)
static inline void putc(int c)
{
   //向IO端口0x11002000直接写入字符,这就是mt6582的uart端口
   //这一句while循环是测试控制位的,具体位作用需参考板子的手册。
   while (!(MT_UART_LSR & 0x20));
   MT_UART_THR = c;
}

这种写入也就是在内核刚开始,有恒等映射的时候,后续page_init二次映射的时候这些地址都会映射到内核的内核空间的其他地方了(后续分析)。

旧版废弃的内容,但应该是正确的

part1

  • arm MMU 一级页表寻址:
    arm协处理器CP15的C2寄存器记录着页基地址(又可以叫做一级页表基地址),对于一个地址addr, 在arm中第一级寻址算法为:C2[31:14] + addr[31:20] + 00,如图:

这里写图片描述
注1:
1) C2中记录的页表基地址,又可以叫做一级页表基地址
2) 一级页表基地址中的每一项元素叫做一级页表项
3) 一级页表项中的内容叫做一级页表描述符
4) 一级页表描述符,描述的是否为一个二级页表,这个得看描述符的最后两位是什么。
注2: 一级寻址只与页表基地址的前18位(一级页表基地址),虚拟地址的前12位(一级页表项的页内偏移)有关 (+最后两位00),没有任何标记位,mmu通过将二者组合成一个32位物理地址,这个就是一级页表项的物理地址,从这个物理地址中读取到的内容就是一级页表描述符。

  • 一级页表描述符:
    arm 开启MMU后,当用户访问一个虚拟地址时,先根据C2和虚拟地址的前12位标记的物理地址中取出一个一级页表描述符,虚拟地址中剩下20位如何解析,是由这个一级页表描述符的低2位决定的。一共四种组合,对应四种不同的解析方式。在__setup_mmu中初始化的一级页表项的最后两位都是10,这里先介绍10。
    一级页表描述符[1:0]为0b10,表示该一级描述符为段描述符。该一级描述符应该如下解析:
    这里写图片描述

虚拟地址->物理地址的过程:
这里写图片描述

这个过程的总体描述如下:
1) 用户访问一个虚拟地址addr。
2) MMU取出addr的前12位,并根据C2定位一级页表描述符X。
3) MMU 发现 X的最后两位为10,则取出x的前12位,拼接addr的后20位,组成物理地址。
4) 访问此物理地址,获取数据返回给用户。

part2

在zImage的反汇编代码中,input_data在_got_start段,值为0x4015,如下:
这里写图片描述

查看其内容:
这里写图片描述

查看piggy.gz:
这里写图片描述

可知整个zImage,实际上就是vmlinux(2.51MB那个)掐头去尾一点,vmlinux是个标准ELF文件,而zImage就是这个vmlinux去了ELF头尾组成的,其二进制差异不过1%20.

也就是说,实际上是vmlinux包含了piggy.gz,而将其裁剪一点,就形成了最终的zImage。
1. vmlinux(小的那个),是一个标准的elf文件,将其掐头去尾后就形成了zImage,这个就是最终的内核镜像。
2. zImage是最终的内核镜像,其内部包含了一个原封不动的piggy.gz,这是一个压缩后的内核。
3. piggy.gz是image通过gzip命令压缩来的。
4. Image文件是一个纯二进制文件,没有elf头,zImage中的最后一句 MOV PC, R4 ; call_kernel,这个R4就是之前kernel解压的地址,就是直接跳到了Image的相对偏移0位置的指令。
5. Image是从vmlinux(大)中抽取出来的,是objcopy -o binary vmlinux(大)来的。vmlinux(大)入口地址的第一条指令,就是Image这个二进制文件的第一条指令。

所以综上所述,zImage解压内核,跳转到Image偏移0处的指令,实际上相当于执行了vmlinux(大)的入口函数。而vmlinux(大)的入口函数为ENTRY(stext),定义在./arch/arm/kernel/head.S(注,zImage的入口函数定义在./arch/arm/boot/compressed/head.S),也就是说vmlinux(大)的入口函数,也是体系结构相关的!