简介:围绕 mbedtls 在 ARM SOC 与 armv9 嵌入式平台上的移植实践,这份 PDF 面向具备 C 语言和交叉编译基础、希望将 TLS 加密库落到实际工程的嵌入式开发者。内容以可复用工程模板为线索,涵盖源码组织、库与头文件整合、Makefile 工具链适配、AES 与 SHA256 等模块的编译裁剪、自测接口调用及 QEMU 环境验证,帮助读者理清目录结构、宏开关与编译配置之间的对应关系,并积累移植排错思路。包内共 1 个 PDF 文件,约 199KB,篇幅精炼,适合作为移植流程速查手册或实验课参考资料。目前已有 335 人学习下载,说明其在嵌入式安全与 mbedtls 入门场景中具有一定参考价值。读者可从中获得工程组织方式、编译配置要点和自测验证方法,适合从裸机或 RTOS 过渡到安全通信开发的工程师参考,也可为课程实验与项目预研提供思路,为后续在真实芯片平台上开展安全通信开发打下基础。
1. 从 x86 到 ARM:mbedtls 移植真正改的是哪几行
一个 AES 加解密的 demo,在 PC 上跑得好好的,交叉编译完丢到板子上,第一条打印还没出来就没了。这种翻车现场我经历过不止一次,问题基本都不在算法本身,而在 mbedtls 默认假设自己活在一个有完整 libc、有time()、有熵源的世界里。mbedtls 移植这件事,说到底就是把这些假设一条条改掉,让这个为嵌入式场景设计的轻量 TLS 库真正认你的 ARM 芯片,而不是认你的开发机。
这篇笔记按我自己从头复现过一遍的顺序走:拉源码、拆目录、搭工程骨架、改 Makefile 指定交叉工具链、开MBEDTLS_SELF_TEST跑算法级自测,最后丢进 QEMU + Buildroot 里跑通并验证。中间卡的几处不藏,尤其是链接期和部署期那两个最容易反复踩的位置。适合手上已经有 ARM 交叉工具链、能对着 Makefile 改两行的人看;做 armv9 平台的 SoC 也好,做 Cortex-M 的裸机也好,思路是同一套。
2. 拆开 mbedtls 源码树:include 与 library 之外还有什么值得留
拿到源码的第一件事不是编译,是判断哪些目录该跟着工程走、哪些只是给你看样例用的。很多人一上来把整个仓库拷进工程,结果提交记录里多出几万行测试数据,升级库的时候又是一场血泪经验。先把目录分工理清楚,后面每一步都省事。
2.1 移植真正要带走的只有两个目录
mbedtls 的顶层目录分工很清楚,下面是常见布局和我一般怎么处理:
| 目录 | 作用 | 移植时是否带走 |
|---|---|---|
include/mbedtls | 公共头文件,含配置头mbedtls_config.h、check_config.h、build_info.h | 要 |
library | 全部.c实现,算法、X.509、SSL 协议栈、平台抽象层 | 要,或编译成.a |
programs | 示例程序,AES 加解密、证书解析、自测入口 | 不要,但值得翻一翻抄调用方式 |
tests | 测试用例与测试向量数据,体量大 | 不要 |
scripts | config.py等辅助脚本,用来做宏的增删 | 只在改配置时需要 |
configs | 若干预置配置模板,面向不同资源约束场景 | 需要时拷一个出来 |
framework、3rdparty | 较新分支引入的子模块与第三方组件 | 一般不要 |
include和library是硬依赖,其余都是工具或者参考。特别提醒一句:较新的分支把测试框架拆成了独立子模块,git clone时没加--recursive的话,framework目录是空的,编译库本身不受影响,但想跑完整测试就会失败,别把这个当成库有问题。
配置头的位置也要先确认。老一些的版本叫include/mbedtls/config.h,3.x 之后改成了include/mbedtls/mbedtls_config.h。这个改名不算大事,但如果你照着旧教程去改config.h,改完发现编译行为一点变化都没有,那就是改了个不存在的文件——我在这个问题上浪费过半个下午。
2.2 源码内嵌和静态库,选哪个
移植方式有两种,取决于这个工程要活多久。
第一种是源码内嵌:把include和library整个拷进你的工程目录,Makefile 里只挑这次用到的.c编。优点是路径直观、改库方便、断点能直接打进算法实现里,第一次移植和单平台小工具用它最快。缺点是升级库要手工再拷一遍,容易和旧文件混在一起;多平台共用一份代码时,每个工程都存一份副本,最后没人说得清哪个是哪个版本。
第二种是先编成静态库再链接:
# 在 mbedtls 源码树里直接编库,CC/AR 指向交叉工具链 make lib \ CC=aarch64-linux-gnu-gcc \ AR=aarch64-linux-gnu-ar \ CFLAGS="-O2 -DMBEDTLS_CONFIG_FILE='\"mbedtls_config.h\"'" # 产物在 library 目录下,一般会有三个 ls library/*.a # libmbedcrypto.a libmbedx509.a libmbedtls.a三个库是分层的:libmbedcrypto.a是纯算法(AES、SHA、RSA、ECC 等),libmbedx509.a依赖它做证书解析,libmbedtls.a再往上才是 TLS 协议栈。只用对称加密和摘要的话,链接第一个就够,体积能省一大截。工程侧只要-I指到头文件目录、-L指到库目录、-lmbedcrypto就完事。
我一般的做法是先用第一种方式把自测跑通,确认工具链、配置头、宏开关这一整套链条没问题,再把库编成.a挪进正式工程。跳过第一步直接上静态库,出问题时你分不清是配置错了还是链接顺序错了,那才是真正的黑匣子。
3. 搭一个能交叉编译的骨架:Makefile 里要动的四处
有了目录规划,接下来把它变成一个敲make就能出二进制的工程。这一步最容易出的错不是语法,而是“编译过了”和“能在目标上跑”之间的差距。
3.1 从最小 C 模板长出第一个可编译目录
找一个只有main.c加一个Makefile的极简 C 工程模板就够了,越简单越好,模板里自带的日志组件、环形缓冲、状态机全都先删掉,它们只会干扰你判断问题出在 mbedtls 还是出在模板。把模板目录复制一份改成你的工程名,再把库文件搬进去:
# 建工作区,拉 mbedtls 源码 mkdir -p ~/work && cd ~/work git clone --recursive <mbedtls 官方仓库地址> mbedtls # 复制模板目录,去掉模板自带的版本控制信息 cp -r c_template mbedtls_test rm -rf mbedtls_test/.git # 把库的头文件和实现搬进工程 cp -r mbedtls/include mbedtls_test/ cp -r mbedtls/library mbedtls_test/这里有个细节值得停一下:cp -r mbedtls/library mbedtls_test/和cp -r mbedtls/library/ mbedtls_test/结果不一样。前者会把library目录本身放进去,得到mbedtls_test/library/;后者会把目录里的内容直接倒进mbedtls_test/,把main.c和几百个.c混在同一层。移植场景下要的是前者,保持目录层级清晰,Makefile 里的路径也好写。
搬完之后工程目录长这样:main.c、Makefile、include/、library/。include下面还有一层mbedtls/,所以头文件包含写成#include "mbedtls/aes.h",Makefile 里-Iinclude就够了,不用-Iinclude/mbedtls。
3.2 Makefile 里要动的四处:CC、CFLAGS、SRCS、LDFLAGS
模板的 Makefile 基本要重写一遍。我把关键部分贴出来,注释里写了每一处为什么这么设:
# 交叉工具链前缀,按手上实际的改:aarch64-linux-gnu- / arm-linux-gnueabihf- / riscv64-linux-gnu- CROSS_COMPILE ?= aarch64-linux-gnu- CC := $(CROSS_COMPILE)gcc AR := $(CROSS_COMPILE)ar STRIP := $(CROSS_COMPILE)strip TARGET := mbedtls_demo BUILD := build # 工程内嵌的头文件目录,include 下面还有一层 mbedtls/ INC := -Iinclude # 关键:只挑这次要编的源文件,不要把 library 全塞进来 # aes.c 和 sha256.c 都依赖 platform_util.c 里的 mbedtls_platform_zeroize SRCS := main.c \ library/aes.c \ library/sha256.c \ library/platform_util.c # 配置头文件名显式指定,避免和系统里装的其他版本混淆 # -DMBEDTLS_SELF_TEST 打开自测代码,让算法源文件把 self_test 函数编进来 CFLAGS := $(INC) -O2 -Wall -Wextra \ -DMBEDTLS_CONFIG_FILE='"mbedtls_config.h"' \ -DMBEDTLS_SELF_TEST # 静态链接,目标 rootfs 缺 libc 时也不会挂 LDFLAGS := -static OBJS := $(addprefix $(BUILD)/,$(SRCS:.c=.o)) all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $@ $(LDFLAGS) # 源文件在子目录里,先把 build/library/ 这种目录建出来 $(BUILD)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) -c $< -o $@ clean: rm -rf $(BUILD) $(TARGET) .PHONY: all clean四个关键点逐个说。CROSS_COMPILE用?=而不是:=,是为了能在命令行覆盖,调试时敲make CROSS_COMPILE=arm-linux-gnueabihf-就能换目标板,不用改文件。CFLAGS里的-DMBEDTLS_CONFIG_FILE='"mbedtls_config.h"'这串引号是故意写两层的:Makefile 把单引号原样交给 shell,shell 剥掉单引号保留双引号,编译器最终收到的宏值才是一个字符串字面量。少一层引号,编译就会报找不到配置头。
SRCS是这套骨架里最需要手动维护的一行。mbedtls 的.c之间有隐式依赖,aes.c用到了platform_util.c里的清零函数,sha256.c同理,一开始只加算法文件必然链接失败。经验做法是先加你确定要的算法,编一次,把所有 undefined reference 逐个补齐,最后SRCS里剩下的就是最小集合。
LDFLAGS里的-static看场景。跑在 Buildroot 生成的完整 rootfs 上其实不加也行,但如果目标环境连 libc 都精简掉了,静态链接是唯一省心的选择,代价是体积从几十 KB 涨到一两 MB。
3.3 第一次链接失败要看的三处
编译报错还好办,链接报错才是新手最容易卡住的地方。三种典型输出,对应三种完全不同的原因:
undefined reference to 'mbedtls_platform_zeroize',说明SRCS里少了platform_util.c。这是 mbedtls 里被引用最多的工具函数,几乎每个算法.c都会调它,第一次编基本都会撞上。
undefined reference to 'printf'或snprintf,说明链接阶段没找到 C 库。交叉工具链默认会链目标板对应的 libc,如果你加了-nostdlib又忘了补,或者用了裸机工具链(arm-none-eabi-),就得自己提供 syscall 桩函数。用arm-none-eabi-编出来的二进制也别指望在 QEMU 跑完整 Linux 的环境里执行,两者根本不是一回事。
multiple definition of 'xxx',十有八九是你把library/拷进工程之后,又在SRCS里同时引了源码树外面那份,或者模板里原本就有一个同名文件。查一下find . -name "aes.c"有几个结果就清楚了。
4. 裁剪与自测:用 MBEDTLS_SELF_TEST 把库验到算法级
能编过只是第一步,能证明编进去的算法算得对,才算移植完成。mbedtls 自带一套自测代码,每个算法都有对应的self_test函数,用标准测试向量跑一遍,返回码直接告诉你哪一组对不上。
4.1 配置文件到底是哪个名字
前面提过配置头改名的事,这里展开说清楚该改什么。打开配置头,你会看到几百个MBEDTLS_*_C宏,默认配置基本是全开的,直接编进去体积会很难看。移植场景下通常要做两件事:关掉这次用不到的模块,打开自测开关。
编译期可以用-DMBEDTLS_CONFIG_FILE='"mbedtls_config.h"'指定配置头文件名,也可以用默认名。指定文件名的好处是你可以放多份配置,比如config_small.h和config_full.h,靠CFLAGS切换,不用来回改同一个文件。这个技巧在同一个工程要给两种规格的板子出固件时特别好使。
4.2 打开自测开关并写调用
-DMBEDTLS_SELF_TEST这个宏的作用,是让算法源文件里#if defined(MBEDTLS_SELF_TEST)包住的那段自测代码参与编译。不开这个宏,mbedtls_sha256_self_test这个符号根本不存在,链接期直接报未定义。main.c里这样调用:
#include <stdio.h> #include "mbedtls/aes.h" #include "mbedtls/sha256.h" #include "mbedtls/version.h" /* 返回 0 表示所有测试向量都对上了,非 0 是负的错误码 */ static int run_selftest(void) { int ret; #if defined(MBEDTLS_SELF_TEST) /* verbose 传 1 会把每组测试向量打印出来,量产固件里传 0 */ ret = mbedtls_sha256_self_test(1); if (ret != 0) { printf("sha256 selftest failed: -0x%04X\n", (unsigned)-ret); return ret; } ret = mbedtls_aes_self_test(1); if (ret != 0) { printf("aes selftest failed: -0x%04X\n", (unsigned)-ret); return ret; } printf("mbedtls %s selftest ok\n", MBEDTLS_VERSION_STRING); #else printf("MBEDTLS_SELF_TEST not enabled, skip\n"); #endif return 0; } int main(void) { return run_selftest() == 0 ? 0 : 1; }参数只有一个verbose,控制是否把中间的测试向量打印出来。调试阶段传1,能看到它是卡在 AES-128-ECB 还是 AES-256-CBC 上;正式固件传0,省掉几百行串口输出。返回值是 mbedtls 风格的负错误码,打印时取负再按%04X输出,对不上时直接查错误码表就能定位。
一个常被忽略的点:main()把自测结果作为进程退出码返回。这一行在自动化里比打印重要得多——CI 里跑一遍,echo $?是 0 就过,不用去 grep 日志。
4.3 用 config.py 做减法和加法
手改配置头容易漏,也容易改出前后矛盾的组合。mbedtls 自带一个配置脚本,增删宏都是一条命令:
# 关掉用不到的网络模块,能省下一大截空间 python3 mbedtls/scripts/config.py \ -f mbedtls_test/include/mbedtls/mbedtls_config.h \ unset MBEDTLS_NET_C # 确认 AES 和 SHA256 是开着的 python3 mbedtls/scripts/config.py \ -f mbedtls_test/include/mbedtls/mbedtls_config.h \ set MBEDTLS_AES_C # 打开自测和平台抽象层,让 mbedtls_printf 有实现 python3 mbedtls/scripts/config.py \ -f mbedtls_test/include/mbedtls/mbedtls_config.h \ set MBEDTLS_SELF_TEST python3 mbedtls/scripts/config.py \ -f mbedtls_test/include/mbedtls/mbedtls_config.h \ set MBEDTLS_PLATFORM_C-f指定要操作的配置文件路径,不给的话会去改源码树里默认那份,很容易改错地方。set和unset之后脚本会做一次依赖检查,比如你unset了MBEDTLS_AES_C却还留着依赖它的模式,脚本会提示。这个检查比人眼可靠,裁剪配置时我一般只通过它来改。
MBEDTLS_PLATFORM_C建议一开始就打开。它提供了mbedtls_printf、mbedtls_calloc、mbedtls_time这一层抽象,后面真要往裸机或者 RTOS 上搬的时候,重定向的入口就在这里,不用回头大改。跳过这一步,等发现需要接管内存分配时,改动面会大得多。
改完配置重新make,先跑一次自测确认算得对,再去做体积优化。顺序反了的话,出问题时分不清是裁剪裁坏了还是算法本身没编对。
5. 避坑与排查:五个把我按在工位上的现场
下面这几条都是真撞过的,按“现象 → 原因 → 解决”写,能对号入座最好。
5.1 编译与链接期的三个坑
现象:链接报undefined reference to 'mbedtls_printf'。原因是你开了-DMBEDTLS_SELF_TEST但没开MBEDTLS_PLATFORM_C,自测代码里的打印走的是mbedtls_printf,而它的实现挂在平台层上。解决是回到配置头把MBEDTLS_PLATFORM_C打开,或者更省事的办法,自测时verbose传0,不触发那条打印路径。
现象:改了配置头,编译行为一点没变。原因有两个可能,一是-DMBEDTLS_CONFIG_FILE的引号在 Makefile 里写错了层数,编译器根本没收到这个宏,用的还是默认配置;二是你改的配置头和实际包含的不是同一个文件,config.h和mbedtls_config.h并存时特别容易搞混。排查方法很直接:make -n把编译命令打出来,肉眼确认-D后面的路径,再用grep -r "MBEDTLS_SELF_TEST" mbedtls_test/include/看宏到底定义在哪份头里。
现象:交叉编译通过,目标板运行报Illegal instruction。原因是工具链架构和目标 CPU 对不上,最常见的是把aarch64-linux-gnu-编出来的东西丢到 32 位 ARM 环境里跑,或者-march设了一个目标芯片不支持的指令集扩展。解决时先看 ELF 头:readelf -h mbedtls_demo | grep Machine,AArch64 还是 ARM 一眼就能看出来,再和板子的uname -m对一下。
5.2 运行与部署期的两个坑
现象:程序启动就挂,或者自测返回-0x0002之类的错误码。先别怀疑算法。mbedtls 里用到随机数的路径在初始化时会去取熵,如果配置里开了MBEDTLS_ENTROPY_HARDWARE_ALT却没实现mbedtls_hardware_poll,链接期就会报未定义;如果你的实现是空函数返回 0,运行期就会在需要熵的地方行为异常。解决是把mbedtls_hardware_poll补上,Linux 环境里读/dev/urandom就行,裸机上得从芯片的 TRNG 取。跑纯算法自测(AES、SHA256)其实不需要熵,但你后面一上 TLS 握手就会撞上,早点补掉省事。
现象:二进制明明拷进去了,QEMU 里执行还是老版本。原因通常是放错了位置或者没触发重新打包。Buildroot 的output/target/是中间产物目录,你手工cp进去的文件在下一次make时可能被覆盖;规范的落点是板级目录下的rootfs_overlay,Buildroot 每次打包都会把 overlay 的内容合并进最终镜像。判断到底跑的是哪个:把新二进制的 md5 和目标机上md5sum的结果对一下,不一致就说明打包链路断了,别在程序里找原因。
6. 在 QEMU + Buildroot 里验证:体积、依赖与一次跑通
把二进制放进 QEMU 环境有三种做法,可靠性差别很大。最省事也最不推荐的是直接cp到output/target/usr/bin/,然后make一次触发 rootfs 重打包——能用,但下次干净编译就没了。规范做法是找到板级配置目录下的rootfs_overlay,把文件放到对应的usr/bin/子目录里,Buildroot 会在 target-finalize 阶段自动合并。第三种是写一个小的 post-build 脚本,把编译产物复制进去,适合产物路径经常变的工程。
# 规范落点:板级 overlay 目录,重启构建后依然生效 cp mbedtls_test/mbedtls_demo \ buildroot/board/qemu/<板型>/rootfs_overlay/usr/bin/ chmod +x buildroot/board/qemu/<板型>/rootfs_overlay/usr/bin/mbedtls_demo # 增量构建,只重跑 target-finalize 和镜像打包 cd buildroot && make # 启动,镜像和启动脚本都在 output/images 下 ./output/images/start-qemu.sh登录之后跑/usr/bin/mbedtls_demo,退出码用echo $?看,0 就说明自测全过。别只看屏幕上的selftest ok,退出码才是自动化能用的信号。
验证阶段我固定看四项,形成一个检查表:
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| ELF 架构 | readelf -h mbedtls_demo | grep Machine | 与目标板uname -m一致 |
| 动态依赖 | readelf -d mbedtls_demo | grep NEEDED | 静态链接时输出为空 |
| 段占用 | size mbedtls_demo | text是主要占用,data/bss很小 |
| 运行结果 | ./mbedtls_demo; echo $? | 退出码 0 |
体积优化放在功能验证之后做,顺序别反。先按默认配置跑通、自测通过,再逐个unset用不到的模块,每裁一项重新跑一次自测。我见过有人一口气关掉十几个宏,最后 TLS 握手失败,回头一个一个二分,花的时间比一开始慢慢裁多好几倍。strip也要在最后做,调试阶段留着符号表,gdb 挂上去能直接看到是哪个self_test里挂的。
从那以后我每次做库移植,不管多简单的场景,都强制走一遍「默认配置跑通自测 → 记录退出码 → 再裁剪 → 再自测」这个循环,中间任何一步跳过,后面都得加倍还回来。希望帮到你。
本文还有配套的精品资源,点击获取