ARM64内存管理一:启动简介

在bootloader将控制权交给linux kernel前,需要完成下面几个动作

  • 初始化系统中ram,并将ram信息告知kernel

  • 准备好device tree blob, 并将首地址写到x0寄存器

  • 解压内核(option)

  • MMU=off, D-cache=off

介绍几个关于内核位置的宏,在启动过程中,经常会用到

  • #define __PHYS_OFFSET (KERNEL_START - TEXT_OFFSET) : kernel space 首地址

  • KERNEL_START: _text=PAGE_OFFSET + TEXT_OFFSET : kernel开始运行的虚拟地址(内核正文段开始的虚拟地址)

  • TEXT_OFFSET : 内核正文地址距离kernel space首地址的偏移量

    1. 对于arm32,该空间一般为32kb(0x8000),用于保存内核页表(process id = 0的pgd), 以及bootloader和kernel间参数传递

    2. 对于arm64, 该空间一般为512kb(0x80000)

  • PAGE_OFFSET : 目前看和__PHYS_OFFSET一样,均表示kernel space首地址

启动代码分析

常见arm指令

  • ldr: ldr r0, [r1] r0=* r1;将r1指向的内容存入r0
    1. ldr r0 0x12345678 : 把0x12345678这个地址中的值存放到r0中, r0=*(0x12345678)
    2. ldr r0, =0x12345678: 此时ldr是个伪指令,功能类似mov,r0=0x12345678
  • str: str r0 [r1, #4] * (r1+4) = r0;将r0中内容存入r1+4中的地址
    1. .macro str_l, src, sym, tmp
    2. adrp \tmp \sym
    3. str \src, [\tmp, :lo12:\sym]
  • str: str r0 [r1] , #4 *r1=r0; r1=r1+4; 将r0中内容存入r1中的地址,并将新地址r1+8存入r1

  • adr: 小范围地址读取指令,将基于pc相对偏移的地址值读取到寄存器中(距离pc 1m以内)
    1. 原理:将符号的21位偏移,加上pc,结果写入到寄存器中
  • adr_l: 将符号地址转变为运行时地址(通过pc relative offset形式)
    1. .macro ldr_l, dst, sym
    2. adrp \dst, \sym
    3. ldr \dst, [\dst, :lo12:\sym]
  • adrp: 以页为单位的大范围地址读取指令,通过该指令可以获得符号的物理地址,当然是page对齐的

  • mrs: 处理器模式切换指令,从指定寄存器读取到临时寄存器

  • msr: 写模式到指定寄存器

  • bic: bic x0, x0, #0xF000 0000 0000 0000: 位清除,将x0的高4位清除

  • rsb: rsb x0, x0, #123: x0=1280-x0

  • ldp,stp: load/store pair; ldp x8, x2, [x0, #0x10] : x0 += 0x10, x8 = *(x0), x2 = *(x0 + 0x8); stp x9, x8, [x4] : *(x4) = x9, *(x4 + 8) = x9;

  • b,bl,blx,bx: 分别为跳转指令, 带返回值的跳转指令, 带返回和状态切换的跳转指令, 带状态切换的跳转指令

head.s中入口函数stext

ENTRY(stext)
	bl	preserve_boot_args  //(1)根据arm64规范,保存x0-x3寄存器;在setup_arch函数中会访问boot_args进行校验
	bl	el2_setup			// Drop to EL1, w20=cpu_boot_mode, (2)判断启动模式为EL1或是EL2,EL2表示支持虚拟化,KVM模块可以顺利启动
	adrp	x24, __PHYS_OFFSET      //将__PHYS_OFFSET保存到x24中
	bl	set_cpu_boot_mode_flag      //见下一篇解释

	bl	__vet_fdt                   //对bootloader传递给kernel的fdt参数进行校验;1. 是否8字节对齐;2. 是否在kernel space的前512M内
	bl	__create_page_tables		// x25=TTBR0, x26=TTBR1, 见下一篇解释
	/*
	 * The following calls CPU setup code, see arch/arm64/mm/proc.S for
	 * details.
	 * On return, the CPU will be ready for the MMU to be turned on and
	 * the TCR will have been set.
	 */
	ldr	x27, =__mmap_switched		// address to jump to after
						// MMU has been enabled
	adr_l	lr, __enable_mmu		// return (PIC) address
	b	__cpu_setup			// initialise processor
ENDPROC(stext)

preserve_boot_args

/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 * 保存x0-x3寄存器,是为了符合arm64规范:x0必须为dtb物理地址,x1-x3必须为0
 * 在setup_arch函数中会访问boot_args并进行校验
 */
preserve_boot_args:
	mov	x21, x0				// x21=FDT, 将dtb的地址暂存在x21中

	adr_l	x0, boot_args			// record the contents of, 将boot_args首地址保存在x0中
	stp	x21, x1, [x0]			// x0 .. x3 at kernel entry, 将x21,x1分别保存在boot_args[0], boot_args[1]
	stp	x2, x3, [x0, #16]        //将x2,x3分别保存在boot_args[2], boot_args[3]

	dmb	sy				// needed before dc ivac with
						// MMU off

	add	x1, x0, #0x20			// 4 x 8 bytes, 将boot_args非参数首地址赋给x1,即x0是boot_args这段memory的首地址,x1是末尾的地址
	b	__inval_cache_range		// tail call, 将x0, x1地址段对应的cacheline设定为无效
ENDPROC(preserve_boot_args)

el2_setup

/*
 * end early head section, begin head code that is also used for
 * hotplug and needs to have the same protections as the text region
 */
	.section ".text","ax"
/*
 * If we're fortunate enough to boot at EL2, ensure that the world is
 * sane before dropping to EL1.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x20 if
 * booted in EL1 or EL2 respectively.
 * 判断cpu模式,只拿部分为例
 */
ENTRY(el2_setup)
	mrs	x0, CurrentEL               
	cmp	x0, #CurrentEL_EL2          //判断是否处于EL2
	b.ne	1f                      //如果不是,则跳转到1f
	mrs	x0, sctlr_el2               //从sctlr_el2读取模式到寄存器
CPU_BE(	orr	x0, x0, #(1 << 25)	)	// Set the EE bit for EL2
CPU_LE(	bic	x0, x0, #(1 << 25)	)	// Clear the EE bit for EL2
	msr	sctlr_el2, x0
	b	2f
1:	mrs	x0, sctlr_el1               //执行到这,说明为EL1模式
CPU_BE(	orr	x0, x0, #(3 << 24)	)	// Set the EE and E0E bits for EL1
CPU_LE(	bic	x0, x0, #(3 << 24)	)	// Clear the EE and E0E bits for EL1
	msr	sctlr_el1, x0
	mov	w20, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret
.........
ENDPROC(el2_setup)

set_cpu_boot_mode_flag

/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in x20. See arch/arm64/include/asm/virt.h for more info.
 * 执行完el2_setup后,w20保存了exception level
 * 分析__boot_cpu_mode的定义,可以有如下结论:
 * 1. 如果cpu启动的时候是EL1 mode,会修改变量__boot_cpu_mode A域,将其修改为BOOT_CPU_MODE_EL1
 * 2. 如果cpu启动的时候是EL2 mode,会修改变量__boot_cpu_mode B域,将其修改为BOOT_CPU_MODE_EL2
 * ENTRY(__boot_cpu_mode)
 *    .long    BOOT_CPU_MODE_EL2--------A
 *    .long    BOOT_CPU_MODE_EL1--------B
 */
ENTRY(set_cpu_boot_mode_flag)
	adr_l	x1, __boot_cpu_mode
	cmp	w20, #BOOT_CPU_MODE_EL2
	b.ne	1f                              
	add	x1, x1, #4                      //el1模式走这里,修改x1的地址为B域
1:	str	w20, [x1]			// This CPU has booted in EL1, *x1=w20;
	dmb	sy
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret
ENDPROC(set_cpu_boot_mode_flag)

__vet_fdt

/*
 * Determine validity of the x21 FDT pointer.
 * The dtb must be 8-byte aligned and live in the first 512M of memory.
 * 见document/boot.txt要求
 * 在512m内是为了在保证kernel space的首地址和fdt首地址在一个pud entry中
 */
__vet_fdt:
	tst	x21, #0x7    //校验x21是否为8字节对齐
	b.ne	1f
	cmp	x21, x24     //是否在小于kernel space的首地址
	b.lt	1f
	mov	x0, #(1 << 29)
	add	x0, x0, x24
	cmp	x21, x0
	b.ge	1f       //是否大于kernel space首地址+512M
	ret
1:
	mov	x21, #0      //fdt地址有误,清零
	ret
ENDPROC(__vet_fdt)

参考文档:

  1. Documentation/arm64/booting.txt
  2. http://www.wowotech.net/armv8a_arch/arm64_initialize_1.html
  3. https://www.jianshu.com/p/4b68f45065c6