第三章第四节 内核启动分析 对于ARM处理器,内核启动大体上可以分为两个阶段:与处理器相关的汇编启动阶段和与处理器无关的C代码启动阶段。汇编启动阶段从head.S(arch/arm/kernel/head.S)文件开始,C代码启动阶段从start_kernel函数(init/main.c)开始。当然,经过压缩的内核镜像文件zImage,在进入汇编启动阶段前还要运行一段自解压代码(arch/arm/boot/compressed/head.S)。 省略一些无关紧要的过程和编译后不运行的代码,该过程的启动流程如图3. 7所示。相对早期linux-2.6.38的版本,linux-3.8.3在汇编启动阶段并没有出现__lookup_machine_type,但这并不意味着内核不再检查bootloader传入的machine_arch_type参数(R1),只是将检查机制推迟到了C代码阶段。 1) __lookup_processor_type__lookup_processor_type函数的具体实现如程序清单3. 1。 程序清单3. 1查找处理器类型函数 __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) .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 __lookup_processor_type函数的主要功能是将内核支持的所有CPU类型与通过程序实际读取的cpu id进行查表匹配。如果匹配成功,将匹配到的proc_info_list的基地址存到r5,否则,r5为0,程序将会进入一个死循环。函数传入参数r9为程序实际读取的cpu id,传出参数r5为匹配到的proc_info_list指针的地址。同时为了使C语言能够调用这个函数,根据APCS(ARM 过程调用标准)规则,简单使用以下代码就能包装成一个C语言版本__lookup_processor_type的API函数,函数的原型为struct proc_info_list *lookup_processor_type(unsigned int)。 ENTRY(lookup_processor_type) stmfd sp!, {r4 - r6, r9, lr} mov r9, r0 bl __lookup_processor_type mov r0, r5 ldmfd sp!, {r4 - r6, r9, pc} ENDPROC(lookup_processor_type) ENTRY和ENDPROC宏的定义如下: #define ENTRY(name) .globl name; name: #define ENDPROC(name) 内核利用一个结构体proc_info_list来记录处理器相关的信息,在文件arch/arm/include/asm/procinfo.h声明了该结构体的类型,如下所示。 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; }; 事实上,在arch/arm/mm/proc-*.S这类文件中,程序才真正给内核所支持的arm处理器的proc_info_list分配了内存空间,例如linux/arch/arm/mm/proc-v6.S文件用汇编语言定义的__v6_proc_info结构体。.section指示符来指定这些结构体编译到.proc.info段。.proc.info的起始地址为 __proc_info_begin,终止位置为__proc_info_end,把它们作为全局变量保存在内存中,链接脚本arch/arm/kernel/vmlinux.lds部分内容参考如下: .init.proc.info : { . = ALIGN(4); __proc_info_begin = .; *(.proc.info.init) __proc_info_end = .; } 2) __vet_atags 在启动内核时, bootloader会向内核传递一些参数。通常,bootloader 有两种方法传递参数给内核:一种是旧的参数结构方式(parameter_struct)——主要是2.6 之前的内核使用的方式;另外一种是现在的内核在用的参数列表(tagged list) 的方式。这些参数主要包括,系统的根设备标志、页面大小、内存的起始地址和大小、当前内核命令参数等。而这些参数是通过struct tag结构体组织,利用指针链接成一个按顺序排放的参数列表。bootloader引导内核启动时,就会把这个列表的首地址放入R2中,传给内核,内核通过这个地址就分析出传入的所有参数。 内核要求参数列表必须存放在RAM物理地址的头16k位置,并且ATAG_CORE类型的参数需要放置在参数的列表的首位。__vet_atags的功能就是初步分析传入的参数列表,判断的方法也很简单。如果这个列表起始参数是ATAG_CORE类型,则表示这是一个有效的参数列表。如果起始参数不是ATAG_CORE,就认为bootloader没有传递参数给内核或传入的参数不正确。 1) __create_page_tables
linux内核使用页式内存管理,应用程序给出的内存地址是虚拟地址,它需要经过若干级页表一级一级的变换,才变成真正的物理地址。32位CPU的虚拟地址大小从0x0000_0000到0xFFFF_FFFF共4G。以段(1 MB)的方式建立一级页表,可以将虚拟地址空间分割成4096个段条目(section entry)。条目也称为“描述符”(Descriptor),每一个段描述符32位,因此一级页表占用16K(0x4000)内存空间。 s3c6410处理器DRAM的地址空间从0x5000_0000开始,上文提到bootloader传递给内核的参数列表存放在RAM物理地址的头16K位置,页表放置在内核的前16K,因此内核的偏移地址为32K(0x8000),由此构成了如图3. 8所示的实际内存分布图。
__create_page_tables函数初始化了一个非常简单页表,仅映射了使内核能够正常启动的代码空间,更加细致的工作将会在后续阶段完善。流程如所示,获取页表物理地址、清空页表区和建立启动参数页表通过阅读源码很容易理解,不加分析。 __enable_mmu函数使能mmu后,CPU发出的地址是虚拟地址,程序正常运行需要映射得到物理地址,为了保障正常地配置mmu,需要对这段代码1:1的绝对映射,映射范围__turn_mmu_on至__turn_mmu_on_end。正常使能mmu后,不需要这段特定的映射了,在后续C代码启动阶段时被paging_init()函数删除。建立__enable_mmu函数区域的页表代码如程序清单3. 2所示。 程序清单3. 2 __enable_mmu页表的建立 //r4 =页表物理地址 //获取段描述符的默认配置flags ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] adr r0, __turn_mmu_on_loc //得到__turn_mmu_on_loc的物理地址 ldmia r0, {r3, r5, r6} sub r0, r0, r3 //计算得到物理地址与虚拟地址的偏差 add r5, r5, r0 //修正得到__turn_mmu_on的物理地址 add r6, r6, r0 //修正得到__turn_mmu_on_end的物理地址 mov r5, r5, lsr #SECTION_SHIFT //1M对齐 mov r6, r6, lsr #SECTION_SHIFT //1M对齐 1: orr r3, r7, r5, lsl #SECTION_SHIFT //生成段描述符:flags + 段基址 str r3, [r4, r5, lsl #PMD_ORDER] //设置段描述绝对映射,物理地址等于虚拟地址。每个段描述符占4字节,PMD_ORDER = 2 cmp r5, r6 addlo r5, r5, #1 //下一段,实际上__turn_mmu_on_end - __turn_mmu_on< 1M blo 1b ............................ __turn_mmu_on_loc: .long . //__turn_mmu_on_loc当前位置的虚拟地址 .long __turn_mmu_on //__turn_mmu_on的虚拟地址 .long __turn_mmu_on_end //__turn_mmu_on_end的虚拟地址 建立内核的映射区页表,分析见程序清单3. 3。 程序清单3. 3内核的映射区页表的建立 //r4 =页表物理地址 mov r3, pc //r3 = 当前物理地址 mov r3, r3, lsr #SECTION_SHIFT //物理地址转化段基址 orr r3, r7, r3, lsl #SECTION_SHIFT //段基址 + flags = 段描述符 //KERNEL_START = 0xC000_8000 SECTION_SHIFT = 20 PMD_ORDER = 2 //由于arm 的立即数只能是8位表示,所有用两条指令实现了将r3存储到对应的页表项中 add r0, r4, #(KERNEL_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER) str r3, [r0, #((KERNEL_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]! ldr r6, =(KERNEL_END - 1) add r0, r0, #1 << PMD_ORDER add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER) //内核映射页表结束的段基址 1: cmp r0, r6 add r3, r3, #1 << SECTION_SHIFT //得到段描述符 strls r3, [r0], #1 << PMD_ORDER //设置段描述符 bls 1b 1) __v6_setup __v6_setup 函数在 proc-v6.S 文件中,在页表建立起来之后,此函数进行一些使能 MMU 之前的初始化操作。 2) __enable_mmu __v6_setup已经为使能 MMU做好了必要的准备,为了保证MMU启动后程序顺利返回,在进入__enable_mmu函数之前,已经将__mmap_switched的虚拟地址(链接地址)存储在R13中。 3) __mmap_switched 程序运行到这里,MMU已经启动,__mmap_switched函数为内核进入C代码阶段做了一些准备工作:复制数据段,清楚BSS段,设置堆栈指针,保存processor ID、machine type(bootloader中传入的)、atags pointer等。最后,终于跳转到start_kernel函数,进入C代码启动阶段。 |