内存映射第一步:idmap & swapper

内存管理之映射第一步:idmap & swapper

导读

衔接前文的概览篇,本文开始介绍完整的映射过程:

  1. lk 跳转到kernel image
  2. idmap与swapper的映射,提供mmu enable之后kernel image空间的访问需求;

本部分处理过程在汇编code中,主要介绍流程;

1. lk 跳转

仅关注跳转的最后一个步骤:

  1. 从emmc中load boot.img中kernel到0x800080000(物理地址);
  2. 定义一个指针变量theKernel,让其指向KERNEL_LOAD_ADDR_PHYS地址,即调用theKernel时,执行KERNEL_LOAD_ADDR_PHYS地址的内容;
  3. 调用theKernel,并传参数进去:FDT_LOAD_ADDR_PHYS;
/* jump to kernel */
static void boot_linux_from_emmc(const struct app_descriptor *app, void *args)
{
    load_image_from_bootimg(KERNEL_LOAD_ADDR, BOOTIMG_IMAGEGZ);//将kernel执行code放到指定位置,虚拟地址为0xFFFF FFF8 0008 0000,对应物理地址为0x800080000;
    cleanup_before_linux();

    void    (*theKernel)(unsigned long fdtAddr);
    theKernel = (void (*)(unsigned long))(KERNEL_LOAD_ADDR_PHYS);//指向地址为物理地址0x800080000
    theKernel (FDT_LOAD_ADDR_PHYS);//参数为:0x800000000
}

//经过简化后的函数:
void load_image_from_bootimg(uintptr_t load_addr, IMAGE_TYPE_E image_type)
{
    unsigned long src_addr = 0;
    unsigned long size     = 0;
	...
	src_addr = BOOT_LOAD_ADDR + g_kernel_start_offset;//在lk中load到mem中的boot.img,需要其中kernel空间位置的起始
	size = g_kernel_size;
	...
    memcpy((void *)load_addr, (void *)src_addr, size);//拷贝到load_addr,即上文传入的KERNEL_LOAD_ADDR,对应物理地址为0x800080000
    flush_cache(load_addr, size);
}

即上述首先load image到指定地址,然后跳转到该地址执行,也就是Kernel 的入口位置head.S中stext;

1.1 一个小疑问

我们查看kernel中各个段的地址,一般通过如下两种方式:

  1. System.map 查看地址
  2. objdump -S vmlinux > vm.S 查看地址
  3. vmlinux.lds.S 查看SECTIONS地址
    通过上述三种方式查看到的head.text地址均为0xFFFF FFF8 0808 0000,与lk跳转地址有128M的偏移;
    这里理解错误的点在于:这里看到的地址为kernel 映射完成后的虚拟地址,而lk跳转的时候,是执行的物理地址(ps,在lk的虚拟地址为0xFFFF FFF8 0008 0000)
    所以其实不是一回事,只要记住lk跳转时为物理地址即可,与后续的虚拟地址无关!!!
    内存映射第一步:idmap & swapper_第1张图片

2. Kernel 入口

入口在head.S的ENTRY(stext) 这里:

ENTRY(stext)
	bl	preserve_boot_args
	bl	el2_setup			// Drop to EL1, w0=cpu_boot_mode
	adrp	x23, __PHYS_OFFSET
	and	x23, x23, MIN_KIMG_ALIGN - 1	// KASLR offset, defaults to 0
	bl	set_cpu_boot_mode_flag
	bl	__create_page_tables
	/*
	 * 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.
	 */
	bl	__cpu_setup			// initialise processor
	b	__primary_switch
ENDPROC(stext)

处理如下:

  1. preserve_boot_args,处理传入的cmdline,即bootarg
  2. el2_setup,处理在el2启动的情况?这里的话应该是hypervisor的情况;
  3. set_cpu_boot_mode_flag,这里用来做两件事情:
    1. 判断preloader是否有成功设置boot mode;
    2. 区分普通mode和hyperversion mode
  4. __create_page_tables:
    1. identity mapping to enable the MMU:idmap
    2. first few MB of the kernel linear mapping to jump to once the MMU has been enabled:swapper
    3. kernel-pgtable.h中描述了页表配置的大小情况,这里保留的size要比实际页面需要大些,实际映射需要;

