Linux内核启动过程(一):head.S学习

2019-07-12 22:33发布

一.arch/arm/kernel/head-armv.S(我的好像就叫head.S) 内核最先执行的一个文件,包括内核入口ENTRY(stext)start_kernel间初始代码 主要作用是检查CPU IDArchitecture Type,初始化BSS等操作,并跳到start_kernel函数。在执行前,处理器应满足以下状态: r0 - should be 0 r1 - unique architecture number MMU - off I-cache - on or off D-cache off 1.head.S分析 .section ".text.head", "ax" .type stext, %function ENTRY(stext)             //内核入口点 msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode //禁止FIQ IRQ,进入管理模式 @ and irqs disabled mrc p15, 0, r9, c0, c0 @ get processor id             //读取CPU ID存入R9 bl __lookup_processor_type @ r5=procinfo r9=cpuid //内核是否支持该架构,输入参数r9=cpuid ,返回值r5=procinfo   movs r10, r5 @ invalid processor (r5=0)?  //如果不支持当前CPUR5返回值为0 beq __error_p @ yes, error 'p'               //如果r5 = 0则打印错误 bl __lookup_machine_type @ r5=machinfo       //内核是否支持该单板 movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a' bl __create_page_tables                            //建立一级页表 ldr r13, __switch_data @ address to jump to after    //设置跳转地址,mmu被使能之后会跳到该地址出(现在还不会过去的) @ mmu has been enabled adr lr, __enable_mmu @ return (PIC) address add pc, r10, #PROCINFO_INITFUNC   在内核映像中,定义了若干proc_info_list表示它支持的CPU,对于ARM架构的CPU,这些结构的源码在arch/arm/mm/目录下。比如proc-v6.S中有如下代码,它表示的所有ARMv6架构的CPU(s3c6410ARMv6架构的)proc_info_list结构。 不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__proc_info_begin,结束地址为__proc_info_end。在arm/kernel/vmlinux.lds.S中可以看到这样的代码: __proc_info_begin = .; /* proc_info_list结构的开始地址*/ *(.proc.info.init) __proc_info_end = .;  /*proc_info_list结构的结束地址*/ __lookup_processor_type函数就是根据前面从协处理器CP15的寄存器C0中读取到CPU ID(存入r9寄存器),从这些proc_info_list结构中找出匹配。 __lookup_processor_type函数首先将标号3的实际地址加载到r3,然后将编译时生成的 __proc_info_begin虚拟地址载入到r5__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7。由于r3r7分别存储的是同一位置标号3的物理地址和虚拟地址,所以儿者相减即得到虚拟地址和物理地址之间的offset利用此offset,将r5r6中保存的虚拟地址转变为物理地址(此时MMU没有开启,无法用编译连接的虚拟地址)然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配。如果编译了多种处理器支持则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r50返回。   不同的machine_desc结构被用来支持不同的开发板,它们都是定义在“.arch.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__arch_info_begin,结束地址为__arch_info_end =。在arm/kernel/vmlinux.lds.S中可以看到这样的代码: __arch_info_begin = .; /* machine_desc结构的开始地址*/ *(.arch.info.init) __arch_info_end = .;  /*machine_desc结构的结束地址*/   U-Boot调用内核时,会在r1寄存器中给出开发板的标记即机器类型ID__lookup_machine_type函数将这个值与machine_desc结构的nr成员比较,如果两者相等则表示找到匹配的machine_desc结构,于是返回它的地址(存到r5中)。如果__arch_info_begin__arch_info_end之间所有的machine_desc结构的nr成员都不等于r1寄存器中的值,则返回0,即r5中值为0 。编码方法与__lookup_processor_type函数完全一样,在此不再详述。   bl __create_page_tables __create_page_tables:函数在arch/arm/kernel/head.S中实现,它完成一级页表的创建。代码如下: /*首先将内核起始地址-0x4000到内核起始地址之间的16K存储器清0*/ mov r0, r4 mov r3, #0 add r6, r0, #0x4000 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b /*proc_info中的mmu_flags加载到r7*/ ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags /*PC指针右移20位,得到内核第一个1MB空间的段地址存入r6,接着根据此值存入映射标识*/ mov r6, pc, lsr #20 @ start of kernel section orr r3, r7, r6, lsl #20 @ flags + kernel base str r3, [r4, r6, lsl #2] @ identity mapping     __switch_data: .long __mmap_switched .long __data_loc @ r4 .long __data_start @ r5 .long __bss_start @ r6 .long _end @ r7 .long processor_id @ r4 .long __machine_arch_type @ r5 .long __atags_pointer @ r6 .long cr_alignment @ r7 .long init_thread_union + THREAD_START_SP @ sp 设置lr寄存器值为__enable_mmu,执行完函数__v6_setup,函数会跳转到__enable_mmu地址处。
add pc, r10, #PROCINFO_INITFUNC 执行这条指令后,程序将会跳转到函数__v6_setup处执行。__v6_setup是在文件arch/arm/mm/proc-v6.S中实现的,它的初始化 TLB, Caches, MMU状态,为下一步开启MMU作准备。然后将原来保存在lr中的地址载入pc(_v6_setup最后一句的作用),跳转到arch/arm/kerne/ head.S__enable_mmu处执行,代码不再详述。   ⑥在开启MMU之前还要做好最后的准备工作,比如无效IcachesDcaches,设置域访问控制器和页表指针等。这些设置是在arch/arm/kerne/ head.S文件中__enable_mmu函数中实现的. __enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP /*开启地址对齐检查*/ orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE /*无效Dcaches*/ bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE/*无效Icaches*/ bic r0, r0, #CR_I #endif 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 /*设置域访问控制器*/ mcr p15, 0, r4, c2, c0, 0 /*设置页表基址寄存器*/ b __turn_mmu_on ⑦当__enable_mmu执行完后,跳转到__turn_mmu_on,该函数会把MMU使能位写入MMU控制寄存器,使能MMU激活虚拟地址。然后将原来保存在sp中的地址载入pc,跳转到head-common.S__mmap_switched,至此代码进入虚拟地址的世界。 .align 5 .type __turn_mmu_on, %function __turn_mmu_on: mov r0, r0 mcr p15, 0, r0, c1, c0, 0 /*使能MMU*/ mrc p15, 0, r3, c0, c0, 0 mov r3, r3 mov r3, r3 mov pc, r13 /*跳转到__mmap_switched*/ (跳到switch_data了)   ⑧在跳转到第二阶段代码的C入口点之前,需要做一些准备工作。包括复制数据段、清除BSS段、保存一些重要的信息到寄存器和内存中。这些都在函数__mmap_switched中实现。这里只粘贴部分代码: __mmap_switched: .type __mmap_switched, %function __mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {r4, r5, r6, r7} cmp r4, r5 /*复制数据段*/ 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b mov fp, #0 /*清除BSS*/ 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b ldmia r3, {r4, r5, r6, r7, sp} str r9, [r4] /* processor ID保存在r9*/ str r1, [r5] /* machine ID报存在r1*/ str r2, [r6] /* atags地址保存在r2*/ bic r4, r0, #CR_A /*Clear 'A' bit*/ stmia r7, {r0, r4} /*将控制寄存器保存到r7定义的内存地址*/ 然后再接下来跳入/init/main.cstart_kernel函数,它是内核第二阶段代码的入口点。 bstart_kernel    /*调用start_kernel函数*/