本文整理了ARM Linxu启动流程的第一阶段——内核自解压,内核版本为3.12.35。我以手上的树莓派b(ARM11)为平台示例来分析uboot跳转到Linux内核运行后做了哪些初始化动作,以及如何转入真正的内核开始运行。
内核版本:Linux-3.12.35
分析文件:linux/arch/arm/boot/compressed/head.S单板:树莓派b
首先给出内核自解压部分的总流程如下:
内核自解压程序的入口:参见arch/arm/boot/compressed/vmlinux.lds(由arch/arm/boot/compressed/vmlinux.lds.in生成):
SECTIONS { ...... *(.data) } . = 0; _text = .; .text : { _start = .; *(.start) *(.text) *(.text.*) *(.fixup) *(.gnu.warning) *(.glue_7t) *(.glue_7) } ...... }
程序的入口点在linux/arch/arm/boot/compressed/head.S中,下面来进行详细分析:
start: .type start,#function .rept 7 mov r0, r0 .endr ARM( mov r0, r0 ) ARM( b 1f ) THUMB( adr r12, BSYM(1f) ) THUMB( bx r12 )
使用.type标号来指明start的符号类型是函数类型,然后重复执行.rept到.endr之间的指令7次,这里一共执行了7次mov r0, r0指令,共占用了4*7 = 28个字节,这是用来存放ARM的异常向量表的。向前跳转到标号为1处执行:
1: mrs r9, cpsr #ifdef CONFIG_ARM_VIRT_EXT bl __hyp_stub_install @ get into SVC mode, reversibly #endif mov r7, r1 @ save architecture ID mov r8, r2 @ save atags pointer
这里将CPU的工作模式保存到r9寄存器中,将uboot通过r1传入的机器码保存到r7寄存器中,将启动参数tags的地址保存到r8寄存器中。
/* * 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 ARM( swi 0x123456 ) @ angel_SWI_ARM THUMB( svc 0xab ) @ angel_SWI_THUMB
这里将CPU的工作模式保存到r2寄存器中,然后判断是否是SVC模式,如果是USER模式就会通过swi指令产生软中断异常的方式来自动进入SVC模式。由于我这里在uboot中已经将CPU的模式设置为SVC模式了,所以就直接跳到not_angel符号处执行。
not_angel: safe_svcmode_maskall r0 msr spsr_cxsf, r9 @ Save the CPU boot mode in @ SPSR
safe_svcmode_maskall是一个宏,定义在arch/arm/include/asm/assembler.h中:
/* * Helper macro to enter SVC mode cleanly and mask interrupts. reg is * a scratch register for the macro to overwrite. * * This macro is intended for forcing the CPU into SVC mode at boot time. * you cannot return to the original mode. */ .macro safe_svcmode_maskall reg:req #if __LINUX_ARM_ARCH__ >= 6 mrs \reg , cpsr eor \reg, \reg, #HYP_MODE tst \reg, #MODE_MASK bic \reg , \reg , #MODE_MASK orr \reg , \reg , #PSR_I_BIT | PSR_F_BIT | SVC_MODE THUMB( orr \reg , \reg , #PSR_T_BIT ) bne 1f orr \reg, \reg, #PSR_A_BIT adr lr, BSYM(2f) msr spsr_cxsf, \reg __MSR_ELR_HYP(14) __ERET 1: msr cpsr_c, \reg 2: #else /* * workaround for possibly broken pre-v6 hardware * (akita, Sharp Zaurus C-1000, PXA270-based) */ setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, \reg #endif .endm
这里的注释已经说明了,这里是强制将CPU的工作模式切换到SVC模式,并且关闭IRQ和FIQ中断。然后将r9中保存的原始CPU配置保存到SPSR中。
#ifdef CONFIG_AUTO_ZRELADDR @ determine final kernel image address mov r4, pc and r4, r4, #0xf8000000 add r4, r4, #TEXT_OFFSET #else ldr r4, =zreladdr #endif内核配置项AUTO_ZRELDDR表示自动计算内核解压地址(Auto calculation of the decompressed kernelimage address),这里没有选择这个配置项,所以保存到r4中的内核解压地址就是zreladdr,这个参数在linux/arch/arm/boot/compressed/Makefile中:
ifneq ($(CONFIG_AUTO_ZRELADDR),y) LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR) endif
而ZRELADDR定义在arch/arm/boot/Makefile中:
ifneq ($(MACHINE),) include $(srctree)/$(MACHINE)/Makefile.boot endif # Note: the following conditions must always be true: # ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET) # PARAMS_PHYS must be within 4MB of ZRELADDR # INITRD_PHYS must be in RAM ZRELADDR := $(zreladdr-y) PARAMS_PHYS := $(params_phys-y) INITRD_PHYS := $(initrd_phys-y)
既然看到了内核解压地址zreladdr,也顺便来看一下params_phys和initrd_phys的值,他们最终由arch/arm/mach-$(SOC)/Makefile.boot决定,我这里使用的soc是bcm2807(bcm2835),他的Makefile.boot内容如下:
zreladdr-y := 0x00008000
params_phys-y := 0x00000100
initrd_phys-y :=0x00800000
这里的params_phys-y和initrd_phys-y是内核参数的物理地址和initrd文件系统的物理地址。其实除了zreladdr外这些地址uboot都会传入的。/* * Set up a page table only if it won't overwrite ourself. * That means r4 < pc && r4 - 16k page directory > &_end. * Given that r4 > &_end is most unfrequent, we add a rough * additional 1MB of room for a possible appended DTB. */ mov r0, pc cmp r0, r4 ldrcc r0, LC0+32 addcc r0, r0, pc cmpcc r4, r0 orrcc r4, r4, #1 @ remember we skipped cache_on blcs cache_on
这里将比较当前PC地址和内核解压地址,只有在不会自覆盖的情况下才会创建一个页表,如果当前运行地址PC < 解压地址r4,则读取LC0+32地址处的内容加载到r0中,否则跳转到cache_on处执行缓存初始化和MMU初始化。LC0是地址表,定义在525行:
.align 2 .type LC0, #object LC0: .word LC0 @ r1 .word __bss_start @ r2 .word _end @ r3 .word _edata @ r6 .word input_data_end - 4 @ r10 (inflated size location) .word _got_start @ r11 .word _got_end @ ip .word .L_user_stack_end @ sp .word _end - restart + 16384 + 1024*1024 .size LC0, . - LC0
LC0+32地址处的内容为:_end -restart + 16384 + 1024*1024,所指的就是程序长度+16k的页表长+1M的DTB空间。继续比较解压地址r4(0x00008000)和当前运行程序的(结束地址+16384 + 1024*1024),如果小于则不进行缓存初始化并置位r4最低位进行标识。
这里稍稍有点绕,分情况总结一下:
(1) PC >= r4:直接进行缓存初始化
(2) PC < r4 && _end + 16384+ 1024*1024 > r4:不进行缓存初始化
(3) PC < r4 && _end + 16384+ 1024*1024 <= r4:执行缓存初始化
这里先暂时不分析cache_on(已补充在文中最后分析),继续沿主线往下分析:
restart: adr r0, LC0 ldmia r0, {r1, r2, r3, r6, r10, r11, r12} ldr sp, [r0, #28]
通过前面LC0地址表的内容可见,这里r0中的内容就是编译时决定的LC0的实际运行地址(特别注意不是链接地址),然后调用ldmia命令依次将LC0地址表处定义的各个地址加载到r1、r2、r3、r6、r10、r11、r12和SP寄存器中去。执行之后各个寄存器中保存内容的意义如下:
(1) r0:LC0标签处的运行地址
(2) r1:LC0标签处的链接地址
(3) r2:__bss_start处的链接地址
(4) r3:_ednd处的链接地址(即程序结束位置)
(5) r6:_edata处的链接地址(即数据段结束位置)
(6) r10:压缩后内核数据大小位置
(7) r11:GOT表的启示链接地址
(8) r12:GOT表的结束链接地址
(9) sp:栈空间结束地址
195~196行这段代码通过反汇编来看着部分代码会更加清晰(反汇编arch/arm/boot /compressed/vmlinux):000000c0 <restart>: c0: e28f0e13 add r0, pc, #304 ; 0x130 c4: e8901c4e ldm r0, {r1, r2, r3, r6, sl, fp, ip} c8: e590d01c ldr sp, [r0, #28]
由于我的环境中实际的运行在物理地址为0x00008000处,所以r0 = pc + 0x130 = 0x00008000 + 0xc0 + 0x8 + 0x130 = 0x00008000 +0x1F8,而0x000081F8物理地址处的内容就是LC0:
000001f8 <LC0>: 1f8: 000001f8 strdeq r0, [r0], -r8 1fc: 006f2a80 rsbeq r2, pc, r0, lsl #21 200: 006f2a98 mlseq pc, r8, sl, r2 ; <UNPREDICTABLE> 204: 006f2a80 rsbeq r2, pc, r0, lsl #21 208: 006f2a45 rsbeq r2, pc, r5, asr #20 20c: 006f2a58 rsbeq r2, pc, r8, asr sl ; <UNPREDICTABLE> 210: 006f2a7c rsbeq r2, pc, ip, ror sl ; <UNPREDICTABLE> 214: 006f3a98 mlseq pc, r8, sl, r3 ; <UNPREDICTABLE> 218: 007f69d8 ldrsbteq r6, [pc], #-152 21c: e320f000 nop {0}
在获取了LC0的链接地址和运行地址后,就可以通过计算这两者之间的差值来判断当前运行的地址是否就是编译时的链接地址。
/* * We might be running at a different address. We need * to fix up various pointers. */ sub r0, r0, r1 @ calculate the delta offset add r6, r6, r0 @ _edata add r10, r10, r0 @ inflated kernel size location
将运行地址和链接地址的偏移保存到r0寄存器中,然后更新r6和r10中的地址,将其转换为实际的运行地址。
/* * The kernel build system appends the size of the * decompressed kernel at the end of the compressed data * in little-endian form. */ 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
注释中说明了,内核编译系统在压缩内核时会在末尾处以小端模式附上未压缩的内核大小,这部分代码的作用就是将该值计算出来并保存到r9寄存器中去。
#ifndef CONFIG_ZBOOT_ROM /* malloc space is above the relocated stack (64k max) */ add sp, sp, r0 add r10, sp, #0x10000 #else /* * With ZBOOT_ROM the bss/stack is non relocatable, * but someone could still run this code from RAM, * in which case our reference is _edata. */ mov r10, r6 #endif
这里将镜像的结束地址保存到r10中去,我这里并没有定义ZBOOT_ROM(如果定义了ZBOOT_ROM则bss和stack是非可重定位的),这里将r10设置为sp结束地址上64kb处(这64kB空间是用来作为堆空间的)。
接下来内核如果配置为支持设备树(DTB)会做一些特别的工作,我这里没有配置(#ifdef CONFIG_ARM_APPENDED_DTB),所以先跳过。/* * Check to see if we will overwrite ourselves. * r4 = final kernel address (possibly with LSB set) * r9 = size of decompressed image * r10 = end of this image, including bss/stack/malloc space if non XIP * We basically want: * r4 - 16k page directory >= r10 -> OK * r4 + image length <= address of wont_overwrite -> OK * Note: the possible LSB in r4 is harmless here. */ add r10, r10, #16384 cmp r4, r10 bhs wont_overwrite add r10, r4, r9 adr r9, wont_overwrite cmp r10, r9 bls wont_overwrite
这里r4、r9和r10中的内容见注释。这部分代码用来分析当前代码是否会和最后的解压部分重叠,如果有重叠则需要执行代码搬移。首先比较内核解压地址r4-16Kb(这里是0x00004000,包括16KB的内核页表存放位置)和r10,如果r4 – 16kB >= r10,则无需搬移,否则继续计算解压后的内核末尾地址是否在当前运行地址之前,如果是则同样无需搬移,不然的话就需要进行搬移了。
总结一下可能的3种情况:
(1) 内核起始地址– 16kB >= 当前镜像结束地址:无需搬移
(2) 内核结束地址 <= wont_overwrite运行地址:无需搬移
(3) 内核起始地址– 16kB < 当前镜像结束地址 && 内核结束地址 > wont_overwrite运行地址:需要搬移
仔细分析一下,这里内核真正运行的地址是0x00004000,而现在代码的运行地址显然已经在该地址之后了反汇编发现wont_overwrite的运行地址是0x00008000+0x00000168),而且内核解压后的空间必然会覆盖掉这里(内核解压后的大小大于0x00000168),所以这里会执行代码搬移。/* * 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. */ add r10, r10, #((reloc_code_end - restart + 256) & ~255) bic r10, r10, #255 /* Get start of code we want to copy and align it down. */ adr r5, restart bic r5, r5, #31
从这里开始会将镜像搬移到解压的内核地址之后,首先将解压后的内核结束地址进行扩展,扩展大小为代码段的大小(reloc_code_end定义在head.s的最后)保存到r10中,即搬运目的起始地址,然后r5保存了restart的起始地址,并进行对齐,即搬运的原起始地址。反汇编查看这里扩展的大小为0x800:
11c: e28aab02 add sl, sl, #2048 ; 0x800 120: e3caa0ff bic sl, sl, #255 ; 0xff 124: e24f506c sub r5, pc, #108 ; 0x6c 128: e3c5501f 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 #ifndef CONFIG_ZBOOT_ROM /* cache_clean_flush may use the stack, so relocate it */ add sp, sp, r6 #endif
这里首先计算出需要搬运的大小保存到r9中,搬运的原结束地址到r6中,搬运的目的结束地址到r9中。注意这里只搬运代码段和数据段,并不包含bss、栈和堆空间。
接下来开始执行代码搬移,这里是从后往前搬移,一直到r6 == r5结束,然后r6中保存了搬移前后的偏移,并重定向栈指针(cache_clean_flush可能会使用到栈)。bl cache_clean_flush adr r0, BSYM(restart) add r0, r0, r6 mov pc, r0
首先调用cache_clean_flush清楚缓存,然后将PC的值设置为搬运后restart的新地址,然后重新从restart开始执行。这次由于进行了代码搬移,所以会在检查自覆盖时进入wont_overwrite处执行。
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 (possibly with LSB set) * r5 = appended dtb size (0 if not present) * r7 = architecture ID * r8 = atags pointer * r11 = GOT start * r12 = GOT end * sp = stack pointer */ orrs r1, r0, r5 beq not_relocated
这里的注释列出了现有所有寄存器值得含义,如果r0为0则说明当前运行的地址就是链接地址,无需进行重定位,跳转到not_relocated执行,但是这里运行的地址已经被移动到内核解压地址之后,显然不会是链接地址0x00000168(反汇编代码中得到),所以这里需要重新修改GOT表中的变量地址来实现重定位。
add r11, r11, r0 add r12, r12, r0 #ifndef CONFIG_ZBOOT_ROM /* * If we're running fully PIC === CONFIG_ZBOOT_ROM = n, * we need to fix up pointers into the BSS region. * Note that the stack pointer has already been fixed up. */ add r2, r2, r0 add r3, r3, r0
这里更新GOT表的运行起始地址到r11和结束地址到r12中去,然后同样更新BSS段的运行地址(需要修正BSS段的指针)。接下来开始执行重定位:
/* * Relocate all entries in the GOT table. * Bump bss entries to _edata + dtb size */ 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
通过r1获取GOT表中的一项,然后对这一项的地址进行修正,如果修正后的地址 < BSS段的起始地址,或者在BSS段之中则再加上DTB的大小(如果不支持DTB则r5的值为0),然后再将值写回GOT表中去。如此循环执行直到遍历完GOT表。来看一下反汇编出来的GOT表,加深理解:
006f2a58 <.got>: 6f2a58: 006f2a49 rsbeq r2, pc, r9, asr #20 6f2a5c: 006f2a94 mlseq pc, r4, sl, r2 ; <UNPREDICTABLE> 6f2a60: 0000466c andeq r4, r0, ip, ror #12 6f2a64: 006f2a90 mlseq pc, r0, sl, r2 ; <UNPREDICTABLE> 6f2a68: 006f2a80 rsbeq r2, pc, r0, lsl #21 6f2a6c: 0000090c andeq r0, r0, ip, lsl #18 6f2a70: 006f2a88 rsbeq r2, pc, r8, lsl #21 6f2a74: 006f2a8c rsbeq r2, pc, ip, lsl #21 6f2a78: 006f2a84 rsbeq r2, pc, r4, lsl #21
以这里的6f2a60: 0000466c为例,0000466c为input_data符号的链接地址,定义在arch/arm/boot/compressed/piggy.gzip.S中,可以算作是全局变量,这里在执行重定位时会将6f2a60地址处的值加上偏移,可得到input_data符号的运行地址,以此完成重定位工作,以后在内核的C代码中如果用到input_data符号就从这里的地址加载。
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在重定位完成后,继续执行not_relocated部分代码,这里循环清零BSS段。
/* * Did we skip the cache setup earlier? * That is indicated by the LSB in r4. * Do it now if so. */ tst r4, #1 bic r4, r4, #1 blne cache_on
这里检测r4中的最低位,如果已经置位则说明在前面执行restart前并没有执行cache_on来打开缓存(见前文),这里补执行。
/* * 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 bl decompress_kernel
到此为止,C语言的执行环境已经准备就绪,设置一些指针就可以开始解压内核了(这里的内核解压部分是使用C代码写的)。
这里r0~r3的4个寄存器是decompress_kernel()函数传参用的,r0传入内核解压后的目的地址,r1传入堆空间的起始地址,r2传入堆空间的结束地址,r3传入机器码,然后就开始调用decompress_clean_flush()函数执行内核解压操作:
void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p, unsigned long free_mem_ptr_end_p, int 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("Uncompressing Linux..."); 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"); }
真正执行内核解压的函数是do_decompress()->decompress()(该函数会根据不同的压缩方式调用不同的解压函数),在解压前会在终端上打印“Uncompressing Linux...”,结束后会打印出“done, booting the kernel.\n”。(这里有一点疑问:这里会在终端输出,那串口的驱动是在什么时候初始化的?)
bl cache_clean_flush bl cache_off mov r1, r7 @ restore architecture number mov r2, r8 @ restore atags pointer
解压完成后就刷新缓存,然后将缓存(包括MMU关闭),这里之所以要打开缓存和MMU是为了加速内核解压。
然后将机器码和内启动参数atags恢复到r1和r2寄存器中,为跳转到解压后的内核代码做准备。
b __enter_kernel
__enter_kernel: mov r0, #0 @ must be 0 ARM( mov pc, r4 ) @ call kernel THUMB( bx r4 ) @ entry point is always ARM
补充:缓存和MMU初始化cache_on的执行流程
/* * Turn on the cache. We need to setup some page tables so that we * can have both the I and D caches on. * * We place the page tables 16k down from the kernel execution address, * and we hope that nothing else is using it. If we're using it, we * will go pop! * * On entry, * r4 = kernel execution address * r7 = architecture number * r8 = atags pointer * On exit, * r0, r1, r2, r3, r9, r10, r12 corrupted * This routine must preserve: * r4, r7, r8 */ .align 5 cache_on: mov r3, #8 @ cache_on function b call_cache_fn
注释中说明了,为了开启I Cache和D Cache,需要建立页表(开启MMU),而页表使用的就是内核运行地址以下的16KB空间(对于我的环境来说地址就等于0x00004000~0x00008000)。同时在运行的过程中r0~r3以及r9、r10和r12寄存器会被使用。
这里首先在r3中保存打开缓存函数表项在cache操作表中的地址偏移(这里为8,cache操作表见后文),然后跳转到call_cache_fn中。
/* * Here follow the relocatable cache support functions for the * various processors. This is a generic hook for locating an * entry and jumping to an instruction at the specified offset * from the start of the block. Please note this is all position * independent code. * * r1 = corrupted * r2 = corrupted * r3 = block offset * r9 = corrupted * r12 = corrupted */ call_cache_fn: adr r12, proc_types #ifdef CONFIG_CPU_CP15 mrc p15, 0, r9, c0, c0 @ get processor ID #else ldr r9, =CONFIG_PROCESSOR_ID #endif 1: ldr r1, [r12, #0] @ get value ldr r2, [r12, #4] @ get mask eor r1, r1, r9 @ (real ^ match) tst r1, r2 @ & mask 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
首先保存cache操作表的运行地址到r12寄存器中,proc_types定义在head.s中的825行:
/* * Table for cache operations. This is basically: * - CPU ID match * - CPU ID mask * - 'cache on' method instruction * - 'cache off' method instruction * - 'cache flush' method instruction * * We match an entry using: ((real_id ^ match) & mask) == 0 * * Writethrough caches generally only need 'on' and 'off' * methods. Writeback caches _must_ have the flush method * defined. */ .align 2 .type proc_types,#object
表中的每一类处理器都包含以下5项(如果不存在缓存操作函数则使用“mov pc, lr”占位):
(1) CPU ID
(2) CPU ID 位掩码(用于匹配CPU类型用)
(3) 打开缓存“cache on”函数入口
(4) 关闭缓存“cache off”函数入口
(5) 刷新缓存“cache flush”函数入口
其中我环境中使用到的ARMv6的cache操作表如下:
.word 0x0007b000 @ ARMv6 .word 0x000ff000 W(b) __armv6_mmu_cache_on W(b) __armv4_mmu_cache_off W(b) __armv6_mmu_cache_flush
我的环境中由于配置了CPU_CP15条件编译项,所以这里将从CP15中获取CPU型号而不是从内核配置项中获取。
然后逐条对cache操作表中的CPU类型进行匹配,如果匹配上了就跳转到相应的函数入口执行。
这里通过反汇编代码来理解一下:
0000044c <call_cache_fn>: 44c: e28fc01c add ip, pc, #28 450: ee109f10 mrc 15, 0, r9, cr0, cr0, {0} 454: e59c1000 ldr r1, [ip] 458: e59c2004 ldr r2, [ip, #4] 45c: e0211009 eor r1, r1, r9 460: e1110002 tst r1, r2 464: 008cf003 addeq pc, ip, r3 468: e28cc014 add ip, ip, #20 46c: eafffff8 b 454 <call_cache_fn+0x8>
这里首先在r12中获取了proc_types的运行地址:pc + 0x8 + 0x1c:
00000470 <proc_types>:然后逐条匹配,最后会匹配到ARMv6部分,cache table中ARMv6部分的代码如下:
5b0: 0007b000 andeq fp, r7, r0 5b4: 000ff000 andeq pc, pc, r0 5b8: eaffff5c b 330 <__armv6_mmu_cache_on> 5bc: ea00001f b 640 <__armv4_mmu_cache_off> 5c0: ea00004f b 704 <__armv6_mmu_cache_flush>
于是PC寄存器就执行上面5b8地址处的内容,这条机器码被翻译为相对跳转到__armv6_mmu_cache_on处执行:
00000330 <__armv6_mmu_cache_on>:
这样__armv6_mmu_cache_on就被调用执行了。
__armv6_mmu_cache_on中会通过写寄存器来开启MMU、I Cache和D Cache,这里具体就不仔细分析了,其中为MMU建立页表有必要分析一下:
前文已经分析过在内核最终运行地址r4下面有16KB的空间(我环境中是0x00004000~0x00008000),这就是用来存放页表的,但是现在要建立的页表在内核真正启动后会被销毁,只是用于零时存放。同时这里将要建立的页表映射关系是1:1映射(即虚拟地址 == 物理地址)。
__setup_mmu: sub r3, r4, #16384 @ Page directory size bic r3, r3, #0xff @ Align the pointer bic r3, r3, #0x3f00首先在r3中保存的是页目录表的基地址,需要将低14位清零进行16KB对齐。
/* * Initialise the page tables, turning on the cacheable and bufferable * bits for the RAM area only. */ mov r0, r3 mov r9, r0, lsr #18 mov r9, r9, lsl #18 @ start of RAM add r10, r9, #0x10000000 @ a reasonable RAM size
这里建立页表,只对物理RAM空间建立cache和buffer。然后通过将r0(r3)中的地址值右移18位再左移18位(即清零r3中地址的低18位),得到物理RAM空间的“初始地址”(其实是估计值)并保存到r9(0x00000000)中去,然后将该地址加上256MB的大小作为物理RAM的“结束地址”(也是估计值)并保存到r10(0x10000000)中去。这里的另一个隐含意思也就是最多映射256MB大小的空间。
mov r1, #0x12 @ XN|U + section mapping orr r1, r1, #3 << 10 @ AP=11 add r2, r3, #16384 1: cmp r1, r9 @ if virt > start of RAM cmphs r10, r1 @ && end of RAM > virt bic r1, r1, #0x1c @ clear XN|U + C + B orrlo r1, r1, #0x10 @ Set XN|U for non-RAM orrhs r1, r1, r6 @ set RAM section settings str r1, [r0], #4 @ 1:1 mapping add r1, r1, #1048576 teq r0, r2 bne 1b
ARM11的 MMU支持两级分页机制,用户通过配置TTBCR可选映射方式,这里只采用一级映射方式,每页的大小为1MB,4GB线性空间占4096个表项。ARM手册中列出了映射的关系如下:
其中Translation table base的值就是页表存放的起始地址值0x00004000。这里虚拟地址转换到物理地址的方式如下:首先将虚拟地址的高12位(页表中的索引)左移2位(正好为14位长度,补齐Translation table base中低14位的空白)得到在页表中的偏移地址,该偏移值加上页表基地址就得到了对应表项的物理地址,然后取出表项中的高12位值作为物理地址的基地址,最后以虚拟地址的低20位作为偏移地址值就得到了最终映射后的物理地址。
这里First-level descriptor中的内容就是这里程序中要填充的值,其中最低2位为0b10表示表项中保存的是Section base address,而不是2级页表的基地址。
来继续分析代码,这里首先r1 =0b 1100 0001 0010 = 0xC12(用于设置MMU区域表项的低12位状态位),r2为页表空间的结束地址,然后开始循环建立页表项。
接着r1比较r9和r10以设置MMU区域表项状态位:(其中r6中的值在前面__armv6_mmu_cache_on中赋值为0x1E)
(1) r1 > r9 && r1 <r10 (r1的值在物理RAM地址范围内):
设置RAM表项的C+B 位来开启cache和buffer,同时清除XN表示可执行code
(2) r1 < r9 || r1 > r10(r1的值在物理RAM地址范围外):
设置RAM表项的XN位并清除C+B位来关闭cache和buffer,不可执行code
在设置完状态为后就要写入页表的相应地址中去了,然后将页表的地址+4(指向下一个表项),物理地址空间+1M设置下一项(下一个需要映射物理地址的基地址),直到填完所有的4096表项。设置完后页表项与映射关系如下:/* * If ever we are running from Flash, then we surely want the cache * to be enabled also for our execution instance... We map 2MB of it * so there is no map overlap problem for up to 1 MB compressed kernel. * If the execution is in RAM then we would only be duplicating the above. */ orr r1, r6, #0x04 @ ensure B is set for this orr r1, r1, #3 << 10 mov r2, pc mov r2, r2, lsr #20 orr r1, r1, r2, lsl #20 add r0, r3, r2, lsl #2 str r1, [r0], #4 add r1, r1, #1048576 str r1, [r0] mov pc, lr ENDPROC(__setup_mmu)
如果代码不是运行在RAM中而是运行在FLASH中的,则映射2MB代码,如果运行在RAM中,则这部分代码重复前面的工作。
同前面一样,设置r1区域表项的低12位,这里首先初始化r1 = 0xC1E,然后计算出当前PC运行地址的基地址到r2和r1中(我的环境中r2 = r1 = 0)。然后计算r2 << 2得到映射基地址在表中的偏移(因为是1:1映射,所以可以这样计算出来),再加上页表的起始地址就得到了该页表项的地址了,然后将这2MB空间的地址写入这两个表项中即完成了整个MMU页表的建立,最后mov pc lr返回。
参考文献:《ARM Linux内核源码剖析》