☰
SMMU 深度解析:从地址翻译到安全隔离的架构枢纽
2026/10/29 7:15:10 网站建设 项目流程

简介:面向ARM SoC与嵌入式底层开发者的SMMU系统学习资料,聚焦armv8/armv9架构下System MMU的地址转换机制与安全应用,适合具备MMU、页表与TrustZone基础、希望深入理解设备侧内存管理的工程师。内容围绕SMMU的timeline与使用模型展开,涵盖非连续物理地址映射、32位Master访问64位空间、Master访问权限过滤、用户态驱动隔离以及设备虚拟化等典型场景,并深入TBU与TCU硬件结构、StreamID、STE与CD表、两级地址翻译流程及安全/非安全双套寄存器等关键知识点。资源包共1个PDF文件,约1.53MB,结构按概念梳理、原理剖析、寄存器与总结递进组织,便于按章节系统阅读。已有252人学习,适合作为SMMU入门到原理理解的参考材料。

1. SMMU:为什么一个“给设备用的 MMU”会变成安全架构的枢纽

很多人第一次接触 SMMU,会把它当成“DMA 版的 MMU”——设备发虚拟地址,SMMU 查页表转成物理地址,仅此而已。这个理解不算错,但漏掉了它真正的分量。SMMU 全称 System Memory Management Unit,属于 Arm 的 System IP,页表格式和 Core MMU 完全一致,理论上可以和 CPU 共用同一套页表。可一旦它把 StreamID、STE、Context Descriptor、Stage 1/Stage 2 这些概念叠上去,它就不再是“翻译器”,而是设备侧内存访问的唯一闸门:谁能访问哪块内存、以什么权限访问、走安全还是非安全通道,全在这里裁决。这也是为什么做 TrustZone、TEE、用户态驱动、设备直通的人,早晚都得把 SMMU 的机制啃透——它不是可选项,是绕不过去的底层设施。

2. 从 MMU 到 SMMU:多 Master 场景逼出来的两级查表结构

2.1 一个 Core 一个 MMU,多个 Master 怎么办

CPU 侧的内存管理很干净:一个 Core 配一个 MMU,MMU 里挂一套页表基址寄存器(TTBRx)加一个翻译控制寄存器(TCR),当前进程用哪套页表,切寄存器就行。问题在于,SoC 里不止 CPU 会访问内存。DMA 控制器、VPU、DPU、各种加速器都是 Master,它们发起访问时没有“当前进程”这个概念,也没有寄存器可以随时切换页表。

如果照搬 MMU 的思路,给每个 Master 配一个 MMU,硬件面积和软件复杂度都会失控。SMMU 的解法是把“区分是谁发的请求”这件事下沉到总线协议里:每个 Master 发出的 transaction 带一个 StreamID,SMMU 拿 StreamID 去查一张表,找到这个 Master 该用的翻译配置。这样一块 SMMU 就能服务多个 Master,每个 Master 各有一套独立的地址空间视图。

这里的关键设计动机是:MMU 的“上下文”由 CPU 的执行流隐式决定,而 SMMU 的“上下文”必须由请求本身显式携带。StreamID 就是这个显式上下文标识。常见的实现思路是,SoC 集成时给每个 Master 的端口静态分配 StreamID 位段,软件侧再按这个分配去建表。

2.2 TBU + TCU:SMMU 内部的职责切分

SMMU 硬件由两部分组成:TBU(Translation Buffer Unit)和 TCU(Translation Control Unit)。TBU 靠近 Master 侧,里面含 TLB,负责缓存已经翻译过的地址;TCU 是全局的翻译控制单元,负责查表、走页表遍历、管理 STE 和 CD 的缓存。多个 TBU 可以挂到一个 TCU 上,TBU 之间通过 DTI(内部总线协议)和 TCU 通信。

