目录
1.前言
kernel版本:5.10
平台:arm64
本专题主要基于《arm64_linux head.S的执行流程》系列文章,前者是基于3.18,本专题针对的是内核5.10。主要分析head.S的执行过程。本文主要记录head.S的create_page_tables执行过程。
这里假设4级页表,使用section mapping
create_page_tables主要完成如下的工作:
- 无效init_pg区域的cacheline;
- 清零init_pg内存区域;
- 在idmap_pg区域为kernel创建恒等映射,由于只在开启MMU时使用,因此只会为部分代码创建映射;
- 在init_pg区域为kernel创建映射
- 再次无效init_pg和idmap_pg区域对应的cacheline
执行完create_page_tables,将得到如下的地址空间布局:
id_map.text为需要创建恒等映射的物理内存区域
id_map_pg:为恒等区域存放的页表区域
init_pg为内核镜像存放的页表区域
当kernel image加载到物理内存后,为.idmap.text段创建了idmap映射,其中.idmap.text属于.text段的一部分,idmap_pg页表空间位于.data段与.text段之间;
为整个kernel image创建了init映射,其中init_pg页表位于.data段
为idmap.text创建恒等映射,实际就是创建idmap页表,通过填充pgd, pud, pmd页表项完成,其中pgd,pud为表描述符,指向下级页表,pmd为块描述符,pmd块描述符指向idmap_text区域
一个pgd页表项可以映射512G, 一个pud页表项可以映射1G,一个pmd页表项可以映射2M
2. 主要宏说明
主要宏 | 说明 |
---|---|
KIMAGE_VADDR | kernel的起始虚拟地址,_text地址, 本例为0xffff800010000000 |
PAGE_OFFSET | Linear Mapping起始虚拟地址,本例为0xffff000000000000 |
PAGE_END | Linear Mapping结束虚拟地址,本例为0xffff800000000000 |
__PHYS_OFFSET | kernel的起始虚拟地址, _text地址,本例为0xffff800010000000 |
KERNEL_START | kernel的起始虚拟地址,_text地址,本例为0xffff800010000000 |
3. create_page_tables
/*
* 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
*/
SYM_FUNC_START_LOCAL(__create_page_tables)
设置初始页表。我们只需要设置内核运行所需要的最基本的页表,包括:
- 用于启用MMU的恒等映射(低地址,TTBR0)
- 一旦MMU被启用,内核线性映射的前几MB必须被映射
mov x28, lr
保存链接地址,通过ret返回
3.1 无效init_pg的dcache区域
/*
* Invalidate the init page tables to avoid potential dirty cache lines
* being evicted. Other page tables are allocated in rodata as part of
* the kernel image, and thus are clean to the PoC per the boot
* protocol.
*/
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
bl __inval_dcache_area
实际就是将init_pg_dir与init_pg_end之间的区域执行invalid dcache,这需要计算出这段区域的size.
如下是对size大小计算的说明:
根据链接脚本arch/arm64/kernel/vmlinux.lds.S中如下定义:
. = ALIGN(PAGE_SIZE);
init_pg_dir = .;
. += INIT_DIR_SIZE;
init_pg_end = .;
本例中:
CONFIG_PGTABLE_LEVELS > 3
SWAPPER_PGTABLE_LEVELS=3 ARM64_SWAPPER_USES_SECTION_MAPS:
引自:http://www.wowotech.net/linux_kenrel/create_page_tables.html
ARM64_SWAPPER_USES_SECTION_MAPS这个宏定义是说明了swapper/idmap的映射是否使用section map。什么是section map呢?我们用一个实际的例子来描述。假设VA是48 bit,page size是4K,那么,在地址映射过程中,地址被分成9(level 0) + 9(level 1) + 9(level 2) + 9(level 3) + 12(page offset),对于kernel image这样的big block memory region,使用4K的page来mapping有点得不偿失,在这种情况下,可以考虑让level 2的Translation table entry指向一个2M 的memory region,而不是下一级的Translation table。所谓的section map就是指使用2M的为单位进行映射。当然,不是什么情况都是可以使用section map,对于kernel image,其起始地址是2M对齐的,因此block size是2M的情况下才OK,对于PAGE SIZE是16K,其Block descriptor指向了一个32M的内存块,PAGE SIZE是64K的时候,Block descriptor指向了一个512M的内存块,因此,只有4K page size的情况下,才可以启用section map。
关于INIT_DIR_SIZE 宏定义如下:
#define INIT_DIR_SIZE (PAGE_SIZE * EARLY_PAGES(KIMAGE_VADDR, _end))
#define EARLY_PAGES(vstart, vend) ( 1 /* PGDIR page */ \
+ EARLY_PGDS((vstart), (vend)) /* each PGDIR needs a next level page table */ \
+ EARLY_PUDS((vstart), (vend)) /* each PUD needs a next level page table */ \
+ EARLY_PMDS((vstart), (vend))) /* each PMD needs a next level page table */
#define EARLY_PGDS(vstart, vend) (EARLY_ENTRIES(vstart, vend, PGDIR_SHIFT))
#define EARLY_PUDS(vstart, vend) (0)
#define EARLY_PMDS(vstart, vend) (EARLY_ENTRIES(vstart, vend, SWAPPER_TABLE_SHIFT))
#define EARLY_ENTRIES(vstart, vend, shift) (((vend) >> (shift)) \
- ((vstart) >> (shift)) + 1 + EARLY_KASLR)
-
#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT)
从kernel启动流程-1.head.S的执行_概述中可知KIMAGE_VADDR就是_text,因此:
-
INIT_DIR_SIZE=(1<<12) * EARLY_PAGES(_text, _end)
-
EARLY_PAGES(_text, _end) =
1+EARLY_PGDS(_text, _end)+EARLY_PUDS(_text, _end)+EARLY_PMDS(_text, _end) -
EARLY_PGDS(_text, _end)=
EARLY_ENTRIES(_text, _end, PGDIR_SHIFT)=
(_end >> PGDIR_SHIFT)- (_text >> PGDIR_SHIFT) + 1 + EARLY_KASLR -
EARLY_PUDS(_text, _end)=
0 -
EARLY_PMDS(_text, _end)=
EARLY_ENTRIES(_text, _end, SWAPPER_TABLE_SHIFT)=
(_end >> SWAPPER_TABLE_SHIFT)- (_text >> SWAPPER_TABLE_SHIFT) + 1 + EARLY_KASLR
KASLR, kernel address space layout randomization,内核地址空间布局随机化,是linux内核的一个非常重要的安全机制。KASLR技术可以让kernel image映射的地址相对于链接地址有个偏移,安全性上有一定的提升。
PGDIR_SHIFT 宏定义如下:
#define PGDIR_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3)
#define SWAPPER_PGTABLE_LEVELS (CONFIG_PGTABLE_LEVELS - 1)
#define IDMAP_PGTABLE_LEVELS (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
#define PAGE_SHIFT CONFIG_ARM64_PAGE_SHIFT
#define CONFIG_ARM64_PAGE_SHIFT 12
计算有多少个PGD页表项:
- PGDIR_SHIFT=(12-3)*(4-0)+3=39
- EARLY_PGDS(_text, _end)=EARLY_ENTRIES(_text, _end, PGDIR_SHIFT)=
(_end >> PGDIR_SHIFT)- (_text >> PGDIR_SHIFT) + 1 + EARLY_KASLR =
(_end >> 39)- (_text >> 39) + 1 + EARLY_KASLR
SWAPPER_TABLE_SHIFT宏定义如下:
#define SWAPPER_TABLE_SHIFT PUD_SHIFT
#define PUD_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3)
计算有多少个PMD页表项:
- PUD_SHIFT= (12 - 3) * (4 - 1) + 3 = 30
- EARLY_PMDS(_text, _end)=EARLY_ENTRIES(_text, _end, SWAPPER_TABLE_SHIFT)=
(_end >> SWAPPER_TABLE_SHIFT)- (_text >> SWAPPER_TABLE_SHIFT) + 1 + EARLY_KASLR=
(_end >> 30)- (_text >> 30) + 1 + EARLY_KASLR
通过上面的换算,因此可得INIT_DIR_SIZE:
- EARLY_PAGES(_text, _end) =1+EARLY_PGDS(_text,_end)+EARLY_PMDS(_text, _end)
=1 +
(_end >> 39)- (_text >> 39) + 1 + EARLY_KASLR +
0 +
(_end >> 30)- (_text >> 30) + 1 + EARLY_KASLR - INIT_DIR_SIZE=(1<<12) * EARLY_PAGES(_text, _end)
= (1<<12) * EARLY_PAGES(_text, _end)
= (1<<12) *
(1 +
(_end >> 39)- (_text >> 39) + 1 + EARLY_KASLR +
0 +
(_end >> 30)- (_text >> 30) + 1 + EARLY_KASLR)
通过编译后生成的arch/arm64/kernel/vmlinux.lds可以看到INIT_DIR_SIZE被编译为:
init_pg_dir = .;
. += ((1 << 12) *
( 1 + (((((_end)) >> (((12 - 3) * (4 - (4 - 4)) + 3))) - ((((((((((-(((1)) << ((((48))) - 1))))) + (0x08000000))) + (0x08000000))))) >> (((12 - 3) * (4 - (4 - 4)) + 3))) + 1 + (1))) + (0)
+ (((((_end)) >> (((12 - 3) * (4 - (1)) + 3))) - ((((((((((-(((1)) << ((((48))) - 1))))) + (0x08000000))) + (0x08000000))))) >> (((12 - 3) * (4 - (1)) + 3))) + 1 + (1)))));
init_pg_end = .;
即:
. += ((1 << 12) * (1 + (((_end >> 39) - (((-(1 << (48 - 1)) + (0x08000000)) + 0x08000000) >> 39) + 1 + (1))) +
(0) +
(((_end >> 30) - (((-(1 << (48 - 1)) + 0x08000000) + 0x08000000) >> 30) + 1 + (1))));
到此处我们可以知道需要invalide的区域大小包含了PGD table和PMD table,由于采用了SECTION MAPPING, 因此没有使用PUD table(大小为0).
3.2 Clear the init page tables
/*
* Clear the init page tables.
*/
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
1: stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
subs x1, x1, #64
b.ne 1b
mov x7, SWAPPER_MM_MMUFLAGS
将init_pg page table区间清零
其中x7保存了SWAPPER_MM_MMUFLAGS,关于SWAPPER_MM_MMUFLAGS 的宏定义如下:
#define SWAPPER_MM_MMUFLAGS (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
/*
* AttrIndx[2:0] encoding (mapping attributes defined in the MAIR* registers).
*/
#define PMD_ATTRINDX(t) (_AT(pmdval_t, (t)) << 2)
/*
* Initial memory map attributes.
*/
#define SWAPPER_PMD_FLAGS (PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S)
/*最低位为01,根据页表描述符为块描述符*/
#define PMD_TYPE_SECT (_AT(pmdval_t, 1) << 0)
3.3 Create the identity mapping
adrp x0, idmap_pg_dir
adrp x3, __idmap_text_start // __pa(__idmap_text_start)
x0保存了id_map区域页表存放的起始地址idmap_pg_dir
x3保存了__idmap_text_start的物理地址,它就是需要创建恒等映射的区域起始地址
idmap_pg_dir定义在arch/arm64/kernel/vmlinux.lds.S中:
#arch/arm64/kernel/vmlinux.lds.S
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
idmap_pg_end = .;
IDMAP_DIR_SIZE宏定义如下:
#define IDMAP_DIR_SIZE (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_PGTABLE_LEVELS (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
mov x5, #VA_BITS_MIN
x5保存了虚拟地址位数VA_BITS_MIN,本例VA_BITS_MIN为48
adr_l x6, vabits_actual
str x5, [x6]
dmb sy
dc ivac, x6 // Invalidate potentially stale cache line
将虚拟地址位数保存到vabits_actual中,vabits_actual定义如下:
u64 __section(".mmuoff.data.write") vabits_actual;
EXPORT_SYMBOL(vabits_actual);
adrp x5, __idmap_text_end
clz x5, x5
cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?
b.ge 1f
x5粗放囊
#arch/arm64/include/asm/pgtable-hwdef.h
#define TCR_T0SZ(x) ((UL(64) - (x)) << TCR_T0SZ_OFFSET)
#define IDMAP_TEXT \
. = ALIGN(SZ_4K); \
__idmap_text_start = .; \
*(.idmap.text) \
__idmap_text_end = .;
x3保存了__idmap_text_end的物理地址,它就是需要创建恒等映射的区域结束地址
虚拟地址最大值前导0的个数 和最高物理地址比较,看是否够用,如果物理地址的前导0多,则说明地址够用,不用扩展,直接跳到1
adr_l x6, idmap_t0sz
str x5, [x6]
dmb sy
dc ivac, x6 // Invalidate potentially stale cache line
如果物理地址的前导0少,则需要扩展,将物理地址的前导0个数存入idmap_t0sz
/*
* If VA_BITS == 48, we don't have to configure an additional
* translation level, but the top-level table has more entries.
*/
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
str_l x4, idmap_ptrs_per_pgd, x5
#define PHYS_MASK_SHIFT (CONFIG_ARM64_PA_BITS)
#define CONFIG_ARM64_PA_BITS 48
前面分析可知PGDIR_SHIFT=39,x4保存了(1<<512)??
1:
ldr_l x4, idmap_ptrs_per_pgd
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
x3保存了需要恒等映射的区域__idmap_text_start的物理地址,赋值给x5;
x6保存了需要恒等映射的区域__idmap_text_end的物理地址(可以参考adr_l的实现)
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
/*
* Map memory for specified virtual address range. Each level of page table needed supports
* multiple entries. If a level requires n entries the next page table level is assumed to be
* formed from n pages.
*
* tbl: location of page table
* rtbl: address to be used for first level page table entry (typically tbl + PAGE_SIZE)
* vstart: start address to map
* vend: end address to map - we map [vstart, vend]
* flags: flags to use to map last level entries
* phys: physical address corresponding to vstart - physical memory is contiguous
* pgds: the number of pgd entries
*
* Temporaries: istart, iend, tmp, count, sv - these need to be different registers
* Preserves: vstart, vend, flags
* Corrupts: tbl, rtbl, istart, iend, tmp, count, sv
*/
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
x0(tbl):存放页表的起始地址,此处为idmap_pg_dir,也就是恒等映射区域页表pgd页表的基地址
x1(rtbl):下一级页表项地址,典型为tbl+PAGE_SIZE
x3(vsatart):开始映射的虚拟地址,此处为__idmap_text_start的物理地址
x6(vend):结束映射的虚拟地址,此处为__idmap_text_end的物理地址
x7(flags): 映射最后一级页表项的标志,此处为SWAPPER_MM_MMUFLAGS
x3(phys):和vstart对应的物理地址,此处为__idmap_text_start
x4(pgds):pgd页表项的个数,此处为idmap_ptrs_per_pgd
此处map_memory主要的功能就是为idmap text创建恒等映射(物理地址等于虚拟地址),.idmap.text段在head.S中声明,idmap text段只包含了head.S的部分代码
引自:http://www.wowotech.net/memory_management/436.html
identity mapping主要是打开MMU的过度阶段,因此对于identity mapping不需要映射整个kernel,只需要映射操作MMU代码相关的部分。如何区分这部分代码呢?当然是利用linux中常用手段自定义代码段。自定义的代码段的名称是".idmap.text"。除此之外,肯定还需要在链接脚本中声明两个标量,用来标记代码段的开始和结束
通过查看System,map可知idmap_text段包含:
44852 ffff800010aca000 T __idmap_text_start
44853 ffff800010aca000 T el2_setup
44854 ffff800010aca05c t set_hcr
44855 ffff800010aca12c t install_el2_stub
44856 ffff800010aca180 t set_cpu_boot_mode_flag
44857 ffff800010aca1a4 T secondary_holding_pen
44858 ffff800010aca1c8 t pen
44859 ffff800010aca1dc T secondary_entry
44860 ffff800010aca1e8 t secondary_startup
44861 ffff800010aca200 t __secondary_switched
44862 ffff800010aca23c t __secondary_too_slow
44863 ffff800010aca248 T __enable_mmu
44864 ffff800010aca2a0 T __cpu_secondary_check52bitva
44865 ffff800010aca2a4 t __no_granule_support
44866 ffff800010aca2c8 t __relocate_kernel
44867 ffff800010aca310 t __primary_switch
44868 ffff800010aca388 T cpu_resume
44869 ffff800010aca3a8 T __cpu_soft_restart
44870 ffff800010aca3e4 T cpu_do_resume
44871 ffff800010aca474 T idmap_cpu_replace_ttbr1
44872 ffff800010aca4a8 t __idmap_kpti_flag
44873 ffff800010aca4ac T idmap_kpti_install_ng_mappings
44874 ffff800010aca4e8 t do_pgd
44875 ffff800010aca500 t next_pgd
44876 ffff800010aca510 t skip_pgd
44877 ffff800010aca550 t walk_puds
44878 ffff800010aca558 t do_pud
44879 ffff800010aca570 t next_pud
44880 ffff800010aca580 t skip_pud
44881 ffff800010aca590 t walk_pmds
44882 ffff800010aca598 t do_pmd
44883 ffff800010aca5b0 t next_pmd
44884 ffff800010aca5c0 t skip_pmd
44885 ffff800010aca5d0 t walk_ptes
44886 ffff800010aca5d8 t do_pte
44887 ffff800010aca5fc t skip_pte
44888 ffff800010aca60c t __idmap_kpti_secondary
44889 ffff800010aca654 T __cpu_setup
44890 ffff800010aca740 T __idmap_text_end
3.3 Map the kernel image
/*
* Map the kernel image (starting with PHYS_OFFSET).
*/
adrp x0, init_pg_dir
mov_q x5, KIMAGE_VADDR // compile time __va(_text)
add x5, x5, x23 // add KASLR displacement
mov x4, PTRS_PER_PGD
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)
map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
x0(tbl):存放页表的起始地址,此处为init_pg_dir
x1(rtbl):下一级页表项地址,典型为tbl+PAGE_SIZE,此处为init_pg_end
x5(vsatart):开始映射的虚拟地址,此处为KIMAGE_VADDR,即_text
x6(vend):结束映射的虚拟地址,此处为KIMAGE_VADDR+(_end-_text)
x7(flags): 映射最后一级页表项的标志,此处为SWAPPER_MM_MMUFLAGS
x3(phys):和vstart对应的物理地址,此处为_text的物理地址。
x4(pgds):pgd页表项的个数,此处为idmap_ptrs_per_pgd
如上代码的作用主要是对整个kernel image创建页表,此处是一个较粗粒度的映射,主要做2M大小的块映射,且对于内核镜像的代码段,数据段权限没有做区分,只是为了此时能访问内核的一些函数,区别于后续所做的内核细粒度映射(页表基址为swapper_pg_dir)。
关于adrp指令:由于当前MMU关闭状态,因此pc为物理地址,而adrp是相对pc的寻址,因此通过adrp获取的地址为物理地址,如:上面的adrp x6, _end
3.4 无效init_pg和idmap_pg的dcache区域
/*
* Since the page tables have been populated with non-cacheable
* accesses (MMU disabled), invalidate those tables again to
* remove any speculatively loaded cache lines.
*/
dmb sy
adrp x0, idmap_pg_dir
adrp x1, idmap_pg_end
sub x1, x1, x0
bl __inval_dcache_area
adrp x0, init_pg_dir
adrp x1, init_pg_end
sub x1, x1, x0
bl __inval_dcache_area
ret x28
无效idmap区域和init page区域的dcache, 然后返回x28中保存的链接地址lr
参考文档
- http://www.wowotech.net/linux_kenrel/create_page_tables.html
ARM64的启动过程之(二):创建启动阶段的页表 - http://www.wowotech.net/memory_management/436.html
ARM64 Kernel Image Mapping的变化