资讯详情

Keil4老STM32工程手工移植mbedTLS并跑通TLS握手

📅 2026/11/3 7:30:45 | 华诺云谱 👁 阅读
Keil4老STM32工程手工移植mbedTLS并跑通TLS握手
手上有个跑了三五年的 STM32 老工程编译器还停在 Keil4 那一套uVision4 ARMCC V5启动文件、外设库、中断向量全是当年一行行抠出来的谁也不想为了一个新需求把它整个迁到新工具链上。可偏偏产品现在要接一个 HTTPS 接口拉配置或者把采集数据经 MQTT over TLS 推到云端安全层这块躲不掉了。这时候大多数人的第一反应是搜Keil 添加 mbedTLS搜出来的结果十有八九是 CubeMX 勾两下、Keil5 里 RTE 组件点上 mbedTLS、LwIP 一配编译通过。问题是 Keil4 里根本没有 RTE 那套组件管理机制也没有 Pack Installer你面对的是一个空的工程树和一堆 .uvproj 配置项mbedTLS 这个东西又不像 FatFs 那样只有几个文件丢进去就能跑——它是一整套源码、一份需要手工裁剪的配置头文件、外加若干必须你自己填的移植桩函数。这篇就把在 Keil4 的老 STM32 工程里手工把 mbedTLS 加进去并跑到握手成功这件事从头拆一遍。适合手上握着 Keil4 工程、不愿意或者没条件迁移工具链、又确实需要在 MCU 上跑 TLS 的开发者也适合那些在 Keil5 里点组件点成功了、但完全不知道背后发生了什么、一旦报错就抓瞎的人——手工加一遍你会把这套库的骨架看得清清楚楚。我会把版本选择、文件取舍、配置裁剪、三个移植桩、以及联调阶段那一串负数值错误码的排查链路都写进来都是踩过之后才明白的东西。1. Keil4 里加 mbedTLS 为什么比想象中麻烦1.1 Keil4 与 Keil5 的工程模型差异先说清楚障碍在哪。Keil5MDK5引入的 RTE 组件机制本质上是把源码从哪来、头文件路径是什么、需要开哪些宏这套知识固化成了 .pdsc 描述文件你勾一下工具自动把文件、包含路径、预定义宏全填好。mbedTLS 在 Pack 里有现成的描述所以 Keil5 用户几乎不需要理解库结构。Keil4 用的是 .uvproj 工程文件没有 RTE工程里的每一个 .c 文件、每一条 Include Path、每一个 Define 都要你自己动手加。这意味着两件事第一你必须知道 mbedTLS 到底由哪些文件组成、哪些是必需的、哪些可以扔第二你必须知道这些文件依赖哪些宏开关否则编译过去也是满地错误。好处也不是没有。手工加一遍之后你对这套库的依赖关系会非常清楚后面做体积裁剪、做硬件加速替换、排查内存问题时比点组件的人有底气得多。而且 Keil4 的工程结构简单直白出问题的时候少一层工具帮你做了什么的黑盒。1.2 mbedTLS 到底是几个东西拼起来的很多人以为 mbedTLS 是个 .lib加进去链接就行。不是。mbedTLS 的发行包解开之后跟你相关的只有三块目录内容在 Keil4 里的处理library/全部 .c 实现约六十个文件涵盖大数运算、对称加密、哈希、X.509 解析、TLS 状态机加入工程靠配置裁剪 链接器去冗余include/mbedtls/对应的头文件以及最关键的config.h加入 Include Pathsconfig.h 要改programs/、tests/示例程序和自测代码依赖文件系统、socket、命令行一律不加加进去必然编译不过除此之外还有一层移植层也就是那几个必须由你提供实现的函数随机数熵源、时间获取、网络收发。这三个东西在 PC 上有现成实现读 /dev/urandom、gettimeofday、BSD socket在裸机 MCU 上一样都没有全靠你填。这三个桩填不对库能编译过但握手一定失败而且报错信息往往指向别处非常容易误判。1.3 版本选择别一上来就拉最新这一步很多人栽跟头。mbedTLS 3.x 相比 2.x 做了大量 API 清理移除了一堆废弃接口对 C99 的要求也更严格。ARMCC V5 这个编译器对 C99 的支持是够用但不完整3.x 里部分代码用到的语法特性和内建属性在 armcc5 上会直接报错你得一个个去改改完还不保证行为正确。我的建议很明确Keil4 ARMCC5 的组合选 2.16.x 的 LTS 分支或者 2.28.x 的 LTS 分支。2.16 系列是这个编译器上验证最充分的网上能查到的报错案例也最多2.28 也可以功能更全一些但个别文件需要打小补丁。3.x 就別在这套工具链上折腾了投入产出比太低。选定版本之后把发行包解压到一个固定的第三方目录比如工程根下建third_party/mbedtls-2.16.10/把library和include两个目录拷进去。不要把整个发行包丢进工程目录programs和tests里有几百个文件Keil4 的工程窗口会被撑爆而且查找文件极不方便。2. 动手之前先算三笔账编译器、Flash、RAM2.1 ARMCC5 的 C99 开关与几个隐性坑Keil4 的 Options for Target 里C/C 选项卡有一个 C99 Mode 的复选框必须勾上。mbedTLS 的代码里到处是行内声明变量、//注释、stdint.h定宽类型不开 C99 直接一片红。如果你习惯在 Misc Controls 里手写参数等价的是--c99。光开 C99 还不够还有几个坑要提前知道第一snprintf。mbedTLS 的证书解析、调试输出里会用到它。ARMCC5 的标准库里有但如果你勾了 Use MicroLIB微库对snprintf的支持是不完整的某些格式符会输出异常甚至崩。稳妥做法是定义MBEDTLS_PLATFORM_SNPRINTF_ALT自己挂一个实现上去或者干脆不要开 MicroLIB。第二time.h。库里有几处用到time_t和time()裸机上根本没有。不处理会在链接阶段报time未定义。解决办法在后面配置裁剪那章讲用MBEDTLS_PLATFORM_TIME_ALT挂自己的实现或者直接关掉时间相关特性。第三__attribute__((weak))。armcc5 是支持的但语法位置敏感如果某个文件报这个属性相关的错多半是宏定义被覆盖了检查一下工程里有没有全局的#define __attribute__(x)之类的脏东西。第四警告等级。mbedTLS 源码在 armcc5 下会产生大量变量定义了未使用有符号无符号比较之类的警告。别一开始就追求零警告那会耗掉你两天时间。把工程警告等级先降到 Level 1等功能跑通了再回头看真正有价值的那几条。2.2 Flash 与 RAM 的预算表心里要有数mbedTLS 不是个小东西。默认配置全开的情况下Flash 占用能到 200KB 以上单次 TLS 会话的 RAM 占用超过 30KB。这在 STM32F103 这类 64KB Flash / 20KB RAM 的片子上是完全放不下的必须裁剪。下面这张表是我在几个常见配置下的实测参考值用的是 ARMCC5 编译、开启链接器未用段清除、TLS 1.2 单向认证、RSA-2048 AES-128-GCM SHA-256 这套组合配置档位Flash 增量单会话 RAM适用芯片全默认配置约 190–220 KB32 KB 以上基本不现实中度裁剪关调试、关文件 IO、关自测、保留 x509 解析约 60–75 KB18–22 KB256KB Flash / 64KB RAM 起极限裁剪关 x509 解析、预置公钥、内容缓冲降到 2KB约 32–40 KB8–11 KB128KB Flash / 32KB RAM 勉强这张表的意义在于你得先确认目标片子扛不扛得住再决定是否继续。如果芯片 RAM 只有 20KB还要跑 RTOS 和其他任务那整条 TLS 链路在片内跑就不合适了得考虑外挂一颗带 TLS 卸载的通信模组让 MCU 只发 AT 指令。这个判断要放在动手之前不然移植完了发现内存不够前面的时间全白费。2.3 工程目录与文件组织方式目录规划我推荐这样project/ ├── app/ 用户代码 ├── drivers/ HAL 或标准外设库 ├── third_party/ │ └── mbedtls-2.16.10/ │ ├── include/mbedtls/ 头文件与 config.h │ └── library/ .c 实现 └── port/ ├── mbedtls_port_entropy.c 熵源移植 ├── mbedtls_port_time.c 时间移植 └── mbedtls_port_net.c 网络收发移植把移植代码单独放在port/下面和第三方源码彻底分开好处是将来升级 mbedTLS 版本时直接把third_party整个换掉就行你的移植层一行不用改。这个习惯一旦养成后面维护成本会低很多。3. 把源码真正塞进 Keil4 工程树3.1 文件分组与最小文件集的取舍在 Keil4 的 Project 窗口里右键新建两个 Group一个叫mbedtls一个叫mbedtls_port。前者放库源码后者放你自己的移植文件。往mbedtls组里加文件有两种流派。流派一是把library/下所有 .c 全加进去靠 config.h 裁剪和链接器的未用段清除来控制体积。流派二是先按功能筛一遍只加必需的文件。我倾向流派一理由很实在mbedTLS 内部文件之间互相引用非常密集你手工筛的时候漏掉一个编译能过、链接报未定义符号然后你得回头去查这个符号在哪个文件里来来回回非常消耗耐心。全加进去编译时间会长一点但一次到位。不过有几个文件是明确不需要的可以直接不加能省一点编译时间net_sockets.c依赖 BSD socket 头文件Keil4 环境里没有加了必然报错。timing.c依赖sys/time.h或 Windows 头文件同样编不过。fs_io.c依赖fopen系列裸机上没有文件系统。ssl_ticket.c会话票据功能用到 AEAD 和额外的密钥管理绝大多数 MCU 场景用不上。ssl_cache.c会话缓存除非你要做多客户端复用。其余文件都加上。加完之后第一次编译一定会报一堆错别慌下一节按错误类型逐个处理。3.2 Include Paths 与预定义宏的填写方式Include Paths 里至少要有两条third_party/mbedtls-2.16.10/include以及如果需要自定义配置再加一条你自己放 config.h 的目录。这里有个细节容易翻车Keil4 的路径是相对于 .uvproj 文件所在目录的但如果你用了绝对路径换台机器或者换个盘符就编不过。养成全部用相对路径的习惯路径分隔符用反斜杠或正斜杠都行Keil 两种都认。预定义宏这一栏Define 框功能裁剪阶段会往里加东西初期可以先空着。但有两条建议一开始就加上MBEDTLS_CONFIG_FILEmy_config.h用来指向你自己的配置文件这样升级库版本时不会覆盖你的配置以及MBEDTLS_USER_CONFIG_FILEuser_config.h用来做增量覆盖。这两个宏在 2.x 系列都支持用起来很省心。3.3 首轮编译错误对照与处理第一次编译大概率会停在下面这几类错误上我把处理方式列成表你照着改就行报错关键字根因处理方式cannot open source input file sys/time.h编译了 timing.c 或 net_sockets.c从工程里移除这两个文件或定义MBEDTLS_TIMING_ALT、禁用MBEDTLS_NET_Cidentifier time_t is undefined未开启时间支持但代码引用关闭MBEDTLS_HAVE_TIME、MBEDTLS_HAVE_TIME_DATE或用MBEDTLS_PLATFORM_TIME_ALTundefined symbol time/snprintf库函数在裸机上缺失定义MBEDTLS_PLATFORM_TIME_ALT/MBEDTLS_PLATFORM_SNPRINTF_ALT并实现对应函数#error MBEDTLS_PLATFORM_C is required依赖顺序问题在 config.h 里确保MBEDTLS_PLATFORM_C开启expected a )出现在宏展开处C99 未开启或旧语法确认--c99生效section .bss exceeds或 RAM overflow默认配置内存占用过大进入第 4 章做配置裁剪与缓冲区缩小这张表覆盖了我遇到过的八成首轮问题。剩下的两成通常和你自己工程的既有宏冲突有关比如你的工程里已经定义了MBEDTLS_前缀的宏或者有全局的#define min/max之类。遇到诡异报错先检查工程全局宏定义列表这是个常被忽略的地方。4. config.h 裁剪把几百 KB 的库压进单片机4.1 从默认配置到最小可用配置的裁剪思路mbedTLS 的 config.h 是一份注释极其详尽的清单每个宏上面都写着这个功能是什么、依赖谁、开多大。默认状态下它基本是全开那是给 PC 平台准备的。裁剪的思路不是去背哪些宏该关而是反过来问自己三个问题第一个问题我要连的服务端用什么加密套件如果你能控制服务端配置自建服务直接把套件锁死到TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256这一类那么对称算法只需要留 AES 和 GCM哈希只需要留 SHA-256密钥交换只需要留 ECDHE椭圆曲线只需要留 secp256r1。这一刀下去Flash 能砍掉一半以上。第二个问题证书怎么验证完整验证需要 X.509 解析器MBEDTLS_X509_USE_C、MBEDTLS_X509_CRT_PARSE_C、ASN.1 解析器、PEM 解析器还要有时间函数做有效期检查。如果你采用预置服务端公钥、跳过证书链验证的方案这一整块都可以关掉代价是失去对中间人攻击的防护——这个取舍要看产品形态内网设备和服务端都在你手里风险和公网设备完全不是一个量级。第三个问题要不要调试输出MBEDTLS_DEBUG_C加上MBEDTLS_ERROR_C能让你看到详细的握手流程日志和错误码中文解释调试阶段非常有用但会占十几 KB Flash。我的做法是调试版全开发布版全关用两个 Target 配置切换。4.2 关键宏的取舍清单照着下面这张表改基本能得到一个能跑、体积可控的配置。左边是宏名中间是建议值右边是我为什么这么选宏名建议理由MBEDTLS_SSL_PROTO_TLS1_2开只保留 TLS 1.2TLS 1.0/1.1 现代服务端基本已禁用1.3 在 2.x 上实现不完整MBEDTLS_SSL_PROTO_DTLS关除非做 CoAP over DTLS否则用不上MBEDTLS_SSL_SRV_C关MCU 通常作客户端关掉服务端能省一大块MBEDTLS_SSL_CLI_C开必需MBEDTLS_KEY_EXCHANGE_RSA_ENABLED关静态 RSA 密钥交换没有前向安全性现代服务端多已弃用MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED开主流组合兼容性最好MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED视情况服务端用 ECC 证书时开启MBEDTLS_ECP_DP_SECP256R1_ENABLED开其余曲线全关只留这一条MBEDTLS_AES_CMBEDTLS_GCM_C开现代套件的主流组合MBEDTLS_CIPHER_MODE_CBC关没有 CBC 套件就不需要能省不少代码MBEDTLS_SHA256_C开必需MBEDTLS_SHA1_C关除非服务端证书链里有 SHA-1 签名的中间证书MBEDTLS_MD5_C关同上能关就关MBEDTLS_BIGNUM_C开RSA 和 ECC 都依赖关不掉MBEDTLS_ENTROPY_C开随机数质量是安全底线MBEDTLS_SSL_MAX_CONTENT_LEN4096 或 2048默认 16384 太大这条对 RAM 影响最直接最后一条要特别强调。MBEDTLS_SSL_MAX_CONTENT_LEN决定了 in/out 两个缓冲区的大小默认 16384 意味着光这两块就吃掉 32KB RAM。改成 4096两个缓冲区降到 8KB总 RAM 立刻从三十几 KB 降到十几 KB。代价是单条记录最大只能传 4KB 明文超过就要分片。对 MQTT 这类小包协议完全够用对 HTTP 下载大文件就不合适了。2.x 后期版本还支持分别设置MBEDTLS_SSL_IN_CONTENT_LEN和MBEDTLS_SSL_OUT_CONTENT_LEN可以把发出去的 buffer 设得更小因为客户端上行数据通常很少。4.3 裁剪之后的验证看 map 文件而不是靠感觉改完配置重新编译通过之后不要急着高兴去工程输出目录找.map文件搜索mbedtls前缀的符号把各个模块的占用列出来。这一步能发现两类问题一类是以为关了其实没关。有些特性之间存在隐式依赖你注释掉了主宏但某个子宏还在或者相反某个你注释掉的宏被另一个开着的宏内部重新定义。map 文件里看到一堆 X.509 相关的符号还在就说明没关干净回去查依赖关系。另一类是局部过大。比如你发现ecp_curves.o占了十几 KB那是因为多条椭圆曲线没关bignum.o占了二十几 KB那是正常的大数运算是刚需除非你上硬件加速。Keil4 里还有个更直观的办法编译完成后看 Build Output 窗口最后那行Program Size: Codexxxxx RO-dataxxxx RW-dataxxxx ZI-dataxxxx。把加了 mbedTLS 前后的数字一减Flash 和 RAM 的净增量就出来了比任何估算都准。我习惯每次大改配置都记一笔这个数字形成自己的经验曲线。5. 三个必须自己填的移植桩5.1 熵源随机数质量决定了整条链路的安全下限TLS 握手里的密钥、随机数、IV 全部依赖一个高质量随机源。mbedTLS 的熵模块MBEDTLS_ENTROPY_C本身是个收集器它需要至少一个熵源喂数据PC 上用的是硬件熵池或/dev/urandomMCU 上你要提供。如果你的 STM32 带硬件随机数外设RNG那是最好的情况。初始化 RNG 时钟、使能外设、写回调函数把 4 字节的随机数读出来喂给 mbedTLS。注意 RNG 外设有启动时间第一次读之前要等DRDY标志置位并且要检查状态寄存器里的种子错误标志SEIS和时钟错误标志CEIS出错就复位重来。这几个检查很多例程都省了实际产品里出过一次随机数全零的故障整个批次设备握手都失败排查了两天才定位到是上电时序问题导致 RNG 没起来。没有硬件 RNG 的片子只能用软件熵源兜底。常见做法是采集 ADC 悬空引脚的低位噪声、内部温度传感器的末位抖动、或者 RC 振荡器计数的微小偏差。这种熵的质量远不如硬件 RNG但聊胜于无。mbedTLS 允许挂多个熵源你可以在硬件 RNG 之外再挂一个软件熵源做混合这样即使硬件源出问题整体熵也不会完全塌陷。还有一种情况你的芯片没有 RNG也不想折腾软件熵源直接把一个预置的固定种子喂给 CTR-DRBG。这样能跑起来握手也能成功但随机数可预测安全性等于零。做功能验证可以上产品绝对不行这一点要心里有数。5.2 时间为什么这个桩不填会握手失败TLS 要时间有两个用途一是验证证书有效期二是参与某些握手消息的计算会话票据、部分扩展。如果你开启了MBEDTLS_HAVE_TIME却没提供time()实现链接会报错如果你关了它但服务端要求校验证书有效期握手会在证书验证阶段返回-0x2700MBEDTLS_ERR_X509_CERT_VERIFY_FAILED。做法分两种。正规做法是接一个 RTC 或者从网络协议比如 SNTP拿到时间然后实现mbedtls_platform_set_time()挂进去#include mbedtls/platform_time.h static mbedtls_time_t port_time(mbedtls_time_t *timer) { mbedtls_time_t now rtc_get_unix_seconds(); /* 你自己实现的 RTC 读取 */ if (timer ! NULL) *timer now; return now; } void mbedtls_port_time_init(void) { mbedtls_platform_set_time(port_time); }注意 RTC 时间的来源问题设备出厂时 RTC 不一定是对的上电后从 1970 年开始走这时候去验证证书会显示证书尚未生效。稳妥做法是首次联网后通过服务端返回的时间校准 RTC之后再走正常的证书校验流程。简化做法是直接关掉MBEDTLS_HAVE_TIME和MBEDTLS_HAVE_TIME_DATE认证模式设为MBEDTLS_SSL_VERIFY_NONE或者只做公钥比对。前者完全跳过证书校验后者把服务端证书的公钥手工烧进固件握手时只比对公钥是否一致而不看签名链和有效期。第二种在资源紧张的场合非常实用安全性比完全不校验强得多能防住大部分中间人代码量又比完整 X.509 解析少几十 KB。5.3 网络收发的 BIO 回调把库和你的通道接起来这是三个桩里最容易写错的一个。mbedTLS 不关心你底下是 TCP socket、串口上的模组 AT 通道、还是某种私有总线它只要求你提供两个函数发数据和收数据。通过mbedtls_ssl_set_bio()注册进去static int net_send(void *ctx, const unsigned char *buf, size_t len) { int fd *(int *)ctx; int ret tcp_send_raw(fd, buf, (int)len, 5000); if (ret TCP_ERR_WOULDBLOCK) return MBEDTLS_ERR_SSL_WANT_WRITE; /* 关键必须原样返回 */ if (ret 0) return MBEDTLS_ERR_NET_SEND_FAILED; return ret; /* 返回实际发送字节数 */ } static int net_recv(void *ctx, unsigned char *buf, size_t len) { int fd *(int *)ctx; int ret tcp_recv_raw(fd, buf, (int)len, 5000); if (ret TCP_ERR_WOULDBLOCK) return MBEDTLS_ERR_SSL_WANT_READ; if (ret 0) return MBEDTLS_ERR_SSL_CONN_EOF; /* 对端关闭 */ if (ret 0) return MBEDTLS_ERR_NET_RECV_FAILED; return ret; }有两个细节必须强调。第一底层是非阻塞 socket 时EAGAIN这类情况必须翻译成MBEDTLS_ERR_SSL_WANT_READ或MBEDTLS_ERR_SSL_WANT_WRITE原样返回绝不能返回 0 或者 -1。返回 0 会被库理解成对端关闭连接返回 -1 会被当成致命错误握手立刻中断。这个坑我在两个项目里都踩过报错信息指向MBEDTLS_ERR_SSL_CONN_EOF看着像服务端主动断开实际是自己代码的问题。第二net_send必须返回实际发送的字节数不能返回 0 表示成功。TCP 的短写sent 小于 len在 mbedTLS 里是允许的库会自己重试剩余部分但前提是你如实返回已发送的长度。返回 0 会被当成发不出去进入 WANT_WRITE 循环最后超时。如果你的通道是模组 AT 指令那net_recv的实现要处理数据还没到的状态。AT 模组的IPD上报是异步的需要一个环形缓冲区做中转net_recv从环形缓冲区里取数据取不到就返回 WANT_READ。这个结构比直接调 socket 复杂但逻辑是一样的。5.4 证书与主机名校验的落地配置握手前的配置顺序有个固定套路顺序错了会返回-0x7100MBEDTLS_ERR_SSL_BAD_INPUT_DATA。正确的顺序是初始化mbedtls_ssl_context和mbedtls_ssl_config调用mbedtls_ssl_config_defaults()设置客户端模式注册 RNG 回调设置认证模式挂 CA 证书链设置主机名最后mbedtls_ssl_setup()绑定配置。主机名这一步特别容易漏。mbedtls_ssl_set_hostname()除了给 SNI 用还参与服务端证书的 CN/SAN 比对。漏了这一步证书链验证能过但主机名校验会被跳过某些服务端会直接拒绝握手。而且这个函数传入的字符串必须在 SSL 上下文销毁前保持有效不能传一个栈上的临时缓冲区。CA 证书的嵌入方式也有讲究。PEM 格式需要MBEDTLS_PEM_PARSE_C和MBEDTLS_BASE64_C还要一个几百字节的解析上下文DER 格式二进制省掉这两块直接把openssl x509 -outform der出来的字节数组烧进固件即可。资源紧张时坚决用 DER。烧进去之后用mbedtls_x509_crt_parse_der()解析注意传的是长度而不是 0。6. 联调阶段那串负数的错误码怎么读6.1 从返回码反推问题所在mbedTLS 所有函数返回负数表示错误这些负数是十六进制编码的模块号加错误号。第一次看到-0x2700这种东西会懵其实有规律高字节是模块标识低字节是具体错误。下表是我在联调阶段最常撞见的几个返回码宏名通常的根因-0x2700MBEDTLS_ERR_X509_CERT_VERIFY_FAILED证书链不完整、时间不对、主机名不匹配-0x7100MBEDTLS_ERR_SSL_BAD_INPUT_DATA配置顺序错、参数为 NULL、未设置主机名-0x7F00MBEDTLS_ERR_SSL_ALLOC_FAILED堆不够或者堆已碎片化-0x6900MBEDTLS_ERR_SSL_WANT_READ不是错误需要继续读-0x6880MBEDTLS_ERR_SSL_WANT_WRITE不是错误需要继续写-0x7880MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY对端正常关闭属于预期结果-0x7780MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE服务端发了致命告警通常是套件不匹配或证书被拒看到-0x6900和-0x6880不要当错误处理它们是还没准备好再来一次的意思正确做法是把这两个值从握手循环里排除出去继续调用。很多初学者写了if (ret ! 0) { 报错; }结果握手永远卡在第一次调用上。6.2 打开调试输出定位卡在哪一步返回码只能告诉你失败了告诉不了你失败在哪一帧。要看细节把MBEDTLS_DEBUG_C和MBEDTLS_ERROR_C打开然后在握手前设置调试级别mbedtls_ssl_conf_dbg(conf, my_debug_print, NULL); mbedtls_debug_set_threshold(4); /* 4 是详细级别看握手流程够用 */my_debug_print就是一个把字符串往串口打的函数注意它接收的字符串不带换行你要自己补。级别调到 4 之后串口会打印出每一步握手消息的类型、长度、协商出来的套件、证书链的解析过程。有了这份日志问题定位就从猜变成看。典型的现象对照日志停在 write client hello之后没有 read server hello说明底层发包或者服务端根本没响应日志显示收到了 server hello 但套件协商失败说明两边支持的算法交集为空日志走到证书验证然后报-0x2700回去查 CA 链和系统时间。6.3 栈溢出这个隐形杀手Keil4 工程的启动文件里Stack_Size默认往往是 0x4001KB甚至更小。mbedTLS 的握手路径上局部变量不少尤其是 RSA 和 ECC 运算函数调用层级深1KB 栈根本不够。现象很有迷惑性程序不报错直接跑飞或者进 HardFault或者表现成某个不相关的全局变量被莫名其妙改写。处理办法是把启动文件里的Stack_Size改大客户端单向认证场景我一般设到 0x1000 到 0x20004KB 到 8KB。改完还要确认链接器没有把栈和堆叠在一起、总 RAM 没有超。如果芯片 RAM 确实紧张另一个思路是把握手过程放在一个专门的任务里跑给这个任务单独分配大栈主栈保持小巧。判断是不是栈问题的快捷方法在mbedtls_ssl_handshake()调用前后各填一个已知模式的数组握手返回后检查这个数组有没有被改写。如果被改写了基本可以确诊栈越界不用再猜别的。6.4 内存不够时的三种降级路线如果实测下来 RAM 就是不够按这个顺序降级第一步缩小MBEDTLS_SSL_MAX_CONTENT_LEN。从 16384 降到 4096 甚至 2048能省出 24KB 以上的 RAM。这一步几乎无痛只要你的应用层单包不超过这个长度。第二步关掉 X.509 完整解析改用预置公钥比对。能省 8 到 15KB Flash 和几 KB RAM同时省掉一堆堆分配。第三步把MBEDTLS_SSL_CONTEXT_SESSION相关特性关掉减少上下文大小。再不行就只能考虑外挂方案了。顺带提一句堆的问题。mbedTLS 的很多结构是动态分配的如果你的工程用的是自己实现的malloc比如简单的内存池一定要确认对齐正确、块头开销算够。我遇到过一次握手随机失败最后发现是自实现的free没有正确合并相邻块跑了几十次握手之后堆碎片化mbedtls_calloc返回 NULL报-0x7F00。改成固定大小的内存池之后问题消失。7. 跑通之后性能、加速与量产运维7.1 握手耗时实测与优化空间软件实现的 RSA-2048 签名验证在 72MHz 的 Cortex-M3 上大概要 400 到 800 毫秒ECC secp256r1 的 ECDHE 运算约 200 到 400 毫秒整个握手过程含网络往返通常在 1.5 到 3 秒之间。这个数字对很多应用是可以接受的但如果你的设备是电池供电、每次上报都要新建连接那握手开销就很可观了。优化手段按性价比排序第一是会话复用把上次握手协商出来的会话参数缓存下来下次连接走简化握手能省掉最贵的非对称运算第二是保持长连接别每次发数据都重连第三才是硬件加速。前两条是架构层面的改起来麻烦但收益大硬件加速是锦上添花。7.2 用芯片自带的加密外设替换软件实现不少 STM32 型号带 AES 和 HASH 硬件加速外设。mbedTLS 提供了替换接口定义MBEDTLS_AES_ALT、MBEDTLS_SHA256_ALT等宏然后自己实现对应的一整套函数注意是一整套不能只实现其中几个头文件里的函数声明一个都不能少。实测下来AES 走硬件能快三到五倍SHA-256 能快两到三倍。但要注意两点一是外设初始化、DMA 传输、等待完成的流程会引入额外开销数据量很小比如几十字节的时候硬件反而不如软件快我测过 32 字节的分组加密软件实现比配置外设 启动 等待 读结果这套流程还快一点二是替换实现必须和 mbedTLS 的接口语义严格一致包括上下文结构体的字段、返回值约定写错一个地方就会得到握手成功但数据解密全是乱码这种诡异现象。有意思的是RSA 和 ECC 这类非对称运算在 STM32 上通常没有硬件加速只能靠软件大数运算这也是握手慢的主要原因。PKA 外设只在部分高性能型号上才有。7.3 证书过期这件事量产之后一定会遇到嵌入式项目里最容易被忽略的运维问题是证书有效期。开发阶段一切正常设备出货一年半载之后服务端换了证书或者根证书到期几万台设备同时连不上。这类故障排查起来非常痛苦因为设备端只报一个-0x2700。几个预防措施值得提前做服务端证书有效期尽量拉长或者使用自动化续期机制设备端在握手失败时把详细错误码和当前 RTC 时间上报到诊断通道方便远程判断是证书问题还是网络问题如果采用预置公钥方案把公钥做成可远程更新的别烧死在代码里。还有一个实践在设备里加一个降级通道。TLS 握手连续失败 N 次之后切到一个受限的明文通道只做固件升级和证书更新升级完再切回来。这个通道当然要有限制条件比如只能在特定网段、只能下载签名过的固件但它能在关键时刻救你一命避免整批设备变砖。我个人在这套东西上折腾最久的地方其实不是配置裁剪也不是移植桩而是那个-0x6900被当成错误处理导致的握手永远失败。当时日志里啥都有就是看不到失败点最后把net_recv的返回值一个个打印出来才反应过来。所以如果你现在正好卡在握手阶段第一件事不是去翻 config.h而是把 BIO 回调里每一个返回值都打到串口上看一眼有没有把 WANT_READ 和 WANT_WRITE 给吞掉。这个检查只花五分钟能省掉两天。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