欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

linux 内存初始化汇编阶段(1)

程序员文章站 2022-04-11 18:57:31
...

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

linux 内存初始化汇编阶段(1)
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。

创建启动页表

linux 内存初始化汇编阶段(1)
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内存管理)。

linux 内存初始化汇编阶段(1)
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_end到__idmap_text_end),其目的是当执行打开MMU的操作时,保证在打开MMU那一点附近的程序代码可以平滑的切换。Identity mapping具体操作分两个阶段。

阶段一通过create_pgd_entry建立中间LEVEL转换表(PGD,PUD)的描述符。该函数在建立好pgd转换表的描述符后如果需要下一级的转换表(PUD,PMD)也会同时建立,最终完成所有中间转换表的建立(ps:由于早期内核页表初始化是按照block 2M来进行映射的因此PMD作为块描述表)。具体操作函数如源码78行: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实现代码的78行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所示:

linux 内存初始化汇编阶段(1)
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也是一个原理):

linux 内存初始化汇编阶段(1)
图5 页表映射结构图

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

linux 内存初始化汇编阶段(1)
图6 arm64 table descriptor type

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