则这里我们主要关注的部分就在__create_page_tables:

3. 映射过程

映射部分这里是用汇编来实现的,本部分主要关注:

  1. 映射层级、页表粒度等是如何实现的?
  2. 映射过程是咋玩的?

3.1 页表划分

通过zcat /proc/config.gz | grep XXX 命令确认平台kernel中的配置情况,比查看config文件更快一些:

3.1.1 VA范围

CONFIG_ARM64_VA_BITS_39=y 决定了VA的地址范围
在linux文档中可以很清晰的看到上述配置如何影响地址范围:
内存映射第一步:idmap & swapper_第2张图片

3.1.2 页表层级

CONFIG_ARM64_4K_PAGES=y 决定了页面配置大小
CONFIG_PGTABLE_LEVELS=3 决定了level级别
页表level和page size决策页表的划分:
内存映射第一步:idmap & swapper_第3张图片

3.1.3 SECTION_MAPS

经过上述两个小节的描述,对于则页表的内容相对比较清晰了,具体来看宏转换:

  1. 根据page粒度,决策是否支持SECTION_MAPS

CONFIG_ARM64_4K_PAGES=y ==> #define ARM64_SWAPPER_USES_SECTION_MAPS 1
CONFIG_PGTABLE_LEVELS=3
这里首先来理解 SECTION_MAPS 从code来看,只有4K页表支持它,那他与其他的映射之间的差异在哪里呢?
内存映射第一步:idmap & swapper_第4张图片

也就是说最小粒度按照section来映射,最低的两个level 共21位,也就是2M大小,更直白的描述就是L2的每个地址直接指向一个2M大小的region,而不需要先找到L3,再指向一个4K的region;

对于kernel image这样的big block memory region,使用4K的page来mapping有点得不偿失,在这种情况下,可以考虑让level 2的Translation table entry指向一个2M 的memory region,而不是下一级的Translation table

  1. SECTION_SIZE转换

所有这些复杂的宏的计算,实质都是为了满足我们上述页表划分的需求,即实现过程:

  1. 计算某一个level的偏移宏:ARM64_HW_PGTABLE_LEVEL_SHIFT
    1. We map (PAGE_SHIFT - 3) at all translation levels and PAGE_SHIFT bits in the final page.
    2. The maximum number of translation levels supported by the architecture is 4.
    3. starting at at level n, we have further ((4 - n) - 1) levels of translation excluding the offset within the page.
  2. 计算SECTION_SIZE 即计算PMD的偏移

具体转换:

#define SECTION_SHIFT               PMD_SHIFT
#define SECTION_SIZE          (_AC(1, UL) << SECTION_SHIFT) //即size为pmd_shift

#if CONFIG_PGTABLE_LEVELS > 2 // levels 配置为3
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2) // 即计算第PMD作为level 2 的 shift
#define PMD_SIZE		(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PTRS_PER_PMD		PTRS_PER_PTE
#endif

/*
 *  ((4 - n) - 1) * (PAGE_SHIFT - 3) + PAGE_SHIFT
 *
 * Rearranging it a bit we get :
 *   (4 - n) * (PAGE_SHIFT - 3) + 3
 */
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n)	((PAGE_SHIFT - 3) * (4 - (n)) + 3) //这里计算出来为21,所以section size就是2M

3.1.4 计算idmap和swapper的页面大小

#define SWAPPER_DIR_SIZE (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE) //这里是8K
#define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE) // 这里是8K

可以看到都是通过level * page的处理,page size我们知道是4K,那么需要计算的就是level

#define SWAPPER_PGTABLE_LEVELS (CONFIG_PGTABLE_LEVELS - 1) //这里是因为采用4K大小的界面,所以采用了 section 的region,则只需要两个page

则SWAPPER_DIR_SIZE 可以固定到是8K

#define IDMAP_PGTABLE_LEVELS (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1) //这里是因为采用4K大小的界面,所以采用了 section 的region

