内核启动流程 --- 启动准备阶段(二)
文章目录
一. 前言
压缩过的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,并保留此段声明,否则保留追究法律责任的权利。