ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入解析Arm Trusted Firmware:从安全启动到平台移植实践

深入解析Arm Trusted Firmware:从安全启动到平台移植实践 拿到一个新板子第一步不是点灯不是跑Hello World而是先回答一个问题谁来把这个SoC的安全启动和运行时特权管理撑起来U-Boot可以等内核可以等但Arm Trusted FirmwareATF不能等。它是一颗Cortex-A/AArch64芯片的地基里最靠底下的那层承重墙。这篇文章我不打算复述官方文档而是按我实际做BSP移植和安全审计时的路径把ATF的架构全景、源码关键点、安全机制和平台移植落地讲透希望能帮到正在啃这部分代码的嵌入式工程师。1. 先从片上的安全世界说起ATF到底在解决什么问题很多工程师第一次接触ATF时最大的困惑是Linux和U-Boot跑得好好的为什么非要插进来一个可信固件原因很简单ARMv8/AArch64的CPU从复位那一刻起就处在EL3而Linux内核只能运行在EL1U-Boot也只能运行在EL2/EL1。中间这层最高特权级没有人管所有安全相关的脏活、累活就没人干。TrustZone把整个SoC切成了两个平行的世界安全世界Secure World和普通世界Normal World。外设和内存通过TZASC、TZPC等控制器被贴上安全或非安全的标签。普通世界的代码物理上摸不到安全资源但问题是谁来配置这些安全资源谁来在启动初期把系统从EL3安全地交接给EL2/EL1谁在后续运行中响应普通世界发起的电源管理请求答案就是ATF。ATF全称Arm Trusted Firmware-A源码仓库叫TF-A是Arm官方维护的EL3参考实现。它干的事可以概括成三件可信启动链的建立与验证、EL3运行时服务PSCI、SMC分发等、安全世界和普通世界的隔离与调度。注意ATF不是某个芯片厂家写的它是通用骨架每一家SoC厂商都需要做平台移植把自己的初始化代码、内存布局、电源操作填进去。所以你打开ATF源码会发现最核心的逻辑BL31框架、PSCI框架是芯片无关的真正复杂的是plat/目录下每家芯片各不相同的平台代码。这个项目适合谁来学说实话不是所有人都有必要深入ATF。如果你是应用层开发者知道有EL3这个东西就够了但如果你是BSP工程师、TEE移植工程师、安全固件审计工程师或者在做芯片bring-up那ATF源码就是你绕不过去的一座大山。我当年被安排做新平台移植时也是从零啃起踩了不少坑这篇就当是把我踩过的坑标出来。1.1 普通世界如何够到安全世界SMC指令的前世今生两个世界不是孤立存在的。普通世界在运行中经常需要安全世界帮它做事比如关机、重启、设置安全配置项。ARM提供了一条专门的指令smcSecure Monitor Call。执行这条指令后CPU会陷入EL3ATF接管根据传递进来的Function ID判断该把请求转发给哪个服务。这个调用过程很像操作系统里的系统调用用户程序通过svc进入内核普通世界通过smc进入EL3。区别在于用户程序的svc是陷入同一个特权世界而smc是跨世界调用中间经过了CPU硬件强制切换。这也是整个安全设计的地基普通世界永远无法直接写安全世界的寄存器只能通过定义好的接口请求安全世界代劳。1.2 ATF和U-Boot、Linux之间的分工这里要澄清一个最常见的混淆ATF和U-Boot到底谁先跑按标准ATF启动流程上电后最先运行的是固化在片内BootROM里的代码它把BL1加载到SRAM并跳转BL1再把BL2加载到SRAM并验证BL2负责加载BL31、BL32可选和BL33BL33就是U-Boot。所以回答是ATF代码整体上先于U-Boot运行但BL31是一个常驻内存的服务型固件U-Boot和内核启动后依然要随时通过SMC调用它来获得PSCI等服务。打个比方U-Boot和Linux是普通世界的两个应用ATF是安全世界的一个常驻内核。前者在明处跑业务后者在暗处管底层资源和安全边界。理解了这层关系后面看启动流程就不会乱。2. 启动全景BL1到BL33如何完成信任链传递ATF的启动过程被拆成了多个Boot Loader阶段每个阶段有明确职责和生命周期。这个设计不是为了显得高大上而是为了在每一级都做一次验签形成一条从根信任点到最终操作系统的信任链。下面这张表是我整理的各阶段职责对照建议收藏着看。阶段运行位置异常级别主要职责生命周期BL1片内SRAM/BootROMEL3最小硬件初始化加载并验证BL2jump到BL2后失效BL2片内SRAMEL3可信启动固件加载BL31/BL32/BL33并验签jump到BL31后失效BL31SRAM或DRAM保留区EL3EL3运行时固件常驻处理SMC/PSCI系统运行期间常驻BL32安全DRAMS-EL1安全世界OS如OP-TEE视方案而定BL33非安全DRAMEL2/EL1普通世界引导程序通常是U-Boot引导内核后失效建议在纸上把这条链画一遍BootROM → BL1 → BL2 → BL31 → BL33 → Linux。每次箭头指向的下一级都会被上一级验证签名任何一级被篡改链就断了系统拒绝启动。这就是可信启动的核心逻辑。2.1 BL1和BL2跑完就死的短命固件BL1通常是芯片出厂时固化在BootROM里的也可以放到外部flash由芯片内部逻辑加载。它只做最少的初始化设置串口、DDR可能还没起来、加载BL2到SRAM、验证BL2、然后跳转。因为BL1代码量极小一般只有几十KB级别所有能用代码解决的事情尽量留给BL2去做。BL2在SRAM里运行这时候DRAM还没有完全可用它负责把BL31、BL32、BL33这些大镜像从flash加载到内存里同时对每个镜像做验签。如果开启了Trusted Board BootTBBRBL2还需要验证证书链。BL2运行完毕后就跳回BL31BL2会把内存信息通过寄存器/共享内存传给BL31自己就被覆盖或废弃了。这里有个工程细节BL2运行在SRAM空间非常紧张如果平台把BL2编译得太大链接阶段就会直接报地址溢出。我在移植时经常为BL2的体积发愁能往BL31挪的代码绝不留在BL2里。2.2 BL31整个EL3世界的核心中枢BL31才是真正陪你走到最后的固件。它常驻内存负责的事情包括EL3异常向量表的建立、GIC中断路由配置、Runtime Services的注册与SMC分发、PSCI电源管理、安全世界与普通世界的上下文切换。从代码上看BL31的入口在bl31/bl31_main.c的bl31_main()函数。它干的事顺序大致是先初始化平台bl31_platform_setup再注册运行时服务runtime_svc_init然后设置异常向量表并开中断最后原地等待普通世界通过SMC来调用它或者直接跳转给BL33。需要注意的是BL31并不是引导完BL33就下班它会一直驻留在内存里随时响应来自普通世界的SMC请求。2.3 PSCILinux内核眼中的ATF普通世界的内核是怎么感知到ATF存在的答案是设备树里的psci节点。比如psci { compatible arm,psci-1.0; method smc; cpu_on 0xC4000003; cpu_off 0xC4000004; };内核里所有CPU热插拔、suspend/resume、系统重启/关机操作都会编译成一次SMC调用把对应的Function ID传给EL3。ATF收到后通过PSCI框架回调到平台定义的具体电源操作函数。也就是说你在内核里执行reboot最终真正操作硬件电源管理寄存器的可能是一段跑在EL3的ATF代码。PSCI框架位于services/std_svc/psci/。它定义了一套与平台无关的协议平台需要实现plat_psci_ops里的函数例如cpu_on、cpu_off、cpu_suspend、system_reset等。移植ATF时这一块几乎是必改项因为你换了芯片power controller的寄存器必然换。3. 源码工程面面观代码仓结构、构建系统与Runtime服务框架从GitHub拉下TF-A源码后第一眼往往会懵目录怎么这么多这里我建议按从入口到平台的顺序去读而不是从plat/开始。代码仓根目录的关键部分大概是这样的。目录内容bl1/bl2/bl31/bl32/各阶段固件主逻辑common/镜像加载、描述符处理等通用逻辑lib/EL3运行时库、PSCI、GIC驱动、MMU页表等services/Runtime ServicesPSCI、SDEI、SPDOP-TEE调度器等plat/平台相关代码含Arm官方参考平台drivers/各种外设驱动串口、GIC、IO等tools/构建辅助工具fiptool、cert_createdocs/官方文档非常值得读读源码的正确姿势先读bl31/bl31_main.c再看services/std_svc/psci/psci_main.c然后跳到plat/arm/board/fvp/这个参考平台感受平台代码的接口长什么样最后才去对照你自己的芯片手册写平台代码。直接一头扎进plat/目录大概率是被各种宏和汇编淹没。3.1 构建系统一行make背后的产物ATF的构建系统是纯Makefile。最朴素的构建命令是make PLATfvp DEBUG1指定PLAT告诉构建系统加载plat/arm/board/fvp/platform.mk。DEBUG1表示带调试符号和详细日志打印。构建完成后在build/fvp/debug/下能找到bl1.bin、bl2.bin、bl31.bin以及打包好的fip.bin。很多人不理解FIP是什么以为直接把bl1.bin烧进flash就行。实际上BL2以后的所有镜像BL31、BL32、BL33都被打包进一个FIPFirmware Image Package文件BL2从flash里读取的是这个FIP从中解析并加载各镜像。FIP的格式很简单本质是文件头加若干镜像条目可以用工具查看fiptool info fip.bin这个工具在tools/fiptool/下构建ATF时会自动生成。我调试时经常用它确认某个镜像有没有被打进FIP里改过U-Boot之后忘记重新打包FIP是新手最常犯的错误之一。3.2 Runtime Service框架一张SMC分发表BL31能处理那么多不同类型的SMC请求靠的是注册表机制。ATF里定义了一个全局的rt_svc_descs段各个服务通过宏把自己的描述符塞进这个段里。以PSCI为例它在services/std_svc/psci/psci_main.c里这样注册DECLARE_RT_SVC( psci_std_svc, OEN_STD_START, OEN_STD_END, SMC_TYPE_FAST, psci_setup, psci_smc_handler );宏的参数分别是服务名、Function ID的OEN范围、调用类型、初始化函数和分发处理函数。SMC调用中的Function ID里有一个OEN字段Owner Encode用来标识这个调用属于哪个所有者是标准服务、安全服务还是SiP服务。BL31拿到一次SMC调用后会解析OEN然后根据OEN去查这张分发表找到对应的处理函数。我们在平台移植时如果想增加自定义的SiP服务比如让普通世界读取某个安全寄存器就可以参照这个模式写一个自己的Runtime Service用OEN_SIP_START和OEN_SIP_END指定OEN范围。这在企业内部固件开发中非常常见。3.3 平台目录长什么样拿FVP当模板Arm官方维护了多个参考平台其中plat/arm/board/fvp/是移植的最佳起点。FVPFixed Virtual Platform是Arm提供的仿真平台可以在没有真实开发板的情况下把ATF、U-Boot、内核完整跑起来。plat/arm/board/fvp/目录下的核心文件大致有platform_def.h平台宏定义内存基地址、UART地址、CPU数等、plat_helpers.S早期汇编初始化、bl31_plat_setup.cBL31平台初始化、plat_topology.cCPU拓扑、plat_psci.cPSCI电源操作等。移植到自己的SoC时基本就是复制这套骨架然后替换成自己芯片的寄存器地址和行为。4. 工程审计的三个重心Trusted Boot、FIP与证书链标题里带了工程审计四个字我重点讲讲如果领导让你对一款ATF安全固件做审计你该从哪几个角度切入。不是让你去审计全部源码那不现实而是要抓住信任根、构建配置、攻击面这三大块。4.1 Trusted Board Boot签名验证链路TBBRTrusted Board Boot Requirements是Arm定义的可信启动规范。打开构建选项TRUSTED_BOARD_BOOT1后ATF会在每一级加载时做签名验证。验证用的证书由tools/cert_create/生成根信任是ROTPKRoot of Trust Public Key通常烧录在芯片的OTP/efuse里。证书链大致是ROTPK验证BL2证书BL2证书里包含BL2镜像的哈希BL2再验证BL31、BL32、BL33各自的证书。为了跑通TBBR你需要配置GENERATE_COT1同时把mbedTLS集成进来。构建命令类似make PLATfvp TRUSTED_BOARD_BOOT1 GENERATE_COT1 \ MBEDTLS_DIR../mbedtls ARM_ROTPK_LOCATIONdevel_rsa \ ROT_KEYrot_key.pem审计时第一步就是看ROTPK是否真正烧进了OTP、是否开启了写保护。如果ROT密钥还能通过某个固件升级接口刷写那整条信任链就是假的。4.2 FIP隐藏的细节往往最致命FIP格式不复杂但审计时容易被忽略。用fiptool info和fiptool dump能看到FIP里每个镜像的类型、UUID、偏移和大小。我习惯把每个镜像导出来重新算一遍哈希和打包时的证书记录对一下确认加载的镜像确实没被替换过。还有一点容易被忽视flash上存BL1的位置和存FIP的位置是否有重叠。如果地址规划失误BL1加载BL2时会从flash里读到被污染的FIP头轻则启动失败重则在安全启动关闭时被恶意数据注入。平台移植时地址空间布局一定要画清楚FIP分区不能和BL1、证书分区交叉。4.3 审计清单我拿到一套ATF会查什么下面这个表格是我实际审计固件时使用的自查清单不一定完整但可以帮你在安全设计阶段就挡住大部分低级问题。审计项检查内容构建配置是否以DEBUG0发布调试宏是否残留ROT密钥是否写入OTP写保护是否开启SMC暴露面平台自定义SMC服务是否做了参数校验内存映射安全内存是否误映射为Non-Secure可访问GIC路由安全中断是否可能被路由到普通世界页表属性是否存在可写可执行W^X违反BL33加载非安全镜像入口地址是否被硬编码且未校验这些点不需要你是一个安全专家只要花时间逐项检查通常能在普通开发者看不到的地方找出问题。比如我曾经在一套固件里发现平台自定义的SiP服务在接收普通世界传入的地址参数时没有做范围校验普通世界可以直接传一个安全内存地址让EL3帮它读。这个问题如果漏掉TrustZone的隔离就被击穿了一半。5. 平台移植落地目录结构、关键接口与最小启动移植ATF到一颗新SoC核心目标只有一个让BL31在板子上跑起来能稳定输出日志能从BL33跳转到U-Boot。完成这一步后面加TEE、加TBBR就有了舞台。5.1 从FVP起步不要直接上板子强烈建议先在FVP上把整个链路跑通再投真实芯片。FVP是Arm官方仿真器免费版需要去官网注册下载它模拟的是一颗Cortex-A系列多核处理器支持跑ATF、U-Boot和Linux。你可以先在FVP上理解启动流程再对照移植你自己的平台。FVP跑ATF的基本启动命令类似这样./FVP_Base_RevC-2xAEMvA \ -C bp.flashloader0.fnamebuild/fvp/debug/bl1.bin \ -C bp.secureflashloader.fnamebuild/fvp/debug/bl1.bin \ -C bp.fip.fnamebuild/fvp/debug/fip.bin看到串口输出NOTICE: BL1: v2.9这类日志就说明ATF在仿真平台上起来了。5.2 最小移植改动清单真正做自己的平台时我会复制一份plat/arm/board/fvp然后按下面这个顺序修改platform_def.h这是第一个要动的文件。定义CPU数量、UART地址、SRAM/DRAM基地址与大小、FIP加载地址、BL31/32/33加载地址等。这个文件里90%的宏都会在编译期影响到链接脚本改错一个地址轻则启动卡死重则无法编译。plat_helpers.S实现平台早期汇编初始化包括plat_my_core_pos返回当前CPU的编号、cache操作等。如果你的CPU有自定义的启动配置寄存器一般在这里操作。bl31_plat_setup.c实现bl31_platform_setup在这里完成串口使能、GIC初始化、内存映射表的建立。串口驱动ATF默认支持16550系列串口如果SoC用的是自研UART需要自己写一个console驱动并注册到ATF的log框架。PSCI电源操作你的SoC的power controller如果和arm标准参考设计差别不大可以先让cpu_on函数只打印日志不真正执行操作先把启动流程跑通再填实现。这里列一个platform_def.h中比较核心的宏示例#define PLAT_PRIMARY_CPU 0x0 #define PLAT_MAX_CPUS 4 #define PLAT_UART_BASE 0x12340000 #define PLAT_DRAM_SIZE 0x80000000 #define BL31_BASE 0x80000000 #define BL31_SIZE 0x100000注意这些地址必须和你的链接脚本以及实际SoC地址映射一致。我在真实项目里踩过最狠的坑是UART地址写错日志完全没有看起来像死机实际是固件在初始化串口时写了一个非法地址触发了EL3同步异常。5.3 从BL31到BL33的跳转BL31的所有初始化完成后CPU最终要交棒给普通世界的U-Boot。这个动作在ATF内部叫Handoff。BL31会从BL2传过来的镜像描述符中找到BL33的入口地址设置好异常级别通常是EL2然后执行eret跳入BL33。如果跳转前MMU配置或异常向量表有问题U-Boot可能起不来。排查思路是先让BL33用一个极小代码块替代U-Boot这个代码块只在入口处循环打印验证跳转本身是否成功。如果小代码块能跑说明ATF跳转没问题再去怀疑U-Boot本身的配置。这种二分头法在bring-up阶段非常高效。5.4 自己平台上的编译命令假设你的平台名改成myboard在根目录下新增plat/myboard/目录构建命令就是make PLATmyboard DEBUG1构建系统会自动搜索plat/myboard/platform.mk并编译。如果你的平台不在编译环境里需要先安装AArch64交叉编译工具链。在Ubuntu等环境下可以这样装sudo apt install gcc-aarch64-linux-gnu装完后用make CROSS_COMPILEaarch64-linux-gnu-指定。注意老方案里还会看到很多人传CROSS_COMPILEaarch64-none-elf-那是裸机工具链也能用但我个人更推荐带-linux-gnu-的工具链因为它带的库和工具更完整调试也更方便。6. 移植和调试中的真实踩坑记录ATF移植调试和普通Linux驱动调试最大的区别在于ATF运行在EL3很多调试手段用不上。你没法在BL31里挂gdb除非用仿真器唯一的窗口经常就是一串串口日志。下面是几个我踩过的坑每一个背后都有一段白头发。6.1 用Arm Compiler 5编AArch64固件的历史性错误先说个工具链问题。很多朋友在找arm compiler 5.06u7这类老编译器因为老的ARM32项目还在用。但ATF的BL31是AArch64代码Arm Compiler 5armcc默认是针对ARMv7时代的裸机编译器用它编AArch64 EL3固件不是简单换个参数就行的事很多场景下它根本编不出正确的AArch64代码或者编出来在FVP上直接跑飞。我建议直接把工具链切到aarch64-linux-gnu-gcc。这是一条纯开源工具链路径没有任何授权困扰。编ATF不需要像编老ARM32项目那样依赖特定收费编译器版本至少我这两年做ATF移植全部用GCC没有遇到过非Arm Compiler不可的情况。6.2 串口一片空白从零定位的四板斧如果你刷进BL1后串口什么都没有先别怀疑日志系统按顺序排查确认BL1有没有真的跑起来。看flash加载地址是否正确BL1有没有被BootROM正确拉到SRAM。确认UART物理地址。对照芯片手册核实platform_def.h里的基地址这个地址错了串口当然没输出。确认UART时钟。很多SoC的UART需要先使能时钟域否则写寄存器没反应。确认console驱动注册。ATF在BL1和BL31会各注册一次console要注意驱动的IO base和时钟频率参数是否匹配。一个实用的技巧是在BL1入口最开始几行就先输出一个固定字符比如在bl31_main之前先打印一个B这样你可以基于能打哪个字来判断代码死在哪个阶段。这个方法土但在bring-up时比任何调试器都直接。6.3 BL31日志正常但永远跳不进U-Boot如果BL31日志打得很欢U-Boot却死活不起来优先怀疑BL33的加载地址。U-Boot通常被链接到DDR里的固定地址而ATF的BL33镜像描述符里记录的加载地址必须和U-Boot的链接地址一致。不一致时BL2会把U-Boot加载到一个地址BL31跳转时又跳到另一个地址直接异常。这类问题的排查方式很简单看最后一段日志里BL31打印的BL33入口地址再用aarch64-linux-gnu-objdump -h u-boot看U-Boot的加载地址两个地址不一致就调整platform_def.h里的宏。另外如果你修改了U-Boot但没有重新生成FIP也会出现跳转地址对但镜像内容旧的诡异现象这种情况在fiptool dump里一眼就能看出来。6.4 一开启MMU和Cache就崩溃BL31初始化到一半会把MMU打开。这个环节非常容易出问题而且表现形式很不直观可能在打开MMU后第一条指令就异常也可能运行几分钟后才随机崩溃。绝大多数原因是内存映射属性配错了。ATF里用mmap_add_region(base, size, attrs)来建立页表映射外设地址必须标记为MT_DEVICE普通内存标记为MT_MEMORY。如果把UART、GIC这类外设映射成了MT_MEMORY开启Cache后CPU会擅自缓存外设寄存器的读取结果你读到的寄存器值可能是几个月前的旧值代码逻辑就会像疯了一样乱跳。还有一个隐藏坑BL31的链接地址、运行地址、加载地址三个值是否一致。ATF对这几者的关系非常敏感。如果BL31加载到DRAM但链接脚本认为它在SRAM运行打开MMU后第一个页表查询就会触发翻译错误。排查时可以用readelf -h bl31.elf和fiptool dump对比加载地址。6.5 SMC调用没有返回值OEN范围与服务注册表的坑你辛苦写好自定义SiP服务烧进固件结果从U-Boot或内核里发SMC调用一点反应都没有。最常见的两个原因Function ID的OEN范围超出了注册时声明的范围BL31查表时根本没匹配到服务或者服务注册的初始化函数没有被调用服务描述符压根没进入rt_svc_descs段。遇到这类问题第一件事是确认自定义SMC功能的OEN值是落在OEN_SIP_START和OEN_SIP_END之间SIP的范围通常是2再确认调用时的Function ID是64位还是32位。ATF里调用格式比较复杂包含fast call标志位、64位标志位等一个bit错了查表结果就完全不同。可以在BL31的smc_handler里先加日志把收到的Function ID原样打印出来和自己的预期值对一下就清楚了。移植ATF这件事本质上不是在写一个功能而是在搭一套体系。把BL31跑起来只是第一步后面还有TBBR、TEE、ff-a、RAS这些扩展在排队。如果能把启动链条里的每一级都吃透再看OP-TEE、U-Boot、内核的交互整个系统在你眼里就不再是一个个孤立的镜像而是一套完整的分层协作。这份源码里藏着的设计思路值得反复咀嚼。
返回列表