[内核内存] [arm64] 内存初始化1---汇编阶段

linux内核版本:linux4.9.115(arm64)

在linux kernel(64位)执行第一条指令前,此时内存的状况如下图:

1.内存出示化前物理地址空间布局标题

BootLoader通过自己的方式了解当前物理内存的布局情况,将image和dtb,拷贝到特定的位置。一般kernel image位于主存首地址偏移TEXT-OFFSET的位置(并非绝对)。Dtb位置由boot自己决定,此时MMU是关闭状。当从boot跳转到linux kernel后,linux kernel需要完全掌握内存系统的控制权。但此时linux kernel对系统的内存布局情况一无所知,下面主要介绍内核是如何一步步的接管并控制内存系统的。

uBoot跳转到linux kernel的时候MMU是关闭的。内核在boot阶段需要开启MMU,开启前需要初始化页表。因此内核启动代码一个重要的功能是设置好相应的页表并开启MMU。

创建启动页表

2.linux初期页表创建流程图标题

在启动初始化的汇编阶段负责创建页表是__create_page_tables。此过程的调用流程如图2所示,而该函数主要负责identity mapping和kernel image map两个工作:

  • identity mapping:是指把idmap_text区域的物理地址映射到相等的虚拟地址上,这种映射完成后,其虚拟地址等于物理地址。idmap_text区域都是一些打开MMU相关的代码.
  • kernel image map:将kernel运行需要的地址(kernel txt、rodata、data、bss等等)进行    映射。

__create_page_tables函数调用流程:

ENTRY(stext):

1.//./arch/arm64/kernel/head.S
2.ENTRY(stext)  //arm64的内核入口
3.    //传递并保持boot参数(dtb地址等)
4.    bl  preserve_boot_args  
5.    bl  el2_setup           // Drop to EL1, w0=cpu_boot_mode  
6.    adrp    x23, __PHYS_OFFSET  
7.    and x23, x23, MIN_KIMG_ALIGN - 1    // KASLR offset, defaults to 0  
8.    bl  set_cpu_boot_mode_flag
9.	/*运行到此处,相关寄存器保存了系统相关的数据信息:cpu_table,FDT地址,__phy_offset 
10.   * 等.
11.   */ 	        
12.    bl  __create_page_tables  
13.    /* 
14.     * The following calls CPU setup code, see arch/arm64/mm/proc.S for 
15.     * details. 
16.     * On return, the CPU will be ready for the MMU to be turned on and 
17.     * the TCR will have been set. 
18.     */  
19.    bl  __cpu_setup         // initialise processor  
20.    b   __primary_switch  
21.ENDPROC(stext)  

在内核初始化时,kernel放在PAGE_OFFSET+TEXT_OFFSET的位置。而idmap_pg_dir和swapper_pg_dir这两张表固定紧挨着内核末尾处。如图3所示。对于初始化转换表在物理内存中的位置分布定义在链接脚本文件中(arch/arm64/kernel/vmlinux.lds.S)代码如下:

1.. = ALIGN(PAGE_SIZE);  
2.idmap_pg_dir = .;  
3.. += IDMAP_DIR_SIZE;  
4.swapper_pg_dir = .;  
5.. += SWAPPER_DIR_SIZE;  

对于arm32架构,kernel image在RAM开始位置的32K的内存中保存了bootliader到kernel传递的tag参数以及内核的页表。而arm64架构中,内核初始化页表被放到了kernel image的bss段后面。且由于64位系统的虚拟地址空间变大,因此需要更多的页来完成启动阶段页表的构建存放工作。根据64位系统虚拟地址空间布局可知,内核空间的地址大小为256T,地址的低48位被分成9+9+9+9+12.因此PGD(L0),PUD(l1),PMD(L2), PTE(L3)这3个转换表都有512个entry,由于每个entry都是8byte。因此每个转换表恰好占一个page,及4K(虚拟地址的位数可自己通过内核配置进行设置,arm64常见的还有38位虚拟地址3级页表4k页面的虚拟地址布局方式)。

在arm64架构系统下,根据链接脚本中定义可知内核分别为idmap translation tables和swapper page translation tables 保留了3个page。3个page分别是3个level的转换表。一般48位的虚拟地址需要4阶的转换表,此处却能保存3个转换表,究其原因是PMD转换表中每个entry的内容不是下一级表地址描述符,而是基于2M的block的映射。因为在内核启动初期内核代码的页表映射是按照block的方式来进行映射的,一个基础内存单位是2M而不是4k(可参考MEMBLOCK内存管理)。

3.内核初始阶段内存映射图

__create_page_tables:

1.__create_page_tables:  
2.    //将lr保存到x28寄存器中
3.    mov x28, lr
4.  
5. /*
6. 	 *这里将(idmap_pg_dir, swapper_pg_end)这段物理地址范围对应的dcache进行
7.  * invalidate。这里的idmap_pg_dir和swapper_pg_end是在vmlinux.lds.S中设置的
8.  */ 
9.    //获取idmap的页表基地址(物理地址)
10.    adrp    x0, idmap_pg_dir  
11.    //获取内核空间页表的尾地址, RESERVED_TTBR0_SIZE值含义还不清楚
12.    adrp    x1, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE  
13.    bl  __inval_cache_range  
14.     
15.    adrp    x0, idmap_pg_dir  
16.    adrp    x6, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE
17.    //循环将一致性转换页表和swapper转换页表所在区域清零  
18.1:  stp xzr, xzr, [x0], #16  
19.    stp xzr, xzr, [x0], #16  
20.    stp xzr, xzr, [x0], #16  
21.    stp xzr, xzr, [x0], #16  
22.    cmp x0, x6  
23.    //如果x0<x6,这jmp 1
24.    b.lo    1b  
25.    //页表属性
26.    mov x7, SWAPPER_MM_MMUFLAGS  
27.  
28.    /* 
29.     * Create the identity mapping. 
30.     */  
31.    adrp    x0, idmap_pg_dir  
32.    adrp    x3, __idmap_text_start      // __pa(__idmap_text_start)  
33.
34./*该#ifndef...#endif为特殊情况的检查判断可跳过*/
35.//PGDIR_SHIFT=39  PAGE_SHIFT=12
36.#ifndef CONFIG_ARM64_VA_BITS_48  
37.#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)  
38.#define EXTRA_PTRS  (1 << (48 - EXTRA_SHIFT))  
39.#if VA_BITS != EXTRA_SHIFT  
40.#error "Mismatch between VA_BITS and page size/number of translation levels"  
41.#endif  
42.  
43.    adrp    x5, __idmap_text_end  
44.    clz x5, x5  
45.    cmp x5, TCR_T0SZ(VA_BITS)   // default T0SZ small enough?  
46.    b.ge    1f          // .. then skip additional level  
47.  
48.    adr_l   x6, idmap_t0sz  
49.    str x5, [x6]  
50.    dmb sy  
51.    dc  ivac, x6        // Invalidate potentially stale cache line  
52.  
53.    create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6  
54.1:  
55.#endif  
56.    //创建PGD,PUD转换表并填充对应的entry
57.    create_pgd_entry x0, x3, x5, x6  
58.    mov x5, x3              // __pa(__idmap_text_start)  
59.    adr_l   x6, __idmap_text_end        // __pa(__idmap_text_end)  
60.    create_block_map x0, x7, x3, x5, x6  
61.  
62.    /* 
63.     * Map the kernel image (starting with PHYS_OFFSET). 
64.     */  
65.    adrp    x0, swapper_pg_dir  
66.    mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)  
67.    add x5, x5, x23         // add KASLR displacement  
68.    create_pgd_entry x0, x5, x3, x6  
69.    adrp    x6, _end            // runtime __pa(_end)  
70.    adrp    x3, _text           // runtime __pa(_text)  
71.    sub x6, x6, x3          // _end - _text  
72.    add x6, x6, x5          // runtime __va(_end)  
73.    create_block_map x0, x7, x3, x5, x6  
74.  
75.    /* 
76.     * Since the page tables have been populated with non-cacheable 
77.     * accesses (MMU disabled), invalidate the idmap and swapper page 
78.     * tables again to remove any speculatively loaded cache lines. 
79.     */  
80.    adrp    x0, idmap_pg_dir  
81.    adrp    x1, swapper_pg_dir + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE  
82.    dmb sy  
83.    bl  __inval_cache_range  
84.  
85.    ret x28  
86.ENDPROC(__create_page_tables)  

下面主要分析__create_page_table的实现流程:

上述代码首先对idmap和swaper页表的内容设定为0(代码16到23行)。因为这些转换表中大部分entry都是没有使用的,PGD和PUD都只有一个entry是有用的,PMD中有用的entry数目是和映射的地址大小有关系。将页表内容清0意味着页表中所有的描述符设定为invalid(描述符的bit 0指示是否有效)。

identity mapping

接着函数创建identity mapping实际就是建立内核中IDMAP_TEXT的一致性mapping(从__idmap_text_start到__idmap_text_end),其目的是当执行打开MMU的操作时,保证在打开MMU那一点附近的程序代码可以平滑的切换。Identity mapping具体操作分两个阶段。

