4. 编译系统与配置宏裁剪
密码库的裁剪最终要落到构建系统上。源码里写了多少 #ifdef,构建系统就得提供对应的开关;开关定义得是否准确,直接决定最终固件里到底链进了哪些目标文件。本节以 mbedTLS 的构建体系为参照对象,说明 Makefile、CMake、Kconfig 三种配置入口的分工,以及如何通过功能宏把库裁剪到最小可用集合。
三种配置入口的分工
在嵌入式密码库工程中,这三者并不是互相替代的关系,而是处在不同层次:
- Kconfig:面向“产品功能选择”,提供菜单化的布尔/整型/字符串选项,输出
.config,再由conf工具生成config.h。适合把“要不要 TLS、要不要 X.509、要不要硬件加速”这类产品级决策暴露给集成人员。 - CMake:面向“目标平台与编译参数”,负责工具链、优化等级、
-D宏定义、源文件集合的筛选。适合跨平台构建和库的静态/动态产物管理。 - Makefile:面向“最小可控构建”,在资源极度受限或需要精确控制每个目标文件时使用,直接列出参与编译的
.c文件与宏定义。
关键原则是:宏定义只能有一个权威来源。如果 Kconfig 生成了 config.h,就不要再在 CMake 的 target_compile_definitions 里重复定义同一批宏,否则会出现“配置界面显示开启、实际编译关闭”的隐蔽问题。
功能宏开关的层级关系
密码库的宏通常分为三层,裁剪时必须按依赖顺序处理:
| 层级 | 典型宏 | 作用 |
|---|---|---|
| 模块级 | MBEDTLS_SSL_TLS_C、MBEDTLS_X509_CRT_PARSE_C | 决定整个模块是否参与编译 |
| 算法级 | MBEDTLS_AES_C、MBEDTLS_SHA256_C、MBEDTLS_ECDSA_C | 决定具体算法实现是否可用 |
| 实现级 | MBEDTLS_AES_ROM_TABLES、MBEDTLS_SHA256_SMALLER、MBEDTLS_ECP_WINDOW_SIZE | 在算法内部做空间/时间权衡 |
依赖是单向的:模块级依赖算法级,算法级依赖实现级。例如关闭 MBEDTLS_SHA256_C 后,MBEDTLS_ECDSA_C 若仍开启,编译会在 ecdsa.c 中报未定义符号,因为 ECDSA 的签名摘要默认走 SHA-256。因此裁剪顺序应当是“先砍模块,再砍算法,最后调实现参数”。
config.h 是常见错误。该文件通常由 config.py 或 Kconfig 自动生成,下次构建会被覆盖。正确做法是修改 mbedtls_config.h 或 .config 源文件。最小化构建实战:一个仅需 AES-GCM + SHA-256 的场景
假设目标是一个只做本地数据加密存储的 MCU 应用,不需要 TLS、不需要非对称算法,只需要 AES-128-GCM 和 SHA-256。裁剪步骤如下。
第一步:确定必需宏集合。 AES-GCM 依赖 AES 分组密码和 GCM 模式,SHA-256 独立。最小集合为:
#define MBEDTLS_AES_C
#define MBEDTLS_GCM_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_PLATFORM_C
#define MBEDTLS_PLATFORM_MEMORY
其中 MBEDTLS_CIPHER_C 是 GCM 的通用密码接口依赖,MBEDTLS_PLATFORM_C 提供内存与打印的平台抽象,若使用自带 calloc/free 可保留。
第二步:用 CMake 精确控制源文件。 不采用 file(GLOB ...) 全量收集,而是显式列出参与编译的文件,避免无关模块被链入:
add_library(crypto_min STATIC
library/aes.c
library/gcm.c
library/sha256.c
library/cipher.c
library/platform.c
)
target_compile_definitions(crypto_min PRIVATE
MBEDTLS_CONFIG_FILE="config_min.h"
)
target_include_directories(crypto_min PUBLIC include)
这里通过 MBEDTLS_CONFIG_FILE 指定自定义配置头,而不是修改默认的 mbedtls_config.h,便于在同一工程中维护多套裁剪配置。
第三步:验证裁剪结果。 编译后用 size 或 arm-none-eabi-size 查看 .text 段大小,并与全量配置对比。同时用 nm 检查是否残留不应出现的符号:
arm-none-eabi-nm -C crypto_min.a | grep -i "rsa\|ecp\|ssl"
# 期望输出为空;若有输出说明裁剪未生效
若发现残留,通常是某个保留模块间接引用了被裁模块,需要顺着符号引用继续关闭上层宏。
条件编译的常见陷阱
裁剪过程中最容易踩的坑有三类。一是宏名拼写错误:MBEDTLS_SHA256_C 写成 MBEDTLS_SHA_256_C,编译器不会报错,只是该宏未定义,对应代码被静默排除,运行时才暴露功能缺失。二是依赖未同步关闭:关闭 MBEDTLS_ENTROPY_C 却保留 MBEDTLS_CTR_DRBG_C,后者依赖前者提供熵源。三是头文件与库版本不一致:应用包含的头文件来自新版本,链接的静态库来自旧版本,宏布局不同会导致结构体大小错位。
gcc -E -dM 导出实际生效的宏集合,与预期清单做 diff,任何多余或缺失的宏都视为构建失败。这能把静默裁剪错误提前到编译阶段暴露。Kconfig 与 CMake 的协同
当产品线较多时,推荐用 Kconfig 管理功能选择,CMake 只消费生成的 config.h。典型流程是:Kconfig 定义 CONFIG_MBEDTLS_AES 等选项,menuconfig 生成 .config,再由脚本把 CONFIG_* 映射为 MBEDTLS_*_C 写入 config.h,最后 CMake 通过 MBEDTLS_CONFIG_FILE 引用它。这样产品经理在菜单里勾选功能,构建系统自动完成宏映射,避免人工维护两套开关。
按上述流程裁剪后,一个原本包含 TLS、X.509、RSA、ECC 的完整库,可以缩减到只保留对称加密与哈希的极小集合。实际缩减幅度取决于原配置,但通常 .text 段能下降一个数量级。真正需要警惕的不是“砍得不够狠”,而是“砍完之后没有验证”——静默失效的宏比编译报错危险得多,因此把宏一致性检查纳入构建流程,是裁剪工作能否稳定复现的关键一步。