MEMORY | INTERRUPT | TIMER | 并发与同步 | 进程管理 | 调度 | uboot | DTB | ARMV8 | ATF | Kernel Data Structure | PHY | LINUX2.6 | 驱动合集 | UART子系统 | USB专题 |

内核启动流程 --- 启动准备阶段(二)

一. 前言

压缩过的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) (当前程序状态寄存器),在任何处理器模式下被访问。它包含了条件标志位、中断禁止位、当前处理器模式标志以及其他的一些控制和状态位。如下所示:
在这里插入图片描述

对应位对应标识功能说明
bit31N用两个补码表示的带符号的运算结果标识,为1表示负数,为0表示非负数
bit30Z为1表示运算结果为0,为0表示运算结果为非0
bit29C加法运算结果进位标志位
bit28V加减法运算指令符号位溢出标志位
bit7I为1表示禁止IRQ中断
bit6F为1表示禁止FIQ中断
bit5T为1表示程序运行于Thumb状态,否则处于ARM状态。
bit4-0M[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_VADDR0xC0008000 PAGE_OFFSET+TEXT_OFFSET内核在内存中的虚拟地址arch/arm/kernel\head.S
PAGE_OFFSET0xC0000000内核虚拟地址空间的起始地址在配置文件中.config中定义CONFIG_PAGE_OFFSET arch/arm/include/asm/memory.h
TEXT_OFFSET0x00008000内核起始位置相对于内存起始位置的偏移/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 注意这里是加132bit
	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_idcpu处理器ID地址,其变量定义在arch/arm/kernel/setup.c中
__machine_arch_typemachine id地址,其变量定义在arch/arm/kernel/setup.c中
__atags_pointeratags的存放地址(或者dtb指针的地址),其变量定义在arch/arm/kernel/setup.c中
cr_alignmentcp15的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)

posted on 2022-11-02 22:23  BSP-路人甲  阅读(315)  评论(0编辑  收藏  举报

导航