这个切分不是随意为之。TBU 放在靠近 Master 的位置,命中 TLB 时翻译延迟极低,适合 DMA 这种高吞吐场景;TCU 集中管理页表遍历和配置缓存,避免每个 Master 都复制一份查表逻辑。ACE-Lite TBU 和 LTI TBU 是两种不同的 Master 接口形态,前者面向具备一致性能力的 Master,后者面向更轻量的本地翻译接口。从软件视角看,这些硬件细节被寄存器抽象掉了,但理解 TBU/TCU 的分工,能解释为什么 TLB 失效的代价在 SMMU 上比在 CPU MMU 上更值得关注——一次 miss 要走 DTI 到 TCU,再走内存里的多级表。

2.3 Stream Table:StreamID 到 STE 的索引

Stream Table 是 SMMU 翻译流程的第一级索引。它的基址由SMMU_STRTAB_BASE给出,表的结构由SMMU_STRTAB_BASE_CFG配置。这里有个容易忽略的点:Stream Table 有两种形态,Linear Stream Table 和 Multi-level Stream Table,选哪种由STRTAB_BASE_CFG里的位段决定。

Linear 形态最直白:StreamID 的数值直接作为下标,查到对应的 STE(Stream Table Entry)。实现简单,查一次就到,但表的大小和 StreamID 空间成正比,StreamID 位宽大而实际使用的 ID 稀疏时,内存浪费明显。Multi-level 形态把 StreamID 拆成多段,逐级索引,类似多级页表,用空间换稀疏性。

/* 伪代码:Linear Stream Table 的索引逻辑 */ /* strtab_base 来自 SMMU_STRTAB_BASE,按 CFG 对齐 */ uint64_t *ste_base = (uint64_t *)strtab_base; uint32_t ste_index = stream_id; /* Linear:StreamID 直接当下标 */ uint64_t *ste = &ste_base[ste_index * STE_SIZE]; /* Multi-level:先取高段定位子表,再取低段定位 STE */ uint32_t level1_idx = (stream_id >> cfg->split_shift) & cfg->split_mask; uint64_t *level2_base = walk_level1(strtab_base, level1_idx); uint64_t *ste = &level2_base[(stream_id & cfg->low_mask) * STE_SIZE];

上面这段不是真实寄存器操作,而是把两种索引方式的差异摊开:Linear 是一次乘法定位,Multi-level 是逐级解引用。参数上,split_shift和split_mask决定了 StreamID 的高低位如何切分,实际配置要对照具体 SMMU 版本的寄存器手册。选 Linear 还是 Multi-level,本质是“StreamID 空间是否稀疏”的取舍——Master 少、ID 连续时 Linear 更省事,ID 空间大而实际占用少时 Multi-level 更省内存。

2.4 STE 与 Context Descriptor:一个 Master 为什么还能有多套页表

查到 STE 只是第一步。STE 里并不直接放页表基址,而是指向 Context Descriptor(CD)。一个 STE 可以对应多个 CD,每个 CD 相当于 MMU 里的TTBRx + TCR组合:页表基址、地址大小、翻译粒度、内存属性都在这里。

这个设计解决了一个现实问题:同一个 Master 在不同场景下可能需要不同的地址空间。比如一个设备在正常驱动下用一套映射,在虚拟化直通场景下用另一套映射,或者在安全/非安全世界切换时换一套。如果 STE 直接绑死页表,就得为每种场景复制整个 STE;引入 CD 这一层,STE 保持稳定,切换时只换 CD 索引。

从机制上看,CD 的存在让 SMMU 的翻译配置有了“两级间接”:StreamID → STE → CD → 页表。每一级都可以独立缓存,也都可以独立失效。常见的实现思路是,TCU 内部缓存 STE 和 CD,TLB 缓存最终的地址翻译结果,失效操作要按粒度分别处理——只改 CD 就只失效 CD 缓存和对应 TLB,不必动整个 Stream Table。

2.5 Stage 1 与 Stage 2:和 MMU 同构的两阶段翻译

SMMU 的地址翻译分 Stage 1 和 Stage 2,和 Armv8 的 MMU 两阶段翻译同构。Stage 1 把 Master 发出的虚拟地址(在 SMMU 语境里常称 VA 或 IOYA)转成中间物理地址(IPA),Stage 2 再把 IPA 转成最终物理地址(PA)。Stage 1 的页表格式和 Core MMU 的页表格式一致,这是“CPU 和 SMMU 可共用页表”说法的来源。

