2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

为了降低启动代码的复杂性,进入linux内核的时候,MMU使关闭的。如果关闭MMU,意味着不能利用高速缓存的性能,那么内核使如何要打开MMU并使能数据高速缓存呢?

  • 在关闭MMU的情况下,处理器访问的地址都是物理地址。当MMU打开后,处理器访问地址就变成虚拟地址

  • 目前的处理器都是多级流水行架构,处理器会提前预取多条指令到流水线中。当打开MMU后,处理器之前预取的指令就会以虚拟地址来访问,到MMU查找对应的物理地址。

    因此,为了保证处理器在开启MMU后,能完成从物理地址到虚拟地址的平滑过渡,首先会创建VA和PA的相等映射,也就是恒等映射(identity mapping)。 建立恒等映射是小范围的,占用的空间通常是内核映像的大小,也就是几兆字节。

注:文章代码分析基于linux-4.9,架构基于aarch64(ARM64)。涉及页表代码分析部分,假设页表映射层级是4,即配置CONFIG_ARM64_PGTABLE_LEVELS=4。地址宽度是48,即配置CONFIG_ARM64_VA_BITS=48。

    /*
     * Setup the initial page tables. We only setup the barest amount which is
     * required to get the kernel running. The following sections are required:
     *   - identity mapping to enable the MMU (low address, TTBR0)
     *   - first few MB of the kernel linear mapping to jump to once the MMU has
     *     been enabled
     */
    __create_page_tables:
        mov    x28, lr                                             #把LR的值存放到X28

这里会建立两种section,分别完成identity mapping和kernel image mapping

    	/*
    	 * Invalidate the idmap and swapper page tables to avoid potential
    	 * dirty cache lines being evicted.
    	 */
    	adrp	x0, idmap_pg_dir                                   #加载idmap_pg_dir到x0
    	adrp	x1, swapper_pg_dir + SWAPPER_DIR_SIZE              #加载swapper_pg_dir到x1
    	bl	__inval_cache_range									   #使页表对应的高速缓存无效,为后面建立内核空间页表映射

这个主要是将identity mapping和kernel image mapping空间的页表对应的高速缓存无效掉,其主要的实现在arch/arm64/kernel/vmlinux.lds.S链接文件中

    #arch/arm64/kernel/vmlinux.lds.S
    	. = ALIGN(PAGE_SIZE);
    	idmap_pg_dir = .;
    	. += IDMAP_DIR_SIZE;
    	swapper_pg_dir = .;
    	. += SWAPPER_DIR_SIZE;

