-
-
[原创]逆向进入内核时代之11.MMU内存管理(可能是最适合逆向人员的内核文章)
-
发表于: 2024-6-17 18:26 3522
-
前言
最近在讲解Linux内核kernel patch的实现原理, 其中有一个难点就是理解Linux Arm64 MMU(虚拟到物理地址映射), 也是Linux内核高峰之一:内存管理,我一直希望用一个简单的方式让大家能够明白, 而不是枯燥的看文档去理解。
·
md, 我终于找到了, 同时也学习到了一些奇怪的知识. 那就是通过DS5仿真环境, 调试MMU, 一目了然, 一切都变得轻松起来.
MMU-Demo源码
https://github.com/nzcv/arm64-mmu
飞书Link
Feishu link: https://qafu03tp9df.feishu.cn/wiki/G87RwptbQiPNI3kgeZiciQt6nyd Password: 6&5g2661
·
MMU概念简单,但细节复杂,难免有讲解不好的地方,微信或留言提供讲解视频
·
为什么学习MMU
- Linux内核在启动之初便快速创建了MMU页表, 并开启了MMU. 如果不对背后的原理掌握清楚很难理解其中背后的深意, 猜测背后的意图. 比如
1 2 3 4 5 6 7 8 9 10 11 12 | ENTRY(__enable_mmu) mrs x1, ID_AA64MMFR0_EL1 ubfx x2, x1, #ID_AA64MMFR0_TGRAN_SHIFT, 4 cmp x2, #ID_AA64MMFR0_TGRAN_SUPPORTED b.ne __no_granule_support update_early_cpu_boot_status 0 , x1, x2 adrp x1, idmap_pg_dir adrp x2, swapper_pg_dir msr ttbr0_el1, x1 / / load TTBR0 msr ttbr1_el1, x2 / / load TTBR1 isb msr sctlr_el1, x0 |
- 同时KernelPatch中页有下面的一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / pgtable uint64_t tcr_el1; asm volatile( "mrs %0, tcr_el1" : "=r" (tcr_el1)); uint64_t t1sz = tcr_el1 << 42 >> 58 ; / / bits(tcr_el1, 21 , 16 ) uint64_t va1_bits = 64 - t1sz; data - >va1_bits = va1_bits; uint64_t tg1 = tcr_el1 << 32 >> 62 ; / / bits(tcr_el1, 31 , 30 ) uint64_t page_shift = 12 ; if (tg1 = = 1 ) { page_shift = 14 ; } else if (tg1 = = 3 ) { page_shift = 16 ; } data - >page_shift = page_shift; static uint64_t __noinline get_or_create_pte(map_data_t * data, uint64_t va, uint64_t pa, uint64_t attr_indx) { ..... } |
只有理解MMU, 才能看懂别人的代码,同时攀越Linux内核高峰之一. 话不多说我们继续...
什么是DS5
ARM DS-5 是一个功能强大的开发工具,适用于从嵌入式设备到复杂 SoC(片上系统)的广泛开发需求。通过集成调试、性能分析和编译工具,DS-5 帮助开发者高效地开发和优化 ARM 平台上的软件。
·
我们在这里用到的最重要的功能就是DS-5 Debugger, 支持模拟硬件并图形化展示Cpu各个寄存器状态. 最重要的一点, 针对MMU提供了调试面板. 这里简单做一下展示.
- 硬件仿真选择
FVP 由 ARM 公司提供,能够模拟 ARM 处理器和系统,使开发者可以在虚拟环境中进行工作.
- 汇编调试窗体
- 内存展示窗体
- MMU窗体
详细展示页表输出地址和相关属性, 查看Memroy Map等. 你还可以输入地址调试MMU转换后的结果.
Question-Driven Learning
带着问题学习(Question-Driven Learning)是一种以问题为导向的学习方法,强调通过解决具体问题来促进知识的掌握和技能的发展.
这里我们采用带着问题去学习, 以看懂下面的代码为目标, 掌握MMU相关的基础知识.
https://github.com/nzcv/KernelPatchQEMU/blob/66b21ece29e61afc6b46a651f2e844d9259ab8b3/arch/arm64/kernel/head.S#L674C1-L685C19
1 2 3 4 5 6 | ENTRY(__enable_mmu) ..... msr ttbr0_el1, x1 / / load TTBR0 msr ttbr1_el1, x2 / / load TTBR1 isb msr sctlr_el1, x0 |
这里的代码带给我们几个疑惑:
- el1是什么?
- ttbr0/1是什么?
- sctlr_又是什么?
这些都跟MMU息息相关, 我们一个个问题解决.
前置了解(硬件特权级别)
在了解Arm MMU页表原理之前呢? 我们了解Arm的硬件特权级别, 在x86设计体系中叫做环. Arm也有类似的机制被称为Exception Level.
64 位 Arm 架构定义了以下特权级别:
- EL0 用于用户空间应用程序;
- EL1 用于操作系统内核;
- EL2 用于虚拟机管理程序;
- EL3 用于固件,它还充当安全世界 (TrustZone) 的守门人;
注意:EL是Exception Level的缩写。
(Q1: 通过上面的信息知道内核工作在EL1级别, 所以上面为什么操作的_el1的寄存器)
MMU-demo示例代码关注 EL1 和 EL2 之间的交互,但请注意,Arm 处理器将始终重置为最高实现的异常级别,即 Armv8-A 基础模型上的 EL3。这很重要,因为架构仅保证重置时的最小已知安全状态:
- MMU 和缓存在最高实现异常级别被禁用;
- 所有异步异常均在最高实现的异常级别被屏蔽;
就是这样;大多数其他系统寄存器在重置时具有架构上未知的值。这意味着我们不知道: - EL2 的 MMU 和缓存是否启用或禁用;
- EL2 是 32 位还是 64 位;
- EL2 及以下版本是否安全或不安全;
注意: 硬件重新上电之后, 会工作在EL3级别. 然后会一级一级降权到EL1级别. 因为本示例为裸机程序, 因此也有类似过程, 会降级到EL2级别. 所以操作的MMU相关寄存器会是_EL2, 这是一定要注意区别其中的差异.
什么是MMU?
MMU简单来说, 就是一个逻辑电路, 完成虚拟地址(logical address)到物理地址(physical address)的映射转换. 我们先来点儿理论知识:
MMU通过查表法, 实现虚拟地址到物理地址的映射功能, 一个虚拟地址有64位, 不同的位分别代表了不同的含义. 同时位信息受到配置寄存器控制. 但是不管怎么样? 记住就是个逐级别查表法, 没什么高深的技术.
·
如何实现这个功能的呢? 我们通过DS5的MMU调试可以知道,需要重点掌握如下几个知识点:
- SCTLR_ELX 寄存器
- TCR_ELX 寄存器
- TTBR_ELX 寄存器
- 页表(Tables)
- 页表类型
- 页表输出地址
- 页表属性
我们知道了MMU采用层级表机制, 那么问题接踵而至, 采用多少级, 每个Level占用多少位?
要解答此问题, 需要理解几个配置参数T0SZ 和 TG0,后面讲对应寄存器的时候也有详细讲解:
- T0SZ 决定了虚拟地址寻址空间大小。size = 2^(64-t0sz)
- TG0 决定了最粒度都大小。分别有4K(0b00),16K(0b10),64K(0b01)
MMU Demo使用: T0SZ=32, TG0=64K。则可以推理出:
PA:16位(2^16 = 64K) L3: 13位 L2: 13位
那么只需要2级页表即可。
##MMU关键知识1.层级转化
ARM64架构使用多级页表层次结构来管理地址转换:
- 第0级 (L0)
- 第1级 (L1)
- 第2级 (L2)
- 第3级 (L3)
每个级别可以映射不同大小的内存块或页面:
- L0 映射较大范围(通常是几个TB)
- L1 映射较小范围(GB级别)
- L2 映射更小范围(MB级别)
- L3 映射最小范围(KB级别,通常是4KB页面, 具体由TG0决定)
注意TG0和T0SZ通过决定了层级和每个层级占有多个bit,切记切记。
MMU关键知识2.TTBRx_ELx
既然MMU的实现时层级表,那么需要一个寄存器存储最开始的起始地址,是的TTBRx_ELx就是存储起始偏移的。
MMU关键知识3.TCR_ELx
这个寄存器是设置页表的一个重要的寄存器(它将控制所有级别的页表),它可以设置有效虚拟地址的位数(除了48bit外,还有42bit,39bit等),granule的大小(4/16/64KB)等等。
上图中,
TxSZ决定64 - 有效虚拟地址的位数;
TGx决定转换颗粒的大小;
IRGN/ORGN设置页表的cacheability;
SH设置页表的shareability等。总之,这个寄存器主要是对页表进行设置。
1 2 | LDR x1, = 0x80807520 / / program tcr on this CPU MSR tcr_el2, x1 |
- T0SZ 的含义
T0SZ 是 TCR_ELx(Translation Control Register for ELx)寄存器中的一个字段,用于指定虚拟地址空间的大小。它定义了地址转换时使用的虚拟地址位数。具体地,T0SZ 的值越大,虚拟地址空间越小,反之亦然。
MMU关键知识4.页表描述
层级表作为一个数组,每个项占有64位,最终最后2个bit标记了对应类型。其他分为了upper和lower attributes包含了,读写权限等,大家可以想想都是内存,为啥有的有可执行权限,有的却只有只读权限,是的就是它在作祟。
- 描述符格式
Each entry in the translation tables (from L0 to L3) is called a descriptor, and it can be either a block descriptor, a table descriptor, or a page descriptor (L3 only).
转换表(从L0到L3)中的每个条目称为描述符,它可以是块描述符、表描述符或页描述符(仅L3)。这些描述符的格式包括多个字段,用于控制MMU如何解释条目。以下是这些描述符中的常见字段:
·
描述符类型:指示描述符是无效的、块、表或页面。
- 00:无效描述符。
- 01:块描述符(L0、L1和L2)。
- 11:表描述符(L0、L1和L2)或页描述符(L3)。
输出地址:指向下一级表的物理地址或块/页的物理基地址。
访问权限 (AP):控制读/写权限。
- 00:无访问权限。
- 01:只读。
- 10:读/写。
- 11:读/写(EL0访问)。
可共享性 (SH):指示内存在不同处理器之间如何共享。
- 00:不可共享。
- 10:外部可共享。
- 11:内部可共享。
访问标志 (AF):指示内存区域是否已被访问。
- 0:未访问。
- 1:已访问。
非安全 (NS):指示内存是安全的还是非安全的。
- 0:安全。
- 1:非安全。
内存属性:定义缓存和其他内存属性。 - MAIR索引:内存属性索引,指向内存属性间接寄存器(MAIR)中的一个值。
禁止执行 (XN):控制执行权限。 - 0:允许执行。
- 1:禁止执行。
连续 (Contig):指示块是否是较大连续内存区域的一部分(仅对于块描述符)。
脏位 (DBM):用于软件管理的脏位跟踪(仅在某些情况下使用)。
- 各级描述符字段
第0级、第1级和第2级描述符
这些描述符可以指向下一级表或直接指向内存块。
- 表描述符(指向下一级表):
- 描述符类型:11
- 下一级表地址:下一级表的物理地址。
- 其他字段控制访问权限、可共享性等。
- 块描述符(映射内存块):
- 描述符类型:01
- 输出地址:块的物理基地址。
- 其他字段控制访问权限、内存属性等。
Block Desc:
1 2 | | 63 | 62 52 | 51 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |XN | Block Addr| Reserved |SH|AP|NS|AF |NG|XN|Cont| AttrIndx | Type | |
XN (bit 54):禁止执行位。
Block Addr (bits 47:30):块的物理地址。
SH (bits 9:8):可共享性字段。
AP (bits 7:6):访问权限字段。
NS (bit 5):非安全位。
AF (bit 4):访问标志。
AttrIndx (bits 3:1):内存属性索引(指向MAIR寄存器)。
Type (bits 1:0):描述符类型(01表示块)。
第3级描述符
这些描述符映射页面(通常是4KB、16KB或64KB页面)。
- 页描述符(映射内存页面):
- 描述符类型:11
- 输出地址:页面的物理基地址。
- 其他字段控制访问权限、内存属性等。
https://blog.csdn.net/dai_xiangjun/article/details/120138732(页表描述)
补充说明:
虽然ARMv8支持64bit的地址空间,但实际上最多只可以使用48bit的地址空间。[63 : 48]这16位是不会作为地址被使用的。在绝大多数情况下,这16位要么是0xFFFF要么是0x0000
[63 : 48] = 0xFFFF,那么它对应的虚拟地址将使用TTBR1_ELx
[63 : 48] = 0x0000,那么它对应的虚拟地址将使用TTBR0_ELx
寻找最低一级的页表(L0)
LDR x1, =0x90000000 // program ttbr0 on this CPU
MSR ttbr0_el2, x1
最终实践:MMU手动计算
https://www.cnblogs.com/jianhua1992/p/16852783.html(建议详细阅读)
在上面的代码开启了MMU之后, MMU就开始工作, 你所访问的所有内存地址都要经过MMU翻译. 通过简单的思考我们就可以得到一个结论:
0x0000000080006984(VA) ---> MMU ---> 0x0000000080006984(PA)
这里DS5给了我们一个非常好滴工具, 方便进行映射关系验证.
如果我们需要算出Output, 我们的需要熟练掌握MMU的计算原理:
MMU起始地址:TTBR0_EL2 = 0x90000000
Input: 0x0000000080006984
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [DEBUG] tcr_el2 / / / Size offset of the memory reagion addressed by ttbr0_el2 (size = 2 ^( 64 - t0sz)) [DEBUG] tcr_el2.t0sz = 32 / / / normal memory, inner write - back, read - allocate, write - allocate, cacheable [DEBUG] tcr_el2.irgn0 = 1 / / / normal memory, outer write - back, read - allocate, write - allocate, cacheable [DEBUG] tcr_el2.orgn0 = 1 / / / Shareability attribute for memory associated with tlb walks using ttbr0_el2 [DEBUG] tcr_el2.sh0 = 3 / / / Granule size for the ttbr0_el2 / / / _64KB = 0b10 [DEBUG] tcr_el2.tg0 = 1 [DEBUG] tcr_el2.res1[ 23 ] = 1 / / / Physical address size / / / 4GB address size / / / _32BITS = 0b000 , [DEBUG] tcr_el2.ps = 0 [DEBUG] tcr_el2.res1[ 31 ] = 1 [DEBUG] tcr_el2 = 0x80807520 |
通过tcr_el2寄存器我们可以知道Granule = 64KB, t0sz = 32bits = 4GB寻址空间. 那么如何分割虚拟地址呢??
- 最权威的就是去翻阅Arm64文档
- 肯定有人遇到了同样的问题(https://www.cnblogs.com/jianhua1992/p/16852783.html)
上面的示意图是针对48位的, 32位就是把前面的截断而已, 尝试一下...
写个程序验证下:
https://godbolt.org/z/frdcnzEod
通过fvp.S的注释我们知道level2的地址第4位索引映射到了0x000080000000-0x00009fffffff, 看起来是对上了我们继续.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | * This code programs the following translation table structure: * * level 2 table @ 0x90000000 * [ # 0]---------------------------\ * level 3 table @ 0x90010000 * [ #7177] 0x00001c090000-0x00001c09ffff, Device, UART0 * [ # 1]---------------------------\ * level 3 table @ 0x90020000 * [ #3072] 0x00002c000000-0x00002c00ffff, Device, GICC * [ #3584] 0x00002e000000-0x00002e00ffff, RW_Data, Non-Trusted SRAM * [ #3840] 0x00002f000000-0x00002f00ffff, Device, GICv3 GICD * [ #3856] 0x00002f100000-0x00002f10ffff, Device, GICv3 GICR * [ #3857] 0x00002f110000-0x00002f11ffff, Device, GICv3 GICR * [ #3858] 0x00002f120000-0x00002f12ffff, Device, GICv3 GICR * [ #3859] 0x00002f130000-0x00002f13ffff, Device, GICv3 GICR * [ #3860] 0x00002f140000-0x00002f14ffff, Device, GICv3 GICR * [ #3861] 0x00002f150000-0x00002f15ffff, Device, GICv3 GICR * [ #3862] 0x00002f160000-0x00002f16ffff, Device, GICv3 GICR * [ #3863] 0x00002f170000-0x00002f17ffff, Device, GICv3 GICR * [ #3864] 0x00002f180000-0x00002f18ffff, Device, GICv3 GICR * [ #3865] 0x00002f190000-0x00002f19ffff, Device, GICv3 GICR * [ #3866] 0x00002f1a0000-0x00002f1affff, Device, GICv3 GICR * [ #3867] 0x00002f1b0000-0x00002f1bffff, Device, GICv3 GICR * [ #3868] 0x00002f1c0000-0x00002f1cffff, Device, GICv3 GICR * [ #3869] 0x00002f1d0000-0x00002f1dffff, Device, GICv3 GICR * [ #3870] 0x00002f1e0000-0x00002f1effff, Device, GICv3 GICR * [ #3871] 0x00002f1f0000-0x00002f1fffff, Device, GICv3 GICR * [ # 4] 0x000080000000-0x00009fffffff, RW_Data, Non-Trusted DRAM * [ # 5] 0x0000a0000000-0x0000bfffffff, RW_Data, Non-Trusted DRAM * [ # 6] 0x0000c0000000-0x0000dfffffff, RW_Data, Non-Trusted DRAM * [ # 7] 0x0000e0000000-0x0000ffffffff, RW_Data, Non-Trusted DRAM |
我们去内存中找一下, 对应数据为0x80000701. 是的, 我们找到了页表项, 那么我们解析下这个页表项的含义.
目前可以确定的是我们在Level2, 那就可以参考下图了.
还是写一段代码, 解析后我们可以知道页表是一个Block同时, Output addresssh是0x80000000.
https://godbolt.org/z/qYeGGf1qv
因为已经触发到了Block了, 不需要继续索引了, 加上PA Offset我们看看??
Output: 0x80000000 + 0x6984 = 0x0000000080006984
完美, 打完收工......, 非常感谢网上那些优秀文章的作者, 让我不再恐惧.
HelpLink
https://ashw-archive.github.io/arm64-hypervisor-tutorial-1.html
https://www.twblogs.net/a/5c1f9719bd9eee16b3daad2a?lang=zh-cn
https://blog.csdn.net/weixin_42585034/article/details/102214784
微信
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课