关于ARM64_HW_PGTABLE_LEVELS,是通过传入的VA bit来计算所需要的level,即translate level:

  1. We resolve the top (va_bits - PAGE_SHIFT) bits with (PAGE_SHIFT - 3) bits at each page table level.
  2. Hence: levels = DIV_ROUND_UP((va_bits - PAGE_SHIFT), (PAGE_SHIFT - 3)) where DIV_ROUND_UP(n, d) => (((n) + (d) - 1) / (d))
  3. we open code DIV_ROUND_UP here: ((((va_bits) - PAGE_SHIFT) + (PAGE_SHIFT - 3) - 1) / (PAGE_SHIFT - 3))

公式转化后即宏的定义:

#define ARM64_HW_PGTABLE_LEVELS(va_bits) (((va_bits) - 4) / (PAGE_SHIFT - 3)) //PAGE_SHIFT 12 44/9 = 4

所以这里问题来了,传入的PHYS_MASK_SHIFT应该是多少呢?在定义中为48,根据头文件链接,然后实际va_bits应该为39,这里就有问题了,这里为3?

3.2 转换过程

过程描述的话比较简单:

  1. 获取需要映射的地址范围;
  2. 对该部分地址清0;
  3. 映射idmap和swapper:建立中间level映射,创建block description
//注释这里说的很明确,会做两个映射:identity mapping  && swapper mapping
__create_page_tables:
	mov	x28, lr

	/*
	 * Invalidate the idmap and swapper page tables to avoid potential
	 * dirty cache lines being evicted.
	 */
	adrp	x0, idmap_pg_dir //获取idmap页表基地址,这个是在vmlinux.lds.Szhong定义的
	adrp	x1, swapper_pg_dir + SWAPPER_DIR_SIZE // 获取页表末尾地址
	bl	__inval_cache_range //将对应段的cacheline设置为无效,为啥?清除掉数据?

	/*
	 * Clear the idmap and swapper page tables.即清零操作,所有描述符均无效
	 */
	adrp	x0, idmap_pg_dir //开始地址
	adrp	x6, swapper_pg_dir + SWAPPER_DIR_SIZE //结束地址
1:	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	cmp	x0, x6
	b.lo	1b

	mov	x7, SWAPPER_MM_MMUFLAGS

	/*
	 * Create the identity mapping.
	 */
	adrp	x0, idmap_pg_dir
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start)

#ifndef CONFIG_ARM64_VA_BITS_48
#define EXTRA_SHIFT	(PGDIR_SHIFT + PAGE_SHIFT - 3) //这里算出来是39
#define EXTRA_PTRS	(1 << (48 - EXTRA_SHIFT))  // 1 << 9

	adrp	x5, __idmap_text_end
	clz	x5, x5
	cmp	x5, TCR_T0SZ(VA_BITS)	// default T0SZ small enough?
	b.ge	1f			// .. then skip additional level

	adr_l	x6, idmap_t0sz
	str	x5, [x6]
	dmb	sy
	dc	ivac, x6		// Invalidate potentially stale cache line

	create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6  //创建一个中间level的translation table中的描述符;
1:
#endif

	create_pgd_entry x0, x3, x5, x6  //创建一个PGD的描述符并创建下一级translation table,完成所有中间level的translation table的创建
	mov	x5, x3				// __pa(__idmap_text_start)
	adr_l	x6, __idmap_text_end		// __pa(__idmap_text_end)
	create_block_map x0, x7, x3, x5, x6  //该函数就是在tbl指定的Translation table中建立block descriptor以便完成address mapping


	/*
	 * Map the kernel image (starting with PHYS_OFFSET).
	 */
	adrp	x0, swapper_pg_dir
	mov_q	x5, KIMAGE_VADDR + TEXT_OFFSET	// compile time __va(_text)
	add	x5, x5, x23			// add KASLR displacement
	create_pgd_entry x0, x5, x3, x6  //建立中间level
	adrp	x6, _end			// runtime __pa(_end)
	adrp	x3, _text			// runtime __pa(_text)
	sub	x6, x6, x3			// _end - _text
	add	x6, x6, x5			// runtime __va(_end)
	create_block_map x0, x7, x3, x5, x6 //建立block descriptor

	/*
	 * Since the page tables have been populated with non-cacheable
	 * accesses (MMU disabled), invalidate the idmap and swapper page
	 * tables again to remove any speculatively loaded cache lines.
	 */
	adrp	x0, idmap_pg_dir
	adrp	x1, swapper_pg_dir + SWAPPER_DIR_SIZE
	dmb	sy
	bl	__inval_cache_range

	ret	x28
