一、页表
1. 内存地址的分解
我们知道linux采用了分页机制,通常采用四级页表,页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)。如下:
其含义定义在arch/arm64/include/asm/pgtable-hwdef.h文件中:
CONFIG_PGTABLE_LEVELS //页表级数
//offset
#define PAGE_SHIFT CONFIG_ARM64_PAGE_SHIFT //页offset,为12
#define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT) //页大小,为4k
//PTE
#define PTRS_PER_PTE (1 << (PAGE_SHIFT - 3)) //PTE位数,为9
#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3)
//PMD
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(2)
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) //2M
#define PMD_MASK (~(PMD_SIZE-1))
#define PTRS_PER_PMD PTRS_PER_PTE //PMD位数,为9
#endif
//PUD
#if CONFIG_PGTABLE_LEVELS > 3
#define PUD_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(1)
#define PUD_SIZE (_AC(1, UL) << PUD_SHIFT) //1G
#define PUD_MASK (~(PUD_SIZE-1))
#define PTRS_PER_PUD PTRS_PER_PTE //PUD位数,为9
#endif
//PGD
#define PGD_SIZE (PTRS_PER_PGD * sizeof(pgd_t)) //4096
#define VA_BITS (CONFIG_ARM64_VA_BITS) //4级页表为48,3级页表为39
#define PGDIR_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
看到这里我们知道4级页表虚拟地址为48位,页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)都是9位,offset是12位。但是有些系统是使用3级页表的,那么他的虚拟地址为39位,页全局目录(PGD),页中间目录(PMD),页表(PTE)都是9位,offset是12位,也就是说,少了页上级目录(PUD)。其实我们使用3级页表就够了,39为的虚拟地址可以操作512g的内存,是完全够用的。我们国产操作系统就是使用3级页表的。最后说明一下。无论是3级页表还是4级页表,我们的物理地址都是48位的。
1.2 页表的数据结构
我们已经确立了页表项的数目,但没有定义其结构。内核提供了4个数据结构(定义在 arch/arm64/include/asm/pgtable-types.h中)来表示页表项的结构。
typedef struct { pteval_t pte; } pte_t;
#define pte_val(x) ((x).pte)
#define __pte(x) ((pte_t) { (x) } )
#if CONFIG_PGTABLE_LEVELS > 2
typedef struct { pmdval_t pmd; } pmd_t;
#define pmd_val(x) ((x).pmd)
#define __pmd(x) ((pmd_t) { (x) } )
#endif
#if CONFIG_PGTABLE_LEVELS > 3
typedef struct { pudval_t pud; } pud_t;
#define pud_val(x) ((x).pud)
#define __pud(x) ((pud_t) { (x) } )
#endif
typedef struct { pgdval_t pgd; } pgd_t;
#define pgd_val(x) ((x).pgd)
#define __pgd(x) ((pgd_t) { (x) } )
typedef struct { pteval_t pgprot; } pgprot_t;
#define pgprot_val(x) ((x).pgprot)
#define __pgprot(x) ((pgprot_t) { (x) } )
typedef u64 pteval_t;
typedef u64 pmdval_t;
typedef u64 pudval_t;
typedef u64 pgdval_t;
从代码中我们看到了以下东西:
- 页表项的结构
pgd_t 用于全局页目录项。
pud_t 用于上层页目录项。
pmd_t 用于中间页目录项。
pte_t 用于直接页表项。
页表项的结构 | pgd_t 用于全局页目录项。pud_t 用于上层页目录项。pmd_t 用于中间页目录项。pte_t 用于直接页表项。 |
1.3 特定于页表的信息
arch/arm64/include/asm/pgtable-hwdef.h
/*
* Level 3 descriptor (PTE).
*/
#define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_FAULT (_AT(pteval_t, 0) << 0)
#define PTE_TYPE_PAGE (_AT(pteval_t, 3) << 0)
#define PTE_TABLE_BIT (_AT(pteval_t, 1) << 1)
#define PTE_USER (_AT(pteval_t, 1) << 6) /* AP[1] */
#define PTE_RDONLY (_AT(pteval_t, 1) << 7) /* AP[2] */
#define PTE_SHARED (_AT(pteval_t, 3) << 8) /* SH[1:0], inner shareable */
#define PTE_AF (_AT(pteval_t, 1) << 10) /* Access Flag */
#define PTE_NG (_AT(pteval_t, 1) << 11) /* nG */
#define PTE_DBM (_AT(pteval_t, 1) << 51) /* Dirty Bit Management */
#define PTE_CONT (_AT(pteval_t, 1) << 52) /* Contiguous range */
#define PTE_PXN (_AT(pteval_t, 1) << 53) /* Privileged XN */
#define PTE_UXN (_AT(pteval_t, 1) << 54) /* User XN */
#define PTE_HYP_XN (_AT(pteval_t, 1) << 54) /* HYP XN */
/*
* Level 2 descriptor (PMD).
*/
#define PMD_TYPE_MASK (_AT(pmdval_t, 3) << 0)
#define PMD_TYPE_FAULT (_AT(pmdval_t, 0) << 0)
#define PMD_TYPE_TABLE (_AT(pmdval_t, 3) << 0)
#define PMD_TYPE_SECT (_AT(pmdval_t, 1) << 0)
#define PMD_TABLE_BIT (_AT(pmdval_t, 1) << 1)
#define PUD_TYPE_TABLE (_AT(pudval_t, 3) << 0)
#define PUD_TABLE_BIT (_AT(pudval_t, 1) << 1)
#define PUD_TYPE_MASK (_AT(pudval_t, 3) << 0)
#define PUD_TYPE_SECT (_AT(pudval_t, 1) << 0)
还有一些用于处理内存页的体系结构相关状态的函数:
函数 | 描述 |
---|---|
pte_present | 页在内存中吗 |
pte_read | 从用户空间可以读取该页吗 |
pte_write | 可以写入到该页吗 |
pte_exec | 该页中的数据可以作为二进制代码执行吗 |
pte_dirty | 页是脏的吗?其内容是否修改过 |
pte_file | 该页表项属于非线性映射吗 |
pte_young | 访问位(通常是_ PAGE_ACCESS )设置了吗 |
pte_rdprotect | 清除该页的读权限 |
pte_wrprotect | 清除该页的写权限 |
pte_exprotect | 清除执行该页中二进制数据的权限 |
pte_mkread | 设置读权限 |
pte_mkwrite | 设置写权限 |
pte_mkexec | 允许执行页的内容 |
pte_mkdirty | 将页标记为脏 |
pte_mkclean | “清除”页,通常是指清除 _PAGE_DIRTY 位 |
pte_mkyoung | 设置访问位,在大多数体系结构上是 _PAGE_ACCESSED |
pte_mkold | 清除访问位 |
以上函数一般是用于设置、删除、查询某个特定的属性(例如,页的写权限)。 |
1.4 页表的格式、
ARM64 处理器把页表称为转换表( translation table ),最多 4 级。分别是页全局目录(PGD),页上级目录(PUD),页中间目录(PMD),页表(PTE)。ARM64 处理器把表项称为描述符( descriptor ),使用 64 位的长描述符格式。描述符的第 0 位指示描述符是不是有效的: 0 表示无效, 1 表示有效;第 1 位指定描述符类型。
- 页目录项,就是在页全局目录(PGD),页上级目录(PUD),页中间目录(PMD)中, 0 表示块( block )描述符, 1 表示表( table )描述符。块描述符存放一个内存块(即巨型页)的起始地址,表描述符存放下一级转换表的地址。
- 页目表项,就是页表(PTE), 0 表示保留描述符, 1 表示页描述符。
1.4.1 页目录项
-
无效描述符:无效描述符的第 0 位是 0 。
-
块描述符:块描述符的最低两位是 01。
-
表描述符:表描述符的最低两位是 11。
1.4.2 页目表项
-
无效描述符:无效描述符的第 0 位是 0。
-
保留描述符:保留描述符的最低两位是 01。
-
页描述符:页描述符的最低两位是 11
无论是页目录项的块描述符还是页表项的页描述符,都是表示一个整体的内存;块描述符一般用于巨型页,而页描述符用于普通页。
他们的内存属性被拆分成一个高属性块和一个低属性块,具体可以看下面的图和表:
位数 | 含义 |
---|---|
59 ~ 62 | 基于页的硬件属性( Page-Based Hardware Attributes ),如果没有实现 ARMv8.2-TTPBHA ,忽略。 |
55 ~ 58 | 保留给软件使用。 |
54 | 在异常级别 0 ,表示 UXN ( Unprivileged execute-Never ),即不允许异常级别0 执行内核代码;在其他异常级别,表示 XN ( execute-Never ),不允许执行。 |
53 | PXN ( Privileged execute-Never ),不允许在特权级别(即异常级别 1/2/3 )执行。 |
52 | 连续( Contiguous ),指示这条转换表项属于一个连续表项集合,一个连续表项集合可以被缓存在一条 TLB 表项里面 |
51 | 脏位修饰符( Dirty Bit Modifier , DBM ),指示页或内存块是否被修改过。 |
11 | 非全局( not global , nG )。 nG 位是 1 ,表示转换不是全局的,是进程私有的,有一个关联的地址空间标识符( Address Space Identifier , ASID ); nG 位是 0 ,表示转换是全局的,是所有进程共享的,内核的页或内存块是所有进程共享的。 |
10 | 访问标志( Access Flag , AF ),指示页或内存块自从相应的转换表描述符中的访问标志被设置为 0 以后是否被访问过。 |
8 ~ 9 | 可共享性( SHareability , SH ), 00 表示不共享, 01 是保留值, 10 表示外部共享, 11 表示内部共享。 |
6 ~ 7 | AP[2:1] ( Data Access Permissions ,数据访问权限)。AP[2] 用来选择只读或读写, 1 表示只读, 0 表示读写; AP[1] 用来选择是否允许异常级别 0 访问, 1 表示允许异常级别 0 访问, 0 表示不允许异常级别 0 访问。 |
5 | 非安全( Non-Secure , NS )。对于安全状态的内存访问,指定输出地址在安全地址映射还是在非安全地址映射。 |
2 ~ 4 | 内存属性索引( memory attributes index , AttrIndx ),指定寄存器 MAIR_ELx中内存属性字段的索引,内存属性间接寄存器( Memory Attribute Indirection Register ,MAIR_ELx )有 8 个 8 位内存属性字段: Attr , n 等于 0 ~ 7 。 |