第6篇:编译流程------prebuild 与 postbuild 脚本详解
本篇将逐行解析 X-CUBE-SBSFU 中的预编译脚本(prebuild.bat)和后编译脚本(postbuild.bat)。这些脚本是编译流程的"自动化工兵",负责密钥嵌入、固件加密、签名生成和打包。看懂这些脚本,你就真正理解了从源码到烧录文件的完整转换过程。
1. 整体编译流程概览
在深入脚本细节之前,我们先建立一个全局视图。从源码到最终烧录文件的完整路径如下:
┌─────────────────────────────────────────────────────────────────┐
│ 第1步:SECoreBin 编译 │
│ │
│ 密钥文件(Binary/*.bin/*.txt) │
│ │ │
│ ▼ │
│ prebuild.bat ────→ se_key.s (密钥汇编文件) │
│ │ postbuild.bat (供 UserApp 使用的后处理脚本) │
│ │ │
│ ▼ │
│ ARMCC 编译 se_crypto_bootloader.c → .o │
│ │ │
│ ▼ │
│ 链接生成 SECoreBin.axf │
│ │ │
│ ▼ │
│ fromelf --bin → SE_CORE_Bin.bin → 转为 .s 汇编数据 │
│ (此 .s 文件供 SBSFU 链接) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 第2步:SBSFU 编译 │
│ │
│ SE_CORE_Bin.s (来自第1步) │
│ + │
│ sfu_boot.c, sfu_loader.c, sfu_mpu_isolation.c ... │
│ + │
│ se_interface_*.c (SE 接口封装) │
│ │ │
│ ▼ │
│ 链接生成 SBSFU.axf (包含 SE Core + SBSFU 状态机) │
│ │ │
│ ▼ │
│ 产物: se_interface_application.o (被 UserApp 链接) │
│ Project.axf (完整 SBSFU 二进制,烧录到 0x08000000) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 第3步:UserApp 编译 │
│ │
│ main.c, ymodem.c, fw_update_app.c ... │
│ + │
│ se_interface_application.o (来自第2步) │
│ │ │
│ ▼ │
│ 链接生成 UserApp.axf → UserApp.bin (原始未加密) │
│ │ │
│ ▼ (postbuild.bat 自动执行) │
│ ┌──────────────────────────────────┐ │
│ │ Step 1: prepareimage.py enc │ AES256-CBC 加密 │
│ │ UserApp.bin → UserApp.sfu│ │
│ ├──────────────────────────────────┤ │
│ │ Step 2: prepareimage.py sha384 │ 计算 SHA384 哈希 │
│ │ UserApp.bin → UserApp.sign│ │
│ ├──────────────────────────────────┤ │
│ │ Step 3: prepareimage.py pack │ ECDSA 签名 + 打包 │
│ │ → UserApp.sfb │ 生成最终固件包 │
│ ├──────────────────────────────────┤ │
│ │ Step 4: prepareimage.py merge │ 合并 SBSFU + UserApp │
│ │ → SBSFU_UserApp.bin │ 一次性烧录镜像 │
│ ├──────────────────────────────────┤ │
│ │ Step 5: prepareimage.py diff │ 生成差分包 (可选) │
│ │ → PartialUserApp.sfb │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2. SECoreBin 预编译脚本(prebuild.bat)逐行解析
prebuild.bat 是 SECoreBin 工程编译前自动执行的脚本。它的核心任务有两个:
- 根据加密方案生成密钥汇编文件 (
se_key.s) - 复制对应加密方案的后处理脚本 给 UserApp 使用
下面是完整脚本的逐段解析(文件路径:2_Images_SECoreBin/MDK-ARM/prebuild.bat)。
第1-10行:初始化
batch
@echo off
echo prebuild.bat : started > %1\\output.txt
set "asmfile=%1\\se_key.s"
::comment this line to force python
::python is used if windows executable not found
pushd %1\..\..\..\..\..\..\Middlewares\ST\STM32_Secure_Engine\Utilities\KeysAndImages
set basedir=%cd%
popd
goto exe:
goto py:
| 行 | 解释 |
|---|---|
@echo off |
关闭命令回显,使输出干净 |
%1 |
Keil 传入的第一个参数,即编译输出目录(如 .\MDK-ARM\NUCLEO-G474RE\) |
%1\\output.txt |
将所有输出重定向到日志文件 |
set "asmfile=..." |
设置 se_key.s 的目标路径 |
pushd ... KeysAndImages |
临时跳转到 prepareimage.py 所在目录 |
set basedir=%cd% |
记录工具目录的绝对路径 |
popd |
返回原目录 |
goto exe: |
跳转到 exe 标签(优先使用 Windows 可执行文件) |
第11-25行:选择执行方式(exe 或 Python)
batch
:exe
::line for window executable
echo Prebuild with windows executable
set "prepareimage=%basedir%\\win\\prepareimage\\prepareimage.exe"
set "python="
if exist %prepareimage% (
goto prebuild
)
:py
::line for python
echo Prebuild with python script
set "prepareimage=%basedir%\\prepareimage.py"
set "python=python "
echo "python: %prepareimage%" >> %1\\output.txt 2>>&1
:prebuild
这段代码实现了一个优雅的降级策略:
如果 prepareimage.exe 存在 → 使用 exe(更快,无需 Python)
↓ (不存在)
使用 prepareimage.py → 需要 Python 环境
注意 set "python=python " 末尾有一个空格------这是批处理拼接命令时的必要细节:%python%%prepareimage% 展开后变成 python prepareimage.py。
第26-37行:清理旧文件
batch
set "crypto_h=%1\\..\\Inc\\se_crypto_config.h"
::clean
if exist %1\\crypto.txt (
del %1\\crypto.txt
)
if exist %asmfile% (
del %asmfile%
)
if exist %1\\postbuild.bat (
del %1\\postbuild.bat
)
每次编译前删除上次生成的文件,确保使用的是最新配置。清理的文件包括:
crypto.txt:上次读取的加密方案名称se_key.s:上次生成的密钥汇编文件postbuild.bat:上次复制的后处理脚本
第40-44行:读取加密方案
batch
::get crypto name
set "command=%python%%prepareimage% conf %crypto_h% > %1\\crypto.txt"
%command%
IF %ERRORLEVEL% NEQ 0 goto error
set /P crypto=<%1\\crypto.txt >> %1\\output.txt 2>>&1
echo crypto %crypto% selected >> %1\\output.txt 2>>&1
这里发生了什么?
prepareimage.py conf se_crypto_config.h
│
│ 读取 #define SECBOOT_CRYPTO_SCHEME SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384
│
▼
输出 "SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384" → crypto.txt
│
▼
set /P crypto=<crypto.txt # 将文件内容读入变量 crypto
prepareimage.py conf 命令解析 C 头文件中的宏定义,提取加密方案名称。这是整个脚本的关键决策点 ------后续所有处理都依赖 %crypto% 变量的值。
第46-49行:设置 Cortex 类型,写入汇编头
batch
set "cortex=V7M"
:: Tabulation before section is mandatory !
echo AREA ^|.SE_Key_Data^|, CODE>%asmfile%
| 行 | 解释 |
|---|---|
set "cortex=V7M" |
指定 ARM Cortex 架构版本。V7M 对应 Cortex-M4(支持 MOVW/MOVT 指令) |
| `AREA | .SE_Key_Data |
> 覆盖写入,后面的 >> 追加写入。
第51-69行:根据加密方案跳转
batch
if "%crypto%"=="SECBOOT_AES128_GCM_AES128_GCM_AES128_GCM" (
set "type=GCM"
goto AES128
)
if "%crypto%"=="SECBOOT_ECCDSA_WITH_AES128_CBC_SHA256" (
set "type=CBC"
goto AES128
)
if "%crypto%"=="SECBOOT_ECCDSA_WITHOUT_ENCRYPT_SHA256" (
goto ECDSA
)
if "%crypto%"=="SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384" (
set "type=CBC"
goto AES256
)
if "%crypto%"=="SECBOOT_ECCDSA_WITHOUT_ENCRYPT_SHA384" (
goto ECDSA384
)
goto end
这段代码的分支逻辑:
加密方案名称 → 跳转目标 处理内容
────────────────────────────────────────────────────────────
SECBOOT_AES128_GCM_AES128_GCM_AES128_GCM → AES128 嵌入 AES128 密钥 (GCM)
SECBOOT_ECCDSA_WITH_AES128_CBC_SHA256 → AES128 嵌入 AES128 密钥 (CBC) → ECDSA 公钥
SECBOOT_ECCDSA_WITHOUT_ENCRYPT_SHA256 → ECDSA 仅嵌入 ECDSA 公钥
SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384 → AES256 嵌入 AES256 密钥 (CBC) → ECDSA384 公钥
SECBOOT_ECCDSA_WITHOUT_ENCRYPT_SHA384 → ECDSA384 仅嵌入 ECDSA P-384 公钥
第71-89行:AES128 密钥处理
batch
:AES128
set "oemkey=%1\\..\\Binary\\OEM_KEY_COMPANY1_key_AES_%type%.bin"
set "command=%python%%prepareimage% trans -k %oemkey% -f SE_ReadKey_1 -v %cortex% >> %asmfile%"
%command%
IF %ERRORLEVEL% NEQ 0 goto error
set "oemkey=%1\\..\\Binary\\OEM_KEY_COMPANY2_key_AES_%type%.bin"
IF NOT EXIST %oemkey% goto :AES128_end
set "command=%python%%prepareimage% trans -k %oemkey% -f SE_ReadKey_2 -v %cortex% >> %asmfile%"
%command%
IF %ERRORLEVEL% NEQ 0 goto error
核心命令解析:
bash
prepareimage.py trans -k <密钥文件> -f <函数名> -v <Cortex版本>
| 参数 | 说明 |
|---|---|
trans |
操作模式:translate,将密钥文件转换为 ARM 汇编代码 |
-k <file> |
输入的密钥文件(二进制 .bin 或文本 .txt) |
-f <name> |
生成的汇编函数名(SE_ReadKey_1、SE_ReadKey_1_Pub 等) |
-v V7M |
Cortex 版本:V6M(Cortex-M0)不支持 MOVW/MOVT,V7M(Cortex-M3/M4/M7)支持 |
生成的汇编函数结构如下(以 SE_ReadKey_1 为例):
asm
SE_ReadKey_1
PUSH {R1-R5} ; 保存寄存器
MOVW R1, #0x454f ; 密钥字节 0-1 (低16位)
MOVT R1, #0x5f4d ; 密钥字节 2-3 (高16位)
MOVW R2, #0x454b ; 密钥字节 4-5
MOVT R2, #0x5f59 ; 密钥字节 6-7
; ... 更多 MOVW/MOVT 指令 ...
STM R0, {R1-R4} ; 将 16 字节 (R1-R4) 写入调用者提供的缓冲区 (*R0)
ADD R0, R0, #16 ; 缓冲区指针偏移 16 字节
; ... 继续写入后续字节 ...
POP {R1-R5} ; 恢复寄存器
BX LR ; 返回
密钥混淆机制 :密钥并不是以连续的二进制数组存储在 Flash 中的。相反,每个密钥字节对(16 位)被拆散到 MOVW/MOVT 指令的立即数字段中,散布在汇编指令之间。攻击者如果想从 Flash 中直接提取密钥,看到的是 ARM 指令的编码流,而不是连续的密钥数据。这是一种轻量级的反逆向混淆。
第92-110行:ECDSA P-256 公钥处理
batch
:ECDSA
set "ecckey=%1\\..\\Binary\\ECCKEY1.txt"
set "command=%python%%prepareimage% trans -k %ecckey% -f SE_ReadKey_1_Pub -v %cortex% >> %asmfile%"
%command%
IF %ERRORLEVEL% NEQ 0 goto error
ECDSA 的处理逻辑与 AES 类似,但密钥文件是文本格式(.txt),且生成的函数名是 SE_ReadKey_1_Pub(公钥)。支持最多 3 个公司的公钥(ECCKEY1/2/3)。
第132-150行:ECDSA P-384 公钥处理
batch
:ECDSA384
set "ecckey=%1\\..\\Binary\\ECCKEY1-384.txt"
set "command=%python%%prepareimage% trans -k %ecckey% -f SE_ReadKey_1_Pub -v %cortex% >> %asmfile%"
与本项目使用的 SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384 对应的就是 P-384 密钥。ECDSA P-384 公钥为 96 字节(曲线点的 X + Y 坐标各 48 字节),比 P-256 的公钥(64 字节)长 50%。
第154-157行:写入汇编尾,复制后处理脚本
batch
:end
echo END >> %asmfile%
set "command=copy %1\\%crypto%.bat %1\\postbuild.bat"
%command%
IF %ERRORLEVEL% NEQ 0 goto error
exit 0
| 行 | 解释 |
|---|---|
echo END >> %asmfile% |
写入 ARM 汇编的 END 指令,标记源文件结束 |
copy %crypto%.bat postbuild.bat |
将对应加密方案的 bat 脚本复制为 UserApp 的后处理脚本 |
例如,当 SECBOOT_CRYPTO_SCHEME = SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384 时:
copy SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384.bat postbuild.bat
这确保了用 AES256-CBC 加密的固件在 UserApp 后处理时使用对应的解密/签名脚本。
生成的 se_key.s 内容解析
实际生成的文件(本项目使用 ECDSA P-384 + AES256-CBC):
asm
AREA |.SE_Key_Data|, CODE
EXPORT SE_ReadKey_1 ; AES256 密钥读取函数
SE_ReadKey_1
PUSH {R1-R5}
MOVW R1, #0x454f ; ; AES 密钥以混淆方式存储
MOVT R1, #0x5f4d
; ... 32 字节 AES256 密钥分散在 16 条 MOVW/MOVT 指令中 ...
POP {R1-R5}
BX LR
EXPORT SE_ReadKey_1_Pub ; ECDSA P-384 公钥读取函数
SE_ReadKey_1_Pub
PUSH {R1-R5}
MOVW R1, #0xf02
MOVT R1, #0x3d9c
; ... 96 字节公钥分散在 48 条 MOVW/MOVT 指令中 ...
POP {R1-R5}
BX LR
END
3. UserApp 后编译脚本(postbuild.bat)逐行解析
postbuild.bat 在 UserApp 工程编译完成后自动执行,负责将原始二进制固件转换为加密+签名的 .sfb 格式。它的完整路径是 2_Images_SECoreBin/MDK-ARM/SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384.bat(由 prebuild.bat 复制而来)。
变量初始化
batch
@echo off
set "projectdir=%1"
set "execname=%~n3"
set "elf=%2"
set "bin=%3"
set "fwid=%4"
set "version=%5"
Keil 传递给 postbuild.bat 的参数(在 Keil → Options for Target → User → After Build 中配置):
| 参数 | 含义 | 示例值 |
|---|---|---|
%1 |
工程目录 | .\MDK-ARM |
%2 |
ELF/AXF 文件完整路径 | .\MDK-ARM\NUCLEO-G474RE\Exe\UserApp.axf |
%3 |
BIN 文件完整路径 | .\MDK-ARM\NUCLEO-G474RE\Exe\UserApp.bin |
%4 |
固件 ID (1/2/3) | 1 |
%5 |
固件版本号 | 由用户定义 |
batch
set "SBSFUBootLoader=%~d0%~p0\\..\\.."
%~d0 = 脚本所在盘符,%~p0 = 脚本所在路径。这行计算出 SBSFU 项目的根目录。
batch
set "programmertool="C:\\Program Files (x86)\\STMicroelectronics\\STM32Cube\\STM32CubeProgrammer\\bin\\STM32_Programmer_CLI""
这是 STM32CubeProgrammer CLI 的默认安装路径。如果你的安装路径不同,需要修改这一行。
输出文件路径定义
batch
set "userAppBinary=%projectdir%\\..\\Binary\\"
set "sfu=%userAppBinary%\\%execname%.sfu" ; 加密后的固件
set "sfb=%userAppBinary%\\%execname%.sfb" ; 最终固件包(含头部+签名)
set "sign=%userAppBinary%\\%execname%.sign" ; SHA384 哈希值
set "headerbin=%userAppBinary%\\%execname%sfuh.bin"; 固件头二进制
set "bigbinary=%userAppBinary%\\SBSFU_%execname%.bin" ; 合并大镜像
set "partialbin=%userAppBinary%\\Partial%execname%.bin" ; 差异二进制
set "partialsfb=%userAppBinary%\\Partial%execname%.sfb" ; 差异固件包
密钥和配置路径定义
batch
set "iv=%SBSFUBootLoader%\\2_Images_SECoreBin\\Binary\\iv.bin"
set "oemkey=%SBSFUBootLoader%\\2_Images_SECoreBin\\Binary\\OEM_KEY_COMPANY%fwid%_key_AES256_CBC.bin"
set "ecckey=%SBSFUBootLoader%\\2_Images_SECoreBin\\Binary\\ECCKEY%fwid%-384.txt"
set "sbsfuelf=%SBSFUBootLoader%\\2_Images_SBSFU\\MDK-ARM\\NUCLEO-G474RE\\Exe\\SBSFU.axf"
set "magic=SFU%fwid%"
set "offset=4096"
set "alignment=16"
| 变量 | 值 | 说明 |
|---|---|---|
iv |
Binary/iv.bin |
AES-CBC 初始化向量(16 字节) |
oemkey |
OEM_KEY_COMPANY1_key_AES256_CBC.bin |
AES-256 加密密钥(32 字节) |
ecckey |
ECCKEY1-384.txt |
ECDSA P-384 签名私钥 |
sbsfuelf |
SBSFU.axf |
SBSFU 的 ELF 文件(用于合并镜像) |
magic |
SFU1 |
固件魔数标识 |
offset |
4096 |
固件头偏移(4KB 对齐) |
选择执行方式
与 prebuild.bat 相同的逻辑------优先使用 exe,备选 Python。
步骤1:AES256-CBC 加密
batch
set "command=%python%%prepareimage% enc -k %oemkey% -i %iv% %bin% %sfu% > %projectdir%\output.txt 2>&1"
%command%
IF %ERRORLEVEL% NEQ 0 goto :error
命令解析:
bash
prepareimage.py enc \
-k OEM_KEY_COMPANY1_key_AES256_CBC.bin # 256位AES密钥
-i iv.bin # 初始化向量(16字节)
UserApp.bin # 输入:原始固件
UserApp.sfu # 输出:加密固件
enc 模式执行 AES-CBC 加密。加密后的文件 .sfu(Secure Firmware Update)比原文件稍大(会有 PKCS#7 填充到 16 字节边界)。
AES 16 字节对齐的硬性要求 :AES 的块大小固定为 128 位(16 字节)。AES-CBC 模式要求输入数据必须是 16 字节的整数倍。如果你的固件二进制文件不是 16 字节对齐的,加密过程会通过 PKCS#7 填充补齐,但这会导致解密后的固件末尾多出填充字节。为避免此问题,建议在链接器中就将固件的
ROM区域配置为 16 字节对齐:
c// Keil Scatter File 中确保 16 字节对齐 LR_IROM1 0x08010000 0x00036000 { ER_IROM1 0x08010000 FIXED 0x00036000 { *.o (RESET, +First) *.o (+RO) .ANY (+RO) .ANY (+XO) } // 16 字节对齐可通过填充实现 }在 IAR 中通过链接器配置
--config_def,在 STM32CubeIDE 中通过.ld文件的ALIGN(16)实现同样效果。三款 IDE 的对齐方式不同,但目的相同。
步骤2:SHA384 哈希计算
bash
prepareimage.py sha384 UserApp.bin UserApp.sign
| 参数 | 说明 |
|---|---|
sha384 |
操作模式:计算 SHA-384 哈希 |
UserApp.bin |
输入:原始未加密固件 (不是加密后的 .sfu!) |
UserApp.sign |
输出:48 字节 SHA384 哈希值 |
为什么对原始固件计算哈希?
因为设备端在解密后会得到原始固件,需要验证的是解密后的固件 与发布者签名时的固件是否一致。如果对密文计算哈希,中间人对密文的任何修改都会通过篡改检测。
步骤3:ECDSA 签名 + 打包生成 .sfb
bash
prepareimage.py pack \
-m SFU1 # 魔数
-k ECCKEY1-384.txt # ECDSA P-384 私钥
-r 4 # 协议版本
-v <version> # 固件版本
-i iv.bin # AES IV
-f UserApp.sfu # 加密固件文件
-t UserApp.sign # SHA384 哈希
UserApp.sfb # 输出:最终固件包
-o 4096 # 固件头偏移(4KB)
pack 模式做以下事情:
- 读取加密固件(
.sfu)和哈希文件(.sign) - 用 ECDSA P-384 私钥对固件头进行签名
- 构建
SE_FwRawHeaderTypeDef结构体(填充魔数、版本、大小、IV、哈希等字段) - 将头部 + 加密固件打包为一个
.sfb文件
.sfb 文件结构:
┌──────────────────────┐
│ FW Header (232B) │ ← SE_FwRawHeaderTypeDef 结构体
│ - SFUMagic: "SFU1" │
│ - FwVersion │
│ - FwSize │
│ - InitVector[16] │
│ - FwTag[48] │ ← SHA384 哈希
│ - HeaderSignature[96]│ ← ECDSA P-384 签名
│ - FwImageState[96] │
│ - PrevFingerprint[32]│
├──────────────────────┤
│ Encrypted FW │ ← AES256-CBC(UserApp.bin)
│ (UserApp.sfu 内容) │
└──────────────────────┘
步骤4:固件头二进制生成 + 合并镜像
bash
prepareimage.py header \
-m SFU1 -k ECCKEY1-384.txt -r 4 -v <version> \
-i iv.bin -f UserApp.sfu -t UserApp.sign -o 4096 \
UserAppsfuh.bin
header 模式单独生成固件头二进制文件(不含加密固件体)。
bash
prepareimage.py merge \
-i UserAppsfuh.bin # 固件头二进制
-s SBSFU.axf # SBSFU ELF 文件
-u UserApp.axf # UserApp ELF 文件
SBSFU_UserApp.bin # 输出:合并镜像
merge 模式生成一个可以在开发阶段一次性烧录的大镜像:
┌──────────────────────┐
│ SBSFU (64KB) │ ← 0x08000000
├──────────────────────┤
│ FW Header (232B) │ ← 0x08010000 (Active Slot 起始)
├──────────────────────┤
│ UserApp (明文) │ ← 0x080100E8
└──────────────────────┘
这个合并镜像可以绕过 YMODEM 下载过程,直接用 STM32CubeProgrammer 烧录进行开发测试。
步骤5(可选):差分固件生成
bash
prepareimage.py diff \
-1 RefUserApp.bin # 参考固件(旧版本)
-2 UserApp.bin # 新固件
PartialUserApp.bin # 差分结果
-a 16 # 16字节对齐
--poffset PartialUserApp.offset # 偏移量文件
diff 模式比较新旧固件的差异,生成差分包,只包含变化的部分。这在 OTA 场景中极为重要------无需传输完整固件,只需传输差异部分。
后续处理(加密、签名、打包)与完整固件相同,最终生成 PartialUserApp.sfb。
清理中间文件
batch
:finish
::backup and clean up the intermediate file
del %sign%
del %sfu%
del %headerbin%
编译完成后自动清理中间文件(.sfu、.sign、sfuh.bin),只保留最终的 .sfb 和合并镜像。
4. prepareimage.py 工具详解
prepareimage.py 是整个编译流水线中的"瑞士军刀"。它支持 10 种操作模式:
| 模式 | 命令 | 作用 | 使用场景 |
|---|---|---|---|
conf |
prepareimage.py conf <config.h> |
读取加密方案配置 | prebuild.bat 中确定加密方案 |
keygen |
prepareimage.py keygen -k <file> -t <type> |
生成密钥对 | 首次部署时生成密钥 |
trans |
prepareimage.py trans -k <key> -f <func> -v <cortex> |
密钥转 ARM 汇编 | prebuild.bat 中生成 se_key.s |
enc |
prepareimage.py enc -k <key> -i <iv> <in> <out> |
AES 加密 | postbuild.bat 中加密固件 |
sha256 |
prepareimage.py sha256 <in> <out> |
SHA-256 哈希 | P-256 方案的哈希计算 |
sha384 |
prepareimage.py sha384 <in> <out> |
SHA-384 哈希 | P-384 方案的哈希计算 |
sign |
prepareimage.py sign -k <key> -n <nonce> <in> <out> |
GCM 认证标签 | AES-GCM 方案的标签生成 |
pack |
prepareimage.py pack -m <magic> -k <key> ... |
打包 .sfb | 最终固件包生成 |
merge |
prepareimage.py merge -i <hdr> -s <sbsfu> -u <userapp> |
合并镜像 | 生成开发烧录镜像 |
diff |
prepareimage.py diff -1 <old> -2 <new> <out> |
差分生成 | OTA 差分包 |
header |
prepareimage.py header -m <magic> ... |
生成头部 | 单独生成固件头文件 |
操作模式详解
conf ------ 读取配置
bash
python prepareimage.py conf se_crypto_config.h
# 输出: SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384
扫描 C 头文件中的 #define SECBOOT_CRYPTO_SCHEME ... 行,输出宏的名称。
trans ------ 密钥转汇编
bash
python prepareimage.py trans \
-k OEM_KEY_COMPANY1_key_AES256_CBC.bin \ # 32字节AES256密钥
-f SE_ReadKey_1 \ # 函数名
-v V7M # Cortex-M4 使用 V7M
>> se_key.s
将二进制密钥文件转换为 ARM Thumb 汇编代码。核心逻辑:
- 读取密钥文件的每个字节
- 每 4 个字节组成一个 32 位值
- 生成
MOVW R?, #low16+MOVT R?, #high16指令对 - 每 16 个字节(4 个寄存器)后用
STM R0, {R1-R4}写入目标缓冲区 - 使用
ADD R0, R0, #16推进缓冲区指针
enc ------ AES 加密
bash
python prepareimage.py enc \
-k OEM_KEY_COMPANY1_key_AES256_CBC.bin \ # AES-256 密钥
-i iv.bin \ # 初始化向量
UserApp.bin \ # 明文
UserApp.sfu # 密文
加密模式取决于加密方案:
AES256_CBC:AES-CBC 模式,PKCS#7 填充AES128_GCM:AES-GCM 模式,使用 Nonce + 附加认证数据(AAD)
pack ------ 打包固件
pack 是最复杂的操作。它需要:
- 读取加密固件
.sfu和哈希.sign - 用私钥对固件头进行 ECDSA 签名
- 构建
SE_FwRawHeaderTypeDef结构体 - 将头部序列化为二进制 + 加密固件体 →
.sfb
bash
python prepareimage.py pack \
-m SFU1 # 魔数(对应槽位1)
-k ECCKEY1-384.txt # ECDSA 私钥
-r 4 # 协议版本
-v 1 # 固件版本
-i iv.bin # AES IV
-f UserApp.sfu # 加密固件
-t UserApp.sign # SHA384 哈希标签
-o 4096 # 头部偏移(4KB对齐)
UserApp.sfb # 输出
5. 编译中可能遇到的常见问题
问题1:路径过长(Windows 260 字符限制)
fatal error: cannot open output file ...: No such file or directory
原因:X-CUBE-SBSFU 的目录嵌套很深,路径容易超过 Windows 的 260 字符限制。
解决方法 :使用 subst 命令创建虚拟驱动器:
cmd
subst X: F:\doxc\IAP\SBSFU_v2.6.2_G474RE_ECC384_AES256_IAR_KEIL_v2.1\SBSFU_v2.6.2
然后在 X: 盘下打开工程。
问题2:Python 未安装
'python' is not recognized as an internal or external command
解决方法:
- 安装 Python 3.x,执行
pip install -r requirements.txt(安装 cryptography 模块) - 或者:prebuild.bat 会自动尝试使用
win/prepareimage/prepareimage.exe,无需 Python
问题3:STM32_Programmer_CLI 路径不对
'STM32_Programmer_CLI' is not recognized
解决方法 :修改 postbuild.bat 中的 programmertool 变量为你实际的安装路径,或者如果你不需要生成合并 ELF 文件(bigelf),可以不传第 6 个参数。
问题4:密钥文件缺失
FileNotFoundError: ... OEM_KEY_COMPANY1_key_AES256_CBC.bin
解决方法 :运行 prepareimage.py keygen 生成密钥:
bash
python prepareimage.py keygen -k OEM_KEY_COMPANY1_key_AES256_CBC.bin -t aes-cbc
python prepareimage.py keygen -k ECCKEY1-384.txt -t ecdsa-p384
问题5:数据库(DBANK)位未禁用
设备行为异常,SBSFU 无法正常启动。
解决方法:通过 STM32CubeProgrammer 检查 Option Bytes,确保 DBANK 位为 0(已禁用)。
6. Keil IDE 中的编译设置
6.1 C/C++ 选项
Options for Target → C/C++ → Preprocessor Symbols:
SECBOOT_CRYPTO_SCHEME=SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384
USE_HAL_DRIVER
STM32G474xx
| 宏 | 作用 |
|---|---|
SECBOOT_CRYPTO_SCHEME |
选择加密方案(影响条件编译和结构体大小) |
USE_HAL_DRIVER |
启用 HAL 库 |
STM32G474xx |
指定 MCU 型号 |
6.2 Linker 选项
Options for Target → Linker → Scatter File:
.\Project.sct
该文件 #include "mapping_sbsfu.h" 和 #include "mapping_fwimg.h",定义了各内存区域的位置和大小。
6.3 User 选项(编译前/后执行命令)
Options for Target → User:
| 阶段 | 命令 | 工程 |
|---|---|---|
| Before Build | prebuild.bat "#P" |
SECoreBin |
| After Build | postbuild.bat "#P" "#L" "#H" 1 1 |
UserApp |
#P = Project directory, #L = Linker output (ELF), #H = Hex output (BIN)。
总结
编译流水线的设计是 X-CUBE-SBSFU 中最精妙的部分之一。通过 prebuild.bat 和 postbuild.bat 的组合,实现了一个零手动操作的自动化安全流水线:
- 编译前 自动根据
SECBOOT_CRYPTO_SCHEME选择密钥、生成汇编代码、复制后处理脚本 - 编译后自动加密固件、计算哈希、ECDSA 签名、打包 .sfb、合并镜像
- 支持差分包 生成,只需将旧版本固件放到
RefUserApp.bin即可自动生成增量更新包
整个流水线在 Keil IDE 中一键完成,开发者无需手动调用任何加密工具------这正是 "secure by default" 设计理念的体现。