调试手记:我是如何通过__create_pgd_mapping函数,一步步跟踪ARM64 Linux内核页表建立的

张开发
2026/5/22 1:17:48 15 分钟阅读
调试手记:我是如何通过__create_pgd_mapping函数,一步步跟踪ARM64 Linux内核页表建立的
ARM64 Linux内核页表建立实战从__create_pgd_mapping开始的调试之旅在ARM64架构的Linux内核开发中内存管理子系统是最为复杂也最令人着迷的部分之一。作为一名长期深耕内核开发的工程师我最近在为一个定制化嵌入式项目调试内存映射问题时不得不深入探究页表建立的完整流程。本文将分享我如何通过__create_pgd_mapping函数作为切入点逐步跟踪ARM64 Linux内核页表建立的全过程希望能为同样面临此类问题的开发者提供一条清晰的调试路径。1. 调试环境与工具准备在开始深入代码之前搭建合适的调试环境至关重要。我使用的是基于Xilinx Zynq UltraScale MPSoC的开发板运行Linux 6.10内核。这个平台提供了完整的ARMv8-A架构支持是研究ARM64内存管理的理想选择。关键调试工具配置KGDB内核调试通过串口或以太网连接实现源码级单步调试QEMU模拟器用于在没有硬件的情况下验证调试方法Trace32调试器硬件级别的调试工具可查看寄存器状态内核打印增强在关键函数添加pr_debug打印输出页表建立过程# 内核配置关键选项 CONFIG_ARM64_4K_PAGESy CONFIG_ARM64_VA_BITS_48y CONFIG_PGTABLE_LEVELS4调试小技巧在开始跟踪前我建议先在内核启动参数中添加memmap2G$0x80000000这样的参数这样可以预留一块已知的物理内存区域用于测试映射避免与内核已有内存区域冲突。2. ARM64页表基础四层架构解析ARM64架构采用四级页表结构PGD/PUD/PMD/PTE对应ARM手册中的L0-L3描述符。理解每一级页表的作用是调试的基础。在4KB页面大小、48位虚拟地址空间的典型配置下页表级别描述符位数索引位数管理范围条目数量PGD (L0)47:399512GB512PUD (L1)38:3091GB512PMD (L2)29:2192MB512PTE (L3)20:1294KB512页表描述符关键位解析Bit 0 (Valid)1表示有效条目0表示无效Bit 1 (Type)0表示块描述符1表示下一级页表Bits [47:12]对于4KB页面的物理地址在调试过程中我经常使用以下命令检查页表内容// 打印PGD条目内容 #define pgd_index(addr) (((addr) PGDIR_SHIFT) (PTRS_PER_PGD - 1)) pgd_t *pgd pgd_offset(current-mm, addr); pr_debug(PGD at %p: 0x%llx, pgd, pgd_val(*pgd));3. 从__create_pgd_mapping开始的调试之旅__create_pgd_mapping是ARM64内核中建立页表映射的入口函数我的调试正是从这里开始。这个函数接受以下关键参数static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys, unsigned long virt, phys_addr_t size, pgprot_t prot, phys_addr_t (*pgtable_alloc)(int), int flags)调试步骤分解锁定机制验证函数首先获取fixmap_lock互斥锁确保页表修改的原子性地址对齐检查通过WARN_ON验证虚拟地址和物理地址的页内偏移是否一致边界计算计算结束地址end PAGE_ALIGN(virt size)PGD条目处理循环处理每个PGD条目调用alloc_init_pud在实际调试中我发现一个常见错误是忽略了对齐检查。当虚拟地址和物理地址的页内偏移不一致时内核会触发警告[ 0.000000] WARNING: CPU: 0 PID: 0 at arch/arm64/mm/mmu.c:123 __create_pgd_mapping_locked0x58/0x1b8 [ 0.000000] Modules linked in: [ 0.000000] CPU: 0 PID: 0 Comm: swapper Not tainted 6.10.0 #14. 深入页表建立流程关键函数分析4.1 alloc_init_pudPUD级别的处理alloc_init_pud函数负责处理PUDPage Upper Directory级别的页表项。在调试过程中我发现几个值得注意的行为PUD表分配当PGD条目为空时会通过pgtable_alloc分配新的PUD表大页支持当满足1GB对齐时会尝试建立1GB的大页映射权限控制通过NO_EXEC_MAPPINGS标志控制可执行权限// 典型的PUD表分配代码路径 if (p4d_none(p4d)) { p4dval_t p4dval P4D_TYPE_TABLE | P4D_TABLE_UXN; phys_addr_t pud_phys pgtable_alloc(PUD_SHIFT); __p4d_populate(p4dp, pud_phys, p4dval); }调试技巧在验证大页映射时可以通过检查描述符的bit[1]来确定是块映射还是下一级页表if (pud_sect(pud)) { pr_debug(1GB block mapping at virt 0x%lx to phys 0x%llx, addr, pud_pfn(*pudp) PAGE_SHIFT); }4.2 alloc_init_cont_pmd与init_pmdPMD级别的处理PMDPage Middle Directory处理是页表建立中最复杂的部分之一因为它同时支持2MB大页和连续的PTE映射。在调试一个DMA缓冲区映射问题时我发现了以下关键点连续映射优化通过CONT_PMD_MASK检查地址是否适合使用连续映射权限继承连续映射会继承基础权限并添加PTE_CONT标志块映射回退当无法使用连续映射时回退到普通PTE映射// 连续PMD映射的判断条件 if ((((addr | next | phys) ~CONT_PMD_MASK) 0) (flags NO_CONT_MAPPINGS) 0) { __prot __pgprot(pgprot_val(prot) | PTE_CONT); }性能考量在实际测试中使用连续PMD映射可以减少TLB miss约15-20%特别是在频繁访问的大型缓冲区上效果明显。4.3 init_pte最终的页表项建立PTEPage Table Entry级别是页表建立的最后一步。在调试过程中我特别注意了以下细节物理地址转换使用__phys_to_pfn将物理地址转换为页帧号权限检查确保已有的PTE条目只修改权限位而非整个条目固定映射使用pte_set_fixmap_offset建立临时映射来修改PTE// 典型的PTE设置代码 ptep pte_set_fixmap_offset(pmdp, addr); set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot)); pte_clear_fixmap();调试陷阱在早期调试中我曾忽略pte_clear_fixmap的调用导致后续的固定映射区域异常。这是一个容易忽视但后果严重的问题。5. 实战调试案例解决自定义驱动内存映射问题在实际项目中我们需要为一块特定的硬件加速器建立特殊的内存映射。以下是调试过程中遇到的问题和解决方案问题现象驱动加载时出现内核oops提示无法访问0xffff800008000000地址。调试过程跟踪映射建立在__create_pgd_mapping设置断点观察目标地址的映射过程检查页表内容使用KGDB在崩溃地址处检查各级页表描述符发现异常PMD级别的描述符标记为无效但驱动预期应该有映射根本原因驱动代码错误地假设内核会自动建立映射而实际上需要先调用ioremap或memremap。解决方案// 正确的驱动映射代码 void __iomem *regs devm_memremap(pdev-dev, phys_addr, size, MEMREMAP_WB); if (IS_ERR(regs)) { dev_err(pdev-dev, Failed to remap region\n); return PTR_ERR(regs); }经验总结在ARM64架构中除了标准的页表建立流程外还需要特别注意设备内存属性使用正确的pgprot_t如pgprot_device缓存策略根据设备需求选择WB/WT/UC等缓存属性对齐要求确保物理地址符合大页或连续映射的对齐要求6. 高级调试技巧与性能优化在深入理解页表建立流程后可以应用一些高级调试技巧和优化方法6.1 页表转储与分析我开发了一个内核模块来转储特定地址范围的页表内容void dump_pagetables(unsigned long addr) { pgd_t *pgd; p4d_t *p4d; pud_t *pud; pmd_t *pmd; pte_t *pte; pgd pgd_offset(current-mm, addr); pr_info(PGD: %px, pgd); p4d p4d_offset(pgd, addr); if (p4d_none(*p4d)) { pr_info( P4D: none); return; } pud pud_offset(p4d, addr); if (pud_none(*pud)) { pr_info( PUD: none); return; } // 继续处理PMD和PTE... }6.2 大页映射优化在性能敏感的应用中主动使用大页映射可以显著减少TLB miss。以下是手动建立1GB大页映射的方法int create_1gb_mapping(unsigned long virt, phys_addr_t phys) { pgprot_t prot PAGE_KERNEL; pgd_t *pgd pgd_offset_k(virt); if (!pud_sect_supported()) return -EINVAL; if ((virt ~PUD_MASK) || (phys ~PUD_MASK)) return -EINVAL; return __create_pgd_mapping(init_mm.pgd, phys, virt, PUD_SIZE, prot, early_pgtable_alloc, 0); }6.3 页表统计与监控通过/proc/meminfo可以监控大页使用情况但对于更细粒度的监控需要扩展内核HugePages_Total: 16 HugePages_Free: 16 HugePages_Rsvd: 0 HugePages_Surp: 0性能数据在我们的测试平台上使用2MB大页代替4KB页可使内存访问延迟降低约30%吞吐量提升25%。

更多文章