linux 内存初始化汇编阶段(1)
在linux kernel(64位)执行第一条指令前,此时内存的状况如下图:
BootLoader通过自己的方式了解当前物理内存的布局情况,将image和dtb,拷贝到特定的位置。一般kernel image位于主存首地址偏移TEXT-OFFSET的位置(并非绝对)。Dtb位置由boot自己决定,此时MMU是关闭状。当从boot跳转到linux kernel后,linux kernel需要完全掌握内存系统的控制权。但此时linux kernel对系统的内存布局情况一无所知,下面主要介绍内核是如何一步步的接管并控制内存系统的。
uBoot跳转到linux kernel的时候MMU是关闭的。内核在boot阶段需要开启MMU,开启前需要初始化页表。因此内核启动代码一个重要的功能是设置好相应的页表并开启MMU。
创建启动页表
在启动初始化的汇编阶段负责创建页表是__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内存管理)。
__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所示:
在代码中,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)
- swapper_pg_dir就是swapper进程(pid=0的进程,就是idle进程)的地址空间,此时x0指向的地 址时该进程页表PGD的基地址。
- 获得内核起始位置对应的虚拟地址
- 创建PAGE_OFFSET(kernel image物理首地址)对应的PGD和PUD描述符
- 创建PMD中的描述符,x5和x6表示映射虚拟地址的起始和结束地址,x7表示地址区域的flags。x3表示需要被映射区域的起始物理地址。
后续内核会按4k分页的方式重新建立页表,此处采用的映射方式颗粒度比较大,level2 table里面是block 描述符,每个block映射2M的区域,按照这种方式只需要3个page就能完成映射kernel镜像的table(L1,L2,L3)如图5所示(identity mapping也是一个原理):
从图5可以看到arm64为table的entry提供了4中不同的描述类型(如图6所示),内核初始化页表创建(identity mapping和kernel image mapping)用到了Table descriptor和Block descriptor.
在内核初期内核通过identity maping和kernel image mapping完成了2M粒度的页表设定,此时内核内存的映射布局情况如图3所示。后续通过stext入口函数中的__primary_switch函数使能MMU(前面的identity mapping就是为执行该段代码做准备),kernel正式进入了虚拟地址空间的世界。不过现在内核通过虚拟地址只能看到identity maping和kernel image mapping这两段地址空间。内核内存初始化的道路才刚开始。