两阶段的分工对应不同的使用场景:非虚拟化场景通常只开 Stage 1,直接把设备的 VA 映射到 PA;虚拟化场景下,Guest 里的驱动建 Stage 1 页表,Hypervisor 建 Stage 2 页表做 IPA 到 PA 的隔离,设备直通时两层都生效。这个结构和 CPU 侧完全对齐,好处是虚拟化软件栈可以复用同一套页表管理逻辑,代价是翻译路径更长,TLB 未命中时要走两级表遍历。

注意:Stage 1 页表格式与 MMU 一致,不代表可以直接把 CPU 的页表基址填进 CD。页表项的权限位、内存属性编码在设备访问语境下的语义需要逐项核对,尤其是可缓存性和共享性字段。

3. 安全与非安全:一块 SMMU 为什么在软件眼里是两个

3.1 两套寄存器、两套 Stream Table

SMMU 的寄存器是 memory-map 的,但它在安全维度上被切成了两套:一套给安全 Master 用,一套给非安全 Master 用。带S前缀的寄存器(如SMMU_S_STRTAB_BASE、SMMU_S_STRTAB_BASE_CFG)属于安全侧,不带S的属于非安全侧。两套寄存器的 memory-map 地址不同,名字也不同。

这里要澄清一个常见误解:这不是 banked 寄存器。banked 通常指同一地址、按当前安全状态自动切换物理存储;而 SMMU 的安全/非安全寄存器是地址空间上分开的两组。硬件上只有一块 SMMU,但从软件视角看,相当于有两个独立的 SMMU 实例,各自有 Stream Table 基址和配置。安全世界的软件配置安全侧,非安全世界的软件配置非安全侧,互不越界。

3.2 Secure STE 与 Non-secure STE 的权限不对称

STE 本身也分 Secure 和 Non-secure。这个区分决定了翻译结果能落到哪类内存:Secure STE 后面的页表翻译,目标可以是 secure memory,也可以是 non-secure memory;Non-secure STE 后面的翻译,只能指向 non-secure memory。这个不对称和 MMU 页表描述符里的 NS 位是同一套逻辑——非安全侧永远不能通过自己的配置把访问导向安全内存。

这个约束是 TrustZone 内存隔离在设备侧的延伸。CPU 侧的隔离靠 MMU 的 NS 位和总线上的安全属性,设备侧的隔离就靠 SMMU 的 STE 安全属性和 Stage 2 的权限检查。一个不支持 TrustZone 的 Master,如果挂在 SMMU 后面,理论上可以通过 SMMU 的配置让它“看起来”具备安全/非安全区分能力——这也是原文提到的“利用 SMMU 支持 TrustZone”的含义。但要注意,这种支持是配置层面的,Master 本身不理解安全状态,最终裁决仍在 SMMU 和总线。

3.3 翻译查找的完整时序

把前面几节串起来,一次 SMMU 翻译的查找顺序大致是:Master 发出带 StreamID 的请求 → TBU 查 TLB,命中则直接返回 → 未命中则经 DTI 送 TCU → TCU 用 StreamID 查 Stream Table 得到 STE → 按 STE 找到 CD → 用 CD 里的页表基址走 Stage 1(和/或 Stage 2)表遍历 → 得到 PA,回填 TLB,返回给 Master。

这个时序里每一步都有缓存和失效的考量。TLB 缓存最终结果,STE/CD 缓存中间配置,任何一层配置变更都要触发对应粒度的失效。血泪经验是:改完 CD 只失效 TLB 而忘了失效 CD 缓存,会出现“配置改了但行为没变”的玄学现象;反过来,频繁全量失效又会让 DMA 性能掉得很难看。失效粒度要和改动范围匹配,这是调 SMMU 时最容易被忽略的一条。

4. 避坑与排查:SMMU 配置翻车的几个典型现场

4.1 设备访问直接挂死,TLB 里却是“命中”