ENDPROC(__create_page_tables)
	.ltorg

经过这段汇编,映射就建立完成了,建立映射的意义在于,我们可以执行mmu enable那段code了,他就在proc.S中
内存映射第一步:idmap & swapper_第5张图片

对应system.map
内存映射第一步:idmap & swapper_第6张图片

4. 总结

待补充:

  1. 源码目录
  2. 上述具体几个创建宏的的定义

更新自2020.08.13

目录 描述
./bootable/lk/app/xxx_boot/boot_kernel.c lk跳转kernel部分处理
./bootable/lk/platform/xxx/include/platform/upgrade.h 相关跳转地址部分定义
./bootable/lk/platform/xxx/bootimg.c img处理相关函数
./kernel-4.9/arch/arm64/kernel/head.S kernel 入口文件
./kernel-4.9/arch/arm64/kernel/proc.S setup_cpu enable mmu部分处理
./kernel-4.9/arch/arm64/kernel/vmlinux.lds.S 链接定义,可以看到各段的VA地址
./kernel-4.9/arch/arm64/include/asm/kernel-pgtable.h page table 相关宏的定义
./kernel-4.9/arch/arm64/include/asm/pgtable-hwdef.h page table HW相关宏定义
./kernel-4.9/Documentation/arm64/memory.txt 内存相关结构示意
./out/target/product/xxx/obj/KERNEL_OBJ/System.map kernel各个函数的内存地址
./out/target/product/xxx/obj/KERNEL_OBJ/vmlinux kernel带符号的文件
  1. 上述具体几个创建宏的的定义
/*
 * Macro to create a table entry to the next page.
 *
 *	tbl:	page table address
 *	virt:	virtual address
 *	shift:	#imm page table shift
 *	ptrs:	#imm pointers per table page
 *
 * Preserves:	virt
 * Corrupts:	tmp1, tmp2
 * Returns:	tbl -> next level table page address
 */
	.macro	create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
	lsr	\tmp1, \virt, #\shift
	and	\tmp1, \tmp1, #\ptrs - 1	// table index
	add	\tmp2, \tbl, #PAGE_SIZE
	orr	\tmp2, \tmp2, #PMD_TYPE_TABLE	// address of next table and entry type
	str	\tmp2, [\tbl, \tmp1, lsl #3]
	add	\tbl, \tbl, #PAGE_SIZE		// next level table page
	.endm

create_pgd_entry code中有描述,即创建各个level的translate page,实质通过create_table_entry创建page

/*
 * Macro to populate the PGD (and possibily PUD) for the corresponding
 * block entry in the next level (tbl) for the given virtual address.
 *
 * Preserves:	tbl, next, virt
 * Corrupts:	tmp1, tmp2
 */
	.macro	create_pgd_entry, tbl, virt, tmp1, tmp2
	create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
#if SWAPPER_PGTABLE_LEVELS > 3
	create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2
#endif
#if SWAPPER_PGTABLE_LEVELS > 2
	create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
	.endm

映射实际block region对应的物理地址;

/*
 * Macro to populate block entries in the page table for the start..end
 * virtual range (inclusive).
 *
 * Preserves:	tbl, flags
 * Corrupts:	phys, start, end, pstate
 */
	.macro	create_block_map, tbl, flags, phys, start, end
	lsr	\phys, \phys, #SWAPPER_BLOCK_SHIFT
	lsr	\start, \start, #SWAPPER_BLOCK_SHIFT
	and	\start, \start, #PTRS_PER_PTE - 1	// table index
	orr	\phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT	// table entry
	lsr	\end, \end, #SWAPPER_BLOCK_SHIFT
	and	\end, \end, #PTRS_PER_PTE - 1		// table end index
9999:	str	\phys, [\tbl, \start, lsl #3]		// store the entry
	add	\start, \start, #1			// next entry
	add	\phys, \phys, #SWAPPER_BLOCK_SIZE		// next block
	cmp	\start, \end
	b.ls	9999b
	.endm

你可能感兴趣的:(#,内存整理,linux,内存管理)