在QEMU中利用OP-TEE与Hafnium对QEMU Secure SMMU进行测试
本文的核心宗旨在于提供一套完整的技术方案,用以测试在QEMU中自行实现的Secure SMMU功能。Hafnium本身可同时支持FVP与QEMU两种平台。鉴于QEMU在标准版本中缺乏对Secure SMMU的支持,当开发者在QEMU中自行实现或扩展了Secure SMMU功能后,便可采用本文所详述的技术路径,利用Hafnium对其实现进行有效的功能性测试与验证。
本方案通过集成OP-TEE、Hafnium及QEMU,旨在实现对ARM架构下SMMU Secure State 功能的深度测试与验证。请注意:QEMU中的Secure SMMU功能仍在RFC中,并未合入upstream,下面代码是使用了该补丁来测试的Secure SMMU功能。
1. 核心技术组件概述
Hafnium
此为谷歌公司为Arm架构设计的开源参考安全分区管理器(SPM)。它遵循并实现了Arm安全分区客户端接口(SPCI)规范,运行于异常级别2(EL2),负责对安全世界(Secure World)中的安全分区进行隔离与管理。
OP-TEE
作为一个开源的可信执行环境(TEE),OP-TEE是Arm生态系统中广泛应用的安全解决方案。在本技术方案中,它构成了整个构建体系及安全启动链(Secure Boot Chain)的基础。
QEMU
QEMU是一款功能强大的开源机器级模拟器与虚拟化平台。本方案将利用其为Arm架构提供的virt虚拟平台,该平台内建了对SMMUv3的完整模拟支持。与QEMU相对应的是Arm的FVP(Fixed Virtual Platforms)。FVP能够提供对Secure SMMU的完整模拟,但在PCIe等设备的支持上不如QEMU完善。QEMU在PCIe和非安全SMMU的模拟方面更为成熟,但其标准版本目前完全不支持Secure SMMU。
Secure SMMU
此为Arm SMMU架构的一项关键特性,它授权在安全世界中运行的软件(例如Hafnium)对SMMU进行直接控制与配置。该功能对于实现对直接内存访问(DMA)设备的高效隔离,并抵御来自非安全世界的DMA攻击,具有至关重要的作用。
2. 研究目标与方法
Hafnium的现有代码库已具备初始化SMMU所需的核心逻辑,并支持对Secure SMMU寄存器的写操作。然而,该功能在默认配置下处于非激活状态。
本研究的核心目标在于激活并验证此项功能。通过该方法,将能够在固件安全启动流程(BL1 → BL2 → BL32/Hafnium)的早期阶段直接完成对SMMU的初始化。此路径使得SMMU初始化代码的测试得以在进入BL33阶段并加载普通世界操作系统(如Linux内核)之前完成,从而实现了更高效、更具针对性的验证。
3. 系统构建流程
系统的构建过程包含以下几个关键阶段,请遵循相应步骤以配置环境并编译所需组件。
阶段一:环境与repo工具配置
# 创建一个本地二进制目录并将其添加至系统PATH变量
mkdir -p ~/.local/bin
PATH="${HOME}/.local/bin:${PATH}"
# 下载并赋予repo工具可执行权限
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.local/bin/repo
chmod a+rx ~/.local/bin/repo
阶段二:OP-TEE项目初始化与代码同步
# 为项目创建工作目录
mkdir optee
cd optee
# 依据qemu_v8 manifest初始化项目仓库
repo init -u https://github.com/OP-TEE/manifest.git -m qemu_v8.xml
# 采用多线程模式同步所有仓库以提升效率
repo sync -j16
阶段三:编译环境配置
以下所有代码的修改均基于默认分支,没有在任何仓库中进行过分支切换操作。
修改 build 构建脚本
进入build目录。在执行编译前,必须对下列文件进行修改,以确保Hafnium的正确集成并启用相关功能:
build/qemu_v8.mk
:
diff --git a/qemu_v8.mk b/qemu_v8.mk
index 0589fd1..d13919b 100644
--- a/qemu_v8.mk
+++ b/qemu_v8.mk
@@ -61,7 +61,7 @@ endif
# 3: SPMC and SPMD at EL3 (in TF-A)
# 2: SPMC at S-EL2 (in Hafnium), SPMD at EL3 (in TF-A)
# 1: SPMC at S-EL1 (in OP-TEE), SPMD at EL3 (in TF-A)
-SPMC_AT_EL ?= n
+SPMC_AT_EL ?= 2
ifneq ($(filter-out n 1 2 3,$(SPMC_AT_EL)),)
$(error Unsupported SPMC_AT_EL value $(SPMC_AT_EL))
endif
@@ -629,11 +629,11 @@ QEMU_RUN_ARGS += -s -S -serial tcp:127.0.0.1:$(QEMU_NW_PORT) -serial tcp:127.0.0
.PHONY: run-only
run-only:
ln -sf $(ROOT)/out-br/images/rootfs.cpio.gz $(BINARIES_PATH)/
- $(call check-terminal)
- $(call run-help)
- $(call launch-terminal,$(QEMU_NW_PORT),"Normal World")
- $(call launch-terminal,$(QEMU_SW_PORT),"Secure World")
- $(call wait-for-ports,$(QEMU_NW_PORT),$(QEMU_SW_PORT))
+# $(call check-terminal)
+# $(call run-help)
+# $(call launch-terminal,$(QEMU_NW_PORT),"Normal World")
+# $(call launch-terminal,$(QEMU_SW_PORT),"Secure World")
+# $(call wait-for-ports,$(QEMU_NW_PORT),$(QEMU_SW_PORT))
cd $(BINARIES_PATH) && $(QEMU_BIN) $(QEMU_RUN_ARGS)
ifneq ($(filter check check-rust,$(MAKECMDGOALS)),)
- 这里需要将SPMC_AT_EL的值从n改为2,以启用Hafnium在EL2中运行。
- 另外将启用默认GUI终端的代码注释掉,是为了在纯命令行环境下(如常用的VSCode remote SSH开发环境下)正常工作。后面会结合QEMU中的
-serial tcp:localhost:15000
参数来输出日志,这样会使得开发方式更加灵活。
开始构建工具链
完成上述修改后,执行工具链的构建。
cd build
make -j16 toolchains
阶段四:Hafnium设置与源码修改
# 切换至Hafnium源码目录
cd ../hafnium
# 初始化并递归更新Hafnium的子模块
git submodule update --init --recursive
# 将Clang工具链的路径添加至系统PATH变量
# (请根据实际路径进行更新)
export PATH="/path/to/your/clang/bin/:$PATH"
核心步骤: 此阶段要求对Hafnium源代码进行修改,以启用对安全SMMU的支持。此项修改涉及代码库中的多个部分。
修改 hafnium 构建脚本和代码
build/BUILD.gn
: 假设hafnium所在绝对路径为:/mnt/sda1/phytium-tee-host/optee/hafnium/
diff --git a/build/BUILD.gn b/build/BUILD.gn
index 5903877..2a6988c 100644
--- a/build/BUILD.gn
+++ b/build/BUILD.gn
@@ -11,6 +11,7 @@ config("compiler_defaults") {
cflags = [
"-gdwarf-4",
"-O2",
+ "-fdebug-prefix-map=../../=/mnt/sda1/phytium-tee-host/optee/hafnium/",
"-Wall",
"-Wextra",
src/BUILD.gn
这是成功开启Hafnium中Secure SMMU功能最关键的一步!
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 406b4db..ad4be8b 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -37,9 +37,10 @@ source_set("src_not_testable_yet") {
":src_testable",
"//project/${project}/${plat_name}",
"//src/arch/${plat_arch}/hypervisor:other_world",
+ "//src/arch/aarch64/arm_smmuv3",
plat_boot_flow,
plat_console,
- plat_iommu,
+ #plat_iommu,
]
}
@@ -71,10 +72,11 @@ source_set("src_testable") {
"//src/arch/${plat_arch}:arch",
"//src/arch/${plat_arch}/hypervisor",
"//src/arch/${plat_arch}/hypervisor:other_world",
+ "//src/arch/aarch64/arm_smmuv3",
"//vmlib",
plat_boot_flow,
plat_console,
- plat_iommu,
+ #plat_iommu,
plat_memory_protect,
]
}
src/arch/aarch64/arm_smmuv3/args.gni
这给定了SMMU的基地址和大小。
diff --git a/src/arch/aarch64/arm_smmuv3/args.gni b/src/arch/aarch64/arm_smmuv3/args.gni
index 942e2e9..6b5bb09 100644
--- a/src/arch/aarch64/arm_smmuv3/args.gni
+++ b/src/arch/aarch64/arm_smmuv3/args.gni
@@ -5,6 +5,6 @@
# https://opensource.org/licenses/BSD-3-Clause.
declare_args() {
- smmu_base_address = ""
- smmu_memory_size = ""
+ smmu_base_address = "0x09050000"
+ smmu_memory_size = "0x00020000"
}
阶段五:固件编译
返回项目顶层build目录以执行最终的固件编译。
cd ../build/
以下两种编译路径任选一个即可。
编译路径A:最小化测试构建 (不含BL33)
此命令将生成运行Hafnium并测试SMMU初始化所需的最简化固件集。
make hafnium arm-tf -j16 DEBUG=1
编译路径B:包含Linux的完整启动构建 (含BL33)
此流程首先构建标准的启动链,随后独立编译Hafnium。
# 默认编译命令不包含Hafnium
make -j16 DEBUG=1
清理并显式编译Hafnium
make hafnium-clean # 如果之前有编译
make -j16 hafnium DEBUG=1
注: 可通过修改项目的Makefile文件,将Hafnium的编译任务整合至默认构建流程中,从而简化操作步骤。
检查输出产物
# BL1
ls -l /mnt/sda1/phytium-tee-host/optee/trusted-firmware-a/build/qemu/debug/bl1/bl1.elf
ls -l /mnt/sda1/phytium-tee-host/optee/out/bin/bl1.bin
# BL2
ls -l /mnt/sda1/phytium-tee-host/optee/trusted-firmware-a/build/qemu/debug/bl2/bl2.elf
# BL32(Hafnium)
ls -l /mnt/sda1/phytium-tee-host/optee/hafnium/out/reference/secure_qemu_aarch64_clang/hafnium.elf
4. 执行与调试方案
QEMU执行 (仅Hafnium环境)
以下命令用于启动QEMU仿真环境,该环境加载了安全固件并启用了SMMUv3模拟,但未加载Linux内核。这里我们对所有smmu相关的tracing points进行了追踪,并输出到qemu.log文件中。
./qemu-system-aarch64 -d trace:help | grep smmu > smmu-events.txt
# 运行QEMU
./qemu-system-aarch64 \
-machine virt,acpi=off,secure=on,mte=on,gic-version=3,virtualization=true,iommu=smmuv3 \
-smp 1 \
-cpu max,sme=on,pauth-impdef=on \
-m 3072 \
-d guest_errors,unimp,invalid_mem \
-trace events=/mnt/sda1/phytium-tee-host/qemu/smmu-events.txt \
-qmp unix:/tmp/qmp-sock13,server=on,wait=off \
-nographic \
-serial tcp:localhost:15000 \
-serial tcp:localhost:15001 \
-bios /mnt/sda1/phytium-tee-host/optee/out/bin/bl1.bin \
-semihosting-config enable=on,target=native \
-monitor stdio \
-D /mnt/sda1/phytium-tee-host/qemu/qemu.log
- TCP 15000端口用于输出Normal world的日志,15001端口用于输出Secure world的日志。
/mnt/sda1/phytium-tee-host/optee/out/bin/bl1.bin
为上面获取的BL1固件的路径,BL1里会自动去加载BL2/BL32等后续阶段;- semihosting-config 特性用于在Hafnium启动阶段获取串口输出。
基于VSCode的GDB调试
为对固件堆栈进行调试,需在启动QEMU时附加-S -s
标志,以使其暂停并等待GDB客户端连接。随后,可利用VSCode中的以下launch.json
配置进行调试。该配置能够在固件各阶段的内存加载地址处加载对应的符号文件。
{
"version": "0.2.0",
"configurations": [
{
"name": "Firmware Debug",
"type": "cppdbg",
"request": "launch",
"program": "/mnt/sda1/phytium-tee-host/optee/trusted-firmware-a/build/qemu/debug/bl1/bl1.elf",
"miDebuggerServerAddress": "localhost:1234",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"cwd": "/mnt/sda1/phytium-tee-host/optee-qemu",
"stopAtEntry": true,
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Set architecture to aarch64",
"text": "set architecture aarch64",
"ignoreFailures": true
},
{
"text": "add-symbol-file /mnt/sda1/phytium-tee-host/optee/trusted-firmware-a/build/qemu/debug/bl2/bl2.elf 0xe05b000"
},
{
"text": "add-symbol-file /mnt/sda1/phytium-tee-host/optee/hafnium/out/reference/secure_qemu_aarch64_clang/hafnium.elf 0xe100000"
}
]
}
]
}
当然,你也可以对安全固件与QEMU一起进行联合调试,QEMU侧的launch.json文件如下:
{
"version": "0.2.0",
"configurations": [
{
"name": "QEMU Secure SMMU Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build-debug/qemu-system-aarch64",
"args": [
"-machine", "virt,acpi=off,secure=on,mte=on,gic-version=3,virtualization=true,iommu=smmuv3",
"-smp", "1",
"-cpu", "max,sme=on,pauth-impdef=on",
"-m", "3072",
"-d", "guest_errors,unimp,invalid_mem",
"-trace", "events=/mnt/sda1/phytium-tee-host/qemu/smmu-events.txt",
"-qmp", "unix:/tmp/qmp-sock13,server=on,wait=off",
"-nographic",
"-serial", "tcp:localhost:15000",
"-serial", "tcp:localhost:15001",
"-bios", "/mnt/sda1/phytium-tee-host/optee/out/bin/bl1.bin",
"-semihosting-config", "enable=on,target=native",
"-monitor", "stdio",
"-D", "/mnt/sda1/phytium-tee-host/qemu/qemu.log",
"-S", "-s"
],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb-multiarch"
}
]
}
若需加载Linux内核,则必须相应地修改QEMU的命令行参数,以指定内核镜像、RAM disk文件的路径。如使用OP-TEE默认的编译产物调试则增加如下参数:
"-kernel", "/mnt/sda1/phytium-tee-host/optee/out/bin/Image",
"-initrd", "/mnt/sda1/phytium-tee-host/optee/out/bin/rootfs.cpio.gz",
"-append", "console=ttyAMA0,38400 keep_bootcon root=/dev/vda2",
执行结果
可以从QEMU运行的qemu.log中看到发生了大量的偏移在0x8000之后的安全寄存器被写入的行为:
smmu_add_mr smmuv3-iommu-memory-region-1-0
smmu_add_mr smmuv3-iommu-memory-region-0-0
smmu_add_mr smmuv3-iommu-memory-region-8-1
smmu_reset_exit
smmuv3_write_mmio addr: 0x8044 val:0x80000000 size: 0x4(0)
smmuv3_read_mmio addr: 0x8044 val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x8020 val:0x0 size: 0x4(0)
smmuv3_write_mmio addr: 0x8020 val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x8024 val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x1c val:0x1 size: 0x4(0)
smmuv3_read_mmio addr: 0x0 val:0xd44101b size: 0x4(0)
smmuv3_read_mmio addr: 0x8004 val:0xa0000005 size: 0x4(0)
smmuv3_read_mmio addr: 0x14 val:0x74 size: 0x4(0)
smmuv3_write_mmio addr: 0x8028 val:0xd75 size: 0x4(0)
smmuv3_read_mmio addr: 0x802c val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x4 val:0x14a0005 size: 0x4(0)
smmuv3_read_mmio addr: 0x8004 val:0xa0000005 size: 0x4(0)
smmuv3_write_mmio addr: 0x8090 val:0x400000000e14e00a size: 0x8(0)
smmuv3_write_mmio addr: 0x809c val:0x0 size: 0x4(0)
smmuv3_write_mmio addr: 0x8098 val:0x0 size: 0x4(0)
smmuv3_write_mmio addr: 0x80a0 val:0x400000000e15300a size: 0x8(0)
smmuv3_write_mmio addr: 0x80a8 val:0x0 size: 0x4(0)
smmuv3_write_mmio addr: 0x80ac val:0x0 size: 0x4(0)
smmuv3_write_mmio addr: 0x8088 val:0x5 size: 0x4(0)
smmuv3_write_mmio addr: 0x8080 val:0x400000000e15c000 size: 0x8(0)
smmuv3_read_mmio addr: 0x8020 val:0x0 size: 0x4(0)
smmuv3_cmdq_consume_out prod:0, cons:0, prod_wrap:0, cons_wrap:0
smmuv3_write_mmio addr: 0x8020 val:0x8 size: 0x4(0)
smmuv3_read_mmio addr: 0x8024 val:0x8 size: 0x4(0)
smmuv3_cmdq_consume_out prod:0, cons:0, prod_wrap:0, cons_wrap:0
smmuv3_write_mmio addr: 0x8020 val:0xc size: 0x4(0)
smmuv3_read_mmio addr: 0x8024 val:0xc size: 0x4(0)
smmuv3_invalidate_all_caches Invalidate all SMMU caches and TLBs
smmu_iotlb_inv_all IOTLB invalidate all
smmuv3_write_mmio addr: 0x803c val:0x1 size: 0x4(0)
smmuv3_read_mmio addr: 0x803c val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x8098 val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x809c val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x8060 val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x8064 val:0x0 size: 0x4(0)
smmuv3_cmdq_consume prod=1 cons=0 prod.wrap=0 cons.wrap=0 is_secure_cmdq=1
smmuv3_cmdq_opcode <--- SMMU_CMD_CFGI_STE_RANGE
smmuv3_cmdq_cfgi_ste_range start=0x0 - end=0xffffffff
smmu_configs_inv_sid_range Config cache INV SID range from 0x0 to 0xffffffff
smmuv3_cmdq_consume_out prod:1, cons:1, prod_wrap:0, cons_wrap:0
smmuv3_write_mmio addr: 0x8098 val:0x1 size: 0x4(0)
smmuv3_read_mmio addr: 0x8098 val:0x1 size: 0x4(0)
smmuv3_read_mmio addr: 0x8098 val:0x1 size: 0x4(0)
smmuv3_read_mmio addr: 0x809c val:0x1 size: 0x4(0)
smmuv3_read_mmio addr: 0x8060 val:0x0 size: 0x4(0)
smmuv3_read_mmio addr: 0x8064 val:0x0 size: 0x4(0)
smmuv3_cmdq_consume prod=2 cons=1 prod.wrap=0 cons.wrap=0 is_secure_cmdq=1
smmuv3_cmdq_opcode <--- SMMU_CMD_CFGI_STE_RANGE
smmuv3_cmdq_cfgi_ste_range start=0x0 - end=0xffffffff
smmu_configs_inv_sid_range Config cache INV SID range from 0x0 to 0xffffffff
smmuv3_cmdq_consume_out prod:2, cons:2, prod_wrap:0, cons_wrap:0
smmuv3_write_mmio addr: 0x8098 val:0x2 size: 0x4(0)
smmuv3_read_mmio addr: 0x8098 val:0x2 size: 0x4(0)
smmuv3_read_mmio addr: 0x8098 val:0x2 size: 0x4(0)
smmuv3_read_mmio addr: 0x809c val:0x2 size: 0x4(0)
smmuv3_cmdq_consume_out prod:2, cons:2, prod_wrap:0, cons_wrap:0
smmuv3_write_mmio addr: 0x8020 val:0xd size: 0x4(0)
smmuv3_read_mmio addr: 0x8024 val:0xd size: 0x4(0)
5. 本文总结与局限性
本文所阐述的方法能够有效验证Hafnium正确初始化SMMU并配置其安全寄存器的能力。然而,该方法并未提供一种机制来测试SMMU的核心功能,即DMA事务的地址转换流程。
因此,在不启动完整Linux内核的条件下对地址转换流程进行测试,需要采用一种不同的技术路径。相关方法将在后续的研究报告中进行详细介绍。