本文整理了ARM Linxu启动流程的第二阶段——start_kernel前启动阶段(汇编部分),内核版本为3.12.35。我以手上的树莓派b(ARM11)为平台示例来分析Linux内核在自解压后到跳转运行start_kernel之前所做的主要初始化工作:包括参数有效性验证、创建初始页表和MMU初始化等。
单板:树莓派b
在内核启动时执行自解压完成后,会跳转到解压后的地址处运行,在我的环境中就是地址0x00008000处,然后内核启动并执行初始化。
首先给出你内核启动的汇编部分的总流程如下:
内核启动程序的入口:参见arch/arm/kernel/vmlinux.lds(由arch/arm/kernel/vmlinux.lds.S生成)。
arch/arm/kernel/vmlinux.lds:
ENTRY(stext) jiffies = jiffies_64; SECTIONS { ...... . = 0xC0000000 + 0x00008000; .head.text : { _text = .; *(.head.text) } .text : { /* Real text segment */ _stext = .; /* Text and read-only data */
此处的TEXT_OFFSET表示内核起始地址相对于RAM地址的偏移值,定义在arch/arm/Makefile中,值为0x00008000:
textofs-y := 0x00008000 ...... # The byte offset of the kernel image in RAM from the start of RAM. TEXT_OFFSET := $(textofs-y)
PAGE_OFFSET表示内核虚拟地址空间的其实地址,定义在arch/arm/include/asm/memory.h中:
#ifdef CONFIG_MMU /* * PAGE_OFFSET - the virtual address of the start of the kernel image * TASK_SIZE - the maximum size of a user space task. * TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area */ #define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)CONFIG_PAGE_OFFSET定义在arch/arm/Kconfig中,采用默认值0xC0000000。
config PAGE_OFFSET hex default 0x40000000 if VMSPLIT_1G default 0x80000000 if VMSPLIT_2G default 0xC0000000
所以,可以看出内核的链接地址采用的是虚拟地址,地址值为0xC0008000。
内核启动程序的入口在linux/arch/arm/kernel/head.S中,head.S中定义了几个比较重要的变量,在看分析程序前先来看一下:
/* * swapper_pg_dir is the virtual address of the initial page table. * We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must * make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect * the least significant 16 bits to be 0x8000, but we could probably * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000. */ #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET) #if (KERNEL_RAM_VADDR & 0xffff) != 0x8000 #error KERNEL_RAM_VADDR must start at 0xXXXX8000 #endif #ifdef CONFIG_ARM_LPAE /* LPAE requires an additional page for the PGD */ #define PG_DIR_SIZE 0x5000 #define PMD_ORDER 3 #else #define PG_DIR_SIZE 0x4000 #define PMD_ORDER 2 #endif .globl swapper_pg_dir .equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE .macro pgtbl, rd, phys add \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE .endm
其中KERNEL_RAM_VADDR表示内核启动地址的虚拟地址,即前面看到的链接地址0xC0008000,同时内核要求这个地址的第16位必须是0x8000。
然后由于没有配置ARM LPAE,则采用一级映射结构,页表的大小为16KB,页大小为1MB。
最后swapper_pg_dir表示初始页表的起始地址,这个值等于内核起始虚拟地址-页表大小=0xC0004000(内核起始地址下16KB空间存放页表)。虚拟地址空间如下图:需要说明一下:在我的环境中,内核在自解压阶段被解压到了0x00008000地址处,由于内核入口链接地址采用的是虚拟地址0xC0008000,这两个地址并不相同;并且此时MMU并没有被使能,所以无法进行虚拟地址到物理地址的转换,程序开始执行后在打开MMU前的将使用位置无关码。
在知道了内核的入口位置后,来看一下此时的设备和寄存器的状态:
/* * Kernel startup entry point. * --------------------------- * * This is normally called from the decompressor code. The requirements * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0, * r1 = machine nr, r2 = atags or dtb pointer. * * This code is mostly position independent, so if you link the kernel at * 0xc0008000, you call this at __pa(0xc0008000). * * See linux/arch/arm/tools/mach-types for the complete list of machine * numbers for r1. * * We're trying to keep crap to a minimum; DO NOT add any machine specific * crap here - that's what the boot loader (or in extreme, well justified * circumstances, zImage) is for. */ .arm __HEAD ENTRY(stext)
注释中说明了,此时的MMU关闭、D-cache关闭、r0 = 0、r1 = 机器码、r2 = 启动参数atags或dtb的地址(我的环境中使用的是atags),同时内核支持的机器码被定义在了linux/arch/arm/tools/mach-types中。我树莓派使用的是:
bcm2708 MACH_BCM2708 BCM2708 3138
下面来逐行分析代码:
THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM. THUMB( bx r9 ) @ If this is a Thumb-2 kernel, THUMB( .thumb ) @ switch to Thumb now. THUMB(1: ) #ifdef CONFIG_ARM_VIRT_EXT bl __hyp_stub_install #endif @ ensure svc mode and all interrupts masked safe_svcmode_maskall r9 mrc p15, 0, r9, c0, c0 @ get processor id bl __lookup_processor_type @ r5=procinfo r9=cpuid
这里的safe_svcmode_maskall是一个宏,定义在arch/arm/include/asm/assembler.h中,它的作用就是确保ARM进入SVC工作模式并屏蔽所有的中断(此时关闭中断的原因是中断向量表尚未建立,内核无能力响应中断)。
然后获取处理器ID保存到r9寄存器中,接着跳转到__lookup_processor_type寻找对应处理器ID的proc_info地址。__lookup_processor_type定义在arch/arm/kernel/head-common.S中:
/* * Read processor ID register (CP#15, CR0), and look up in the linker-built * supported processor list. Note that we can't use the absolute addresses * for the __proc_info lists since we aren't running with the MMU on * (and therefore, we are not in the correct address space). We have to * calculate the offset. * * r9 = cpuid * Returns: * r3, r4, r6 corrupted * r5 = proc_info pointer in physical address space * r9 = cpuid (preserved) */ __lookup_processor_type: adr r3, __lookup_processor_type_data ldmia r3, {r4 - r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b mov r5, #0 @ unknown processor 2: mov pc, lr ENDPROC(__lookup_processor_type)
首先获取处理器相关信息表的运行地址并保存到r3寄存器中。内核将所有的处理器信息都保存在proc_info_list结构体表中,它的定义如下(asm/procinfo.h):
/* * Note! struct processor is always defined if we're * using MULTI_CPU, otherwise this entry is unused, * but still exists. * * NOTE! The following structure is defined by assembly * language, NOT C code. For more information, check: * arch/arm/mm/proc-*.S and arch/arm/kernel/head.S */ struct proc_info_list { unsigned int cpu_val; unsigned int cpu_mask; unsigned long __cpu_mm_mmu_flags; /* used by head.S */ unsigned long __cpu_io_mmu_flags; /* used by head.S */ unsigned long __cpu_flush; /* used by head.S */ const char *arch_name; const char *elf_name; unsigned int elf_hwcap; const char *cpu_name; struct processor *proc; struct cpu_tlb_fns *tlb; struct cpu_user_fns *user; struct cpu_cache_fns *cache; };
结构体中描述了CPU相关的信息,其中__cpu_mm_mmu_flags、__cpu_io_mmu_flags和__cpu_flush这三个字段将会在head.s中使用到。处理器相关信息都被保存在.init.proc.info段中:
/* * Look in <asm/procinfo.h> for information about the __proc_info structure. */ .align 2 .type __lookup_processor_type_data, %object __lookup_processor_type_data: .long . .long __proc_info_begin .long __proc_info_end .size __lookup_processor_type_data, . - __lookup_processor_type_data
vmlinux.lds:
.init.proc.info : { . = ALIGN(4); __proc_info_begin = .; *(.proc.info.init) __proc_info_end = .; }
其中每种类型处理器的信息定义在arch/arm/mm/proc-*.S下,例如我的环境定义在proc-v6.S中:
.section ".proc.info.init", #alloc, #execinstr /* * Match any ARMv6 processor core. */ .type __v6_proc_info, #object __v6_proc_info: .long 0x0007b000 .long 0x0007f000 ALT_SMP(.long \ PMD_TYPE_SECT | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ | \ PMD_FLAGS_SMP) ALT_UP(.long \ PMD_TYPE_SECT | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ | \ PMD_FLAGS_UP) .long PMD_TYPE_SECT | \ PMD_SECT_XN | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ b __v6_setup ......
回到__lookup_processor_type程序中,程序接着在r4、r5和r6中保存__lookup_processor_type_data、__proc_info_begin和__proc_info_end的链接地址(即虚拟地址),然后通过r3 = r3 – r4得到运行地址和链接地址之间的偏移值并将r5和r6中的地址值修正为__proc_info_begin和__proc_info_end的运行地址。
然后从proc_info_list结构中取出cpu_val和cpu_mask字段的内容,和r9中保存的处理器ID进行比较,若匹配上了则通过r5寄存器返回当前处理器的proc_info_list结构信息运行地址,否则r5 = r5 + PROC_INFO_SZ(即将r5指向下一条处理器的proc_info_list结构提信息)继续进行匹配。若全部匹配失败,则r5返回0。movs r10, r5 @ invalid processor (r5=0)? THUMB( it eq ) @ force fixup-able long branch encoding beq __error_p @ yes, error 'p'
回到外层函数后,这里会先将返回值付给r10,然后判断是否返回值是否为0,若为0表示没有匹配到对应的处理器信息,调用__error_p打印出错信息并进入死循环,内核启动失败。
#ifdef CONFIG_ARM_LPAE mrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0 and r3, r3, #0xf @ extract VMSA support cmp r3, #5 @ long-descriptor translation table format? THUMB( it lo ) @ force fixup-able long branch encoding blo __error_p @ only classic page table format #endif
这里ARM_LAPE表示大物理内存扩展,我的环境下并没有配置该项,暂不考虑。
#ifndef CONFIG_XIP_KERNEL adr r3, 2f ldmia r3, {r4, r8} sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET) add r8, r8, r4 @ PHYS_OFFSET #else ldr r8, =PHYS_OFFSET @ always constant in this case #endif
这里将计算起始RAM物理地址并保存到r8中,计算的方法同前面获取CPU信息结构地址的方法类似,首先获取标号为2处的运行地址和链接地址(通过反汇编查看,我的环境分别是:0x00008070和0xC0008070),一减之后就得到了运行地址和物理地址的差值(0xC0000000),然后用这个差值加上PAGE_OFFSET(0xC0000000)即可得到实际物理内存的起始地址PHYS_OFFSET(0x00000000)。
现在来查看反汇编代码,加深理解:
c0008040: e28f3028 add r3, pc, #40 ; 0x28 c0008044: e8930110 ldm r3, {r4, r8} c0008048: e0434004 sub r4, r3, r4 c000804c: e0888004 add r8, r8, r4 ...... c0008070: c0008070 andgt r8, r0, r0, ror r0 c0008074: c0000000 andgt r0, r0, r0
这里r3 = 0x00008040 + 0x8 + 0x28 = 0x00008070,r4 =0xC0008070,r8 = 0xC0000000,在经过偏移处理后,r8的值就变成了0x00000000,即物理RAM首地址在内存地址空间中的偏移PAGE_OFFSET。(这里有一点疑问,如果我这里内核的运行地址并不是在0x00008000,那这个计算出道的物理RAM首地址不是就不正确了?)
/* * r1 = machine no, r2 = atags or dtb, * r8 = phys_offset, r9 = cpuid, r10 = procinfo */ bl __vet_atags
现在来确认一下寄存器中保存内容的含义:
r1:机器码
r2:atag或者dtb的地址
r8:物理内存地址偏移
r9:获取到的CPU ID
r10:处理器信息结构地址
然后调用__vet_atags来验证r2中地址值得有效性
/* Determine validity of the r2 atags pointer. The heuristic requires * that the pointer be aligned, in the first 16k of physical RAM and * that the ATAG_CORE marker is first and present. If CONFIG_OF_FLATTREE * is selected, then it will also accept a dtb pointer. Future revisions * of this function may be more lenient with the physical address and * may also be able to move the ATAGS block if necessary. * * Returns: * r2 either valid atags pointer, valid dtb pointer, or zero * r5, r6 corrupted */ __vet_atags: tst r2, #0x3 @ aligned? bne 1f ldr r5, [r2, #0] #ifdef CONFIG_OF_FLATTREE ldr r6, =OF_DT_MAGIC @ is it a DTB? cmp r5, r6 beq 2f #endif cmp r5, #ATAG_CORE_SIZE @ is first tag ATAG_CORE? cmpne r5, #ATAG_CORE_SIZE_EMPTY bne 1f ldr r5, [r2, #4] ldr r6, =ATAG_CORE cmp r5, r6 bne 1f 2: mov pc, lr @ atag/dtb pointer is ok 1: mov r2, #0 mov pc, lr ENDPROC(__vet_atags)
首先验证是否4字节地址对齐,若不对齐则直接将r2内容清空并返回。接着读取r2地址处的内容到r5寄存器中,这里若配置了CONFIG_OF_FLATTREE就会判断是否是DTB,我的环境中并没有配置。
然后进行atag的验证,若是atag,则r2地址处的内容将保存tag_header中的size值(arch/arm/include/uapi/asm/setup.h),同时内核也要求atag信息的第一项必须是ATAT_CORE类型的项
struct tag_header { __u32 size; __u32 tag; }; ...... struct tag_core { __u32 flags; /* bit 0 = read-only */ __u32 pagesize; __u32 rootdev; };
该CORE项的size值为sizeof(struct tag_header) + sizeof(struct tag_core) >> 2,正好等于ATAG_CORE_SIZE:
#define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
比较完size值后就将地址值偏移4字节读取tag值,比较是否等于ATAG_CORE,若是则验证通过则跳转到标号2处直接返回。
#ifdef CONFIG_SMP_ON_UP bl __fixup_smp #endif #ifdef CONFIG_ARM_PATCH_PHYS_VIRT bl __fixup_pv_table #endif bl __create_page_tables
然后我这里没有配置CONFIG_SMP_ON_UP和CONFIG_ARM_PATCH_PHYS_VIRT选项(他们的内核配置解释分别为Allowbooting SMP kernel on uniprocessor systems和Patch physical tovirtual translations at runtime),接下来就要跳转到__create_page_tables中创建初始页表了。
/* * Setup the initial page tables. We only setup the barest * amount which are required to get the kernel running, which * generally means mapping in the kernel code. * * r8 = phys_offset, r9 = cpuid, r10 = procinfo * * Returns: * r0, r3, r5-r7 corrupted * r4 = page table (see ARCH_PGD_SHIFT in asm/memory.h) */ __create_page_tables: pgtbl r4, r8 @ page table address
这里的注释中说明了,创建初始页表的过程只会创建内核代码部分地址的页表。
这里的pgtbl r4, r8表示获取存放页表首地址的运行时地址(物理地址)到r4中去,它在反汇编中被翻译成:
c0008078 <__create_page_tables>: c0008078: e2884901 add r4, r8, #16384 ; 0x4000
可见这里的r4值就是0x00004000,正好是页表的起始物理地址。
/* * Clear the swapper page table */ mov r0, r4 mov r3, #0 add r6, r0, #PG_DIR_SIZE 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b
然后将页表内存空间清零,从0x00004000~0x00008000的空间都清零。
#ifdef CONFIG_ARM_LPAE /* * Build the PGD table (first level) to point to the PMD table. A PGD * entry is 64-bit wide. */ mov r0, r4 add r3, r4, #0x1000 @ first PMD table address orr r3, r3, #3 @ PGD block type mov r6, #4 @ PTRS_PER_PGD mov r7, #1 << (55 - 32) @ L_PGD_SWAPPER 1: #ifdef CONFIG_CPU_ENDIAN_BE8 str r7, [r0], #4 @ set top PGD entry bits str r3, [r0], #4 @ set bottom PGD entry bits #else str r3, [r0], #4 @ set bottom PGD entry bits str r7, [r0], #4 @ set top PGD entry bits #endif add r3, r3, #0x1000 @ next PMD table subs r6, r6, #1 bne 1b add r4, r4, #0x1000 @ point to the PMD tables #ifdef CONFIG_CPU_ENDIAN_BE8 add r4, r4, #4 @ we only write the bottom word #endif #endif
由于没有配置ARM_LPAE,这一部分内容先暂时不做分析,接着往下看。
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags /* * Create identity mapping to cater for __enable_mmu. * This identity mapping will be removed by paging_init(). */ adr r0, __turn_mmu_on_loc ldmia r0, {r3, r5, r6} sub r0, r0, r3 @ virt->phys offset add r5, r5, r0 @ phys __turn_mmu_on add r6, r6, r0 @ phys __turn_mmu_on_end mov r5, r5, lsr #SECTION_SHIFT mov r6, r6, lsr #SECTION_SHIFT
这里开始创建特殊映射来满足开启MMU的需求,该映射将会在内核后续初始化执行paging_init()时被销毁。
首先从处理器的procinfo结构中获取__cpu_mm_mmu_flags参数保存在r7中,然后获取标号__turn_mmu_on_loc处的运行地址保存到r0中,然后使用前文中类似的手段获得__trun_mmu_on和__trun_mmu_on_end入口处的实际运行的物理地址保存到r5和r6寄存器中。
__turn_mmu_on_loc: .long . .long __turn_mmu_on .long __turn_mmu_on_end
然后由于我的环境中没有开启LAPE,采用一级映射方式,映射单位为1M,所以这里的SECTION_SHIFT为20。这里对r5和r6中的值右移20位,得到了__trun_mmu_on和__trun_mmu_on_end的物理基地址。
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping cmp r5, r6 addlo r5, r5, #1 @ next section blo 1b
这里将r5左移20位后或上r7中的标识位,得到了对应的First-level descriptor(即也表中存放的一级描述符,参见《ARM Linux启动流程分析——内核自解压阶段》),然后将这个描述符写到页表中对应的项中去。
这里包括__trun_mmu_on和__trun_mmu_on_end之间地址空间的特殊映射方式同样采用的是1:1映射,因此这里计算对应页表向的方式如下:
页表地址 = 映射物理基址 << PMD_ORDER(2)
例如:我环境中__trun_mmu_on物理地址为0xc0433398,它的基地址为0xc04,转换为对应的表项为0x3010,所以会在0x00004000+0x3010处的页表地址中写入“页描述符”,该描述描述符中的基址同样为0xc04。在进行虚拟地址到物理地址的转换过程中,即可实现x线性转换(转换方式参见《ARM Linux启动流程分析——内核自解压阶段》)。
如此循环映射完整个__turn_mmu_on部分代码,映射后的地址空间如下图:
/* * Map our RAM from the start to the end of the kernel .bss section. */ add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER) ldr r6, =(_end - 1) orr r3, r8, r7 add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER) 1: str r3, [r0], #1 << PMD_ORDER add r3, r3, #1 << SECTION_SHIFT cmp r0, r6 bls 1b
映射完开启MMU部分的代码后,接下来开始映射内核。
首先将PAGE_OFFSET(0xc0008000)右移(20-2)位在加上r4(页表物理基地址)得到内核起始链接地址对应页表项的物理地址,保存到r0中。
接着获取内核代码的结束虚拟地址(包括了bss段)保存到r6中,_end定义在vmlinux.lds中:
_edata_loc = __data_loc + SIZEOF(.data); . = ALIGN(0); __bss_start = .; . = ALIGN(0); .sbss : AT(ADDR(.sbss) - 0) { *(.sbss) *(.scommon) } . = ALIGN(0); .bss : AT(ADDR(.bss) - 0) { *(.bss..page_aligned) *(.dynbss) *(.bss) *(COMMON) } . = ALIGN(0); __bss_stop = .; _end = .; .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) } .comment 0 : { *(.comment) } }
在我的环境中,它的值为0xc116c517。
接着将r7或上r8得到First-leveldescriptor保存到r3中(该值的高12位为0),然后计算内核结束虚拟地址对应应页表项的物理地址保存到r6中。接下来的代码将r0~r6中的页表项循环循环填充上需要映射的First-level descriptor,每一次循环都会将r3+1<<SECTION_SHIFT,即加上基地址增量。建立映射表后的内存映射关系如下:
/* * Then map boot params address in r2 if specified. * We map 2 sections in case the ATAGs/DTB crosses a section boundary. */ mov r0, r2, lsr #SECTION_SHIFT movs r0, r0, lsl #SECTION_SHIFT subne r3, r0, r8 addne r3, r3, #PAGE_OFFSET addne r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER) orrne r6, r7, r0 strne r6, [r3], #1 << PMD_ORDER addne r6, r6, #1 << SECTION_SHIFT strne r6, [r3]
在映射完内核之后就需要映射内核启动参数了,内核的启动参数地址保存在r2中。在前面的程序中已经对r2中启动参数地址的有效性进行了验证,如果无效则现在r2中的值就是0,将不做映射操作。
此处代码中的前两行就是为了判断该值是否为0,如果不为0才进行映射操作。
首先获取启动参数地址相对于物理RAM的偏移值并保存到r3中,然后再对该值加上PAGE_OFFSET(0xc0000000)得到其所需映射到的虚拟地址,接着找到对应的页表和生成First-level descriptor,最后连续写入连续的两项页表项来完成2页的映射。也就是说不论内核启动参数有多大,这里默认只映射2MB的内存。mov pc, lr
映射完3个内存区间后,我这里过滤掉其他未定义的条件编译项后直接看到代码中执行返回操作,初始页表创建完成。
我的环境中内核启动参数的地址为0x00000100,所以映射结果如下:
/* * The following calls CPU specific code in a position independent * manner. See arch/arm/mm/proc-*.S for details. r10 = base of * xxx_proc_info structure selected by __lookup_processor_type * above. On return, the CPU will be ready for the MMU to be * turned on, and r0 will hold the CPU control register value. */ ldr r13, =__mmap_switched @ address to jump to after @ mmu has been enabled adr lr, BSYM(1f) @ return (PIC) address mov r8, r4 @ set TTBR1 to swapper_pg_dir ARM( add pc, r10, #PROCINFO_INITFUNC ) THUMB( add r12, r10, #PROCINFO_INITFUNC ) THUMB( mov pc, r12 ) 1: b __enable_mmu
这里首先保存__mmap_switched函数的链接地址(虚拟地址)到r13中,它是MMU开启后的第一个要跳转运行的虚拟地址。
然后保存返回地址为下文中标号1处的地址,此处为b __enable_mmu;接着保存r4中的页表物理地址到r8寄存其中,最后就跳转到架构相关的处理器初始化函数中执行初始化,为开启MMU做准备工作;在执行完初始化函数后,将返回到lr保存的地址运行,开启MMU。
这里的PROCINFO_INITFUNC宏定义为16,此时PC的值正好为参数__cpu_flush的值
struct proc_info_list { unsigned int cpu_val; unsigned int cpu_mask; unsigned long __cpu_mm_mmu_flags; /* used by head.S */ unsigned long __cpu_io_mmu_flags; /* used by head.S */ unsigned long __cpu_flush; /* used by head.S */ const char *arch_name;
__v6_proc_info: .long 0x0007b000 .long 0x0007f000 ALT_SMP(.long \ PMD_TYPE_SECT | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ | \ PMD_FLAGS_SMP) ALT_UP(.long \ PMD_TYPE_SECT | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ | \ PMD_FLAGS_UP) .long PMD_TYPE_SECT | \ PMD_SECT_XN | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ b __v6_setup
在前文中已经看到,我的环境中在proc-v6.S已经定义了参数__cpu_flush的内容为b __v6_setup,所以这里会执行__v6_setup函数:
/* * __v6_setup * * Initialise TLB, Caches, and MMU state ready to switch the MMU * on. Return in r0 the new CP15 C1 control register setting. * * We automatically detect if we have a Harvard cache, and use the * Harvard cache control instructions insead of the unified cache * control instructions. * * This should be able to cover all ARMv6 cores. * * It is assumed that: * - cache type register is implemented */ __v6_setup:
__v6_setup函数主要是配置CPU的寄存器,这里不再详细分析了,函数将会初始化TLB、Cache以及开启MMU的一些必要的状态(例如将页表物理地址设置到TTB:Translation Table Base中),然后通过r0寄存器返回CP15 C1控制寄存器中的设置值。
/* * Setup common bits before finally enabling the MMU. Essentially * this is just loading the page table pointer and domain access * registers. * * r0 = cp#15 control register * r1 = machine ID * r2 = atags or dtb pointer * r4 = page table (see ARCH_PGD_SHIFT in asm/memory.h) * r9 = processor ID * r13 = *virtual* address to jump to upon completion */ __enable_mmu: #if defined(CONFIG_ALIGNMENT_TRAP) && __LINUX_ARM_ARCH__ < 6 orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE bic r0, r0, #CR_I #endif
完成了上面的准备工作后就要开启MMU了,本质上这里仅仅是加载了页表指针和域访问控制寄存器。
首先这里根据内核配置选项再对r0中返回的CR15 CR1进行配置,这些宏定义在arch/arm/include/asm/cp15.h中,表示了寄存器每一位的定义,其中部分内容如下:
#define CR_M (1 << 0) /* MMU enable */ #define CR_A (1 << 1) /* Alignment abort enable */ #define CR_C (1 << 2) /* Dcache enable */ #define CR_W (1 << 3) /* Write buffer enable */ #define CR_P (1 << 4) /* 32-bit exception handler */ #define CR_D (1 << 5) /* 32-bit data address range */ #define CR_L (1 << 6) /* Implementation defined */ #define CR_B (1 << 7) /* Big endian */ #define CR_S (1 << 8) /* System MMU protection */ #define CR_R (1 << 9) /* ROM MMU protection */ #define CR_F (1 << 10) /* Implementation defined */ #define CR_Z (1 << 11) /* Implementation defined */ #define CR_I (1 << 12) /* Icache enable */ #define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */ #define CR_RR (1 << 14) /* Round Robin cache replacement */ #define CR_L4 (1 << 15) /* LDR pc can set T bit */ #define CR_DT (1 << 16) ......
例如,如果内核CONFIG_CPU_DCACHE_DISABLE,则这里会清除CR_C位来使D-Cache失能等等。
#ifndef CONFIG_ARM_LPAE mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \ domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \ domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \ domain_val(DOMAIN_IO, DOMAIN_CLIENT)) mcr p15, 0, r5, c3, c0, 0 @ load domain access register
接下来这里设置域访问控制寄存器,ARM处理器使用域管理内存访问权限,它将虚拟内存区域划分为几个区域,为每个区域附于访问控制权限来进行保护和控制。
从手册中看到,L1页描述符中第[5:8]位标识所在的域(4位最大能表示16个域)。域访问控制寄存器CP15 C3为一个32位寄存器:
其中每一项Dx占两位(共16项),表示该域的权限,其含义如下:
代码中domain_val宏定义如下(arch/arm/include/asm/domian.h):
#define DOMAIN_KERNEL 0 #define DOMAIN_TABLE 0 #define DOMAIN_USER 1 #define DOMAIN_IO 2 ...... #define DOMAIN_NOACCESS 0 #define DOMAIN_CLIENT 1 #ifdef CONFIG_CPU_USE_DOMAINS #define DOMAIN_MANAGER 3 #else #define DOMAIN_MANAGER 1 #endif ...... #define domain_val(dom,type) ((type) << (2*(dom)))
由于没有配置CPU_USE_DOMAINS,所以不使用Manager权限展开后r5和c3寄存器的值为
0x00000015,对应关系如下:
D0 —— DOMAIN_KERNEL\DOMAIN_TABLE—— DOMAIN_CLIENT
D1 —— DOMAIN_USER —— DOMAIN_CLIENT
D2 —— DOMAIN_IO —— DOMAIN_CLIENT
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer #endif b __turn_mmu_on ENDPROC(__enable_mmu)
接下来将r4中保存的页表物理地址写入C2的TTB中,其实这一步骤已经在__v6_setup中已经做过了,这里重复了一次,需要注意的是r4中页表物理地址的低5位已经被处理过了(页表地址必须32字节对齐),用于设置PD0和PD1等,C2寄存器如下:
在设置完TTB后,就要跳转到__turn_mmu_on开启MMU了。
ENTRY(__turn_mmu_on) mov r0, r0 instr_sync mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg instr_sync mov r3, r3 mov r3, r13 mov pc, r3 __turn_mmu_on_end:
这里首先将前面配置过的r0寄存器写入C1寄存器中,如此MMU就被启动了,然后将前面保存在r13中的__mmap_switched函数的链接地址(虚拟地址)赋值到r3中,最后跳转到__mmap_switched函数执行。
注意:由于在执行461行代码(mcr p15, 0, r0, c1, c0, 0)后MMU已经开启了,CPU在取指时已经采用虚拟地址,需经过页表的转换,但是此时的PC寄存器的值却还是按原来的顺序取指(例如在执行461行代码时,我的环境中PC的值为0x004333a0+8),也即如果不对__turn_mmu_on函数进行线性1:1映射的话,0x00XXXXXX处的地址无法解析,程序将无法继续运行。
/* * The following fragment of code is executed with the MMU on in MMU mode, * and uses absolute addresses; this is not position independent. * * r0 = cp#15 control register * r1 = machine ID * r2 = atags/dtb pointer * r9 = processor ID */ __INIT __mmap_switched: adr r3, __mmap_switched_data ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b
在跳转到__mmap_switched后,页表建立完毕,MMU处于激活状态,将使用绝对地址执行,不再采用位置无关代码,所以从这里开始也就不需要再区分链接地址和实际的运行物理地址了。
首先将__mmap_switched_data的地址保存到r3中,然后将__data_loc、_sdata、__bss_start和_end变量的地址保存到r4、r5、r6和r7寄存器中。注意r3后面的叹号,r3的值会递增。
.align 2 .type __mmap_switched_data, %object __mmap_switched_data: .long __data_loc @ r4 .long _sdata @ r5 .long __bss_start @ r6 .long _end @ r7 .long processor_id @ r4 .long __machine_arch_type @ r5 .long __atags_pointer @ r6 #ifdef CONFIG_CPU_CP15 .long cr_alignment @ r7 #else .long 0 @ r7 #endif .long init_thread_union + THREAD_START_SP @ sp .size __mmap_switched_data, . - __mmap_switched_data
它们的地址值在我的环境中如下:
c05ab2ac <__mmap_switched_data>: c05ab2ac: c1080000 mrsgt r0, (UNDEF: 8) c05ab2b0: c1080000 mrsgt r0, (UNDEF: 8) c05ab2b4: c10bccec smlattgt fp, ip, ip, ip c05ab2b8: c116c518 tstgt r6, r8, lsl r5
然后比较__data_loc和_sdata的值是否一致,若不一致则需要拷贝数据段。其中__data_loc是内核镜像中数据段的存储位置,在开启CONFIG_XIP_KERNEL后该值不等于_sdata值,在vmlinux.lds.S中定义如下:
#ifdef CONFIG_XIP_KERNEL __data_loc = ALIGN(4); /* location in binary */ . = PAGE_OFFSET + TEXT_OFFSET; #else __init_end = .; . = ALIGN(THREAD_SIZE); __data_loc = .; #endif .data : AT(__data_loc) { _data = .; /* address in memory */ _sdata = .;
而_sdata是数据段的链接位置。若开启CONFIG_XIP_KERNEL,则这两个值不等,所以需要将数据段拷贝到链接地址处(在RAM中)。在我的环境中,这两个值一致,不需要拷贝。
mov fp, #0 @ Clear BSS (and zero fp) 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b ARM( ldmia r3, {r4, r5, r6, r7, sp}) THUMB( ldmia r3, {r4, r5, r6, r7} ) THUMB( ldr sp, [r3, #16] ) str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type str r2, [r6] @ Save atags pointer cmp r7, #0 bicne r4, r0, #CR_A @ Clear 'A' bit stmneia r7, {r0, r4} @ Save control register values b start_kernel ENDPROC(__mmap_switched)
接下来首先清空BSS段,然后将processor_id、__machine_arch_type、__atags_pointer、cr_alignment和init_thread_union + THREAD_START_SP值依次读取到r4、r5、r6、r7和sp中。其中processor_id、__machine_arch_type和__atags_pointer是定义在arch/arm/kerne/setup.c中的全局变量,分别用于保存处理器ID、bootloader传入的机器ID和启动参数地址。
然后依次将r9、r1和r2中保存的相应参数写入到这些全局变量中去,以便后面执行start_kernel之后的程序中使用到。
接着判断C7中的值是否为0,由于我的环境中已经配置了CONFIG_CPU_CP15,所以该值不为0,保存的是cr_alignment全局变量的地址。cr_alignment全局变量被定义在arch/arm/kernel/entry-armv.S中,和他一起使用的还有cr_no_alignment。他们分别被用来保存启用和禁用“A域”的CP15 C1寄存器值。
“A域”的作用是控制是否启用访问对齐检查,若开启,在未对齐的访问内存会发生data abort trap。程序中分别将启用A域和禁用A域的C1寄存器值保存到cr_alignment和cr_no_alignment全局变量中以备后续使用。
最后跳转到start_kernel函数进行进一步的初始化动作,内核启动的汇编部分到这里结束。最后总结一下这部分主要完成了以下初始化:
(1)验证处理器ID、内核启动参数等地址的有效性;
(2)创建初始页表,完成内核代码、启动参数和内核启动MMU代码这3部分内存映射;
(3)开启MMU并保存参数。
参考文献:1、《ARM Linux内核源码剖析》
2、《ARM11 数据手册》