arm-linux内核启动学习笔记(一)
2015-12-02 21:43
686 查看
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++) != '\0') { 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(大)的入口函数,也是体系结构相关的!
相关文章推荐
- 第二天 Linux常见命令
- Linux简单命令之一
- Linux 内核链表
- linux下C++ STL hash_map的使用以及使用char *型变量作为Key值的一大“坑”
- LINUX下SQLPLUS无法使用删除及上下键
- linux内存屏蔽技术
- LINUX 安全运维 (五)
- linux0.11 内核启动代码分析(二)
- Windows中查找命令的路径 (类似Linux中的which命令)
- linux 软件卸载
- Linux里类似批处理中的pause命令
- Linux下使用FreeTDS访问MS SQL Server 2005数据库(包含C测试源码)
- windows复制文件到Linux或Windows,有关FTP的配置与使用
- 由一个线程例子引发的思考(转载)
- Centos7.0 修改防火墙为iptables
- linux学习(三)如何在linux系统下利用vi编辑C/C++程序
- Linux学习笔记(6)之vi编辑器基本操作
- Linux下Rsync+sersync实现数据双向实时同步
- 如何建立linux下的 LNMP 环境 + VSFTPD
- 详解CentOS设置163的yum源的过程