调用__inval_cache_range函数来使idmap_pg_dir和swapper_pg_dir + SWAPPER_DIR_SIZE区间内页表i对应的高速缓存无效i,也就使先清空idmap_pg_dir对应大小为IDMAP_DIR_SIZE和swapper_pg_dir对应大小为SWAPPER_DIR_SIZE页表的高速缓存。

  • idmap_pg_dir是identity mapping使用的页表,也就是物理地址和虚拟地址是相等的,主要是解决打开MMU后,从物理地址转换成虚拟地址,防止MMU开启后,无法获取页表
  • swapper_pg_dir是kernel image mapping初始阶段使用的页表,swapper_pg_dirLinux内核编译后,kernel image是需要进行映射的,包括text,data等各种。请注意,这里的内存是一段连续内存。也就是说页表(PGD/PUD/PMD)都是连在一起的,地址相差PAGE_SIZE(4k)
    	/*
    	 * 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

这段话的含义很明白了,将idmap_pg_dir赋值给x0,将swapper_pg_dir + SWAPPER_DIR_SIZE赋值给x6,然后将这段空间的页表的内容设置为0。

    	mov	x7, SWAPPER_MM_MMUFLAGS

SWAPPER_MM_MMUFLAGS宏描述了段映射的属性,它实现在arch/arm64/include/asm/kernel-pgtable.h头文件中

    #arch/arm64/include/asm/kernel-pgtable.h
    #define SWAPPER_MM_MMUFLAGS	(PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
    #define SWAPPER_PMD_FLAGS	(PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S)

其中定义了内存属性为普通内存MT_NORMAL,其属性含义如下:

  • PMD_TYPE_SECT表示一个块映射,

  • PMD_SECT_AF设置为块映射的访问权限,该Bit用来表示该页表是否第一次是使用,当程序访问对应的page或者section的时候,就会使用该entry,如果从来没有被访问过,那么其值等于0,否者等于1)。该bit主要被操作系统用来跟踪一个page是否被使用过(最近是否被访问),当该page首次被创建的时候,AF等于0,当代码第一次访问该page的时候,会产生MMU fault,这时候,异常处理函数应该设定AF等于1,从而阻止下一次访问该page的时候产生MMU Fault。

  • PMD_SECT_S表示块映射的共享属性

    | SH[1:0]|Normalmemory| | :-----: | :-----: | | 00 | Non-shareable | | 01 | 无效 | | 10 | outershareable | | 11 | innershareble |

    		/*
    	 * Create the identity mapping.
    	 */
    	adrp	x0, idmap_pg_dir                                                     -----(1)
    	adrp	x3, __idmap_text_start		                                         -----(2)
    
    #ifndef CONFIG_ARM64_VA_BITS_48                                                  -----(3)
    #define EXTRA_SHIFT	(PGDIR_SHIFT + PAGE_SHIFT - 3)                               -----(4)
    #define EXTRA_PTRS	(1 << (48 - EXTRA_SHIFT))                                    -----(5)
    
    	/*
    	 * If VA_BITS < 48, it may be too small to allow for an ID mapping to be
    	 * created that covers system RAM if that is located sufficiently high
    	 * in the physical address space. So for the ID map, use an extended
    	 * virtual range in that case, by configuring an additional translation
    	 * level.
    	 * First, we have to verify our assumption that the current value of
    	 * VA_BITS was chosen such that all translation levels are fully
    	 * utilised, and that lowering T0SZ will always result in an additional
    	 * translation level to be configured.
    	 */
    #if VA_BITS != EXTRA_SHIFT                                                      -----(6)
    #error "Mismatch between VA_BITS and page size/number of translation levels"
    #endif
    
    	/*
    	 * Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
    	 * entire ID map region can be mapped. As T0SZ == (64 - #bits used),
    	 * this number conveniently equals the number of leading zeroes in
    	 * the physical address of __idmap_text_end.
    	 */
    	adrp	x5, __idmap_text_end                                               -----(7)
    	clz	x5, x5                                                                 
    	cmp	x5, TCR_T0SZ(VA_BITS)	// default T0SZ small enough?                  -----(8)
    	b.ge	1f			// .. then skip additional level                       
    
    	adr_l	x6, idmap_t0sz                                                     -----(9)
    	str	x5, [x6]
    	dmb	sy
    	dc	ivac, x6		// Invalidate potentially stale cache line
    
    	create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6                 -----(10)
    1:
    #endif
    
    	create_pgd_entry x0, x3, x5, x6                                             -----(11)
    	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                                         -----(12)
    1. 将加载idmap_pg_dir的物理地址到x0寄存器,idmap_pg_dir使恒等映射的页表,其定义在vmlinux.lds.S链接文件中定义了
    	. = ALIGN(PAGE_SIZE);
    	idmap_pg_dir = .;
    	. += IDMAP_DIR_SIZE;
    	swapper_pg_dir = .;

这里分配给idmap_pg_dir的u页面大小为IDMAP_DIR_SIZE,而IDMAP_DIR_SIZE实现在arch/arm64/include/asm/kernel-pgtable.h头文件中,通常大小使3个连续的4K页面。

    #define IDMAP_DIR_SIZE		(IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
    #define IDMAP_PGTABLE_LEVELS (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
    #define PHYS_MASK_SHIFT (CONFIG_ARM64_PA_BITS)

这里的CONFIG_ARM64_PA_BITS配置的是48. 这里的含义是,计算采用section mapping的话,需要几个页来存放table。ARM64_HW_PGTABLE_LEVELS,很关键,根据配置的物理地址线的宽度计算需要的页面数,

    #define ARM64_HW_PGTABLE_LEVELS(va_bits) (((va_bits) - 4) / (PAGE_SHIFT - 3))

完成的公司转换成为

    ((((va_bits) - PAGE_SHIFT) + (PAGE_SHIFT - 3) - 1) / (PAGE_SHIFT - 3))

