首页
社区
课程
招聘
[原创]逆向进入内核时代之11.MMU内存管理(可能是最适合逆向人员的内核文章)
发表于: 2024-6-17 18:26 3378

[原创]逆向进入内核时代之11.MMU内存管理(可能是最适合逆向人员的内核文章)

2024-6-17 18:26
3378

前言

最近在讲解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

  1. 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
  1. 同时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提供了调试面板. 这里简单做一下展示.

  1. 硬件仿真选择
    FVP 由 ARM 公司提供,能够模拟 ARM 处理器和系统,使开发者可以在虚拟环境中进行工作.
    图片描述
  2. 汇编调试窗体
    图片描述
  3. 内存展示窗体
    图片描述
  4. 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

这里的代码带给我们几个疑惑:

  1. el1是什么?
  2. ttbr0/1是什么?
  3. 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调试可以知道,需要重点掌握如下几个知识点:

  1. SCTLR_ELX 寄存器
  2. TCR_ELX 寄存器
  3. TTBR_ELX 寄存器
  4. 页表(Tables)
  5. 页表类型
  6. 页表输出地址
  7. 页表属性

图片描述
我们知道了MMU采用层级表机制, 那么问题接踵而至, 采用多少级, 每个Level占用多少位?
要解答此问题, 需要理解几个配置参数T0SZ 和 TG0,后面讲对应寄存器的时候也有详细讲解:

  1. T0SZ 决定了虚拟地址寻址空间大小。size = 2^(64-t0sz)
  2. 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架构使用多级页表层次结构来管理地址转换:

  1. 第0级 (L0)
  2. 第1级 (L1)
  3. 第2级 (L2)
  4. 第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

图片描述

  1. T0SZ 的含义
    T0SZ 是 TCR_ELx(Translation Control Register for ELx)寄存器中的一个字段,用于指定虚拟地址空间的大小。它定义了地址转换时使用的虚拟地址位数。具体地,T0SZ 的值越大,虚拟地址空间越小,反之亦然。

图片描述

MMU关键知识4.页表描述

层级表作为一个数组,每个项占有64位,最终最后2个bit标记了对应类型。其他分为了upper和lower attributes包含了,读写权限等,大家可以想想都是内存,为啥有的有可执行权限,有的却只有只读权限,是的就是它在作祟。

  1. 描述符格式
    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):用于软件管理的脏位跟踪(仅在某些情况下使用)。
  1. 各级描述符字段

第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寻址空间. 那么如何分割虚拟地址呢??

  1. 最权威的就是去翻阅Arm64文档
  2. 肯定有人遇到了同样的问题(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

微信

图片描述

最后于 2024-6-18 08:03 被周晓梦Chew编辑 ,原因:
收藏
免费 6
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//