阶段一通过create_pgd_entry建立中间LEVEL转换表(PGD,PUD)的描述符。该函数在建立好pgd转换表的描述符后如果需要下一级的转换表(PUD,PMD)也会同时建立,最终完成所有中间转换表的建立(ps:由于早期内核页表初始化是按照block 2M来进行映射的因此PMD作为块描述表)。具体操作函数如源码57行:create_pgd_entry x0, x3, x5, x6。其中x0表示一致性映射转换表的首地址,即是pdg页表的首地址。而x3指具体要创建哪一个地址的描述符,此处x3实际表示image中IDMAP_TEXT段首地址对应的虚拟地址,由内核自己定义。create_pgd_entry具体代码如下:

1.    .macro  create_pgd_entry, tbl, virt, tmp1, tmp2  
2.    create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2  
3.#if SWAPPER_PGTABLE_LEVELS > 3  
4.    create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2  
5.#endif  
6.#if SWAPPER_PGTABLE_LEVELS > 2  
7.    create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2  
8.#endif  
9.    .endm  

create_table_entry这个宏定义主要是用来创建translation table的描述符,具体创建哪一个level的转换表描述符是有tbl参数指定的。如果是table descriptor那么该表对应的entry需要指向下一级页表的基地址。而该表需要创建的entry在表中的位置由需要映射的虚拟地址的[47:39]位指定,该entry需要跳入下一级转换表的首地址在程序中自己实现(此处是tbl+4K,因为3个页表紧挨且页表大小为1个page)。create_table_entry函数的实现如下:

1..macro  create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2  
2.    lsr \tmp1, \virt, #\shift	-----(1)
3.    and \tmp1, \tmp1, #\ptrs - 1	-----(2)  
4.    add \tmp2, \tbl, #PAGE_SIZE  -----(3)
5.    orr \tmp2, \tmp2, #PMD_TYPE_TABLE	-----(4)
6.    str \tmp2, [\tbl, \tmp1, lsl #3]  -----(5)
7.    add \tbl, \tbl, #PAGE_SIZE	-----(6)  
8.    .endm  

(1)如果是PGD,那么shift等于PGDIR_SHIFT,也就是39了。我们知道arm64架构中L0 index(PGD index)使用虚拟地址的bit[47:39]。如果是PUD,那么shift等于PUD_SHIFT,也就是30了(注意:L1 index(PUD index)使用虚拟地址的bit[38:30])。要想找到virt这个地址(实际传入的是物理地址,当然,我们本来就是要建立和物理地址一样的虚拟地址的mapping)在translation table中的index,当然需要右移shift个bit了。

(2)除了右移操作,我们还需要mask操作(ptrs - 1实际上就是掩码)。对于PGD,其index占据9个bit,因此mask是0x1ff。同样的,对于PUD,其index占据9个bit,因此mask是0x1ff。至此,tmp1就是virt地址在translation table中对应的index了。

(3)如果是table描述符,需要指向另外一个level的translation table,在哪里呢?答案就是next page,因为linux链接脚本中的3个连续的idmap_pg_dir的page定义表明在一致性映射页表中PGD和PUD紧靠且各自大小为1个page。

(4)光有下一级translation table的地址不行,还要告知该描述符是否有效(set bit 0),该描述符的类型是哪一种类型(set bit 1表示是table descriptor),至此,描述符内容准备完毕,保存在tmp2中

(5)最关键的一步,将描述符写入页表中。之所以有“lsl #3”操作,是因为一个描述符占据8个Byte。

(6)将translation table的地址移到next level,以便进行下一步设定。

一定不会忽略这样的一个细节。获取__idmap_text_start(x3)和__idmap_text_end(x6)的代码是不一样的,对于__idmap_text_start直接使用了adrp    x3,__idmap_text_start,而对于__idmap_text_end使用了adr_l    x6, __idmap_text_end。具体使用哪一个是和该地址是否4K对齐相关的。__idmap_text_start一定是4K对齐的,而__idmap_text_end就不一定了,虽然在有些内核版本中__idmap_text_end也是4K对齐的,不过没有任何协议保证这一点,为了保险起见,代码使用了adr_l,确保获取正确的__idmap_text_end的物理地址。

回到create_pgd_entry函数中,该函数填充了内核中IDMAP_TEXT整个内存区域所需要的转换表描述符。其实在PGD和PUD两个表中分别各只有一个entry有效。到此identity mapping第一阶段结束。

第二阶段主要是PMD block描述表的设定了。主要过程是__create_page_table实现代码的60行create_block_map x0, x7, x3, x5, x6。create_block_map实现代码如下:

1..macro  create_block_map, tbl, flags, phys, start, end  
2.lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT  
3.lsr \start, \start, #SWAPPER_BLOCK_SHIFT  
4.and \start, \start, #PTRS_PER_PTE - 1   // table index  
5.orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT  // table entry  
6.lsr \end, \end, #SWAPPER_BLOCK_SHIFT  
7.and \end, \end, #PTRS_PER_PTE - 1       // table end index  
8.999:    str \phys, [\tbl, \start, lsl #3]       // store the entry  
9.add \start, \start, #1          // next entry  
10.add \phys, \phys, #SWAPPER_BLOCK_SIZE       // next block  
11.cmp \start, \end  
12.b.ls    9999b  
13..endm  

该函数就是在tbl指定的Translation table中建立block descriptor以便完成address mapping。具体mapping的内容是将start 到 end这一段虚拟地址mapping到phys开始的PA上去。其实这里的代码逻辑和上面类似就不详述,需要提及的是PTE已经进入了最后一个level的mapping,因此描述符中除了地址信息之外(占据bit[47:21],还需要memory attribute和memory accesse的信息。对于这个场景,PMD中是block descriptor,因此描述符中还包括了block attribute域,分成upper block attribute[63:52]和lower block attribute[11:2]。对这些域的定义如图4所示:

4. 8字节entry数据中的block attribute描述图

在代码中,block attribute是通过flags参数传递的,MM_MMUFLAGS定义如下:

14.#define MM_MMUFLAGS    PMD_ATTRINDX(MT_NORMAL) | PMD_FLAGS  
15.#define PMD_FLAGS    PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S 

MT_NORMAL表示该段内存的memory type是普通memory(对应AttrIndx[2:0]),而不是device什么的。PMD_TYPE_SECT 说明该描述符是一个有效的(bit 0等于1)的block descriptor(bit 1等于0)。PMD_SECT_AF中的AF是access flag的意思,表示该memory block(或者page)是否被最近被访问过。当然,这需要软件的协助。如果该bit被设置为0,当程序第一次访问的时候会产生异常,软件需要将给bit设置为1,之后再访问该page的时候,就不会产生异常了。不过当软件认为该page已经old enough的时候,也可以clear这个bit,表示最近都没有访问该page。这个flag是硬件对page reclaim算法的支持,找到最近不常访问的那些page。当然在这个场景下,我们没有必要enable这个特性,因此将其设定为1。PMD_SECT_S对应SH[1:0],描述memory的sharebility。这些内容和memory attribute相关。最关心的当然也是最熟悉的是memory access control,这是通过AP[2:1]域来控制的。这里该域被设定为00b,表示EL1状态下是RW,EL0状态不可访问。UXN和PXN是用来控制可执行权限的,这里UXN和PXN都是0,表示EL1和EL0状态下都是excutable的。

kernel image mapping

创建kernel space页表就是将kernel镜像占用的虚拟地址空间[vir(_text), vir(_end)]映射到当前kernel镜像当前所在的物理内存地址空间上,table存放到swapper_pg_dir当前所在的物理内存地址处。

1.adrp    x0, swapper_pg_dir  -------(1)
2.mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  ---------(2)
3.add x5, x5, x23         // 如果不支持内核镜像加载地址随机化,x23为0  
4.create_pgd_entry x0, x5, x3, x6  ----------(3)
5.adrp    x6, _end            // runtime __pa(_end)  
6.adrp    x3, _text           // runtime __pa(_text)  
7.sub x6, x6, x3          // _end - _text  
8.add x6, x6, x5          // runtime __va(_end)  
9.create_block_map x0, x7, x3, x5, x6  ---------(4)
  1. swapper_pg_dir就是swapper进程(pid=0的进程,就是idle进程)的地址空间,此时x0指向的地址是该进程页表PGD的基地址。
  2. 获得内核起始位置对应的虚拟地址
  3. 创建PAGE_OFFSET(kernel image物理首地址)对应的PGD和PUD描述符
  4. 创建PMD中的描述符,x5和x6表示映射虚拟地址的起始和结束地址,x7表示地址区域的flags。x3表示需要被映射区域的起始物理地址。

后续内核会按4k分页的方式重新建立页表,此处采用的映射方式颗粒度比较大,level2 table里面是block 描述符,每个block映射2M的区域,按照这种方式只需要3个page就能完成映射kernel镜像的table(L1,L2,L3)如图5所示(identity mapping也是一个原理):

图5 页表映射结构图

从图5可以看到arm64为table的entry提供了4中不同的描述类型(如图6所示),内核初始化页表创建(identity mapping和kernel image mapping)用到了Table descriptor和Block descriptor.

图6 arm64 table descriptor type

在内核初期内核通过identity maping和kernel image mapping完成了2M粒度的页表设定,此时内核内存的映射布局情况如图3所示。后续通过stext入口函数中的__primary_switch函数使能MMU(前面的identity mapping就是为执行该段代码做准备),kernel正式进入了虚拟地址空间的世界。不过现在内核通过虚拟地址只能看到identity maping和kernel image mapping这两段地址空间。内核内存初始化的道路才刚开始。 

感兴趣读者可以参考:https://blog.csdn.net/u012489236/article/details/114454232

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值