结合vmlinux.lds,上面的公式就是: ((48-12)+(12-3)-1) / (12-3) = (36+9-1)/9 = 44/9 = 4,最终IDMAP_DIR_SIZE为3个页面,即PGD/PUD/PMD页表,每一级页表占据一个页面。

    1. 主要是将__idmap_text_start放到x3中,idmap是kernel image中的一个段,其位于 arch/arm64/kernel/vmlinux.lds.S中,定义如下,即定义了一个以__idmap_text_start开始,__idmap_text_end结束的段,该段会被放在vmlinux的代码段中。
            . = ALIGN(SZ_4K);                               \
            VMLINUX_SYMBOL(__idmap_text_start) = .;         \
            *(.idmap.text)                                  \
            VMLINUX_SYMBOL(__idmap_text_end) = .;

除了开机启动时,打开MMU外,内核里还有很对场景需要恒等映射的,如唤醒处理其的函数cpu_do_resume

    1. 如果没有定义CONFIG_ARM64_VA_BITS_48,表示虚拟地址的宽度配置项,就需要使用create_table_entry。主要是解决要标识映射的物理地址超出VA_BITS覆盖范围的问题。此时基本上创建identity mapping是没有什么大问题的,但是,如果物理内存的地址位于非常高的位置,那么在进行identity mapping就有问题了,因为有可能你配置的VA_BITS不够大,超出了虚拟地址的范围。这时候,就需要扩展virtual address range了。当然,如果配置了48bits的VA_BITS就不存在这样的问题了,因为ARMv8最大支持的VA BITS就是48个,根本不可能扩展了。
    1. 在虚拟地址地址不是48 bit,而系统内存的物理地址又放到了非常非常高的位置,这时候,为了完成identity mapping,我们必须要扩展虚拟地址,那么扩展多少呢?扩展到48个bit。扩展之后,增加了一个EXTRA的level,地址映射关系是EXTRA—>PGD—>……,其中EXTRA_SHIFT等于(PGDIR_SHIFT + PAGE_SHIFT - 3)。
    1. 扩展之后,地址映射多个一个level,我们称之EXTRA level,该level的Translation table中有多少个entry呢?EXTRA_PTRS给出了答案。
    1. linux kernel中,对地址映射是有要求的,即要求PGD是满的。例如:48 bit的虚拟地址,4k的page size,对应的映射关系是PGD(9-bit)+PUD(9-bit)+PMD(9-bit)+PTE(9-bit)+page offset(12-bit),对于42bit的虚拟地址,64k的page size,对应的映射关系是PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。这两种例子有一个共同的特点就是PGD中的entry数目都是满的,也就是说索引到PGD的bit数目都是PAGE_SIZE-3。如果不满足这个关系,linux kernel会认为你的配置是有问题的。注意:这是内核的要求,实际上ARM64的硬件没有这么要求。

    正因为正确的配置下,PGD都是满的,因此扩展之后EXTRA_SHIFT一定是等于VA_BITS的,否则一定是你的配置有问题。我们延续上一个实例来说明如何扩展虚拟地址的bit数目。对于42bit的虚拟地址,64k的page size,扩展之后,虚拟地址是48个bit,地址映射关系是EXTRA(6-bit)+ PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。

    1. x5保存了__idmap_text_end的物理地址,之所以这么做是因为需要确定identity mapping的最高的物理地址,计算该物理地址的前导0有多少个,从而可以判断该地址是否是位于物理地址空间中比较高的位置。零计数指令CLZ,指令用于计算最高符号位与第一个1之间的0的个数
    1. 宏定义TCR_T0SZ可以计算给定虚拟地址数目下,前导0的个数。如果虚拟地址是48的话,那么前导0是16个。如果当前物理地址的前导0的个数(x5的值)还有小于当前配置虚拟地址的前导0的个数,那么就需要扩展。比较__idmap_text_end是否超过了VM_BITS所能达到的地址范围,如果没有,则跳转到标签1出,即11处,如果有,则跳转到9
    1. 创建extra translation table的entry。具体传递的参数如下
    • x0:页表地址idmap_pg_dir
    • x3:准备映射的虚拟地址(虽然x3保存的是物理地址,但是identity mapping嘛,VA和PA都是一样的)
    • EXTRA_SHIFT:正常建立最高level mapping的时候, shift是PGDIR_SHIFT,但是,由于物理地址位置太高,需要额外的映射,因此这里需要再加上一个level的mapping,因此shift需要PGDIR_SHIFT + (PAGE_SHIFT - 3)。
    • EXTRA_PTRS:增加了一个level的Translation table,我们需要确定这个增加level的Translation table中包含的描述符的数目,EXTRA_PTRS给出了这个参数。
    1. create_pgd_entry,建立各个中间level的table描述符,后面详细介绍,此外还将__idmap_text_start放到x5寄存器中,__idmap_text_end放到x6寄存器中
    1. 创建最后一个level translation table的entry。该entry可能是page descriptor,也可能是block descriptor,具体传递的参数如下
    • x0:指向最后一个level的translation table
    • x7:要创建映射的memory attribute
    • x3:物理地址
    • x5:虚拟地址的起始地址(其实和x3一样)
    • x6:虚拟地址的结束地址

