mmu是arm64芯片上的一个内存管理单元,使用mmu的机制可以快速通过硬件将虚拟地址转换成物理地址,本文介绍crash工具的使用,可以比较方便解析内核的结构,故根据虚拟地址向物理地址转换的过程来梳理掌握crash工具的使用,并复习一下内存的基本知识。
对于arm64机器的页表设置,每个机器的环境不一致,但是原理都是一致的,本文机器环境如下
CONFIG_ARM64_VA_BITS_39=y CONFIG_ARM64_4K_PAGES=y CONFIG_PGTABLE_LEVELS=3
为了找到符合我机器的说明,我翻了很早的内核版本docs,可以参考信息如下
AArch64 Linux memory layout with 4KB pages + 3 levels: Start End Size Use ----------------------------------------------------------------------- 0000000000000000 0000007fffffffff 512GB user ffffff8000000000 ffffffffffffffff 512GB kernel Translation table lookup with 4KB pages: +--------+--------+--------+--------+--------+--------+--------+--------+ |63 56|55 48|47 40|39 32|31 24|23 16|15 8|7 0| +--------+--------+--------+--------+--------+--------+--------+--------+ | | | | | | | | | | | v | | | | | [11:0] in-page offset | | | | +-> [20:12] L3 index | | | +-----------> [29:21] L2 index | | +---------------------> [38:30] L1 index | +-------------------------------> [47:39] L0 index +-------------------------------------------------> [63] TTBR0/1
根据上面可以知道,当前环境最大内存大小是512GB,因为是三级页表,所以这里默认没有L0 index [47:39]
。为了概念的统一,我这里将三级页表也叫做Level 1和Level 2和Level 3以及 in-page offset,同四级页表一个叫法。
关于页表的转换,手册默认是按照四级页表计算,如下图示
我们根据三级页表计算,下面开始演示。
为了拿到一个已经映射的地址,我们可以加载一个ko,用其中的数据地址,或者直接使用某个进程的task_struct内存地址,本文获取的是test.ko启动kthread的task_struct
5190 2 5 ffffff80c0002b80 RU 0.0 0 0 [spinlock_thread]
可以看到此task_struct地址是ffffff80c0002b80,下面开始转换
对于上面的虚拟地址,我们按位可以解析如下
可以得到pgd_offset=3,pmd_offset=0,pte_offset=0xb80。并且我们知道其结构体大小如下
crash> struct pgd_t -x typedef struct { pgdval_t pgd; } pgd_t; SIZE: 0x8 crash> struct pud_t -x typedef struct { p4d_t p4d; } pud_t; SIZE: 0x8 crash> struct pmd_t -x typedef struct { pmdval_t pmd; } pmd_t; SIZE: 0x8 crash> struct pte_t -x typedef struct { pteval_t pte; } pte_t; SIZE: 0x8
可以看到,因为我们是64位系统,存放一个页表的size就是8,而我们页表换算的时候,是按照基地址+offset计算的,这个offset值的是多少个地址,而不是地址的size,所以我们获得基地址之后,计算下一级页表的存放物理地址的时候,应该乘上size,也就是8,因为实际存放这个64位地址要占用8个字节,那么这里我们就可以知道信息如下
我们获取的是内核的地址,那么其基地址来源于TTBR1_EL1,对于内核,其默认值存放在变量init_mm.pgd中,我们可以查看如下
crash> p init_mm.pgd $1 = (pgd_t *) 0xffffffc009eea000
我们可以根据这个基地址开始计算pgd
pgd的地址就是TTBR的地址 加上 pgd_offset * 8 ,所以得出如下
0xffffffc009eea018 = 0xffffffc009eea000 + 0x3 * 8
此时读取其值就是下一级页表的基地址
crash> rd ffffffc009eea018 -x ffffffc009eea018: 00000001ff20f003
值得注意的是,虚拟地址只是操作系统抽象的东西,所以存放的下一级页表的基地址是物理地址,不是虚拟地址,我们拿到地址 0x00000001ff20f003
对于arm64而言,内存不是恒等映射的,而是线性映射,也就是我们拿到的物理地址,其虚拟地址通常是 加上 ffffff8000000000
的offset 的线性映射的,所以我们拿到的地址0x00000001ff20f003 的虚拟地址是
0xffffff81ff20f003 = 0x00000001ff20f003 + 0xffffff8000000000
我们验证一下线性映射地址即可,如下
crash> ptov 0x00000001ff20f003 VIRTUAL PHYSICAL ffffff81ff20f003 1ff20f003
这里虚拟地址的bit [0:1]
用来表示是Block/Page,这里是3,也就是Page,我们寻找下一级页表。
pmd的地址是 对应的虚拟地址 去掉 bit0和bit1 后 加上 offset,这么说可能有点绕,转换计算公式如下
0xffffff81ff20f000 = 0xffffff81ff20f003 & ~0x3 + 0x0 * 8
然后读取其值即可获得下一级页表基地址的物理地址
crash> rd ffffff81ff20f000 -x ffffff81ff20f000: 00000001ff20e003
同样的,这里通过命令转换,当然也可以自己手动加上0xffffff8000000000的线性映射的offset。
crash> ptov 1ff20e003 VIRTUAL PHYSICAL ffffff81ff20e003 1ff20e003
这里看到还是3,说明不是Block,而是Page。
pmd的地址计算和pud一致,如下
0xffffff81ff20e010 = 0xffffff81ff20e003 & ~0x3 + 0x2 * 8
此时读取下一级页表
crash> rd ffffff81ff20e010 -x ffffff81ff20e010: 00680000c0002707
这里我们接下来读的是pte的地址,0x00680000c0002707 具备upper attr和lower attr。关于页属性的描述如下
对于pte地址,其值是0x00680000c0002707,那么其属性值如下表示
除了属性值,其他部分就是物理页面基地址了,我们需要去掉属性值,提取物理页面基地址,那么如下
680000c0002707 & ~0xfff = 680000c0002000 # 去掉低12位 680000c0002000 & 0x7fffffffff = c0002000 # 去掉高25位
通过上面的pte地址,去掉属性之后,我们得到了物理页也就是0xc0002000,我们知道page-in offset是0xb80,那么真实的页就是
0xc0002b80 = 0xc0002000 + 0xb80
这里就计算出来了虚拟地址对于的物理页地址了。
同样的,在crash中,默认提供的vtop会自动帮我们计算,vtop的结果如下
crash> vtop ffffff80c0002b80 VIRTUAL PHYSICAL ffffff80c0002b80 c0002b80 PAGE DIRECTORY: ffffffc009eea000 PGD: ffffffc009eea018 => 1ff20f003 PMD: ffffff81ff20f000 => 1ff20e003 PTE: ffffff81ff20e010 => 680000c0002707 PAGE: c0002000 PTE PHYSICAL FLAGS 680000c0002707 c0002000 (VALID|SHARED|AF|PXN|UXN) PAGE PHYSICAL MAPPING INDEX CNT FLAGS ffffffff02e00080 c0002000 dead000000000400 0 0 0
可以看到,我们手动计算的地址转换过程和vtop提供的信息一样。
上面已经介绍过转换过程了,下面用户地址为例的转换就快速过一下。做个验证
为了测试,提供一个简单的代码
#include <stdio.h> #include <unistd.h> char str[] = "hello kylin"; int main() { printf("vtop -c %d %p [%s] \n", getpid(), str, str); while(1); return 0; }
我们拿到信息如下
vtop -c 13725 0x411038 [hello kylin] crash> vtop -c 13725 0x411038 VIRTUAL PHYSICAL 411038 8c2c3038 PAGE DIRECTORY: ffffff81f262b000 PGD: ffffff81f262b000 => 1f2c5a003 PMD: ffffff81f2c5a010 => 1f5fd0003 PTE: ffffff81f5fd0088 => e800008c2c3f43 PAGE: 8c2c3000 PTE PHYSICAL FLAGS e800008c2c3f43 8c2c3000 (VALID|USER|SHARED|AF|NG|PXN|UXN|DIRTY) VMA START END FLAGS FILE ffffff805b21a840 411000 412000 100873 /root/test PAGE PHYSICAL MAPPING INDEX CNT FLAGS ffffffff0210b0c0 8c2c3000 ffffff806d097b29 1 1 80014 uptodate,lru,swapbacked
此时知道了物理地址是0x8c2c3038,读取内容即可
crash> rd -8 -p 8c2c3038 16 8c2c3038: 68 65 6c 6c 6f 20 6b 79 6c 69 6e 00 00 00 00 00 hello kylin.....
一切正常,下面开始手动推导
我们先获取基地址,对于进程,基地址在task_struct的struct_mm的pgd
crash> set 13725 PID: 13725 COMMAND: "test" TASK: ffffff81f0a09d00 [THREAD_INFO: ffffff81f0a09d00] CPU: 7 STATE: TASK_RUNNING (ACTIVE)
这里我们拿到了task_struct的地址是ffffff81f0a09d00,那么mm的值是0xffffff81f5fa2ec0
struct task_struct { [1160] struct mm_struct *mm; } crash> struct task_struct.mm ffffff81f0a09d00 -o struct task_struct { [ffffff81f0a0a188] struct mm_struct *mm; } crash> struct task_struct.mm ffffff81f0a09d00 -x mm = 0xffffff81f5fa2ec0
此时我们获得了进程的pgd地址如下
crash> struct mm_struct.pgd 0xffffff81f5fa2ec0 pgd = 0xffffff81f262b000
我们拿到地址是0xffffff81f262b000,此时虚拟地址是0x411038,计算如下
ffffff81f262b000 = ffffff81f262b000 + 0 crash> rd ffffff81f262b000 -x ffffff81f262b000: 00000001f2c5a003 crash> ptov 00000001f2c5a003 VIRTUAL PHYSICAL ffffff81f2c5a003 1f2c5a003
我们拿到地址0xffffff81f2c5a003,继续计算
ffffff81f2c5a010 = ffffff81f2c5a003 & ~0x3 + 0x2 * 8 crash> rd ffffff81f2c5a010 -x ffffff81f2c5a010: 00000001f5fd0003 crash> ptov 00000001f5fd0003 VIRTUAL PHYSICAL ffffff81f5fd0003 1f5fd0003
我们拿到地址0xffffff81f5fd0003,最后计算pte
ffffff81f5fd0088 = ffffff81f5fd0003 & ~0x3 + 0x11 * 8 crash> rd ffffff81f5fd0088 -x ffffff81f5fd0088: 00e800008c2c3f43 crash> ptov 00e800008c2c3f43 VIRTUAL PHYSICAL e7ff808c2c3f43 e800008c2c3f43
我们拿到了e800008c2c3f43的地址,需要去掉属性bit,计算可得
e800008c2c3f43 & ~0xfff = e800008c2c3000 # 去掉低12位 e800008c2c3f43 & 0x7fffffffff = 8c2c3000 # 去掉高25位
此时物理页起始是0x8c2c3000,对于数据的物理地址是
8c2c3038 = 8c2c3000 + 0x38
这样我们就计算出来物理页地址了,我们读取其内容验证一下
crash> rd -8 -p 8c2c3038 16 8c2c3038: 68 65 6c 6c 6f 20 6b 79 6c 69 6e 00 00 00 00 00 hello kylin.....
可以发现,其物理地址存放的内容正是我们设置的 "hello kylin"。 实验完成。
根据上面的内容,我们通过物理地址和虚拟地址将页表的转换过了一遍,借助工具crash可以随时挂上内核的kcore进行调试判断,不会像gdb内核一样复杂并且不方便。