ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)

本文整理了ARM Linxu启动流程的第二阶段——start_kernel前启动阶段(汇编部分),内核版本为3.12.35。我以手上的树莓派b(ARM11)为平台示例来分析Linux内核在自解压后到跳转运行start_kernel之前所做的主要初始化工作:包括参数有效性验证、创建初始页表和MMU初始化等。


内核版本:Linux-3.12.35
分析文件:arch/arm/kernel/head.S、head-common.S、proc-v6.S

单板:树莓派b


在内核启动时执行自解压完成后,会跳转到解压后的地址处运行,在我的环境中就是地址0x00008000处,然后内核启动并执行初始化。

首先给出你内核启动的汇编部分的总流程如下:

ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)_第1张图片

内核启动程序的入口:参见arch/arm/kernel/vmlinux.lds(由arch/arm/kernel/vmlinux.lds.S生成)。

arch/arm/kernel/vmlinux.lds:

ENTRY(stext)
jiffies = jiffies_64;
SECTIONS
{
......
 . = 0xC0000000 + 0x00008000;
 .head.text : {
  _text = .;
  *(.head.text)
 }
 .text : { /* Real text segment		*/
  _stext = .; /* Text and read-only data	*/

此处的TEXT_OFFSET表示内核起始地址相对于RAM地址的偏移值,定义在arch/arm/Makefile中,值为0x00008000:

textofs-y	:= 0x00008000
......
# The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)

PAGE_OFFSET表示内核虚拟地址空间的其实地址,定义在arch/arm/include/asm/memory.h中:

#ifdef CONFIG_MMU

/*
 * PAGE_OFFSET - the virtual address of the start of the kernel image
 * TASK_SIZE - the maximum size of a user space task.
 * TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area
 */
#define PAGE_OFFSET		UL(CONFIG_PAGE_OFFSET)
CONFIG_PAGE_OFFSET定义在arch/arm/Kconfig中,采用默认值0xC0000000。
config PAGE_OFFSET
	hex
	default 0x40000000 if VMSPLIT_1G
	default 0x80000000 if VMSPLIT_2G
	default 0xC0000000

所以,可以看出内核的链接地址采用的是虚拟地址,地址值为0xC0008000。

内核启动程序的入口在linux/arch/arm/kernel/head.S中,head.S中定义了几个比较重要的变量,在看分析程序前先来看一下:

/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */
#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#ifdef CONFIG_ARM_LPAE
	/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE	0x5000
#define PMD_ORDER	3
#else
#define PG_DIR_SIZE	0x4000
#define PMD_ORDER	2
#endif

	.globl	swapper_pg_dir
	.equ	swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

	.macro	pgtbl, rd, phys
	add	\rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
	.endm

其中KERNEL_RAM_VADDR表示内核启动地址的虚拟地址,即前面看到的链接地址0xC0008000,同时内核要求这个地址的第16位必须是0x8000。

然后由于没有配置ARM LPAE,则采用一级映射结构,页表的大小为16KB,页大小为1MB。

最后swapper_pg_dir表示初始页表的起始地址,这个值等于内核起始虚拟地址-页表大小=0xC0004000(内核起始地址下16KB空间存放页表)。虚拟地址空间如下图:

需要说明一下:在我的环境中,内核在自解压阶段被解压到了0x00008000地址处,由于内核入口链接地址采用的是虚拟地址0xC0008000,这两个地址并不相同;并且此时MMU并没有被使能,所以无法进行虚拟地址到物理地址的转换,程序开始执行后在打开MMU前的将使用位置无关码。

在知道了内核的入口位置后,来看一下此时的设备和寄存器的状态:

/*
 * 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 or dtb 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.
 */
	.arm

	__HEAD
ENTRY(stext)

注释中说明了,此时的MMU关闭、D-cache关闭、r0 = 0、r1 = 机器码、r2 = 启动参数atags或dtb的地址(我的环境中使用的是atags),同时内核支持的机器码被定义在了linux/arch/arm/tools/mach-types中。我树莓派使用的是:

bcm2708                    MACH_BCM2708             BCM2708                            3138

 下面来逐行分析代码:

 THUMB(	adr	r9, BSYM(1f)	)	@ Kernel is always entered in ARM.
 THUMB(	bx	r9		)	@ If this is a Thumb-2 kernel,
 THUMB(	.thumb			)	@ switch to Thumb now.
 THUMB(1:			)

#ifdef CONFIG_ARM_VIRT_EXT
	bl	__hyp_stub_install
#endif
	@ ensure svc mode and all interrupts masked
	safe_svcmode_maskall r9

	mrc	p15, 0, r9, c0, c0		@ get processor id
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid

这里的safe_svcmode_maskall是一个宏,定义在arch/arm/include/asm/assembler.h中,它的作用就是确保ARM进入SVC工作模式并屏蔽所有的中断(此时关闭中断的原因是中断向量表尚未建立,内核无能力响应中断)。

然后获取处理器ID保存到r9寄存器中,接着跳转到__lookup_processor_type寻找对应处理器ID的proc_info地址。__lookup_processor_type定义在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, __lookup_processor_type_data
	ldmia	r3, {r4 - r6}
	sub	r3, r3, r4			@ get offset between virt&phys
	add	r5, r5, r3			@ convert virt addresses to
	add	r6, r6, r3			@ physical address space
1:	ldmia	r5, {r3, r4}			@ value, mask
	and	r4, r4, r9			@ mask wanted bits
	teq	r3, r4
	beq	2f
	add	r5, r5, #PROC_INFO_SZ		@ sizeof(proc_info_list)
	cmp	r5, r6
	blo	1b
	mov	r5, #0				@ unknown processor
2:	mov	pc, lr
ENDPROC(__lookup_processor_type)

首先获取处理器相关信息表的运行地址并保存到r3寄存器中。内核将所有的处理器信息都保存在proc_info_list结构体表中,它的定义如下(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;
};

结构体中描述了CPU相关的信息,其中__cpu_mm_mmu_flags、__cpu_io_mmu_flags和__cpu_flush这三个字段将会在head.s中使用到。处理器相关信息都被保存在.init.proc.info段中:

/*
 * Look in  for information about the __proc_info structure.
 */
	.align	2
	.type	__lookup_processor_type_data, %object
__lookup_processor_type_data:
	.long	.
	.long	__proc_info_begin
	.long	__proc_info_end
	.size	__lookup_processor_type_data, . - __lookup_processor_type_data

vmlinux.lds:

 .init.proc.info : {
  . = ALIGN(4); __proc_info_begin = .; *(.proc.info.init) __proc_info_end = .;
 }

其中每种类型处理器的信息定义在arch/arm/mm/proc-*.S下,例如我的环境定义在proc-v6.S中:

	.section ".proc.info.init", #alloc, #execinstr

	/*
	 * Match any ARMv6 processor core.
	 */
	.type	__v6_proc_info, #object
__v6_proc_info:
	.long	0x0007b000
	.long	0x0007f000
	ALT_SMP(.long \
		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
......

回到__lookup_processor_type程序中,程序接着在r4、r5和r6中保存__lookup_processor_type_data、__proc_info_begin和__proc_info_end的链接地址(即虚拟地址),然后通过r3 = r3 – r4得到运行地址和链接地址之间的偏移值并将r5和r6中的地址值修正为__proc_info_begin和__proc_info_end的运行地址。

然后从proc_info_list结构中取出cpu_val和cpu_mask字段的内容,和r9中保存的处理器ID进行比较,若匹配上了则通过r5寄存器返回当前处理器的proc_info_list结构信息运行地址,否则r5 = r5 + PROC_INFO_SZ(即将r5指向下一条处理器的proc_info_list结构提信息)继续进行匹配。若全部匹配失败,则r5返回0。

	movs	r10, r5				@ invalid processor (r5=0)?
 THUMB( it	eq )		@ force fixup-able long branch encoding
	beq	__error_p			@ yes, error 'p'

回到外层函数后,这里会先将返回值付给r10,然后判断是否返回值是否为0,若为0表示没有匹配到对应的处理器信息,调用__error_p打印出错信息并进入死循环,内核启动失败。

#ifdef CONFIG_ARM_LPAE
	mrc	p15, 0, r3, c0, c1, 4		@ read ID_MMFR0
	and	r3, r3, #0xf			@ extract VMSA support
	cmp	r3, #5				@ long-descriptor translation table format?
 THUMB( it	lo )				@ force fixup-able long branch encoding
	blo	__error_p			@ only classic page table format
#endif

这里ARM_LAPE表示大物理内存扩展,我的环境下并没有配置该项,暂不考虑。

#ifndef CONFIG_XIP_KERNEL
	adr	r3, 2f
	ldmia	r3, {r4, r8}
	sub	r4, r3, r4			@ (PHYS_OFFSET - PAGE_OFFSET)
	add	r8, r8, r4			@ PHYS_OFFSET
#else
	ldr	r8, =PHYS_OFFSET		@ always constant in this case
#endif

这里将计算起始RAM物理地址并保存到r8中,计算的方法同前面获取CPU信息结构地址的方法类似,首先获取标号为2处的运行地址和链接地址(通过反汇编查看,我的环境分别是:0x00008070和0xC0008070),一减之后就得到了运行地址和物理地址的差值(0xC0000000),然后用这个差值加上PAGE_OFFSET(0xC0000000)即可得到实际物理内存的起始地址PHYS_OFFSET(0x00000000)。

现在来查看反汇编代码,加深理解:

c0008040:	e28f3028 	add	r3, pc, #40	; 0x28
c0008044:	e8930110 	ldm	r3, {r4, r8}
c0008048:	e0434004 	sub	r4, r3, r4
c000804c:	e0888004 	add	r8, r8, r4
......
c0008070:	c0008070 	andgt	r8, r0, r0, ror r0
c0008074:	c0000000 	andgt	r0, r0, r0

这里r3 = 0x00008040 + 0x8 + 0x28 = 0x00008070,r4 =0xC0008070,r8 = 0xC0000000,在经过偏移处理后,r8的值就变成了0x00000000,即物理RAM首地址在内存地址空间中的偏移PAGE_OFFSET。(这里有一点疑问,如果我这里内核的运行地址并不是在0x00008000,那这个计算出道的物理RAM首地址不是就不正确了?

	/*
	 * r1 = machine no, r2 = atags or dtb,
	 * r8 = phys_offset, r9 = cpuid, r10 = procinfo
	 */
	bl	__vet_atags

现在来确认一下寄存器中保存内容的含义:

r1:机器码

r2:atag或者dtb的地址

r8:物理内存地址偏移

r9:获取到的CPU ID

r10:处理器信息结构地址

然后调用__vet_atags来验证r2中地址值得有效性

/* 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.  If CONFIG_OF_FLATTREE
 * is selected, then it will also accept a dtb pointer.  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.
 *
 * Returns:
 *  r2 either valid atags pointer, valid dtb pointer, or zero
 *  r5, r6 corrupted
 */
__vet_atags:
	tst	r2, #0x3			@ aligned?
	bne	1f

	ldr	r5, [r2, #0]
#ifdef CONFIG_OF_FLATTREE
	ldr	r6, =OF_DT_MAGIC		@ is it a DTB?
	cmp	r5, r6
	beq	2f
#endif
	cmp	r5, #ATAG_CORE_SIZE		@ is first tag ATAG_CORE?
	cmpne	r5, #ATAG_CORE_SIZE_EMPTY
	bne	1f
	ldr	r5, [r2, #4]
	ldr	r6, =ATAG_CORE
	cmp	r5, r6
	bne	1f

2:	mov	pc, lr				@ atag/dtb pointer is ok

1:	mov	r2, #0
	mov	pc, lr
ENDPROC(__vet_atags)

首先验证是否4字节地址对齐,若不对齐则直接将r2内容清空并返回。接着读取r2地址处的内容到r5寄存器中,这里若配置了CONFIG_OF_FLATTREE就会判断是否是DTB,我的环境中并没有配置。

然后进行atag的验证,若是atag,则r2地址处的内容将保存tag_header中的size值(arch/arm/include/uapi/asm/setup.h),同时内核也要求atag信息的第一项必须是ATAT_CORE类型的项

struct tag_header {
	__u32 size;
	__u32 tag;
};

......

struct tag_core {
	__u32 flags;		/* bit 0 = read-only */
	__u32 pagesize;
	__u32 rootdev;
};

该CORE项的size值为sizeof(struct tag_header) + sizeof(struct tag_core) >> 2,正好等于ATAG_CORE_SIZE:

#define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)

比较完size值后就将地址值偏移4字节读取tag值,比较是否等于ATAG_CORE,若是则验证通过则跳转到标号2处直接返回。

#ifdef CONFIG_SMP_ON_UP
	bl	__fixup_smp
#endif
#ifdef CONFIG_ARM_PATCH_PHYS_VIRT
	bl	__fixup_pv_table
#endif
	bl	__create_page_tables

然后我这里没有配置CONFIG_SMP_ON_UP和CONFIG_ARM_PATCH_PHYS_VIRT选项(他们的内核配置解释分别为Allowbooting SMP kernel on uniprocessor systems和Patch physical tovirtual translations at runtime),接下来就要跳转到__create_page_tables中创建初始页表了。

/*
 * 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 = phys_offset, r9 = cpuid, r10 = procinfo
 *
 * Returns:
 *  r0, r3, r5-r7 corrupted
 *  r4 = page table (see ARCH_PGD_SHIFT in asm/memory.h)
 */
__create_page_tables:
	pgtbl	r4, r8				@ page table address

这里的注释中说明了,创建初始页表的过程只会创建内核代码部分地址的页表。

这里的pgtbl  r4, r8表示获取存放页表首地址的运行时地址(物理地址)到r4中去,它在反汇编中被翻译成:

c0008078 <__create_page_tables>:
c0008078:	e2884901 	add	r4, r8, #16384	; 0x4000

可见这里的r4值就是0x00004000,正好是页表的起始物理地址。

	/*
	 * Clear the swapper page table
	 */
	mov	r0, r4
	mov	r3, #0
	add	r6, r0, #PG_DIR_SIZE
1:	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	teq	r0, r6
	bne	1b

然后将页表内存空间清零,从0x00004000~0x00008000的空间都清零。

#ifdef CONFIG_ARM_LPAE
	/*
	 * Build the PGD table (first level) to point to the PMD table. A PGD
	 * entry is 64-bit wide.
	 */
	mov	r0, r4
	add	r3, r4, #0x1000			@ first PMD table address
	orr	r3, r3, #3			@ PGD block type
	mov	r6, #4				@ PTRS_PER_PGD
	mov	r7, #1 << (55 - 32)		@ L_PGD_SWAPPER
1:
#ifdef CONFIG_CPU_ENDIAN_BE8
	str	r7, [r0], #4			@ set top PGD entry bits
	str	r3, [r0], #4			@ set bottom PGD entry bits
#else
	str	r3, [r0], #4			@ set bottom PGD entry bits
	str	r7, [r0], #4			@ set top PGD entry bits
#endif
	add	r3, r3, #0x1000			@ next PMD table
	subs	r6, r6, #1
	bne	1b

	add	r4, r4, #0x1000			@ point to the PMD tables
#ifdef CONFIG_CPU_ENDIAN_BE8
	add	r4, r4, #4			@ we only write the bottom word
#endif
#endif

由于没有配置ARM_LPAE,这一部分内容先暂时不做分析,接着往下看。

	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, __turn_mmu_on_loc
	ldmia	r0, {r3, r5, r6}
	sub	r0, r0, r3			@ virt->phys offset
	add	r5, r5, r0			@ phys __turn_mmu_on
	add	r6, r6, r0			@ phys __turn_mmu_on_end
	mov	r5, r5, lsr #SECTION_SHIFT
	mov	r6, r6, lsr #SECTION_SHIFT

这里开始创建特殊映射来满足开启MMU的需求,该映射将会在内核后续初始化执行paging_init()时被销毁。

首先从处理器的procinfo结构中获取__cpu_mm_mmu_flags参数保存在r7中,然后获取标号__turn_mmu_on_loc处的运行地址保存到r0中,然后使用前文中类似的手段获得__trun_mmu_on和__trun_mmu_on_end入口处的实际运行的物理地址保存到r5和r6寄存器中。

__turn_mmu_on_loc:
	.long	.
	.long	__turn_mmu_on
	.long	__turn_mmu_on_end

然后由于我的环境中没有开启LAPE,采用一级映射方式,映射单位为1M,所以这里的SECTION_SHIFT为20。这里对r5和r6中的值右移20位,得到了__trun_mmu_on和__trun_mmu_on_end的物理基地址。

1:	orr	r3, r7, r5, lsl #SECTION_SHIFT	@ flags + kernel base
	str	r3, [r4, r5, lsl #PMD_ORDER]	@ identity mapping
	cmp	r5, r6
	addlo	r5, r5, #1			@ next section
	blo	1b

这里将r5左移20位后或上r7中的标识位,得到了对应的First-level descriptor(即也表中存放的一级描述符,参见《ARM Linux启动流程分析——内核自解压阶段》),然后将这个描述符写到页表中对应的项中去。

这里包括__trun_mmu_on和__trun_mmu_on_end之间地址空间的特殊映射方式同样采用的是1:1映射,因此这里计算对应页表向的方式如下:

页表地址 = 映射物理基址 << PMD_ORDER(2)

例如:我环境中__trun_mmu_on物理地址为0xc0433398,它的基地址为0xc04,转换为对应的表项为0x3010,所以会在0x00004000+0x3010处的页表地址中写入“页描述符”,该描述描述符中的基址同样为0xc04。在进行虚拟地址到物理地址的转换过程中,即可实现x线性转换(转换方式参见《ARM Linux启动流程分析——内核自解压阶段》)。

如此循环映射完整个__turn_mmu_on部分代码,映射后的地址空间如下图:

ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)_第2张图片

	/*
	 * Map our RAM from the start to the end of the kernel .bss section.
	 */
	add	r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
	ldr	r6, =(_end - 1)
	orr	r3, r8, r7
	add	r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1:	str	r3, [r0], #1 << PMD_ORDER
	add	r3, r3, #1 << SECTION_SHIFT
	cmp	r0, r6
	bls	1b

映射完开启MMU部分的代码后,接下来开始映射内核。

首先将PAGE_OFFSET(0xc0008000)右移(20-2)位在加上r4(页表物理基地址)得到内核起始链接地址对应页表项的物理地址,保存到r0中。

接着获取内核代码的结束虚拟地址(包括了bss段)保存到r6中,_end定义在vmlinux.lds中:

 _edata_loc = __data_loc + SIZEOF(.data);
 . = ALIGN(0); __bss_start = .; . = ALIGN(0); .sbss : AT(ADDR(.sbss) - 0) { *(.sbss) *(.scommon) } . = ALIGN(0); .bss : AT(ADDR(.bss) - 0) { *(.bss..page_aligned) *(.dynbss) *(.bss) *(COMMON) } . = ALIGN(0); __bss_stop = .;
 _end = .;
 .stab 0 : { *(.stab) } .stabstr 0 : { *(.stabstr) } .stab.excl 0 : { *(.stab.excl) } .stab.exclstr 0 : { *(.stab.exclstr) } .stab.index 0 : { *(.stab.index) } .stab.indexstr 0 : { *(.stab.indexstr) } .comment 0 : { *(.comment) }
 .comment 0 : { *(.comment) }
}

在我的环境中,它的值为0xc116c517。

接着将r7或上r8得到First-leveldescriptor保存到r3中(该值的高12位为0),然后计算内核结束虚拟地址对应应页表项的物理地址保存到r6中。接下来的代码将r0~r6中的页表项循环循环填充上需要映射的First-level descriptor,每一次循环都会将r3+1<

ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)_第3张图片

	/*
	 * Then map boot params address in r2 if specified.
	 * We map 2 sections in case the ATAGs/DTB crosses a section boundary.
	 */
	mov	r0, r2, lsr #SECTION_SHIFT
	movs	r0, r0, lsl #SECTION_SHIFT
	subne	r3, r0, r8
	addne	r3, r3, #PAGE_OFFSET
	addne	r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
	orrne	r6, r7, r0
	strne	r6, [r3], #1 << PMD_ORDER
	addne	r6, r6, #1 << SECTION_SHIFT
	strne	r6, [r3]

在映射完内核之后就需要映射内核启动参数了,内核的启动参数地址保存在r2中。在前面的程序中已经对r2中启动参数地址的有效性进行了验证,如果无效则现在r2中的值就是0,将不做映射操作。

此处代码中的前两行就是为了判断该值是否为0,如果不为0才进行映射操作。

首先获取启动参数地址相对于物理RAM的偏移值并保存到r3中,然后再对该值加上PAGE_OFFSET(0xc0000000)得到其所需映射到的虚拟地址,接着找到对应的页表和生成First-level descriptor,最后连续写入连续的两项页表项来完成2页的映射。也就是说不论内核启动参数有多大,这里默认只映射2MB的内存。

	mov	pc, lr

映射完3个内存区间后,我这里过滤掉其他未定义的条件编译项后直接看到代码中执行返回操作,初始页表创建完成。

我的环境中内核启动参数的地址为0x00000100,所以映射结果如下:


ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)_第4张图片
	/*
	 * 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_processor_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
	mov	r8, r4				@ set TTBR1 to swapper_pg_dir
 ARM(	add	pc, r10, #PROCINFO_INITFUNC	)
 THUMB(	add	r12, r10, #PROCINFO_INITFUNC	)
 THUMB(	mov	pc, r12				)
1:	b	__enable_mmu

这里首先保存__mmap_switched函数的链接地址(虚拟地址)到r13中,它是MMU开启后的第一个要跳转运行的虚拟地址。

然后保存返回地址为下文中标号1处的地址,此处为b __enable_mmu;接着保存r4中的页表物理地址到r8寄存其中,最后就跳转到架构相关的处理器初始化函数中执行初始化,为开启MMU做准备工作;在执行完初始化函数后,将返回到lr保存的地址运行,开启MMU。

这里的PROCINFO_INITFUNC宏定义为16,此时PC的值正好为参数__cpu_flush的值

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;
__v6_proc_info:
	.long	0x0007b000
	.long	0x0007f000
	ALT_SMP(.long \
		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

在前文中已经看到,我的环境中在proc-v6.S已经定义了参数__cpu_flush的内容为b __v6_setup,所以这里会执行__v6_setup函数:

/*
 *	__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:

__v6_setup函数主要是配置CPU的寄存器,这里不再详细分析了,函数将会初始化TLB、Cache以及开启MMU的一些必要的状态(例如将页表物理地址设置到TTB:Translation Table Base中),然后通过r0寄存器返回CP15 C1控制寄存器中的设置值。

/*
 * 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 or dtb pointer
 *  r4  = page table (see ARCH_PGD_SHIFT in asm/memory.h)
 *  r9  = processor ID
 *  r13 = *virtual* address to jump to upon completion
 */
__enable_mmu:
#if defined(CONFIG_ALIGNMENT_TRAP) && __LINUX_ARM_ARCH__ < 6
	orr	r0, r0, #CR_A
#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

完成了上面的准备工作后就要开启MMU了,本质上这里仅仅是加载了页表指针和域访问控制寄存器。

首先这里根据内核配置选项再对r0中返回的CR15 CR1进行配置,这些宏定义在arch/arm/include/asm/cp15.h中,表示了寄存器每一位的定义,其中部分内容如下:

#define CR_M	(1 << 0)	/* MMU enable				*/
#define CR_A	(1 << 1)	/* Alignment abort enable		*/
#define CR_C	(1 << 2)	/* Dcache enable			*/
#define CR_W	(1 << 3)	/* Write buffer enable			*/
#define CR_P	(1 << 4)	/* 32-bit exception handler		*/
#define CR_D	(1 << 5)	/* 32-bit data address range		*/
#define CR_L	(1 << 6)	/* Implementation defined		*/
#define CR_B	(1 << 7)	/* Big endian				*/
#define CR_S	(1 << 8)	/* System MMU protection		*/
#define CR_R	(1 << 9)	/* ROM MMU protection			*/
#define CR_F	(1 << 10)	/* Implementation defined		*/
#define CR_Z	(1 << 11)	/* Implementation defined		*/
#define CR_I	(1 << 12)	/* Icache enable			*/
#define CR_V	(1 << 13)	/* Vectors relocated to 0xffff0000	*/
#define CR_RR	(1 << 14)	/* Round Robin cache replacement	*/
#define CR_L4	(1 << 15)	/* LDR pc can set T bit			*/
#define CR_DT	(1 << 16)
......

例如,如果内核CONFIG_CPU_DCACHE_DISABLE,则这里会清除CR_C位来使D-Cache失能等等。

#ifndef CONFIG_ARM_LPAE
	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		@ load domain access register

接下来这里设置域访问控制寄存器,ARM处理器使用域管理内存访问权限,它将虚拟内存区域划分为几个区域,为每个区域附于访问控制权限来进行保护和控制。

从手册中看到,L1页描述符中第[5:8]位标识所在的域(4位最大能表示16个域)。域访问控制寄存器CP15 C3为一个32位寄存器:

其中每一项Dx占两位(共16项),表示该域的权限,其含义如下:

ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)_第5张图片

代码中domain_val宏定义如下(arch/arm/include/asm/domian.h):

#define DOMAIN_KERNEL	0
#define DOMAIN_TABLE	0
#define DOMAIN_USER	1
#define DOMAIN_IO	2
......
#define DOMAIN_NOACCESS	0
#define DOMAIN_CLIENT	1
#ifdef CONFIG_CPU_USE_DOMAINS
#define DOMAIN_MANAGER	3
#else
#define DOMAIN_MANAGER	1
#endif
......
#define domain_val(dom,type)	((type) << (2*(dom)))

由于没有配置CPU_USE_DOMAINS,所以不使用Manager权限展开后r5和c3寄存器的值为

0x00000015,对应关系如下:

D0 —— DOMAIN_KERNEL\DOMAIN_TABLE—— DOMAIN_CLIENT

D1 —— DOMAIN_USER —— DOMAIN_CLIENT

D2 —— DOMAIN_IO —— DOMAIN_CLIENT

	mcr	p15, 0, r4, c2, c0, 0		@ load page table pointer
#endif
	b	__turn_mmu_on
ENDPROC(__enable_mmu)

接下来将r4中保存的页表物理地址写入C2的TTB中,其实这一步骤已经在__v6_setup中已经做过了,这里重复了一次,需要注意的是r4中页表物理地址的低5位已经被处理过了(页表地址必须32字节对齐),用于设置PD0和PD1等,C2寄存器如下:

在设置完TTB后,就要跳转到__turn_mmu_on开启MMU了。

ENTRY(__turn_mmu_on)
	mov	r0, r0
	instr_sync
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	instr_sync
	mov	r3, r3
	mov	r3, r13
	mov	pc, r3
__turn_mmu_on_end:

这里首先将前面配置过的r0寄存器写入C1寄存器中,如此MMU就被启动了,然后将前面保存在r13中的__mmap_switched函数的链接地址(虚拟地址)赋值到r3中,最后跳转到__mmap_switched函数执行。

注意:由于在执行461行代码(mcr  p15, 0, r0, c1, c0, 0)后MMU已经开启了,CPU在取指时已经采用虚拟地址,需经过页表的转换,但是此时的PC寄存器的值却还是按原来的顺序取指(例如在执行461行代码时,我的环境中PC的值为0x004333a0+8),也即如果不对__turn_mmu_on函数进行线性1:1映射的话,0x00XXXXXX处的地址无法解析,程序将无法继续运行。

/*
 * 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/dtb pointer
 *  r9  = processor ID
 */
	__INIT
__mmap_switched:
	adr	r3, __mmap_switched_data

	ldmia	r3!, {r4, r5, r6, r7}
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

在跳转到__mmap_switched后,页表建立完毕,MMU处于激活状态,将使用绝对地址执行,不再采用位置无关代码,所以从这里开始也就不需要再区分链接地址和实际的运行物理地址了。

首先将__mmap_switched_data的地址保存到r3中,然后将__data_loc、_sdata、__bss_start和_end变量的地址保存到r4、r5、r6和r7寄存器中。注意r3后面的叹号,r3的值会递增。

	.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
#ifdef CONFIG_CPU_CP15
	.long	cr_alignment			@ r7
#else
	.long	0				@ r7
#endif
	.long	init_thread_union + THREAD_START_SP @ sp
	.size	__mmap_switched_data, . - __mmap_switched_data

它们的地址值在我的环境中如下:

c05ab2ac <__mmap_switched_data>:
c05ab2ac:	c1080000 	mrsgt	r0, (UNDEF: 8)
c05ab2b0:	c1080000 	mrsgt	r0, (UNDEF: 8)
c05ab2b4:	c10bccec 	smlattgt	fp, ip, ip, ip
c05ab2b8:	c116c518 	tstgt	r6, r8, lsl r5

然后比较__data_loc和_sdata的值是否一致,若不一致则需要拷贝数据段。其中__data_loc是内核镜像中数据段的存储位置,在开启CONFIG_XIP_KERNEL后该值不等于_sdata值,在vmlinux.lds.S中定义如下:

#ifdef CONFIG_XIP_KERNEL
	__data_loc = ALIGN(4);		/* location in binary */
	. = PAGE_OFFSET + TEXT_OFFSET;
#else
	__init_end = .;
	. = ALIGN(THREAD_SIZE);
	__data_loc = .;
#endif

	.data : AT(__data_loc) {
		_data = .;		/* address in memory */
		_sdata = .;

而_sdata是数据段的链接位置。若开启CONFIG_XIP_KERNEL,则这两个值不等,所以需要将数据段拷贝到链接地址处(在RAM中)。在我的环境中,这两个值一致,不需要拷贝。

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

 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
	cmp	r7, #0
	bicne	r4, r0, #CR_A			@ Clear 'A' bit
	stmneia	r7, {r0, r4}			@ Save control register values
	b	start_kernel
ENDPROC(__mmap_switched)

接下来首先清空BSS段,然后将processor_id、__machine_arch_type、__atags_pointer、cr_alignment和init_thread_union + THREAD_START_SP值依次读取到r4、r5、r6、r7和sp中。其中processor_id、__machine_arch_type和__atags_pointer是定义在arch/arm/kerne/setup.c中的全局变量,分别用于保存处理器ID、bootloader传入的机器ID和启动参数地址。

然后依次将r9、r1和r2中保存的相应参数写入到这些全局变量中去,以便后面执行start_kernel之后的程序中使用到。

接着判断C7中的值是否为0,由于我的环境中已经配置了CONFIG_CPU_CP15,所以该值不为0,保存的是cr_alignment全局变量的地址。cr_alignment全局变量被定义在arch/arm/kernel/entry-armv.S中,和他一起使用的还有cr_no_alignment。他们分别被用来保存启用和禁用“A域”的CP15 C1寄存器值。

    

“A域”的作用是控制是否启用访问对齐检查,若开启,在未对齐的访问内存会发生data abort trap。程序中分别将启用A域和禁用A域的C1寄存器值保存到cr_alignment和cr_no_alignment全局变量中以备后续使用。


最后跳转到start_kernel函数进行进一步的初始化动作,内核启动的汇编部分到这里结束。最后总结一下这部分主要完成了以下初始化:

(1)验证处理器ID、内核启动参数等地址的有效性;

(2)创建初始页表,完成内核代码、启动参数和内核启动MMU代码这3部分内存映射;

(3)开启MMU并保存参数。


参考文献:1、《ARM Linux内核源码剖析》

                    2、《ARM11 数据手册》

你可能感兴趣的:(Linux,Kernel)