现象:改了 CD 里的页表基址,设备访问行为没变化,甚至访问到旧地址。原因:TCU 缓存了旧的 CD,TLB 里也可能残留旧翻译,只更新了内存里的表项而没有执行对应的失效操作。解决:按改动范围执行 CD 缓存失效和 TLB 失效,确认失效完成后再让设备发起新访问;调试阶段可以先做全量失效排除缓存干扰,再逐步缩小粒度。

4.2 Stage 2 开了但设备还是能访问不该访问的内存

现象:虚拟化场景下配了 Stage 2 页表,Guest 设备仍然能碰到 Host 内存。原因:常见的是 STE 里的 Stage 2 使能位没配对,或者 CD 指向的 Stage 1 配置让 IPA 落到了预期之外的区间,Stage 2 表本身没覆盖到那段 IPA。解决:先确认 STE 中 Stage 2 相关字段确实生效,再逐段核对 Stage 1 输出的 IPA 范围与 Stage 2 表的映射范围是否对齐,重点看边界地址。

4.3 非安全侧配置影响了安全侧行为

现象:非安全软件改了自己的 Stream Table 配置,安全侧设备访问异常。原因:误以为两套寄存器是 banked 的,按同一地址操作,实际写到了错误的一组;或者 StreamID 分配时安全/非安全 Master 的 ID 段有重叠,查表串了。解决:核对寄存器地址映射,确认S前缀寄存器的实际偏移;检查 SoC 集成时 StreamID 的静态分配表,确保安全和非安全 Master 的 ID 空间不交叠。

4.4 内存属性配错导致一致性问题

现象:DMA 读写的数据和 CPU 看到的不一致,偶发、难复现。原因:CD 或页表项里的可缓存性、共享性属性配错,比如该 Non-cacheable 的缓冲区配成了 Cacheable,或者 inner-shareable / outer-shareable 选错,导致设备写入没有及时对 CPU 可见。解决:按缓冲区的实际共享范围选属性,DMA 缓冲区通常走 Non-cacheable 或按架构要求做缓存维护;改属性后记得失效对应 TLB,否则旧属性还在缓存里。

4.5 32 位 Master 访问高地址空间失败

现象:32 位 Master 只能访问低 4GB,映射到高地址就出错。原因:Master 发出的地址位宽有限,SMMU 的输入地址宽度受 Master 接口约束,不是配了 64 位页表就能解决。解决:利用 SMMU 把 Master 的 32 位地址翻译到高物理地址,输入侧保持 32 位宽,输出侧按 CD 配置的地址大小走;核对 CD 里的输入地址大小字段与 Master 实际位宽一致。

5. 设计取舍:SMMU 为什么把复杂度堆在表结构上

回头看 SMMU 的整体设计,会发现它把几乎所有的灵活性都压在了“表”上:Stream Table 的 Linear/Multi-level 二选一,STE 到 CD 的一对多,Stage 1/Stage 2 的可组合,安全/非安全两套寄存器。硬件本身不做策略判断,只做查表和权限裁决,策略全部由软件通过建表来表达。

这个取舍的代价是软件复杂度:建表逻辑要覆盖稀疏/稠密、单阶段/双阶段、安全/非安全各种组合,失效逻辑要按粒度分层。收益是硬件可以做得相对通用,同一块 SMMU 能适配从简单 DMA 到虚拟化直通的各种场景,且页表格式与 MMU 对齐让软件栈可以复用。对比“每个 Master 一个专用 MMU”的方案,SMMU 用查表间接换来了面积和复用性,用软件复杂度换来了硬件通用性。

验证一套 SMMU 配置是否真的按预期工作,我一般会走三步:先确认 StreamID 到 STE 的索引在预期位置(读回 STE 内容核对),再确认 CD 里的页表基址和属性字段与建表时一致,最后用一次已知地址的访问去比对翻译结果,同时观察 TLB 命中/失效行为是否符合预期。三步都过,才敢说配置生效了,而不是“看起来没报错”。

从那以后我每次动 SMMU 配置,都强制走一遍“改表 → 按粒度失效 → 读回核对 → 已知地址验证”的流程,不再相信“改完就能用”的直觉。这套机制不复杂,但每一层缓存和每一套寄存器都在提醒你:设备侧的内存访问没有后悔药,配错了就是静默的错误数据或者挂死。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询