环境与准备
- 处理器架构: arm64
- 内核源码: linux-6.6.29
- Ubuntu版本: 20.04.1
- 代码阅读工具: vim+ctags+cscope
本文将演示如何通过crash工具手动遍历页表,解析一个用户态虚拟地址对应的物理地址。这不仅有助于理解Linux内存管理机制,也是调试内存相关问题的必备技能。
主要内容如下:
- 准备crash环境
- 通过手动页表遍历查找物理地址
- 使用
vtop命令快速查找物理地址
- 读取页面内容进行验证
1. 准备crash环境
1.1 下载编译crash工具
1) 下载
git clone https://github.com/crash-utility/crash.git
2)编译
make target=arm64
1.2 准备测试代码
我们首先编写一个简单的C程序来生成一个需要追踪的虚拟地址。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#define MAP_SIZE 4096
#define handle_err(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while(0)
int main(int argc, char **argv)
{
char *addr;
addr = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS| MAP_PRIVATE, -1, 0);
if (addr == MAP_FAILED)
handle_err("fail to mmap");
printf("@ addr:%lx @\n", (unsigned long)addr);
memset(addr, 0x55, MAP_SIZE);
while(1);
return 0;
}
程序逻辑很简单:通过mmap申请4KB的匿名内存,打印出起始虚拟地址,然后用0x55填充这块内存(此时内核才真正分配物理页),最后通过while(1)让进程挂起,防止退出后内存被释放。
在QEMU中后台运行此程序:
[root@liebao mnt]# ./pt_walk &
[root@liebao mnt]# @ addr:ffffb1849000 @
我们得到了目标虚拟地址:ffffb1849000。
1.3 获取ramdump
在QEMU监控器中(快捷键 Ctrl-a + c),执行以下命令创建内存转储文件:
dump-guest-memory crash.img
1.4 使用crash解析ramdump
使用编译好的crash工具加载转储文件和内核镜像:
crash -m vabits_actual=48 crash.img ./vmlinux
2. 通过手动页表遍历查找物理地址
本节将逐步演示如何从进程的页表根(PGD)开始,逐级查找,最终得到物理地址。如果你想深入了解操作系统的页表管理机制,可以参考相关文章: Linux内核页表管理-那些鲜为人知的秘密。
2.1 确认页表级数
首先确认当前内核配置的页表级数。
crash> sys config | grep PGTABLE_LEVELS
CONFIG_PGTABLE_LEVELS=4
结果显示为4级页表,因此我们的遍历顺序是:PGD -> PUD -> PMD -> PTE。
2.2 获取进程的PGD页表地址
PGD(页全局目录)是页表遍历的起点,它存储在进程的mm_struct中。我们先找到目标进程的mm_struct地址,再从中提取pgd。
crash> task mm
PID: 83 TASK: ffff0000039a3b00 CPU: 2 COMMAND: "pt_walk"
mm = 0xffff000003998980,
crash> mm_struct.pgd 0xffff000003998980
pgd = 0xffff0000039d6000,
因此,进程的PGD页表基地址为:0xffff0000039d6000。
2.3 计算PGD表项
1. 计算PGD索引
对于4级页表,虚拟地址(VA)中用于索引PGD表项的位是 [47:39](共9位)。计算公式为:
idx = (VA >> (9+9+9+12)) & 0x1ff
crash> p/x (0xffffb1849000 >> (9+9+9+12)) & 0x1ff
$1 = 0x1ff
2. 获取PGD表项地址
PGD中每个表项占8字节,因此目标表项地址为:PGD_BASE + idx * 8。
crash> p/x 0xffff0000039d6000 + 0x1ff*8
$17 = 0xffff0000039d6ff8
3. 读取PGD表项内容并解析下一级地址
使用rd命令读取该地址内容,得到页表项(PTE)值。
crash> rd 0xffff0000039d6ff8
ffff0000039d6ff8: 08000000439d3003 .0.C....
使用pte命令解析这个PTE值,获取其指向的下一级页表(PUD)的物理地址。
crash> pte 08000000439d3003
PTE PHYSICAL FLAGS
8000000439d3003 439d3000 (VALID)
4. 将物理地址转换为内核虚拟地址
为了方便后续访问,我们用ptov命令将其转换为内核虚拟地址。
crash> ptov 439d3000
VIRTUAL PHYSICAL
ffff0000039d3000 439d3000
至此,我们得到了下一级(PUD)页表的基地址:ffff0000039d3000。
2.4 计算PUD表项
1. 计算PUD索引
虚拟地址中用于索引PUD表项的位是 [38:30]。公式:
idx = (VA >> (9+9+12)) & 0x1ff
crash> p/x (0xffffb1849000>>(9+9+12)) & 0x1ff
$19 = 0x1fe
2. 获取PUD表项地址并解析
crash> p/x 0xffff0000039d3000 + 0x1fe*8
$20 = 0xffff0000039d3ff0
crash> rd 0xffff0000039d3ff0
ffff0000039d3ff0: 0800000043aad003 ...C....
crash> pte 0800000043aad003
PTE PHYSICAL FLAGS
800000043aad003 43aad000 (VALID)
crash> ptov 43aad000
VIRTUAL PHYSICAL
ffff000003aad000 43aad000
我们得到了下一级(PMD)页表的基地址:ffff000003aad000。
2.5 计算PMD表项
1. 计算PMD索引
虚拟地址中用于索引PMD表项的位是 [29:21]。公式:
idx = (VA >> (9+12)) & 0x1ff
crash> p/x (0xffffb1849000>>(9+12)) & 0x1ff
$21 = 0x18c
2. 获取PMD表项地址并解析
crash> p/x 0xffff000003aad000 +0x18c*8
$22 = 0xffff000003aadc60
crash> rd 0xffff000003aadc60
ffff000003aadc60: 0800000043ab3003 .0.C....
crash> pte 0800000043ab3003
PTE PHYSICAL FLAGS
800000043ab3003 43ab3000 (VALID)
crash> ptov 43ab3000
VIRTUAL PHYSICAL
ffff000003ab3000 43ab3000
我们得到了最后一级(PTE)页表的基地址:ffff000003ab3000。
2.6 计算PTE表项并获取物理地址
1. 计算PTE索引
虚拟地址中用于索引PTE表项的位是 [20:12]。公式:
idx = (VA >> (12)) & 0x1ff
crash> p/x (0xffffb1849000>> 12) & 0x1ff
$23 = 0x49
2. 获取PTE表项地址并解析最终物理页地址
crash> p/x 0xffff000003ab3000 + 0x49*8
$24 = 0xffff000003ab3248
crash> rd 0xffff000003ab3248
ffff000003ab3248: 00e80000424f7f43 C.OB....
crash> pte 00e80000424f7f43
PTE PHYSICAL FLAGS
e80000424f7f43 424f7000 (VALID|USER|SHARED|AF|NG|PXN|UXN|DIRTY)
pte命令已经直接显示了物理页框的基地址。我们再次用ptov验证其映射关系。
crash> ptov 424f7000
VIRTUAL PHYSICAL
ffff0000024f7000 424f7000
结论:通过手动遍历4级页表,我们最终确定用户虚拟地址 ffffb1849000 对应的物理地址为 424f7000。
3. 通过vtop命令快速查找物理地址
手动遍历对于理解原理很有帮助,但crash工具已经提供了更便捷的命令 vtop (Virtual TO Physical) 来完成这个任务。
3.1 设置目标进程上下文
在使用vtop前,需要先切换到目标进程的上下文。
crash> ps | grep pt_walk
> 83 80 2 ffff0000039a3b00 RU 0.0 1812 932 pt_walk
crash> set 83
PID: 83
COMMAND: "pt_walk"
TASK: ffff0000039a3b00 [THREAD_INFO: ffff0000039a3b00]
CPU: 2
STATE: TASK_RUNNING (ACTIVE)
3.2 使用vtop查看物理地址
现在直接对虚拟地址使用vtop命令。
crash> vtop ffffb1849000
VIRTUAL PHYSICAL
ffffb1849000 424f7000
PAGE DIRECTORY: ffff0000039d6000
PGD: ffff0000039d6ff8 => 8000000439d3003
PUD: ffff0000039d3ff0 => 800000043aad003
PMD: ffff000003aadc60 => 800000043ab3003
PTE: ffff000003ab3248 => e80000424f7f43
PAGE: 424f7000
PTE PHYSICAL FLAGS
e80000424f7f43 424f7000 (VALID|USER|SHARED|AF|NG|PXN|UXN|DIRTY)
VMA START END FLAGS FILE
ffff00000397ce70 ffffb1849000 ffffb184c000 100073
PAGE PHYSICAL MAPPING INDEX CNT FLAGS
fffffc0000093dc0 424f7000 ffff0000039b13a9 ffffb1849 2 3fffe00000a0008 uptodate,mappedtodisk,swapbacked
命令输出不仅给出了物理地址 424f7000,还列出了从PGD到PTE各级页表项的地址和内容,与我们手动遍历的结果完全一致,验证了我们之前的手动分析是正确的。
4. 读取页面内容进行验证
最后,我们可以使用rd命令读取该虚拟地址(或对应的内核直接映射区地址)的内容,验证是否为我们程序填充的0x55。
crash> rd ffffb1849000 -64 0x1000
ffffb1849000: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849010: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849020: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849030: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849040: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849050: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849060: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849070: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849080: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849090: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18490a0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18490b0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18490c0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18490d0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18490e0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18490f0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849100: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849110: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849120: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849130: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849140: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849150: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849160: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849170: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849180: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb1849190: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18491a0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18491b0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18491c0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
ffffb18491d0: 5555555555555555 5555555555555555 UUUUUUUUUUUUUUUU
输出显示,整个页面的内容都是0x55,与我们程序中的memset(addr, 0x55, MAP_SIZE);操作相符,完整地验证了整个从虚拟地址到物理地址的映射以及数据内容的一致性。
通过本次实战,我们不仅掌握了使用crash工具进行内存管理核心操作的两种方法(手动遍历与vtop命令),也加深了对Linux内核页表工作原理的理解。这类技能对于内核调试、性能分析和安全研究都至关重要。如果你想深入探讨更多内核或系统级调试技术,欢迎在云栈社区交流分享。