1. create_table_entry

这个宏定义主要是用来创建一个中间level的translation table中的描述符。如果用linux的术语,就是创建PGD、PUD或者PMD的描述符。如果用ARM64术语,就是创建L0、L1或者L2的描述符。具体创建哪一个level的Translation table descriptor是由tbl参数指定的,tbl指向了该translation table的内存。

    	.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_table_entry宏自带6个参数,部分参数说明如下

  • tbl:指向了该translation table的内存,决定了具体创建哪一个level的Translation table descriptor
  • virt:要创建地址映射的那个虚拟地址
  • shift表示这一级页表的在虚拟地址中的偏移
  • ptr表示这一级页表是几位的
  • tmp1和tmp2是临时变量

其处理流程如下:

  1. tmp1中保存virt地址对应在Translation table中的entry index

  2. 初始阶段的页表(PGD/PUD/PMD/PTE)都是排列在一起的,每一个占用一个page。也就是说,如果create_table_entry当前操作的是PGD,那么tmp2这时候保存了下一个level的页表,也就是PUD了。

  3. 光有下一级translation table的地址不行,还要告知该描述符是否有效(bit 0),该描述符的类型是哪一种类型(bit 1)。对于中间level的页表,该描述符不可能是block entry,只能是table type的描述符,因此该描述符的最低两位是0b11。

    #define PMD_TYPE_TABLE (_AT(pmdval_t, 3) << 0)

  4. 把页表项内容放到指定的页表项当中,之所以有“lsl #3”操作,是因为一个描述符占据8个Byte

  5. 结束的时候tbl会加上一个PAGE_SIZE,也就是tbl变成了下一级页表的地址

2. create_pgd_entry

这个宏的作用并不仅仅是创建pgd,实际上该函数不仅仅创建PGD中的描述符,如果需要下一级的translation table,例如PUD、PMD,也需要同时建立,最终的要求是能够完成所有中间level的translation table的建立(其实每个table中都是只建立了一个描述符),仅仅留下PTE,由其他代码来完成。

    	.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

create_pgd_entry宏自带4个参数,部分参数说明如下

  • tbl:tbl是pgd translation table的地址
  • virt: 具体要创建哪一个地址的描述符由virt指定
  • tmp1和tmp2是临时变量

其处理流程如下:

  1. 这里通过create_table_entry调用该函数在PGD中为虚拟地址virt创建一个table type的描述符。
  2. SWAPPER_PGTABLE_LEVELS其实定义了swapper进程地址空间的页表的级数,可能3,也可能是2,具体中间的Translation table有多少个level是和配置相关的,如果是section mapping,那么中间level包括PGD和PUD就OK了,PMD是最后一个level。如果是page mapping,那么需要PGD、PUD和PMD这三个中间level,PTE是最后一个level。当然,如果整个page level是3或者2的时候,也有可能不存在PUD或者PMD这个level。
  3. 当SWAPPER_PGTABLE_LEVELS > 3的时候,需要创建PUD这一级的Translation table。
  4. 当SWAPPER_PGTABLE_LEVELS > 2的时候,需要创建PMD这一级的Translation table。
  • 当虚拟地址是48个bit,4k page size,这时候page level等于4,映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>Page table(L3)—>page

202306111244219771.png

但是如果采用了section mapping,映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>section。在create_pgd_entry函数中将创建PGD和PUD这两个中间level

202306111244232032.png

3. create_block_map

该函数就是在tbl指定的Translation table中建立block descriptor以便完成address mapping,以2M为大小进行映射。具体mapping的内容是将start 到 end这一段VA mapping到phys开始的PA上去,代码如下:

    	.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

create_pgd_entry宏自带5个参数,部分参数说明如下

  • tbl:tbl是pgd translation table的地址
  • flags:表示当前页表项指示的是block还是page
  • phys: 要映射的物理地址的起始地址
  • start和end分表表示物理地址要映射到的虚拟地址的开始和结束

