Home Linux下的TTBR1_EL1寄存器使用探究
Post
Cancel

Linux下的TTBR1_EL1寄存器使用探究


本文分析基于Linux-5.10.100,架构基于ARM64 V8.3,假设页表映射层级为4,即CONFIG_ARM64_PGTABLE_LEVELS=4,地址宽度为48,即CONFIG_ARM64_VA_BITS=48。

在Linux系统中,通过MMU进行虚实地址转换时,会依赖于TTBR1_EL1和TTBR0_EL0两个寄存器。其中TTBR1_EL1指向内核地址空间的页表基地址,TTBR0_EL0指向用户地址空间的页表基地址。

内存映射.png

Linux中所有内核线程共享内核地址空间,因此TTBR1_EL1中的值为swapper_pg_dir, swapper_pg_dir在arm64/include/asm/pgtable.h中的定义为:

extern pgd_t swapper_pg_dir[PTRS_PER_PGD];

在使用有效虚拟地址长度为48位,四级页表映射(CONFIG_PGTABLE_LEVELS=4)的情况下,PTRS_PER_PGD为512,即pgd表项共512项。swapper_pg_dir挂载在内核线程共用的mm_struct init_mm中:

struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	MMAP_LOCK_INITIALIZER(init_mm)
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.arg_lock	=  __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	.user_ns	= &init_user_ns,
	.cpu_bitmap	= CPU_BITS_NONE,
	INIT_MM_CONTEXT(init_mm)
};

进程地址空间相互隔离,因此TTBR0_EL0中的值为每个进程独有的,在进程描述符task_struct中存在对内存地址空间的定义:

struct task_struct {
    ...
    struct mm_struct *mm;
    struct mm_struct *active_mm;
    ...
}

其中对于用户进程来说,mm和active_mm均指向其用户地址空间,而对于内核线程,mm为NULL,active_mm指向其借用的那个进程的进程地址空间。地址空间的页表基址定义在mm_struct中:

struct mm_struct {
  	...
  	pgd_t * pgd;
    ...
};

在进程切换时,将task_struct->mm->pgd加载到TTBR0_EL0中以实现对进程地址空间的切换。

TTBR1寄存器的变化

在内核启动过程中,首先实现的是内核的恒等映射,内核此时使用的是一级页表init_idmap_pg_dir,放在TTBR0寄存器中,TTBR1寄存器中保存的是空白页表reserved_pg_dir,将整个kernel、FDT以及预留的swap区域的虚拟地址连续映射到相等的物理地址处,此处的内核仅具备读、执行权限。

随后,将设置好的页表init_pg_dir放入TTBR1寄存器,将page walk使用的寄存器由TTBR0转换为TTBR1,代码开始在EL1区域内运行。

最后,通过paging_init实现页初始化,将swapper_pg_dir加载到TTBR1寄存器并且使用swapper_pg_dir替换init_mm的pgd:

1
2
3
4
5
6
7
void __init paging_init(void)
{
	...
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
	init_mm.pgd = swapper_pg_dir;
	...
}

Arm64支持ASID(Address Space ID),为每个进程分配一个ASID,标识自己的进程地址空间,以避免在进程切换时刷新TLB,性能。TLB entry的ASID来自于TTBRx_EL1寄存器,在TLB block在缓存的同时,将当前进程的ASID缓存到对应的TLB entry中。在进程运行的过程中,TTBR1寄存器的高位会加入当前进程的ASID的信息,验证代码如下:

static void switch_ttbr1() {
	unsigned long orig_ttbr1;

    asm volatile(
		"	mrs %0, ttbr1_el1\n"
		"	msr ttbr1_el1, %0\n"
		: "=r"(orig_ttbr1)
		: 
	);
	printk("ttbr1_el1: 0x%llx\n", orig_ttbr1);


	pgd_t *pgdp = lm_alias(swapper_pg_dir);
	phys_addr_t ttbr1 = phys_to_ttbr(virt_to_phys(pgdp));
	if (system_supports_cnp() && !WARN_ON(pgdp != lm_alias(swapper_pg_dir)))
		ttbr1 |= TTBR_CNP_BIT;

	unsigned long asid = ASID(current->active_mm);
	ttbr1 &= ~TTBR_ASID_MASK;
	ttbr1 |= FIELD_PREP(TTBR_ASID_MASK, asid);
	printk("ttbr1 with ASID: 0x%llx\n", ttbr1);

	write_sysreg(ttbr1, ttbr1_el1);
	isb();
	post_ttbr_update_workaround();

}

通过qemu启动内核,触发上述代码,通过dmesg查看得到:

image-20230817223605241.png

其中swapper_pg_dir所对应的物理地址为0x0000417e2001,而该内核线程对应进程的ASID为0x0344,二者或运算得到完整的TTBR1_EL1寄存器的值。

This post is licensed under CC BY 4.0 by the author.