分析代码
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)
map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
俄罗斯套娃 idmap_ptrs_per_pgd
第2行的含义是获取变量 idmap_ptrs_per_pgd 的物理地址,并物理地址里面的值加载到x4里面
ldr_l对应的宏定义如下
/*
* @dst: destination register (32 or 64 bit wide)
* @sym: name of the symbol
* @tmp: optional 64-bit scratch register to be used if <dst> is a
* 32-bit wide register, in which case it cannot be used to hold
* the address
*/
.macro ldr_l, dst, sym, tmp=
.ifb \tmp
adrp \dst, \sym
ldr \dst, [\dst, :lo12:\sym]
.else
adrp \tmp, \sym
ldr \dst, [\tmp, :lo12:\sym]
.endif
.endm
其中,“tmp=”没有参数传入,所以为空,走 .ifb 分支,ifb表示 if blank
而变量 idmap_ptrs_per_pgd 的定义如下
u64 idmap_ptrs_per_pgd = PTRS_PER_PGD;
PTRS_PER_PGD
而 PTRS_PER_PGD 的定义如下:
$ grep -rnw PTRS_PER_PGD arch/arm64/
arch/arm64/include/asm/pgtable-hwdef.h:72:#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
所以 PTRS_PER_PGD 依赖于 VA_BITS 和 PGDIR_SHIFT,而 VA_BITS 的值我们之前讨论过,为48, PGDIR_SHIFT 我们先看一下其定义
PGDIR_SHIFT
页目录偏移(PGDIR_SHIFT):这个宏定义了页目录级别(Page Directory Level)的页大小的对数。在ARM64架构中,页表结构可能包括多级,如页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)和页表项(PTE)。PGDIR_SHIFT指定了从物理地址中提取页全局目录索引所需的位数。我们继续寻找其值 PGDIR_SHIFT 为多少
$ grep -rnw PGDIR_SHIFT arch/arm64/
arch/arm64/include/asm/pgtable-hwdef.h:69:#define PGDIR_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
PGDIR_SHIFT 又扯出两个宏定义,分别是 CONFIG_PGTABLE_LEVELS 和 ARM64_HW_PGTABLE_LEVEL_SHIFT
CONFIG_PGTABLE_LEVELS
其中,CONFIG_PGTABLE_LEVELS 的值如下:
$ cat .config | grep CONFIG_PGTABLE_LEVELS
CONFIG_PGTABLE_LEVELS=4
所以 ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS) 的值为 ARM64_HW_PGTABLE_LEVEL_SHIFT(0)
ARM64_HW_PGTABLE_LEVEL_SHIFT
而 ARM64_HW_PGTABLE_LEVEL_SHIFT 的值如下
$ grep -rnw ARM64_HW_PGTABLE_LEVEL_SHIFT arch/arm64/
arch/arm64/include/asm/pgtable-hwdef.h:41:#define ARM64_HW_PGTABLE_LEVEL_SHIFT(n) ((PAGE_SHIFT - 3) * (4 - (n)) + 3)
而我们前面算过,n的值为0,所以宏定义为 ((PAGE_SHIFT - 3) * (4 - (0)) + 3) == ((PAGE_SHIFT - 3) * 4 + 3)
PAGE_SHIFT
PAGE_SHIFT 的宏定义如下:
$ grep -rnw PAGE_SHIFT arch/arm64/
arch/arm64/include/asm/page-def.h:14:#define PAGE_SHIFT CONFIG_ARM64_PAGE_SHIFT
CONFIG_ARM64_PAGE_SHIFT
见到 CONFIG_ARM64_PAGE_SHIFT 应该就是终点,不太可能再俄罗斯套娃了
$ cat .config | grep CONFIG_ARM64_PAGE_SHIFT
CONFIG_ARM64_PAGE_SHIFT=12
最后的小套娃
所以最终 PGDIR_SHIFT 的值为 ((PAGE_SHIFT - 3) * 4 + 3) == 39
这就是 L0 索引的偏移
PTRS_PER_PGD 的值为 1 << (48 - 39) == 512,正如宏定义的名字描述,页全局目录共有512个表项。
区别
那么本章节的
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
和上一章节的
#1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT) 的区别在哪里呢?
这两个宏都用于计算页表条目数量,但它们的目的、计算基础和用途有本质区别:
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
含义
计算虚拟地址空间中PGD包含的条目数量
VA_BITS:虚拟地址位宽(如48位)
PGDIR_SHIFT:PGD级别页表所覆盖的地址偏移位数
计算示例
如果 VA_BITS = 48, PGDIR_SHIFT = 39
PTRS_PER_PGD = 1 << (48 - 39) = 1 << 9 = 512
表示虚拟地址空间需要512个PGD条目
用途
定义PGD页表的大小(条目数量)
用于虚拟地址到物理地址的转换过程中
#1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)
含义
计算需要多少个PGD条目才能映射整个物理地址空间
PHYS_MASK_SHIFT:物理地址位宽(如48位)
PGDIR_SHIFT:PGD级别页表所覆盖的地址偏移位数
计算示例
如果 PHYS_MASK_SHIFT = 48, PGDIR_SHIFT = 39
1 << (48 - 39) = 1 << 9 = 512
表示需要512个PGD条目来覆盖整个物理地址空间
用途
在启动代码中动态计算需要初始化的PGD条目数量
用于物理内存映射的初始化
关键区别对比
| 特性 | PTRS_PER_PGD | #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT) |
| 计算基础 | 虚拟地址空间 (VA_BITS) | 物理地址空间 (PHYS_MASK_SHIFT) |
| 目的 | 虚拟内存管理 | 物理内存映射 |
| 使用场景 | 编译时常量定义 | 运行时计算(汇编指令) |
| 数值关系 | 可能相等,但含义不同 | 可能相等,但含义不同 |
| 在代码中的位置 | 头文件中的宏定义 | 汇编代码中的立即数 |
总结
PTRS_PER_PGD:管理整个虚拟地址空间
另一个:仅映射实际存在的物理内存
实际情况
实际上,当 VA_BITS_MIN 为 48 时,几乎不会 执行到这段特定代码流程。
/*
* 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
因为这段代码逻辑上存在一个矛盾,它很可能是早期代码的遗留物,或者是为了应对一种极其特殊且现在不太可能的配置。
让我们来详细分析一下为什么。
核心矛盾点
这段代码位于一个 #else 分支,它的大前提是 VA_BITS == 48。但同时,它又位于一个更大的条件判断之后:
adrp x5, __idmap_text_end
clz x5, x5
cmp x5, TCR_T0SZ(VA_BITS_MIN) // default T0SZ small enough?
b.ge 1f // .. then skip VA range extension
// 只有当 (number of leading zeros in __idmap_text_end) < TCR_T0SZ(VA_BITS_MIN)
// 即 __idmap_text_end 的物理地址太高,默认的 VA 空间太小,无法容纳身份映射时,
// 才会进入下面的代码块,其中就包含 #else 分支。
TCR_T0SZ(VA_BITS_MIN) 的值是 64 - VA_BITS_MIN。
如果 VA_BITS_MIN = 48,那么 TCR_T0SZ(48) = 64 - 48 = 16。
clz x5, x5 计算的是 __idmap_text_end 物理地址的前导零个数。一个地址的前导零越多,说明它所在的物理地址越低。
矛盾就在这里:
要使代码进入这个分支,需要 x5 (前导零个数) < 16。这意味着 __idmap_text_end 的物理地址必须非常高,其二进制形式的前面只有少于16个0。
对于一个 48 位的系统:
最高物理地址是 0xFFFF_FFFF_FFFF (~256TB)。
一个地址的前导零少于16个,意味着它的数值大于 (1 << (64 - 16)) = (1 << 48) = 0x1_0000_0000_0000。
这已经超出了 48 位物理地址的最大范围 0xFFFF_FFFF_FFFF。
因此,在 VA_BITS_MIN = 48 且物理地址空间也是 48 位的系统中,__idmap_text_end 的物理地址根本不可能高到使 clz 结果小于 16。b.ge 1f 条件永远成立,代码会直接跳过整个扩展块,根本不会执行到 #else 分支。
可能执行到的极端场景(理论上)
除非系统配置满足以下所有苛刻条件,这段代码才可能被执行:
VA_BITS_MIN == 48:内核编译时设定的最小虚拟地址宽度是48位。
PHYS_MASK_SHIFT > 48:系统实际支持的物理地址位宽大于48位(例如 52位)。这样物理地址空间(e.g., 4PB)就大于身份映射的虚拟地址空间(256TB)。
内核加载位置极高:__idmap_text_end 的物理地址必须位于非常高的、超出 48 位地址范围的区域(即 > 0xFFFF_FFFF_FFFF),导致计算出的前导零个数小于 64 - 48 = 16。
举例说明
假设 PHYS_MASK_SHIFT = 52(物理地址空间为 4PB)。
假设 VA_BITS_MIN = 48(身份映射初始 VA 空间为 256TB)。
现在,将内核加载到物理地址 0x200_0000_0000(2TB)处。这完全在 52 位物理地址范围内,但已经非常高。
计算 __idmap_text_end 的物理地址(假设为 0x200_0000_1000)。
用 clz 计算该地址的前导零:64 - 52 = 12,所以前导零个数是 52?不对,clz 是计算从最高位开始连续的0。0x200_0000_1000 的二进制是 0000...0010 0000...,其前导零个数是 64 - 41 = 23? (这里需要精确计算,但可以肯定它远大于16)。
为了满足 clz < 16,地址必须非常高,比如 0xFFFF_0000_0000(已经超过了48位范围,但在52位范围内)。其前导零个数会非常少。
只有在第三种情况下,代码才会判断默认的 48 位身份映射空间不够用,需要进入扩展流程。又因为 VA_BITS == 48,所以会走到 #else 分支,执行如下代码:
mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT) //e.g., 1 << (52 - 39) = 1<<13 = 8192
str_l x4, idmap_ptrs_per_pgd, x5
这意味着身份映射的顶级页表(PGD)需要 8192 个条目,而不是通常 48-bit VA/PA 系统下的 1 << (48-39) = 512 个条目,以覆盖更大的物理地址范围。
现实情况
这种配置(VA_BITS=48 但 PHYS_MASK_SHIFT>48)在现实中极其罕见,甚至可能是不支持的。更常见且合理的配置是:
VA_BITS=48, PHYS_MASK_SHIFT=48:这是主流 48-bit ARMv8服务器的标准配置。此场景下,该代码流程永远不会被执行。
VA_BITS=52, PHYS_MASK_SHIFT=48 (或更高):如果需要更大的虚拟地址空间,内核会直接编译为 VA_BITS=52,这样身份映射初始空间就是 4PB,完全可以覆盖 48位 的物理空间,同样不会进入这个扩展分支。
结论
上述这段代码,在 VA_BITS_MIN 为 48 的标准配置下,是一个永远不会被触发的“死代码”路径。它很可能是为了处理一种理论上存在但实践中几乎不会遇到的极端边界情况,或者是早期代码演进过程中遗留下来的。
在正常的、一致的 48-bit VA/48-bit PA 系统中,身份映射的初始化会使用默认的、足够大的 48 位空间,直接跳过整个扩展逻辑。
准备建立恒等映射
第3行和第4行表示 x5和x6分别存储 __idmap_text_start 和 __idmap_text_end 的物理地址,也就是恒等映射区域的起始和结束物理地址。
adr_l 不是标准的arm64指令或者伪指令,在linux kernel的定义如下
/*
* @dst: destination register (64 bit wide)
* @sym: name of the symbol
*/
.macro adr_l, dst, sym
adrp \dst, \sym
add \dst, \dst, :lo12:\sym
.endm
其实就是把一个值的物理地址存放在某个寄存器中。
第6行表示开始建立恒等映射的列表,后面会有3章详细讲解其内容
375

被折叠的 条评论
为什么被折叠?