其处理流程如下:

  • 前6行当中,phys和flag计算得到页表项的内容,通过start得到页表的index开始,通过end得到页表的计数。
  • 9999循环映射从phys开始的地址映射到start—end的区域

4. 如何创建页表

负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。前文提到identity mapping主要是打开MMU的过度阶段,因此对于identity mapping不需要映射整个kernel,只需要映射操作MMU代码相关的部分。首先我们在回头看看create_page_tables的流程

  • 对identity mapping和kernel mapping对应的区域invalid cache操作
  • identity和swapper的页表区域清零操作
  • 对identity区域进行映射,映射的过程就是调用create_pgd_entry和create_block_map的过程
  • 对kernel区域进行映射,映射的过程就是调用create_pgd_entry和create_block_map的过程

4.1 identity区域进行映射

202306111244242473.png

create_page_tables创建了一个恒等映射,把idmap.text段的虚拟地址映射到相同的物理地址上,这个映射的页表在idmap_pg_dir页表中,映射的起始地址为idmap_text_start,结束地址为idmap_text_end.

4.2 创建内核映像的页表

    	/*
    	 * 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
    	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
    
    	/*
    	 * 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
  • 加载swapper_pg_dir页表的基地址到x0寄存器

  • 加载内核映像即将要映射的虚拟地址为KIMAGE_VADDR + TEXT_OFFSET,即__va(_text)

  • 内核映像的虚拟地址要加上KASLR,x23寄存器存放了_PHYS_OFFSET的值

  • 内核映像的起始地址为_text,结束地址为_end,计算内环境映像借宿的虚拟地址

  • 调用create_block_map建立页表映射

    • x0:tbl是pgd translation table的地址
    • x7:表示当前页表项指示的是block还是page
    • x3: 要映射的物理地址的起始地址
    • x5和x6分表表示物理地址要映射到的虚拟地址的开始和结束
  • 使恒等映射页表idmap_pg_dir和内核态页表swapper_pg_dir对应的高速缓存无效,虽然目前还没有使能MMU,但是数据可能被提前取到高速缓存中,因此要清对应的高速缓存并使它无效

所以函数会创建两个映射:

  • 一段是identity mapping,其实就是把地址等于物理地址的那些虚拟地址mapping到物理地址上去,打开MMU相关的代码需要这样的mapping

  • kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、rodata、data、bss等等)进行映射了。

    具体的映射情况可以参考下图:

202306111244252304.png

5 开启MMU

    ENTRY(__enable_mmu)
    	mrs	x1, ID_AA64MMFR0_EL1
    	ubfx	x2, x1, #ID_AA64MMFR0_TGRAN_SHIFT, 4
    	cmp	x2, #ID_AA64MMFR0_TGRAN_SUPPORTED
    	b.ne	__no_granule_support
    	update_early_cpu_boot_status 0, x1, x2
    	adrp	x1, idmap_pg_dir
    	adrp	x2, swapper_pg_dir
    	msr	ttbr0_el1, x1			// load TTBR0
    	msr	ttbr1_el1, x2			// load TTBR1
    	isb
    	msr	sctlr_el1, x0
    	isb
    	/*
    	 * Invalidate the local I-cache so that any instructions fetched
    	 * speculatively from the PoC are discarded, since they may have
    	 * been dynamically patched at the PoU.
    	 */
    	ic	iallu
    	dsb	nsh
    	isb
    	ret
    ENDPROC(__enable_mmu)

__enable_mmu函数传递一个参数

  • x0:表示SCTLR_EL1的值

因为ARM64处理器有两个页表的地址寄存器,一个使TTBR0,另外一个使TTBR1,当虚拟地址的第63位为0时,选择TTBR0指向的页表;当虚拟地址的第63位为1时,,选择TTBR1指向的页表。

由于ARM64的Soc的处理器物理地址的起始地址是从0开始,物理内存大小不可能太大,所以在做恒等映射的时候,我们采用TTBR0来映射低256TB大小的地址空间,而把内核映像映射到内核空间时,采用TTBR1。该函数通过msr ttbr0_el1, x1和msr ttbr1_el1, x2来完成配置。

参考文档

ARM64 Kernel Image Mapping的变化

内存初始化(上)

阅读全文