内核启动流程 --- 启动准备阶段(二)
文章目录
一. 前言
压缩过的kernel入口第一个文件源码位置在/kernel/arch/arm/boot/compressed/head.S。它将调用decompress_kernel()函数进行解压,解压完成后,打印出信息“Uncompressing Linux…done,booting the kernel”。解压缩完成后,调用gunzip()函数(或unlz4()、或bunzip2()、或unlz())将内核放于指定位置,开始启动内核。内核启动流程 — 自解压(一)
启动内核时,首先进入Linux内核启动准备阶段,入口函数为stext(/kernel/arch/arm/kernel/head.S),由内核链接脚本/kernel/arch/arm/kernel/vmlinux.lds决定(linux内核链接脚本vmlinux.lds分析(十一))。
二. 内核启动准备阶段流程总述
- 关闭IRQ、FIQ中断,进入SVC模式。调用setmode宏实现;
- 校验处理器ID,检验内核是否支持该处理器;若不支持,则停止启动内核。调用__lookup_processor_type函数实现;
- 校验机器码,检验内核是否支持该机器;若不支持,则停止启动内核。调用__lookup_machine_type函数实现;
- 检查uboot向内核传参ATAGS格式是否正确,调用__vet_atars函数实现;
- 建立虚拟地址映射页表。此处建立的页表为粗页表,在内核启动前期使用。Linux对内存管理有更精细的要求,随后会重新建立更精细的页表。调用__create_page_tables函数实现。
- 跳转执行__switch_data函数,其中调用__mmap_switched完成最后的准备工作。
- 复制数据段、清除bss段,设置栈,目的是构建C语言运行环境;
- 保存处理器ID号、机器码、uboot向内核传参地址;
- b start_kernel跳转至内核初始化阶段。
/* * 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 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. */ __HEAD ENTRY(stext) setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode @ and irqs disabled mrc p15, 0, r9, c0, c0 @ get processor id bl __lookup_processor_type @ r5=procinfo r9=cpuid movs r10, r5 @ invalid processor (r5=0)? THUMB( it eq ) @ force fixup-able long branch encoding beq __error_p @ yes, error 'p' bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? THUMB( it eq ) @ force fixup-able long branch encoding beq __error_a @ yes, error 'a' /* * r1 = machine no, r2 = atags, * r8 = machinfo, r9 = cpuid, r10 = procinfo */ bl __vet_atags #ifdef CONFIG_SMP_ON_UP bl __fixup_smp #endif bl __create_page_tables /* * 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_machine_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 ARM( add pc, r10, #PROCINFO_INITFUNC ) THUMB( add r12, r10, #PROCINFO_INITFUNC ) THUMB( mov pc, r12 ) 1: b __enable_mmu ENDPROC(stext)
三. 内核启动准备阶段代码详解
3.1 关闭IRQ、FIQ中断,进入SVC模式
3.1.1 调用代码段
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
通过设置CRSR寄存器来确保处理器进入管理(SVC)模式,并且禁止中断。
3.1.2 什么是svc模式?为什么要切换到svc模式?如何切换到svc模式?
1. ARMV6体系的几种工作模式:
英文缩写名称 | 中文名称 | 模式类别 | 解释 | M[4:0]值 |
---|---|---|---|---|
USR | 用户模式 | 特权模式 | 正常程序工作模式,此模式下程序不能够访问一些受操作系统保护的系统资源,应用程序也不能直接进行处理器模式的切换 | 0b10000 |
SYS | 系统模式 | 普通模式 | 操作系统特权任务模式,用于支持操作系统的特权任务等,可以访问系统保护的系统资源,也可以直接切换到其它模式等特权 | 0b11111 |
SVC | 管理模式 | 特权模式、异常模式 | 操作系统保护模式 | 0b10011 |
USR | 用户模式 | 普通模式 | 正常程序工作模式,此模式下程序不能够访问一些受操作系统保护的系统资源,应用程序也不能直接进行处理器模式的切换 | 0b10000 |
FIQ | 快中断模式 | 特权模式、异常模式 | 支持高速数据传输及通道处理,FIQ异常响应时进入此模式 | 0b10001 |
IRQ | 中断模式 | 特权模式、异常模式 | 用于通用中断处理,IRQ异常响应时进入此模式 | 0b10010 |
ABT | 中止模式 | 特权模式、异常模式 | 用于支持虚拟内存和/或存储器保护 | 0b10111 |
UND | 未定义模式 | 特权模式、异常模式 | 支持硬件协处理器的软件仿真,未定义指令异常响应时进入此模式 | 0b11011 |
2. 为什么要切换到svc模式?
首先 7种模式中,除用户usr模式外,其它模式均为特权模式。对于为何此处是svc模式,而不是其他某种格式?分析如下:
(1)中止abt和未定义und模式
首先可以排除中止abt和未定义und模式,它们都是非正常模式,此处程序是正常运行的,所以不应该设置CPU为其中任何一种模式。
(2)快中断fiq和中断irq模式
对于快中断fiq和中断irq来说,内核初始化的时候,没什么中断要处理和能够处理(中断环境未设置),而且即使是注册了中服务程序后,能够处理中断,那么这两种模式,也是自动切换过去的,所以,此处也不应该设置为其中任何一种模式。
(3)用户usr模式
从理论上来说,可以设置CPU为用户usr模式,但是由于此模式无法直接访问很多的硬件资源,而内核的初始化,又必须要去访问这类资源,所以不能设置为用户usr模式。
(4)系统sys模式 vs 管理svc模式
首先,sys模式和usr模式相比,所用的寄存器组,基本上都是一样的,但是增加了一些在usr模式下不能访问的资源。而svc模式本身就属于特权模式,本身就可以访问那些受控资源,此外,比sys模式还多了些自己模式下的影子寄存器,所以,相对sys模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源。
因此,此处将CPU设置为SVC模式。
3. 如何切换到svc模式?
ARM工作模式的切换由CPSR寄存器控制。CPSR表示当前程序状态寄存器,SPSR表示备份的程序状态寄存器。
CPSR:程序状态寄存器(current program status register) (当前程序状态寄存器),在任何处理器模式下被访问。它包含了条件标志位、中断禁止位、当前处理器模式标志以及其他的一些控制和状态位。如下所示:
对应位 | 对应标识 | 功能说明 |
---|---|---|
bit31 | N | 用两个补码表示的带符号的运算结果标识,为1表示负数,为0表示非负数 |
bit30 | Z | 为1表示运算结果为0,为0表示运算结果为非0 |
bit29 | C | 加法运算结果进位标志位 |
bit28 | V | 加减法运算指令符号位溢出标志位 |
bit7 | I | 为1表示禁止IRQ中断 |
bit6 | F | 为1表示禁止FIQ中断 |
bit5 | T | 为1表示程序运行于Thumb状态,否则处于ARM状态。 |
bit4-0 | M[4:0] | 模式控制位 用于配置CPU当前的工作模式 |
综上,需要将CPSR的[4:0]设置成10011就可以切换到SVC模式。
3.1.2 为什么要关中断?怎么关中断?
1. 为什么要关中断?
在启动的过程中,中断环境并没有完全准备好(中断向量表和中断处理函数没有完成设置),一旦有中断产生,可能会导致意想不到的结果,比如:程序跑飞、cpu跑死。因此,在准备好中断环境之前,需要关闭所有中断。
2. 怎么关中断?
中断的关闭同样由CPSR寄存器控制,具体请看上一小节。综上,需要将CPSR的bit6和bit7设置为1
3.2 检验内核是否支持该处理器
3.2.1 调用代码段
mrc p15, 0, r9, c0, c0 @ get processor id bl __lookup_processor_type @ r5=procinfo r9=cpuid movs r10, r5 @ invalid processor (r5=0)?
3.2.2 寄存器说明
通过读协处理器CP15的寄存器C0获取CPUID,并存储到r9中,判断内核是否支持当前CPU。如果支持,r5寄存器返回一个用来描述处理器的结构体的地址,否则r5为0。CPU ID格式如图所示:
可以看到对于mini6410,r9 = 0x410fb76x。
3.2.3 相关知识
在内核映像中,定义了若干proc_info_list表示它支持的CPU(它的结构体原型在/arch/arm/include/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; };
对于ARM架构的CPU,这些架构体系相关结构对应的实例定义在arch/arm/mm/目录下。比如arch/arm/mm/proc-v6.S中有如下代码,它表示的所有ARMv6架构的CPU(mini6410是ARMv6架构的)的proc_info_list结构。
.section ".proc.info.init", #alloc, #execinstr /* * Match any ARMv6 processor core. */ .type __v6_proc_info, #object __v6_proc_info: .long 0x0007b000 /*cpu_val*/ .long 0x0007f000 /*cpu_mask*/
不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__proc_info_begin,结束地址为__proc_info_end。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:
__proc_info_begin = .; *(.proc.info.init) __proc_info_end = .;
3.2.4 处理流程分析
__lookup_processor_type函数就是根据前面从协处理器CP15的寄存器C0中读取到CPU ID(存入r9寄存器),从这些proc_info_list结构中找出匹配。它是在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, 3f @将标号3的物理地址加载到r3 ldmda r3, {r5 - r7} @ldmda中,da的意思是传递完后地址递减,并且按从右到左的顺序操作目的寄存器 @r7=标号3的虚拟地址 @r6=__proc_info_end虚拟地址 @r5 = __proc_info_begin虚拟地址 sub r3, r3, r7 @ get offset between virt&phys 得到物理地址和虚拟地址的差值 add r5, r5, r3 @ convert virt addresses to r5 =__proc_info_begin对应的物理地址 add r6, r6, r3 @ physical address space r6 =__proc_info_end对应的物理地址 @ldmia用于将地址上的值加载到寄存器上,作用是保存使用到的寄存器. ia模式表示:每次传送后地址+4;(after increase) 1: ldmia r5, {r3, r4} @ value, mask 将proc_info_list结构中cpu_val、cpu_mask分别存放在r3, r4中 and r4, r4, r9 @ mask wanted bits r4 = cpu_mask&CPU_ID =0x0007f000 & 0x410fb76x = 0x0007b000 teq r3, r4 @比较 beq 2f @如果相等,找到匹配的proc_info_list结构,跳转到标号2处 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 @否则, r5指向下一个proc_info_list结构 blo 1b @没有则跳转到标号1继续比较 mov r5, #0 @ unknown processor 2: mov pc, lr ENDPROC(__lookup_processor_type) /* * Look in <asm/procinfo.h> and arch/arm/kernel/arch.[ch] for * more information about the __proc_info and __arch_info structures. */ .long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end
__proc_info_begin、 __proc_info_end和“.”这三个数据都是在连接内核时确定的,它们都是虚拟地址,前两个表示proc_info_list结构的开始地址和结束地址,“.”表示当前行代码在编译连接后的虚拟地址。因为MMU没有开启,所以我们此时还不能直接使用这些地址。所以在访问proc_info_list结构前,需要先将它的虚拟地址转化为物理地址。
__lookup_processor_type函数首先将标号3的实际地址加载到r3,然后将编译时生成的 __proc_info_begin虚拟地址载入到r5,__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7。由于r3和r7分别存储的是同一位置标号3的物理地址和虚拟地址,所以二者相减即得到虚拟地址和物理地址之间的offset。利用此offset,将r5和r6中保存的虚拟地址转变为物理地址然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配。如果编译了多种处理器支持则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r5置0返回。
注意,需要配置CONFIG_CPU_V6 = y(在配置菜单中, Processor Type->)。在arch/arm/mm/Makefile中有如下行,会把这个架构相关的定义编进内核。
obj-$(CONFIG_CPU_V6) += proc-v6.o
3.3 检验内核是否支持该机器(即开发板)
3.3.1 调用代码段
bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)?
3.3.2 相关知识
内核中对每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义了开发板相关的一些属性和函数,如机器的类型ID、起始I/O物理地址、Bootloader传入的参数地址、中断初始化函数、I/O映射函数等。对于mini6410开发板在arch/arm/mach-s3c64xx/ mach-mini6410.c有如下代码:
MACHINE_START(MINI6410, "MINI6410") /* Maintainer: Ben Dooks <ben-linux@fluff.org> */ .boot_params = S3C64XX_PA_SDRAM + 0x100, .init_irq = s3c6410_init_irq, .map_io = mini6410_map_io, .init_machine = mini6410_machine_init, .timer = &s3c24xx_timer, MACHINE_END
其中:宏MACHINE_START、MACHINE_END在arch/arm/include/asm/mach/arch.h文件中定义:
/* * Set of macros to define architecture features. This is built into * a table by the linker. */ #define MACHINE_START(_type,_name) \ static const struct machine_desc __mach_desc_##_type \ __used \ __attribute__((__section__(".arch.info.init"))) = { \ .nr = MACH_TYPE_##_type, \ .name = _name, #define MACHINE_END \ };
所以上面代码扩展开来就是:
static const struct machine_desc __mach_desc_MINI6410 \ __used \ __attribute__((__section__(".arch.info.init"))) = { \ .nr = MACH_TYPE_MINI6410, \ .name =MINI6410, .boot_params = S3C64XX_PA_SDRAM + 0x100, .init_irq = s3c6410_init_irq, .map_io = mini6410_map_io, .init_machine = mini6410_machine_init, .timer = &s3c24xx_timer, };
不同的machine_desc结构被用来支持不同的开发板,它们都是定义在“.arch.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__arch_info_begin,结束地址为__arch_info_end =。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:
__arch_info_begin = .; *(.arch.info.init) __arch_info_end = .;
3.3.3 处理流程分析
__lookup_processor_type函数也是在arch/arm/kernel/head-common.S中定义的,代码如下:
/* * Lookup machine architecture in the linker-build list of architectures. * Note that we can't use the absolute addresses for the __arch_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. * * r1 = machine architecture number * Returns: * r3, r4, r6 corrupted * r5 = mach_info pointer in physical address space */ __lookup_machine_type: adr r3, 3b @将标号3b的物理地址加载到r3 ldmia r3, {r4, r5, r6} @ldmda中, ia的意思是每次传送后地址+4 @r4=标号3的虚拟地址 @r5=__arch_info_begin虚拟地址 @r6 = __arch_info_end虚拟地址 sub r3, r3, r4 @ get offset between virt&phys 得到物理地址和虚拟地址的差值 add r5, r5, r3 @ convert virt addresses to r5 =__arch_info_begin对应的物理地址 add r6, r6, r3 @ physical address space r6 =__arch_info_end对应的物理地址 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type @r5 是machine_desc结构体的地址 @r3 =machine_desc结构体定义的nr成员,即机器ID teq r3, r1 @ matches loader number? @r1是uboot传入的机器类型ID beq 2f @ found 如果相等,找到匹配的proc_info_list结构,跳转到标号2处 add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc 否则指向下一个machine_desc结构体 cmp r5, r6 @是否已经比较完所有的machine_desc结构体 blo 1b @没有则继续比较 mov r5, #0 @ unknown machine 比较完毕,未匹配,r5 =0 2: mov pc, lr ENDPROC(__lookup_machine_type) /* * Look in <asm/procinfo.h> and arch/arm/kernel/arch.[ch] for * more information about the __proc_info and __arch_info structures. */ .long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end
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 。
注意,对于mini6410开发板uboot传入的机器ID为2520,对应machine_desc结构在arch/arm/mach-s3c64xx/ mach-mini6410.c文件中定义,需要配置CONFIG_MACH_MINI6410=y,因为这个描述机器结构的文件要编进内核中。在arch/arm/mach-s3c64xx/Makefile中 有如下定义:
obj-$(CONFIG_MACH_MINI6410) += mach-mini6410.o mini6410-lcds.o
3.4 检查atags合法性
3.4.1 调用代码段
/* * r1 = machine no, r2 = atags, * r8 = machinfo, r9 = cpuid, r10 = procinfo */ bl __vet_atags
3.4.2 处理流程分析
r2为中存放的是标记列表atags指针(u-boot传入)。__vet_atags用来检查atags合法性。__vet_atags函数也是在arch/arm/kernel/head-common.S中定义的,代码如下:
/* 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. 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. * * r8 = machinfo * * Returns: * r2 either valid atags pointer, or zero * r5, r6 corrupted */ __vet_atags: tst r2, #0x3 @ aligned?是否4字节对齐,如果bit_3为1,zero==0,如果bit_2为0,则zero==1, bne 1f @没对齐则跳转到标号1处 ldr r5, [r2, #0] @ is first tag ATAG_CORE? 获取tag大小并存入r5中 subs r5, r5, #ATAG_CORE_SIZE bne 1f ldr r5, [r2, #4] @将tag的类型放入r5中 ldr r6, =ATAG_CORE @将ATAG_CORE值放入r6中 cmp r5, r6 @比较 r5, r6 bne 1f @不相等跳转到标号1处 mov pc, lr @ atag pointer is ok 相等则说明atag是合法性,函数调用完毕,返回*/ 1: mov r2, #0 @ atag非法,将r2置为0 mov pc, lr @函数调用完毕,返回 ENDPROC(__vet_atags)
3.5 创建一级页表
3.5.1 当前r8/r9/r10寄存器值
head.S中首先确定了processor type和 machine type,之后就是创建页表。通过前面的两步,我们已经确定了processor type 和 machine type。此时,一些特定寄存器的值如下所示:
- r8 = machine info (struct machine_desc的基地址)
- r9 = cpu id (通过cp15协处理器获得的cpu id)
- r10 = procinfo (struct proc_info_list的基地址)
3.5.2 基础知识
由于CPU要开启MMU进入虚地址执行模式,因此必须先通过__create_page_tables建立一个临时的page table(将来这个table会被抛弃,重新建立)。
函数中出现的宏及其解释:
宏 | 默认值 | 定义 | 所在文件 |
---|---|---|---|
KERNEL_RAM_VADDR | 0xC0008000 PAGE_OFFSET+TEXT_OFFSET | 内核在内存中的虚拟地址 | arch/arm/kernel\head.S |
PAGE_OFFSET | 0xC0000000 | 内核虚拟地址空间的起始地址 | 在配置文件中.config中定义CONFIG_PAGE_OFFSET arch/arm/include/asm/memory.h |
TEXT_OFFSET | 0x00008000 | 内核起始位置相对于内存起始位置的偏移 | /arc/arm/makefile |
PHYS_OFFSET | 平台相关0x50000000 | 物理内存的起始地址 | arch/arm/mach-s3c64x’x/include/mach/memory.h |
ARM 指令中, 第二个操作数 (operand2) 如果是常数表达式的话, 该常数须对应8位位图 (Pattern), 即常数是由一个8位的常数循环移位偶数位得到的 ,所以 :
add r0, r4, #(KERNEL_START & 0xff000000) >> 18 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
3.4.3 Translation for a 1MB section, ARMv6 format
3.5.4 处理流程分析
__create_page_tables:函数在arch/arm/kernel/head.S中实现,它完成一级页表的创建。代码如下:
/* * 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 = machinfo * r9 = cpuid * r10 = procinfo * * Returns: * r0, r3, r6, r7 corrupted * r4 = physical page table address */ __create_page_tables: /* .macro pgtbl, rd ldr \rd, =(KERNEL_RAM_PADDR - 0x4000) .endm */ pgtbl r4 @ page table address 通过这个宏将r4(0x50004000)设置成存放页表的基地址,往后r4值一直没有变 /* Clear the 16K level 1 swapper page table 页表将4GB的地址空间分成若干个1MB的段(section),因此页表包含4096个页表项(section entry)。每个页表项是32bits(4 bytes),因而页表占用4096*4=16k的内存空间。下面的代码是将这16k的页表清0。 */ mov r0, r4 @r4=0x50000000+0x8000-0x4000=0x50004000 mov r3, #0 add r6, r0, #0x4000 @下面用循环展开,优化性能r6=0x50008000 1: str r3, [r0], #4 @将r3中的字数据写入以r0为地址的存储器中,并将新地址r0+4写入r0。 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b /*这个数据依构架而不同,数据是用汇编文件配置的: arch/arm/mm/proc-xxx.S(此时MMU已关闭)*/ @将proc_info中的mmu_flags加载到r7 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, __enable_mmu_loc ldmia r0, {r3, r5, r6} sub r0, r0, r3 @ virt->phys offset 得到物理地址和虚拟地址的偏移量 add r5, r5, r0 @ phys __enable_mmu 修正r5的值为__enable_mmu的物理地址 add r6, r6, r0 @ phys __enable_mmu_end 修正r6的值为__enable_mmu_end的物理地址 mov r5, r5, lsr #20 @ __enable_mmu的段基址(页表项中的偏移(4字节一个页表项)) mov r6, r6, lsr #20 @ __enable_mmu_end的段基址(页表项中的偏移(4字节一个页表项)) /* 其实这个特定的映射就是仅映射__enable_mmu功能函数区的页表,以保证在启用mmu时代码的正确执行,映射方式为平行映射--1:1映射(物理地址=虚拟地址) 第一行:通过__enable_mmu的段基址右移20位,然后或上映射属性(r7)得到描述符,最后存储在r3中。当前__enable_mmu的段基址是物理地址。 第二行:设置页表:mem[r4+r5*4]=r3,因为页表的每一项是32bits(4字节),所以要乘以4(<<2) ------------------------------------------------------------------------------------------------------------- 物理内存 | 映射方式 | 虚拟内存 ------------------------------------------------------------------------------------------------------------- __enable_mmu - __enable_mmu_end 修正后的物理地址 | 平行映射 | __enable_mmu - __enable_mmu_end 修正后的物理地址 ------------------------------------------------------------------------------------------------------------- */ 1: orr r3, r7, r5, lsl #20 @ flags + kernel base str r3, [r4, r5, lsl #2] @ identity mapping teq r5, r6 @ 理论上一次就够了,这个函数应该不会大于1M吧 addne r5, r5, #1 @ next section 注意这里是加1个32bit bne 1b /* * Now setup the pagetables for our kernel direct * mapped region. * 现在为了内核直接映射区来设置页表 * 即为kernel镜像所占有空间,即KERNL_START到KERNEL_END建立内存映射 * 由于这块内核虚拟空间要映射到SDRAM中内核映像0x50008000开始处,所以第一个1M的描述符(保存到r3寄存器中)和上面的是样的 * 这里是将整个内核空间的直接映射区全部映射完毕--以段的方式(1M) ------------------------------------------------------------------------------------------------ 物理内存 | 映射方式 | 虚拟内存 ------------------------------------------------------------------------------------------------ 0x50008000 + 内核大小 | 正常映射 | 0xc0008000 - 0xc0008000 + 内核大小 ------------------------------------------------------------------------------------------------ */ mov r3, pc @ pc为当前运行地址,因为mmu未开始,等价于物理地址 mov r3, r3, lsr #20 @ r3 = 当前物理地址的段基址 orr r3, r7, r3, lsl #20 @ r3 = 要映射物理地址的描述符 /* 下面两句 实际上是 r0 = 内核空间地址(VA)的[31-20] + r4 = 要写入描述符的页表项地址 r4=ttb基址 =0x50004000 ARM的一条指令是32位,其中只能用指令机器码32位中的低12位来表示要操作的常数 */ add r0, r4, #(KERNEL_START & 0xff000000) >> 18 @ 取内核空间地址(VA)的[31-24]位,8位立即数 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! @ 再取内核空间地址(VA)的[23-20]位,和上一句加起来共12位,“!”执行后将运算值写回r0 ldr r6, =(KERNEL_END - 1) add r0, r0, #4 @ 指向下一个页表项地址 add r6, r4, r6, lsr #18 @ r6= 用来标识ttb中内核地址空间结束的检索地址 r6 = r4 + (r6>>20)*4 = r4 + r6>>18 1: cmp r0, r6 add r3, r3, #1 << 20 @ 指向下一个物理段 strls r3, [r0], #4 @ 设置物理地址段的描述符 ls(Unsigned lower or same)无符号小于等于 bls 1b /* * Then map first 1MB of ram in case it contains our boot params. 映射SDRAM开始1M的地址空间,那里保存了uboot传递给内核的启动参数 --------------------------------------------------------------------------------------------------------------- 物理内存 | 映射方式 | 虚拟内存 --------------------------------------------------------------------------------------------------------------- 0x50000000 _0x50100000 | 直接映射 | 0xc0000000 - 0xc0100000 --------------------------------------------------------------------------------------------------------------- */ add r0, r4, #PAGE_OFFSET >> 18 @r0 = 0x50004000+(0xc0000000>>20)*4 = 0x50004000+(0xc0000000>>18) orr r6, r7, #(PHYS_OFFSET & 0xff000000) @r6 = (0x50000000 & 0xff000000) | r7(flags) 得到描述符 .if (PHYS_OFFSET & 0x00f00000) orr r6, r6, #(PHYS_OFFSET & 0x00f00000) .endif str r6, [r0] @ 设置物理地址段的描述符到指定的页表项 /* 下面是调试信息的输出函数区,这里做了IO内存空间的节映射 */ #ifdef CONFIG_DEBUG_LL //在.config文件定义 CONFIG_DEBUG_LL=y 使能早期打印 #ifndef CONFIG_DEBUG_ICEDCC //“Kernel low-level debugging via EmbeddedICE DCC channel”调试信息重定向到EmbeddedICE的DCC通道。这仅对于ARM9类型的ICE仿真器可以使用。 /* * Map in IO space for serial debugging. * This allows debug messages to be output * via a serial console before paging_init. #define S3C_ADDR_BASE 0xF6000000 #define S3C_ADDR(x) (S3C_ADDR_BASE + (x)) arch/arm/mach-s3c6400/include/mach/debug-macro.S .macro addruart, rp, rv ldr \rp, = S3C_PA_UART @0x7F005000 ldr \rv, = (S3C_VA_UART + S3C_PA_UART & 0xfffff) #if CONFIG_DEBUG_S3C_UART != 0 @ 在.config文件中定义 CONFIG_DEBUG_S3C_UART=0 add \rp, \rp, #(0x400 * CONFIG_DEBUG_S3C_UART) add \rv, \rv, #(0x400 * CONFIG_DEBUG_S3C_UART) #endif .endm */ addruart r7, r3 @r7 = 0x7F005000,r3 = 0xF6000000 + 0x7F005000 & 0xfffff = 0xF6005000 mov r3, r3, lsr #20 mov r3, r3, lsl #2 @r3 = 0xF60 * 4 add r0, r4, r3 @ 得到页表项的地址 rsb r3, r3, #0x4000 @ PTRS_PER_PGD*sizeof(long) = 2048 *sizeof(long) @ RSB指令称为逆向减法指令,用于把操作数2减去操作数1,并将结果存放到目的寄存器中 cmp r3, #0x0800 @ limit to 512MB, movhi r3, #0x0800 @ hi --- C set and Z clear (unsigned higher) add r6, r0, r3 @ r6 = 要写入对应描述符的页表项结束地址 mov r3, r7, lsr #20 @ r3 = 0x7F005000 >> 20 = 0x7F0 ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags ldr r7, [r10, #12] 得到io_mmuflags orr r3, r7, r3, lsl #20 @ 得到描述符 1: str r3, [r0], #4 @ mem[r0]=r3,r0 = r0+4 add r3, r3, #1 << 20 @ 指向下一个要映射的物理段 teq r0, r6 bne 1b #else /* CONFIG_DEBUG_ICEDCC */ /* we don't need any serial debugging mappings for ICEDCC */ ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags #endif /* !CONFIG_DEBUG_ICEDCC */ #if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS) //未定义 ........ #endif #ifdef CONFIG_ARCH_RPC //RPC是远程过程调用(Remote Procedure Call)的缩写形式 未定义 ........ #endif #endif mov pc, lr ENDPROC(__create_page_tables)
3.5.5 映射关系图
3.6 使能mmu
/* * 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_machine_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 //把标号1的链接地址保存到lr寄存器中,函数返回时使用 ARM( add pc, r10, #PROCINFO_INITFUNC ) // pc = r10 +12 THUMB( add r12, r10, #PROCINFO_INITFUNC ) //忽略 THUMB( mov pc, r12 ) //忽略 1: b __enable_mmu //使能mmu ENDPROC(stext)
代码执行顺序如下:
(1) 先调用PROCINFO_INITFUNC 处的函数,初始化TLB,缓存和MMU状态
(2)返回到标号1 — __enable_mmu处继续执行,打开mmu
3.6.1 初始化TLB,缓存和MMU状态
调用代码段
add pc, r10, #PROCINFO_INITFUNC
其中r10 =为存放procinfo的地址,armv6架构对应表的procinfo定义在arch/arm/mm/proc-v6.S中:
/* * Match any ARMv6 processor core. */ .type __v6_proc_info, #object __v6_proc_info: @ r10指向的位置 .long 0x0007b000 @ 4字节 .long 0x0007f000 @ 4字节 /* ALT_SMP(.long \ @ mini6410是单核处理器,这里不关注 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 @ r10+ 12 的位置 .long cpu_arch_name .long cpu_elf_name /* See also feat_v6_fixup() for HWCAP_TLS */ .long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA|HWCAP_TLS .long cpu_v6_name .long v6_processor_functions .long v6wbi_tlb_fns .long v6_user_fns .long v6_cache_fns .size __v6_proc_info, . - __v6_proc_info
/* * __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: #ifdef CONFIG_SMP @ mini6410是单核处理器,这里不关注 ALT_SMP(mrc p15, 0, r0, c1, c0, 1) @ Enable SMP/nAMP mode ALT_UP(nop) orr r0, r0, #0x20 ALT_SMP(mcr p15, 0, r0, c1, c0, 1) ALT_UP(nop) #endif mov r0, #0 mcr p15, 0, r0, c7, c14, 0 @ clean+invalidate D cache 操作协处理器,没什么好讲的,查对应cpu手册即可 mcr p15, 0, r0, c7, c5, 0 @ invalidate Entire Instruction Cache mcr p15, 0, r0, c7, c15, 0 @ clean+invalidate cache 在架构上被定义为统一缓存操作,并且没有效果 mcr p15, 0, r0, c7, c10, 4 @ Data Synchronization Barrier (DSB),drain write buffer #ifdef CONFIG_MMU @ 有定义 mcr p15, 0, r0, c8, c7, 0 @ invalidate I + D TLBs mcr p15, 0, r0, c2, c0, 2 @ TTB control register 16KB size ALT_SMP(orr r4, r4, #TTB_FLAGS_SMP) ALT_UP(orr r4, r4, #TTB_FLAGS_UP) mcr p15, 0, r4, c2, c0, 1 @ load TTB1 转换表基寄存器1 保存第一级表的物理地址(r4) #endif /* CONFIG_MMU * adr r5, v6_crval ldmia r5, {r5, r6} #ifdef CONFIG_CPU_ENDIAN_BE8 orr r6, r6, #1 << 25 @ big-endian page tables 未定义 #endif mrc p15, 0, r0, c1, c0, 0 @ read control register bic r0, r0, r5 @ clear bits them orr r0, r0, r6 @ set them mov pc, lr @ return to head.S:__ret @返回lr存放的地址处,即标号1处 /* * V X F I D LR * .... ...E PUI. .T.T 4RVI ZFRS BLDP WCAM * rrrr rrrx xxx0 0101 xxxx xxxx x111 xxxx < forced * 0 110 0011 1.00 .111 1101 < we want */ .type v6_crval, #object v6_crval: crval clear=0x01e0fb7f, mmuset=0x00c0387d, ucset=0x00c0187c
3.6.2 开启mmu
调用代码段
1: b __enable_mmu
/* * 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 pointer * r4 = page table pointer * r9 = processor ID * r13 = *virtual* address to jump to upon completion */ __enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP @有定义 orr r0, r0, #CR_A /* Alignment abort enable 打开对齐校验 */ #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 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \ // domain 1 includes all user memory only domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \ //domain 0 includes all kernel memory only domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \ domain_val(DOMAIN_IO, DOMAIN_CLIENT)) //domain 2 includes all IO only 1 << (1*2)|0 << (1*2)|0 << (1*2)|2 << (1*2) mcr p15, 0, r5, c3, c0, 0 @ load domain access register mcr p15, 0, r4, c2, c0, 0 @ load page table pointer //写入TTBR0 b __turn_mmu_on ENDPROC(__enable_mmu) /* * Enable the MMU. This completely changes the structure of the visible * memory space. You will not be able to trace execution through this. * If you have an enquiry about this, *please* check the linux-arm-kernel * mailing list archives BEFORE sending another post to the list. * * r0 = cp#15 control register * r1 = machine ID * r2 = atags pointer * r9 = processor ID * r13 = *virtual* address to jump to upon completion * * other registers depend on the function called upon completion */ .align 5 __turn_mmu_on: mov r0, r0 mcr p15, 0, r0, c1, c0, 0 @ write control reg 这段代码的核心,把r0的值写入到c1中,这时候,MMU就已经打开了 mrc p15, 0, r3, c0, c0, 0 @ read id reg mov r3, r3 mov r3, r13 //__mmap_switched mov pc, r3 __enable_mmu_end: ENDPROC(__turn_mmu_on)
3.7 跳转到start_kernel前的准备工作
3.7.1 准备工作总述
经过前面的分析,我们已经知道MMU已经打开,后续都是在虚拟地址上执行。并且kernel代码段的链接地址都已经映射到对应的物理地址上了。接下来调用到__mmap_switched,做一些跳转到start_kernel前的准备动作。
在跳转到start_kernel前,需要做如下准备动作:
-
数据段的准备
通过System.map可以看到数据段对应的连接地址区域如下:
c0719d3c A _etext
c071a000 A __data_loc
c071a000 D _data
c071a000 D _sdata
…
c075f380 D _edata
c075f380 A _edata_loc -
bss段的准备
因为后续start_kernel之后都是在C语言环境下运行,所以需要对bss段进行设置并清空。
c075f3a4 A __bss_start
c079fd64 A __bss_stop
c079fd64 b __key.11063
c079fd64 A _end -
一些后续会访问到的变量的设置
因为后续C语言代码会访问到一些变量,并且这些变量的值在启动过程中是存储到特殊寄存器中的。那么就需要把寄存器上的值搬移到对应的变量的地址上。有如下这些变量:
(1)cpu id (processor ID)
(2)machine id(machine type)
(3)atags的存放地址(或者dtb指针的地址)(atags pointer) -
一些后续会访问到的变量的设置
因为后续start_kernel之后都是在C语言环境下运行,需要完成其堆栈环境。当前进程堆栈指针的设置。
最后就可以跳转到start_kernel中了。接下来分析的__mmap_switched的代码也是根据这些准备动作来的。
3.7.2 准备工作代码分析
1. __mmap_switched_data
首先分析一下__mmap_switched_data这个数据结构,这个结构中存放了__mmap_switched过程中使用的变量的地址。定义在arch/arm/kernel/head-common.S
.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 .long cr_alignment @ r7 .long init_thread_union + THREAD_START_SP @ sp .size __mmap_switched_data, . - __mmap_switched_data
各标号含义如下:
标号 | 含义 |
---|---|
__data_loc | 数据段存储地址 |
_sdata | 数据段起始地址 |
__bss_start | 堆栈段起始地址 |
_end | 堆栈段结束地址 |
processor_id | cpu处理器ID地址,其变量定义在arch/arm/kernel/setup.c中 |
__machine_arch_type | machine id地址,其变量定义在arch/arm/kernel/setup.c中 |
__atags_pointer | atags的存放地址(或者dtb指针的地址),其变量定义在arch/arm/kernel/setup.c中 |
cr_alignment | cp15的c1寄存器的值的地址,也就是mmu控制寄存器的值,其变量定义在arch/arm/kernel/entry-armv.S中 |
init_thread_union + THREAD_START_SP | 在初始化的时候,将SP都指向了stack的最高位置,它并没有使用thread_info这个结构体。前面的启动过程都当成一个特殊的线程看待。这个线程从head.s的第一行语句开始一直工作到内核启动完成 |
2. 当前各寄存值器说明
通过前面章节的分析,有几个特殊寄存器存放了如下值:
寄存器 | 值 |
---|---|
r0 | 存放了cp15协处理器c1寄存器的值,也就是MMU控制器的值 |
r1 | 存放了由uboot传过来的mechine id (使用设备树版本内核时,可以不传入值) |
r2 | 存放atags的地址(或者dtb的地址) |
r9 | 存放了cpu处理器id |
3. 代码分析
/* * 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 pointer * r9 = processor ID */ __INIT __mmap_switched: adr r3, __mmap_switched_data @ 将__mmap_switched_data的地址加载到r3中 /* 将__mmap_switched_data(r3)上的值分别加载到r4、r5、r6、r7寄存器中,并判断数据段存储地址(r4)和数据段起始地址(r5),如果不一样的话需要搬移到数据段起始地址(r5)上 经过上述动作,r4、r5、r6、r7寄存器分别存放了如下值: r4 = __data_loc:数据段存储地址 r5 = _sdata:数据段起始地址 r6 = __bss_start:堆栈段起始地址 r7 = _end:堆栈段结束地址 */ ldmia r3!, {r4, r5, r6, r7} @"!"执行后r3自加 cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b mov fp, #0 @ Clear BSS (and zero fp)清空堆栈段 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b /* 继续将r3地址处的值分别加载到r4、r5、r6、r7、sp寄存器中,注意前面操作的时候有个"!",此时r3地址指向了processor_id。 经过上述动作,r4、r5、r6、r7寄存器分别存放了如下值 r4 = processor_id变量地址:其内容是cpu处理器ID r5 = __machine_arch_type变量地址:其内容是machine id r6 = __atags_pointer变量地址:其内容是atags的存放地址(或者dtb指针的地址) r7 = cr_alignment变量地址:其内容是cp15的c1的寄存器的值 sp = init_thread_union + THREAD_START_SP,设置了当前进程的堆栈 */ 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 bic r4, r0, #CR_A @ Clear 'A' bit stmia r7, {r0, r4} @ Save control register values b start_kernel @ 跳转到start_kernel中,也就是启动流程的第二阶段。 ENDPROC(__mmap_switched)
本文来自博客园,作者:BSP-路人甲,转载请注明原文链接:https://www.cnblogs.com/jianhua1992/p/16852790.html,并保留此段声明,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了