1. Secure Boot与FIP包加载流程概述
在ARM架构的嵌入式系统开发中,Secure Boot是一个至关重要的安全机制。它通过建立信任链的方式,确保系统从启动开始就运行经过验证的可信代码。而FIP(Firmware Image Package)包则是ARM Trusted Firmware(ATF)中用于打包各种启动镜像的标准容器格式。
作为在ARM平台开发多年的工程师,我经常需要深入分析BL1加载BL2的流程。这个过程中最关键的环节就是从FIP包中正确读取BL2镜像。在实际项目中,很多开发者会遇到FIP包加载失败的问题,导致系统回退到直接加载BL2.bin文件的方式。本文将基于QEMU模拟环境,带你深入代码层面分析这个流程。
2. 环境准备与问题定位
2.1 基础环境搭建
首先需要搭建ATF的开发环境,建议使用以下命令编译:
bash复制make arm-tf DEBUG=1
make -f qemu_v8.mk run-only
编译完成后,你会在build/qemu/debug/目录下看到生成的fip.bin文件。这个文件包含了BL2等镜像的打包数据。但在默认情况下,系统可能会显示"Firmware Image Package header check failed"的警告信息,这表明FIP包的校验失败了。
2.2 问题现象分析
当FIP包加载失败时,系统会回退到直接从文件系统加载BL2.bin的模式。虽然这种方式也能工作,但它绕过了Secure Boot的安全检查机制,失去了信任链保护的意义。
通过hexdump工具查看fip.bin的内容:
bash复制hexdump -C fip.bin | less
可以看到文件头部应该包含特定的魔数标识(0xAA640001)。如果这个标识正确但依然加载失败,问题通常出在平台特定的IO策略实现上。
3. FIP包加载流程深度解析
3.1 加载流程函数调用链
BL1加载BL2的主要函数调用关系如下:
code复制bl1_load_bl2
├── bl1_plat_get_image_desc
├── bl1_plat_handle_pre_image_load
├── load_auth_image
│ └── load_image
│ ├── plat_get_image_source
│ ├── io_open
│ ├── io_read
│ └── io_close
└── bl1_plat_handle_post_image_load
其中最关键的是load_image函数,它负责实际的镜像加载工作。
3.2 plat_get_image_source函数分析
在QEMU平台上,plat_get_image_source函数定义于plat/qemu/common/qemu_io_storage.c中。这个函数决定了从哪里以及如何获取镜像数据。
默认的IO策略定义如下:
c复制static const struct plat_io_policy policies[] = {
[FIP_IMAGE_ID] = {
&memmap_dev_handle,
(uintptr_t)&fip_block_spec,
open_memmap
},
[BL2_IMAGE_ID] = {
&fip_dev_handle,
(uintptr_t)&bl2_uuid_spec,
open_fip
},
};
对于BL2镜像(ID=1),它会尝试从FIP包中读取。但如果FIP包本身加载失败(ID=0的情况),整个流程就会中断。
3.3 FIP包校验机制
FIP包的头部校验是通过is_valid_header函数完成的:
c复制static inline int is_valid_header(fip_toc_header_t *header)
{
if ((header->name == TOC_HEADER_NAME) &&
(header->serial_number != 0)) {
return 1;
} else {
return 0;
}
}
其中TOC_HEADER_NAME定义为0xAA640001。如果这个校验失败,我们需要检查:
- FIP包是否完整生成
- 内存映射是否正确
- 字节序是否匹配
4. 解决方案与代码修改
4.1 强制使用FIP包的修改方案
为了确保系统总是从FIP包加载BL2,可以修改plat_get_image_source函数:
c复制int plat_get_image_source(unsigned int image_id, uintptr_t *dev_handle,
uintptr_t *image_spec)
{
const struct plat_io_policy *policy = get_io_policy(image_id);
int result;
if (image_id == 0) {
return get_alt_image_source(image_id, dev_handle, image_spec);
}
result = policy->check(policy->image_spec);
if (result == 0) {
*image_spec = policy->image_spec;
*dev_handle = *(policy->dev_handle);
} else {
result = get_alt_image_source(image_id, dev_handle, image_spec);
}
return result;
}
这个修改确保当主路径失败时,系统会尝试从备用路径加载FIP包。
4.2 FIP包结构解析
一个完整的FIP包结构如下:
code复制------------------
| ToC Header |
|----------------|
| ToC Entry 0 |
|----------------|
| ToC Entry 1 |
|----------------|
| ToC End Marker |
|----------------|
| |
| Data 0 |
| |
|----------------|
| |
| Data 1 |
| |
------------------
每个ToC Entry都包含一个UUID,用于标识不同的镜像。BL2镜像的UUID定义在include/tools_share/firmware_image_package.h中:
c复制#define UUID_TRUSTED_BOOT_FIRMWARE_BL2 \
{{0x5f, 0xf9, 0xec, 0x0b}, {0x4d, 0x22}, {0x3e, 0x4d}, \
0xa5, 0x44, {0xc3, 0x9d, 0x81, 0xc7, 0x3f, 0x0a} }
5. BL2镜像加载的详细过程
5.1 FIP文件打开流程
当系统尝试从FIP包加载BL2时,会调用fip_file_open函数:
c复制static int fip_file_open(io_dev_info_t *dev_info, const uintptr_t spec,
io_entity_t *entity)
{
const io_uuid_spec_t *uuid_spec = (io_uuid_spec_t *)spec;
result = io_open(backend_dev_handle, backend_image_spec,
&backend_handle);
/* 跳过FIP头部 */
result = io_seek(backend_handle, IO_SEEK_SET,
(signed long long)sizeof(fip_toc_header_t));
/* 遍历ToC条目 */
do {
result = io_read(backend_handle,
(uintptr_t)¤t_fip_file.entry,
sizeof(current_fip_file.entry),
&bytes_read);
if (result == 0) {
if (compare_uuids(¤t_fip_file.entry.uuid,
&uuid_spec->uuid) == 0) {
found_file = 1;
}
}
} while (!found_file);
/* 找到后设置文件位置 */
if (found_file) {
current_fip_file.file_pos = 0;
entity->info = (uintptr_t)¤t_fip_file;
}
}
5.2 镜像数据读取过程
找到正确的ToC Entry后,系统会通过fip_file_read函数读取实际的镜像数据:
c复制static int fip_file_read(io_entity_t *entity, uintptr_t buffer, size_t length,
size_t *length_read)
{
fip_file_state_t *fp = (fip_file_state_t *)entity->info;
/* 计算数据在FIP包中的位置 */
file_offset = fp->entry.offset_address + fp->file_pos;
result = io_seek(backend_handle, IO_SEEK_SET,
(signed long long)file_offset);
/* 读取数据到内存 */
result = io_read(backend_handle, buffer, length, length_read);
/* 更新文件位置 */
if (result == 0) {
fp->file_pos += *length_read;
}
return result;
}
6. 调试技巧与常见问题
6.1 调试方法
- 增加调试打印:在关键函数中添加INFO打印,跟踪执行流程
- 使用hexdump:验证FIP包的二进制内容是否正确
- 控制执行流程:在特定位置添加死循环,便于调试
c复制void __dead2 plat_error_handler(int err)
{
while (1)
wfi();
}
6.2 常见问题排查
-
FIP包加载失败:
- 检查文件路径是否正确
- 验证文件权限
- 确认文件内容完整
-
UUID匹配失败:
- 检查BL2的UUID定义是否一致
- 确认字节序问题
- 验证ToC Entry的读取是否正确
-
内存映射问题:
- 检查设备树配置
- 验证内存地址范围
- 确认MMU配置
7. Secure Boot的完整流程思考
虽然本文主要关注FIP包的加载流程,但完整的Secure Boot还应该包��:
- 信任链建立:BL1→BL2→BL31→BL32/BL33的完整验证
- 加解密机制:镜像的签名验证和可能的解密过程
- 抗回滚保护:版本号检查防止降级攻击
在实际项目中,建议绘制详细的流程图来理清各个阶段的交互关系。这不仅能帮助理解,也便于后续维护和问题排查。
