【避坑实录】Qt 4.8.6 + Paho MQTT C客户端 + OpenSSL静态链接的血泪史
📌 前置声明:本文所述问题均在嵌入式交叉编译环境(ARM架构)下发生,但问题根因与解决方案同样适用于其他嵌入式平台。桌面开发遇到类似问题?恭喜你,至少不用半夜爬起来重启设备了。
📖 一、项目背景与问题概述
1.1 技术栈
| 组件 | 版本 | 说明 |
|---|---|---|
| Qt | 4.8.6 | 没错,这就是那个2012年发布的古董级Qt |
| MQTT客户端 | Paho C 1.3.12 | Eclipse出品的经典MQTT库 |
| OpenSSL | 1.1.1w | 板子上其他模块都在用 |
| 目标平台 | ARM嵌入式 | 跑的是Linux |
| 交叉编译工具链 | ARM | 庙小妖风大 |
1.2 原始需求
做一个MQTT配置文件,用来实现:
- MQTT连接地址、端口号可配置
- TLS/SSL开关
- 匿名登录/用户名密码登录切换
- 证书路径配置
- 日志开关(
g_bModuleLogEnabled和g_bMqttServerLogEnabled)
1.3 遇到的问题(按时间顺序)
- 非TLS连接正常,TLS连接失败 :
rc=-8(SSL配置错误) - undefined symbol错误 :
OPENSSL_sk_pop_free, version OPENSSL_1_1_0 - 静态库符号丢失:明明链接了OpenSSL静态库,但运行时找不到符号
🔍 二、问题分析过程
2.1 第一层坑:rc=-8 神秘错误码
项目跑起来后,非TLS连接一切正常。一旦启用TLS:
★★☆ MQTTClient_connect返回: rc=-8
错误: MQTTClient_connect失败, rc=-8
未知错误码: -8
Paho MQTT的 rc=-8 是什么错误?翻遍Paho源码,找到定义:
cpp
// MQTTClient.h
#define MQTTCLIENT_NULL_PARAMETER -8
rc=-8 = 空指针参数。Why?
看代码中的问题代码(请勿模仿):
cpp
// ❌ 错误写法:toUtf8()返回临时QByteArray,指针下一秒就失效
conn_opts.username = settings.value("username").toString().toUtf8().constData();
conn_opts.password = settings.value("password").toString().toUtf8().constData();
sslOptions.trustStore = m_caCertPath.toUtf8().constData();
生命周期问题:临时对象在语句结束时就析构了,指针指向的内容变成垃圾数据。
cpp
// toUtf8()返回的是临时QByteArray对象
// .constData()返回的是这个临时对象内部的const char*指针
// 语句结束后,临时QByteArray被析构,内存被释放
// conn_opts.username指向的内存已经无效了!
✅ 正确写法:
cpp
// 保存为成员变量,生命周期跟随对象
m_mqttUsernameUtf8 = m_mqttUsername.toUtf8();
m_mqttPasswordUtf8 = m_mqttPassword.toUtf8();
m_caCertPathUtf8 = m_caCertPath.toUtf8();
conn_opts.username = m_mqttUsernameUtf8.constData();
conn_opts.password = m_mqttPasswordUtf8.constData();
sslOptions.trustStore = m_caCertPathUtf8.constData();
2.2 第二层坑:OPENSSL_sk_pop_free 符号丢失
修复空指针问题后,重新编译部署,结果:
undefined symbol: OPENSSL_sk_pop_free, version OPENSSL_1_1_0
这个错误的意思是:运行时动态链接器在加载 libssl.so.1.1 时,找不到带 OPENSSL_1_1_0 版本标签的 OPENSSL_sk_pop_free 符号。
等等,我们不是静态链接OpenSSL吗?怎么还依赖系统的动态库?
2.3 排查过程详解
2.3.1 第一步:在开发环境检查编译产物
🔧 在开发环境执行(PC上,不是板子)
检查编译出来的库文件大小:
bash
# 进入编译输出目录
cd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/MqttServer/
# 查看libMqttServer.so.1.0.0文件
ls -la libMqttServer.so.1.0.0
期望输出:
-rwxrwxr-x 1 dcuser dcuser 1074328 Apr 28 15:26 libMqttServer.so.1.0.0
ls -la 命令详解:
| 字段 | 含义 | 示例值 |
|---|---|---|
- |
文件类型(- = 普通文件,d = 目录,l = 符号链接) | - |
rwxrwxrwx |
所有者/组/其他人的权限(r=读,w=写,x=执行) | rwxrwxrwx |
1 |
硬链接数量 | 1 |
dcuser |
文件所有者 | dcuser |
dcuser |
文件所属组 | dcuser |
1074328 |
文件大小(字节) | 1074328 |
Apr 28 15:26 |
最后修改时间 | Apr 28 15:26 |
libMqttServer.so.1.0.0 |
文件名 | libMqttServer.so.1.0.0 |
📝 怎么看 :这里文件大小只有 1,074,328 字节(约1MB),明显偏小。如果是完整链接了OpenSSL静态库(OpenSSL 1.1.1静态库 libcrypto.a + libssl.a 加起来有几十MB),最终产物应该大得多。
2.3.2 第二步:检查OpenSSL符号是否存在
🔧 在开发环境执行
用 strings 命令搜索符号:
bash
# 检查是否包含OPENSSL_sk_pop_free符号
strings libMqttServer.so.1.0.0 | grep OPENSSL_sk_pop_free
期望输出:无输出!
strings 命令详解:
- 功能:从二进制文件中提取所有可打印字符串
- 原理:扫描文件内容,提取长度>=4的连续可打印字符序列
- 用途:查看二进制文件中嵌入的字符串、符号名等
📝 怎么看 :无输出说明 libMqttServer.so.1.0.0 中没有 OPENSSL_sk_pop_free 符号!但我们明明链接了 openssl-1.1.1w/libcrypto.a,这个符号应该存在才对。
2.3.3 第三步:确认符号在静态库中存在
🔧 在开发环境执行
用 nm 命令查看静态库的符号表:
bash
# 先找到OpenSSL静态库的位置
cd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/openssl-1.1.1w
# 用nm命令查看libcrypto.a中的符号
nm libcrypto.a | grep OPENSSL_sk_pop_free
期望输出:
00000674 T OPENSSL_sk_pop_free
U OPENSSL_sk_pop_free
U OPENSSL_sk_pop_free
...(更多行)
nm 命令详解:
| 输出格式 | 含义 |
|---|---|
T OPENSSL_sk_pop_free |
符号在 text段(T) 有定义(definition) |
U OPENSSL_sk_pop_free |
符号是 未定义(U),需要其他库提供 |
00000674 |
符号在目标文件中的偏移地址 |
符号类型对照表:
| 类型 | 含义 |
|---|---|
A |
绝对值(absolute),链接时不会改变 |
B/b |
BSS段(uninitialized data) |
C |
公共符号(common),类似于多个目标文件共用的全局变量 |
D |
已初始化数据段(initialized data) |
T |
text段代码,有定义! |
U |
未定义,需要链接器从其他库找到 |
W |
弱符号(weak),可以被其他符号覆盖 |
📝 怎么看 :看到 T OPENSSL_sk_pop_free 说明符号在 libcrypto.a 的某个目标文件里确实有定义 。但前面有很多 U(未定义),说明这个符号被很多目标文件使用但自己没定义(依赖其他目标文件)。
2.3.4 第四步:检查动态库是否有版本符号
🔧 在开发环境的FileReleaseFiles目录执行(这个目录是部署到板子的文件)
查看板子上的OpenSSL动态库版本:
bash
# 进入FileReleaseFiles目录
cd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/FileReleaseFiles/nandflash/lib
# 查看OpenSSL相关文件
ls -la libssl* libcrypto*
期望输出:
lrwxrwxrwx 1 dcuser dcuser 16 12月 30 10:58 libcrypto.so -> libcrypto.so.1.1
lrwxrwxrwx 1 dcuser dcuser 2062552 7月 12 2024 libcrypto.so.1.1
lrwxrwxrwx 1 dcuser dcuser 13 12月 30 10:58 libssl.so -> libssl.so.1.1
lrwxrwxrwx 1 dcuser dcuser 13 12月 30 10:58 libssl.so.1 -> libssl.so.1.1
-rwxrwxr-x 1 dcuser dcuser 456964 7月 12 2024 libssl.so.1.1
符号链接链:
libcrypto.so → libcrypto.so.1.1 (主版本符号链接)
libssl.so → libssl.so.1.1 (主版本符号链接)
libssl.so.1 → libssl.so.1.1 (次版本符号链接,兼容旧程序)
📝 怎么看:
.so.1.1是实际的动态库文件(注意日期是2024年7月).so和.so.1是符号链接,指向.so.1.1- 这说明板子用的是 OpenSSL 1.1.1 (因为是
.so.1.1)
2.3.5 第五步:检查动态库是否有版本符号
🔧 在开发环境执行
查看动态库中OPENSSL版本符号:
bash
# 在FileReleaseFiles目录下执行
strings libcrypto.so.1.1 | grep "OPENSSL_1_"
期望输出:
OPENSSL_1_1_0
OPENSSL_1_1_0a
OPENSSL_1_1_0c
OPENSSL_1_1_0d
OPENSSL_1_1_0f
OPENSSL_1_1_0h
OPENSSL_1_1_0i
OPENSSL_1_1_0k
OPENSSL_1_1_0l
OPENSSL_1_1_0n
OPENSSL_1_1_0q
OPENSSL_1_1_0r
OPENSSL_1_1_0s
OPENSSL_1_1_0t
OPENSSL_1_1_0u
OPENSSL_1_1_0w
📝 怎么看 :这些是OpenSSL的版本标签 (version script)。带这些版本标签的符号,比如 OPENSSL_sk_pop_free@OPENSSL_1_1_0,表示这些符号属于 OPENSSL_1_1_0 版本接口。
2.4 系统内OpenSSL版本全景分析
在实际项目中,一个嵌入式系统往往存在多个OpenSSL副本,它们类型不同、用途不同、来源不同。搞清楚这些,是解决问题的前提。
2.4.1 第一步:分析开发环境中的OpenSSL
🔧 在开发环境执行
查看开发环境中有哪些OpenSSL相关目录:
bash
# 列出项目目录下的OpenSSL相关目录
ls -la /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/ | grep -i openssl
期望输出:
drwxr-xr-x 31 dcuser dcuser 4096 Apr 14 11:30 openssl-1.1.1w
📝 怎么看:
- 目录名
openssl-1.1.1w表明这是一个 OpenSSL 1.1.1w 版本 w后缀表示这是 OpenSSL 1.1.1 系列的某个子版本(1.1.1a~1.1.1w)- 这个目录是我们交叉编译OpenSSL时创建的,用于给MqttServer等模块静态链接用
查看这个OpenSSL目录的内容:
bash
# 进入openssl-1.1.1w目录
cd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/openssl-1.1.1w
# 查看目录结构
ls -la
期望输出:
drwxr-xr-x 31 dcuser dcuser 4096 Apr 14 11:30 .
drwxrwxr-x 22 dcuser dcuser 4096 Apr 14 11:30 apps
drwxr-xr-x 3 dcuser dcuser 4096 Apr 14 11:30 crypto
drwxr-xr-x 3 dcuser dcuser 4096 Apr 14 11:30 doc
drwxr-xr-x 4 dcuser dcuser 4096 Apr 14 11:30 include
drwxr-xr-x 2 dcuser dcuser 4096 Apr 14 11:30 ms
drwxr-xr-x 6 dcuser dcuser 4096 Apr 14 11:30 ssl
drwxr-xr-x 2 dcuser dcuser 4096 Apr 14 11:30 test
drwxr-xr-x 2 dcuser dcuser 4096 Apr 14 11:30 tools
drwxrwxr-x 6 dcuser dcuser 4096 Apr 14 11:30 util
📝 怎么看:
include/:OpenSSL头文件(编译时用)lib/:静态库文件(链接时用)- 没有
.so动态库文件,说明这是纯静态库编译
确认是静态库还是动态库:
bash
# 查看lib目录下有什么
ls -la /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/openssl-1.1.1w/lib/
期望输出:
-r--r--r-- 2 dcuser dcuser 4096 Jan 20 2023 libcrypto.a
-r--r--r-- 2 dcuser dcuser 4096 Jan 20 2023 libssl.a
-r--r--r-- 1 dcuser dcuser 197568 Jan 20 2023 libcrypto.a
📝 怎么看:
- 只有
.a文件(静态库),没有.so文件(动态库) - 这是因为编译时使用了
--prefix=/xxx --openssldir=/xxx no-shared配置选项 - 用途 :我们项目中的
MqttServer模块静态链接了这个OpenSSL
🔍 小知识:OpenSSL编译选项说明
| 编译选项 | 效果 |
|---|---|
no-shared |
只编译静态库,不生成动态库 |
shared |
生成动态库(.so文件) |
no-shared --prefix=/xxx |
安装到指定目录,只安装静态库 |
2.4.2 第二步:分析部署到板子的OpenSSL
🔧 在开发环境执行(FileReleaseFiles目录)
查看FileReleaseFiles目录下的OpenSSL文件:
bash
# 进入FileReleaseFiles目录(这是部署到板子的文件)
cd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/FileReleaseFiles/nandflash/lib
# 查看OpenSSL相关文件
ls -la libssl* libcrypto*
期望输出:
lrwxrwxrwx 1 dcuser dcuser 16 12月 30 10:58 libcrypto.so -> libcrypto.so.1.1
lrwxr-xr-x 1 dcuser dcuser 2062552 7月 12 2024 libcrypto.so.1.1
lrwxrwxrwx 1 dcuser dcuser 13 12月 30 10:58 libssl.so -> libssl.so.1.1
lrwxrwxrwx 1 dcuser dcuser 13 12月 30 10:58 libssl.so.1 -> libssl.so.1.1
-rwxr-xr-x 1 dcuser dcuser 456964 7月 12 2024 libssl.so.1.1
📝 怎么看:
| 文件 | 类型 | 说明 |
|---|---|---|
libcrypto.so.1.1 |
动态库 | 实际文件,2MB大小,2024年7月编译 |
libssl.so.1.1 |
动态库 | 实际文件,456KB大小,2024年7月编译 |
libcrypto.so |
符号链接 | 指向 libcrypto.so.1.1 |
libssl.so |
符号链接 | 指向 libssl.so.1.1 |
libssl.so.1 |
符号链接 | 指向 libssl.so.1.1(兼容旧程序) |
符号链接链解释:
libcrypto.so → libcrypto.so.1.1
↑
│(主版本符号链接,程序运行时加载这个)
libssl.so → libssl.so.1.1
↑
│(主版本符号链接)
libssl.so.1 → libssl.so.1.1
↑
│(次版本符号链接,兼容只用libssl.so.1的程序)
为什么有两个 .so.1 和 .so?
这是Linux动态库版本管理机制:
.so.1.1:real name(真实文件名),包含实际代码.so.1:soname(ABI兼容名),表示主版本号.so:linker name(链接时用的名字),程序链接时用这个
关键区别:
| 对比项 | openssl-1.1.1w(开发环境) | FileReleaseFiles(板子) |
|---|---|---|
| 文件类型 | 静态库 .a |
动态库 .so.1.1 |
| 编译选项 | no-shared |
shared |
| 版本符号 | 没有 | 有 OPENSSL_1_1_0 |
| 用途 | 给MqttServer静态链接 | 给其他模块动态链接 |
| 存放位置 | 开发环境交叉编译产物 | 部署到板子的系统库 |
2.4.3 第三步:检查板子上的OpenSSL版本符号
🔧 在开发环境执行(FileReleaseFiles目录)
查看动态库中的版本符号:
bash
# 在FileReleaseFiles目录下执行
strings libcrypto.so.1.1 | grep "OPENSSL_1_"
期望输出:
OPENSSL_1_1_0
OPENSSL_1_1_0a
OPENSSL_1_1_0c
OPENSSL_1_1_0d
OPENSSL_1_1_0f
OPENSSL_1_1_0h
OPENSSL_1_1_0i
OPENSSL_1_1_0k
OPENSSL_1_1_0l
OPENSSL_1_1_0n
OPENSSL_1_1_0q
OPENSSL_1_1_0r
OPENSSL_1_1_0s
OPENSSL_1_1_0t
OPENSSL_1_1_0u
OPENSSL_1_1_0w
📝 怎么看:
- 这些是OpenSSL的版本标签(version script)
OPENSSL_1_1_0是基础版本OPENSSL_1_1_0a~OPENSSL_1_1_0w是次版本(bugfix和安全性更新)- 带版本标签的符号如
OPENSSL_sk_pop_free@OPENSSL_1_1_0表示这个符号属于OPENSSL_1_1_0接口
2.4.4 第四步:在板子上查看OpenSSL
📱 在板子上执行
在板子上检查OpenSSL库文件:
bash
ls -la /mnt/nandflash/lib/libssl* /mnt/nandflash/lib/libcrypto*
期望输出:
lrwxrwxrwx 1 root 1000 18 Apr 28 16:41 /mnt/nandflash/lib/libMqttServer.so -> libMqttServer.so.1
lrwxrwxrwx 1 root 1000 22 Apr 28 16:41 /mnt/nandflash/lib/libMqttServer.so.1 -> libMqttServer.so.1.0.0
lrwxrwxrwx 1 root 1000 22 Apr 28 11:55 /mnt/nandflash/lib/libMqttServer.so.1.0 -> libMqttServer.so.1.0.0
-rwxr-xr-x 1 1000 1000 4308057 Apr 28 16:45 /mnt/nandflash/lib/libMqttServer.so.1.0.0
查看OpenSSL动态库版本:
bash
openssl version
期望输出:
OpenSSL 1.1.1w 30 Sep 2020
📝 怎么看:
OpenSSL 1.1.1w是板子系统的OpenSSL版本- 日期是2020年9月30日(最后的稳定版)
- 这是板子系统原有的OpenSSL
查看OpenSSL动态库依赖:
bash
ldd /usr/bin/openssl 2>/dev/null | grep ssl
# 或者
ldd which openssl 2>/dev/null | grep ssl
期望输出:
libssl.so.1.1 => /lib/arm-linux-gnueabi/libssl.so.1.1 (0xb6ee0000)
📝 怎么看:
- 程序依赖
libssl.so.1.1 - 实际加载的是
/lib/arm-linux-gnueabi/libssl.so.1.1
2.4.5 第五步:查看哪些模块使用OpenSSL
🔧 在开发环境执行(FileReleaseFiles目录)
检查其他模块是否依赖OpenSSL动态库:
bash
# 查看所有.so文件的OpenSSL符号
strings /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/FileReleaseFiles/nandflash/lib/*.so 2>/dev/null | grep -i "OPENSSL\|libssl\|libcrypto" | head -30
期望输出:
OPENSSL_die
OPENSSL_gmtime
OPENSSL_gmtime_adj
OPENSSL_sk_find
OPENSSL_sk_value
OPENSSL_sk_push
OPENSSL_sk_new
OPENSSL_sk_pop_free
OPENSSL_gmtime_diff
OPENSSL_sk_num
OPENSSL_sk_sort
OPENSSL_sk_new_null
OPENSSL_hexstr2buf
OPENSSL_cleanse
OPENSSL_sk_free
📝 怎么看 :这些都是OpenSSL的内部符号,说明FileReleaseFiles目录下的某些 .so 模块使用了OpenSSL动态库。
查看具体哪些模块依赖OpenSSL:
bash
# 查看libEnergyConsumeServer.so的依赖(举例)
ldd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/FileReleaseFiles/nandflash/lib/libEnergyConsumeServer.so 2>/dev/null | grep -E "ssl|crypto"
期望输出:
libssl.so.1.1 => /home/dcuser/work/.../libssl.so.1.1 (0xb6ee0000)
libcrypto.so.1.1 => /home/dcuser/work/.../libcrypto.so.1.1 (0xb6f40000)
📝 怎么看 :其他模块(如 libEnergyConsumeServer)动态链接 了板子上的 libssl.so.1.1 和 libcrypto.so.1.1。
2.4.6 OpenSSL版本总结
经过以上分析,我们搞清楚了三件事:
1. openssl-1.1.1w(开发环境交叉编译产物)
- 类型 :静态库(
.a文件) - 编译选项 :
no-shared,无版本符号 - 用途:给MqttServer模块静态链接用
- 存放位置 :
/home/dcuser/work/.../openssl-1.1.1w/lib/ - 使用者 :MqttServer(通过
.pro文件的LIBS配置)
2. libssl.so.1.1 / libcrypto.so.1.1(FileReleaseFiles部署文件)
- 类型 :动态库(
.so.1.1文件) - 编译选项 :
shared,有版本符号 - 用途:给其他模块动态链接用
- 存放位置 :
FileReleaseFiles/nandflash/lib/ - 部署位置 :板子
/mnt/nandflash/lib/ - 使用者:libEnergyConsumeServer 等其他模块
3. 板子系统原有的OpenSSL
- 类型:动态库(系统库)
- 版本:OpenSSL 1.1.1w
- 存放位置 :
/lib/arm-linux-gnueabi/ - 使用者:系统程序
一张图说明关系:
┌─────────────────────────────────────────────────────────────────┐
│ 开发环境 │
│ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ openssl-1.1.1w/ │ │ FileReleaseFiles/ │ │
│ │ (静态库) │ │ nandflash/lib/ │ │
│ │ │ │ │ │
│ │ libcrypto.a │ │ libssl.so.1.1 ──────┼───┼──→ MqttServer(pro LIBS)
│ │ libssl.a │ │ libcrypto.so.1.1 │ │
│ │ │ │ │ │
│ │ ❌ 无版本符号 │ │ ✅ 有版本符号 │ │
│ │ │ │ │ │
│ │ 用途: 静态链接 │ │ 用途: 动态链接 │ │
│ └──────────────────┘ └────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│
│ 部署到板子
▼
┌─────────────────────────────────────────────────────────────────┐
│ 板子环境 │
│ │
│ ┌──────────────────────────────┐ ┌────────────────────────┐ │
│ │ /mnt/nandflash/lib/ │ │ /lib/arm-linux-gnueabi│ │
│ │ │ │ │ │
│ │ libMqttServer.so.1.0.0 ←───┼──┼─── (whole-archive) │ │
│ │ (已静态链接OpenSSL) │ │ │ │
│ │ │ │ libssl.so.1.1 ←──────┼──→ 其他模块
│ │ libssl.so.1.1 ─────────────┼──┼─── (动态链接) │ │
│ │ libcrypto.so.1.1 │ │ │ │
│ │ │ │ 系统OpenSSL 1.1.1w │ │
│ └──────────────────────────────┘ └────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2.5 问题根因分析
经过上面排查,定位到问题:
- 我们编译用的OpenSSL (
openssl-1.1.1w)是静态库 ,编译时用了no-shared选项 - 板子上的OpenSSL (
libssl.so.1.1)是动态库,有版本标签 - 静态链接的OpenSSL符号没有版本标签
- 运行时动态链接器加载
libssl.so.1.1,它期望OPENSSL_sk_pop_free@OPENSSL_1_1_0 - 但我们的
libMqttServer.so里只有没有版本标签的OPENSSL_sk_pop_free - 链接器认为符号不匹配 ,所以报
undefined symbol
等等,还有一个问题 :我们明明静态链接了OpenSSL,为什么 libMqttServer.so 里完全没有 OpenSSL符号?
2.6 第三层坑:链接器符号裁剪(Symbol Stripping)
问题根因 :链接器认为 OPENSSL_sk_pop_free 没有被直接引用,所以把它丢弃了。
这是一种"死代码消除"优化。链接器很"聪明",它会扫描所有目标文件,只保留那些确实被调用的符号。
但问题是调用链是:
libMqttServer.so → libpaho-mqtt3cs.a → libssl.a → libcrypto.a
(Paho调用SSL)(SSL调用crypto)
Paho 调用了 OpenSSL 的某些函数,这些函数内部调用了 OPENSSL_sk_pop_free。但链接器在静态库 上只做直接依赖分析:
- 链接器看到
libMqttServer.so调用了 Paho 的MQTTClient_connect - Paho 里有
SSLSocket_connect,这是直接引用,保留 SSLSocket_connect内部调用了 OpenSSL 的SSL_connect,这也是直接引用,保留SSL_connect内部调用了OPENSSL_sk_pop_free...但这是间接调用 ,链接器觉得"反正没人在我这里调用OPENSSL_sk_pop_free",就丢弃了
2.7 第四层坑:链接顺序
即使符号没被丢弃,还可能有链接顺序问题。
静态库链接黄金法则 :被依赖的库放后面
为什么?
链接器处理静态库时,会从头到尾扫描一次。扫描到一个目标文件时,如果它能满足某个未定义符号,就把这个目标文件加入链接;如果不能满足,就跳过。
所以:
libA.a → libB.a(A依赖B)→ B必须放后面- 如果B放前面,链接器扫完B就结束了,回头找不到A需要的符号
当前链接顺序问题:
qmake
# ❌ 错误顺序:libssl依赖libcrypto,但ssl放前面了
-Wl,-Bstatic
$$OPENSSL_LIBPATH/libssl.a # 先扫ssl
$$OPENSSL_LIBPATH/libcrypto.a # 再扫crypto,但这时ssl可能已经错过了某些crypto的符号
$$PAHO_LIBPATH/libpaho-mqtt3cs.a
-Wl,-Bdynamic
正确顺序应该是:
libpaho-mqtt3cs.a → libssl.a → libcrypto.a
↑ Paho依赖OpenSSL,所以OpenSSL在后面
↑ libssl依赖libcrypto,所以crypto在ssl后面
🛠️ 三、解决方案
3.1 方案选择
| 方案 | 优点 | 缺点 |
|---|---|---|
| A. 重新编译OpenSSL(生成动态库) | 一劳永逸 | 其他模块可能不兼容 |
| B. 让MqttServer用系统的OpenSSL动态库 | 简单 | 依赖系统库,版本可能不一致 |
C. 使用 --whole-archive 强制链接所有符号 |
不改变现有OpenSSL | 库体积变大 |
我们选择方案C------因为板子上其他模块已经稳定运行在OpenSSL 1.1.1动态库上,我们不想动它。
3.2 核心操作:修改 .pro 文件
修改 MqttServer.pro 的 LIBS 部分:
qmake
# ❌ 修改前的错误配置
LIBS += \
-L$${TELD_LIBPATH} \
-L$${PAHO_LIBPATH} \
-L$${OPENSSL_LIBPATH} \
-Wl,-Bstatic \
$$OPENSSL_LIBPATH/libssl.a \
$$OPENSSL_LIBPATH/libcrypto.a \
$$PAHO_LIBPATH/libpaho-mqtt3cs.a \
-Wl,-Bdynamic \
-lcurl \
-ljson-c \
-lpthread \
-ldl
qmake
# ✅ 修改后的正确配置
LIBS += \
-L$${TELD_LIBPATH} \
-L$${PAHO_LIBPATH} \
-L$${OPENSSL_LIBPATH} \
-Wl,-whole-archive \ # 🔥 关键1:强制链接整个静态库
$$PAHO_LIBPATH/libpaho-mqtt3cs.a \ # 🔥 关键2:Paho放前面
$$OPENSSL_LIBPATH/libssl.a \
$$OPENSSL_LIBPATH/libcrypto.a \
-Wl,-no-whole-archive \ # 🔥 关闭whole-archive
-Wl,-Bdynamic \
-lcurl \
-ljson-c \
-lpthread \
-ldl
3.3 关键参数解释
| 参数 | 含义 |
|---|---|
-Wl,-whole-archive |
告诉链接器:后面的静态库所有符号都要保留,不许裁剪 |
-Wl,-no-whole-archive |
恢复默认行为,只保留被引用的符号 |
-Wl,-Bstatic |
强制后面的库使用静态链接 |
-Wl,-Bdynamic |
恢复动态链接(用于curl等动态库) |
| 链接顺序 | paho → ssl → crypto,被依赖的放后面 |
3.4 重新编译步骤
🔧 在开发环境执行(Qt Creator或命令行)
步骤一:清理旧构建产物
bash
# 方法1:Qt Creator操作
# Build → Clean All
# 方法2:命令行清理
cd /path/to/build/directory
make clean
# 或者直接删掉构建目录
rm -rf build/
步骤二:重新配置qmake
bash
# Qt Creator操作
# 右键项目 → Run qmake
# 方法2:命令行
qmake /path/to/MqttServer.pro
步骤三:重新编译
bash
# Qt Creator操作
# Build → Rebuild All
# 方法2:命令行
make -j4
步骤四:验证符号是否链接成功
bash
# 在MqttServer编译输出目录执行
cd /home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/MqttServer/
# 检查文件大小(应该明显变大)
ls -la libMqttServer.so.1.0.0
✅ 期望输出:
-rwxrwxr-x 1 dcuser dcuser 4308057 Apr 28 16:38 libMqttServer.so.1.0.0
文件大小对比:
| 状态 | 文件大小 | 说明 |
|---|---|---|
| 修改前 | 1,074,328 字节 | 符号被裁剪,不完整 |
| 修改后 | 4,308,057 字节 | 完整链接,体积增大4倍 |
bash
# 再次检查OPENSSL符号
strings libMqttServer.so.1.0.0 | grep OPENSSL_sk_pop_free
✅ 期望输出:
OPENSSL_sk_pop_free
OPENSSL_sk_pop_free
3.5 部署到板子
📱 在板子上执行
第一步:查看当前库文件状态
bash
ls -la /mnt/nandflash/lib/libMqttServer*
期望输出:
lrwxrwxrwx 1 root root 18 Apr 28 16:41 libMqttServer.so -> libMqttServer.so.1
lrwxrwxrwx 1 root root 22 Apr 28 16:41 libMqttServer.so.1 -> libMqttServer.so.1.0.0
lrwxrwxrwx 1 root root 22 Apr 28 11:55 libMqttServer.so.1.0 -> libMqttServer.so.1.0.0
-rwxr-xr-x 1 root 1000 1000 1074748 Apr 28 15:26 libMqttServer.so.1.0.0
📝 怎么看:
libMqttServer.so.1.0.0的时间戳是 4月28日15:26 ,大小是 1,074,748 字节(旧版本)- 符号链接链:
libMqttServer.so → libMqttServer.so.1 → libMqttServer.so.1.0.0 - 注意
libMqttServer.so.1.0是一个多余的旧链接(日期12月30日)
第二步:拷贝新编译的库文件
bash
# 从开发环境拷贝到板子(通过scp、U盘等方式)
# 假设通过NFS挂载或SCP
scp dcuser@开发机:/home/dcuser/work/cartech_120_DOUB_NET/cartech_120/CSCU_A1_GIT/MqttServer/libMqttServer.so* root@板子IP:/mnt/nandflash/lib/
需要拷贝的4个文件:
libMqttServer.so.1.0.0 # 实际文件 (4.3MB)
libMqttServer.so.1.0 # 符号链接 → libMqttServer.so.1.0.0
libMqttServer.so.1 # 符号链接 → libMqttServer.so.1.0.0
libMqttServer.so # 符号链接 → libMqttServer.so.1.0.0
第三步:验证拷贝结果
bash
ls -la /mnt/nandflash/lib/libMqttServer* && echo "=== OPENSSL符号验证 ===" && strings /mnt/nandflash/lib/libMqttServer.so.1.0.0 | grep OPENSSL_sk_pop_free
✅ 期望输出:
lrwxrwxrwx 1 root root 18 Apr 28 16:41 /mnt/nandflash/lib/libMqttServer.so -> libMqttServer.so.1
lrwxrwxrwx 1 root root 22 Apr 28 16:41 /mnt/nandflash/lib/libMqttServer.so.1 -> libMqttServer.so.1.0.0
lrwxrwxrwx 1 root root 22 Apr 28 11:55 /mnt/nandflash/lib/libMqttServer.so.1.0 -> libMqttServer.so.1.0.0
-rwxr-xr-x 1 1000 1000 4308057 Apr 28 16:45 /mnt/nandflash/lib/libMqttServer.so.1.0.0
=== OPENSSL符号验证 ===
OPENSSL_sk_pop_free
OPENSSL_sk_pop_free
📝 怎么看:
libMqttServer.so.1.0.0大小变成 4,308,057 字节 ✅- 时间戳变成 16:45 ✅
OPENSSL_sk_pop_free存在 ✅
第四步:检查符号链接链完整性
bash
# 查看所有符号链接
ls -la /mnt/nandflash/lib/libMqttServer.so*
# 解析libMqttServer.so的最终指向
readlink -f /mnt/nandflash/lib/libMqttServer.so
期望输出:
/mnt/nandflash/lib/libMqttServer.so.1.0.0
readlink -f 命令详解:
- 功能:递归解析符号链接,返回最终的真实文件路径
-f:完整路径(不是相对路径)- 即使符号链接断链,也能告诉你最终指向哪里
第五步:删除多余文件(可选)
bash
# 删除多余的旧符号链接
rm /mnt/nandflash/lib/libMqttServer.so.1.0
# 验证
ls -la /mnt/nandflash/lib/libMqttServer.so*
✅ 最终期望状态:
libMqttServer.so → libMqttServer.so.1
libMqttServer.so.1 → libMqttServer.so.1.0.0
libMqttServer.so.1.0.0(实际文件,4.3MB)
📋 四、完整MQTT配置文件实现
4.1 配置文件 mqtt_config.ini
ini
[MQTT]
# MQTT Broker服务器地址
broker=ue083e07.ala.cn-hangzhou.emqxsl.cn
# MQTT端口号
# 1883 - 非加密TCP端口
# 8883 - SSL/TLS加密端口
port=8883
# 是否启用TLS/SSL加密连接
# true = 启用TLS (使用端口8883)
# false = 非加密连接 (使用端口1883)
enable_tls=true
# CA证书路径 (TLS启用时需要)
ca_cert_path=/mnt/nandflash/sbin/mqtt/emqxsl-ca.crt
# 是否验证服务器证书 (TLS启用时有效)
# true = 验证服务器证书
# false = 不验证服务器证书 (仅测试用,生产环境勿用!)
verify_certificate=false
# 是否使用匿名连接
# true = 不使用用户名密码
# false = 使用用户名和密码
anonymous=false
# 用户名 (anonymous=false时需要)
username=DC120
# 密码 (anonymous=false时需要)
password=DC120
# 客户端ID后缀 (实际ID = client_id_suffix_进程ID)
client_id_suffix=tcu_server
[LOG]
# 是否启用MQTT模块日志
module_log_enabled=true
# 是否启用协议解析日志
protocol_log_enabled=true
4.2 配置加载代码
cpp
// MqttServer.h 成员变量声明(节选关键部分)
private:
// UTF-8编码的字符串(解决指针生命周期问题)
QByteArray m_mqttUsernameUtf8;
QByteArray m_mqttPasswordUtf8;
QByteArray m_caCertPathUtf8;
// SSL选项结构体
MQTTClient_SSLOptions m_sslOptions;
// 配置参数
QString m_mqttBroker;
int m_mqttPort;
QString m_caCertPath;
QString m_mqttUsername;
QString m_mqttPassword;
QString m_clientId;
bool loadMqttConfig();
bool isTlsEnabled();
bool isAnonymousLogin();
bool isVerifyCertificate();
cpp
// MqttServer.cpp 配置加载实现
bool CMqttServer::loadMqttConfig()
{
QSettings settings(MQTT_CONFIG, QSettings::IniFormat);
// ========== MQTT连接配置 ==========
settings.beginGroup("MQTT");
// Broker地址:仅当配置文件为空时才使用默认值
QString broker = settings.value("broker", "").toString();
m_mqttBroker = broker.isEmpty() ? "broker.emqx.io" : broker;
// 端口号
QString portStr = settings.value("port", "").toString();
m_mqttPort = portStr.isEmpty() ? 1883 : portStr.toInt();
// CA证书路径
QString caCert = settings.value("ca_cert_path", "").toString();
if(!caCert.isEmpty()) {
m_caCertPath = caCert;
}
m_caCertPathUtf8 = m_caCertPath.toUtf8();
// 用户名(仅存储,不立即转UTF-8)
QString username = settings.value("username", "").toString();
if(!username.isEmpty()) {
m_mqttUsername = username;
}
// 密码
QString password = settings.value("password", "").toString();
if(!password.isEmpty()) {
m_mqttPassword = password;
}
// 生成客户端ID
QString clientIdSuffix = settings.value("client_id_suffix", "tcu_server").toString();
m_clientId = QString("%1_%2").arg(clientIdSuffix).arg(getpid());
// TLS开关
QString tlsStr = settings.value("enable_tls", "false").toString().toLower();
bool bTlsEnabled = (tlsStr == "true" || tlsStr == "1" || tlsStr == "yes");
// 匿名登录开关
QString anonStr = settings.value("anonymous", "true").toString().toLower();
bool bAnonymous = (anonStr != "false" && anonStr != "0" && anonStr != "no");
// 证书验证开关
QString verifyCertStr = settings.value("verify_certificate", "true").toString().toLower();
bool bVerifyCert = (verifyCertStr == "true" || verifyCertStr == "1" || verifyCertStr == "yes");
settings.endGroup();
// ========== 日志配置 ==========
settings.beginGroup("LOG");
QString moduleLogStr = settings.value("module_log_enabled", "false").toString().toLower();
g_bModuleLogEnabled = (moduleLogStr == "true" || moduleLogStr == "1" || moduleLogStr == "yes");
QString protocolLogStr = settings.value("protocol_log_enabled", "false").toString().toLower();
g_bMqttServerLogEnabled = (protocolLogStr == "true" || protocolLogStr == "1" || protocolLogStr == "yes");
settings.endGroup();
// ========== 日志输出 ==========
mqttLog("loadMqttConfig", QString("===== 加载MQTT配置文件 ====="));
mqttLog("loadMqttConfig", QString("Broker: %1:%2").arg(m_mqttBroker).arg(m_mqttPort));
mqttLog("loadMqttConfig", QString("TLS: %1, 匿名登录: %2").arg(bTlsEnabled?"启用":"禁用").arg(bAnonymous?"是":"否"));
mqttLog("loadMqttConfig", QString("CA证书: %1").arg(m_caCertPath));
mqttLog("loadMqttConfig", QString("用户名: %1").arg(m_mqttUsername.isEmpty() ? "(空)" : m_mqttUsername));
mqttLog("loadMqttConfig", QString("Client ID: %1").arg(m_clientId));
mqttLog("loadMqttConfig", QString("模块日志: %1").arg(g_bModuleLogEnabled ? "开启" : "关闭"));
mqttLog("loadMqttConfig", QString("协议日志: %1").arg(g_bMqttServerLogEnabled ? "开启" : "关闭"));
mqttLog("loadMqttConfig", QString("===== MQTT配置加载完成 ====="));
return true;
}
4.3 TLS连接代码
cpp
void CMqttServer::startMqttClient()
{
// ... 前面的代码省略 ...
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
// 🔥 关键:保存UTF-8编码的字符串(解决指针生命周期问题)
m_mqttUsernameUtf8 = m_mqttUsername.toUtf8();
m_mqttPasswordUtf8 = m_mqttPassword.toUtf8();
// 连接参数设置
conn_opts.keepAliveInterval = 60;
conn_opts.connectTimeout = 30;
conn_opts.cleansession = 1;
conn_opts.automaticReconnect = 0;
// 用户名密码
if(!isAnonymousLogin()) {
conn_opts.username = m_mqttUsernameUtf8.constData();
conn_opts.password = m_mqttPasswordUtf8.constData();
mqttLog("startMqttClient", QString(" 使用用户名密码连接: %1/******").arg(m_mqttUsername));
} else {
conn_opts.username = NULL;
conn_opts.password = NULL;
mqttLog("startMqttClient", " 使用匿名连接 (无用户名密码)");
}
// ========== TLS配置 ==========
if(isTlsEnabled()) {
// 🔥 初始化SSL选项结构体
m_sslOptions = (MQTTClient_SSLOptions)MQTTClient_SSLOptions_initializer;
if(isVerifyCertificate()) {
// 验证服务器证书
m_sslOptions.trustStore = m_caCertPathUtf8.constData();
m_sslOptions.enableServerCertAuth = 1;
mqttLog("startMqttClient", " SSL已启用 (验证服务器证书)");
} else {
// 不验证服务器证书(测试用)
m_sslOptions.trustStore = NULL;
m_sslOptions.enableServerCertAuth = 0;
mqttLog("startMqttClient", " SSL已启用 (不验证服务器证书)");
}
m_sslOptions.sslVersion = MQTT_SSL_VERSION_DEFAULT;
conn_opts.ssl = &m_sslOptions;
mqttLog("startMqttClient", " ssl.sslVersion=DEFAULT");
}
// 开始连接
mqttLog("startMqttClient", "开始连接MQTT服务器...");
int rc = MQTTClient_connect(m_client, &conn_opts);
if(rc != MQTTCLIENT_SUCCESS) {
mqttLog("startMqttClient", QString("★★☆ MQTTClient_connect返回: rc=%1").arg(rc));
// 错误码解析
switch(rc) {
case -5: mqttLog("startMqttClient", " 错误: 连接被拒绝 (5)"); break;
case -4: mqttLog("startMqttClient", " 错误: 错误的协议版本 (4)"); break;
case -3: mqttLog("startMqttClient", " 错误: 无效的客户端ID (3)"); break;
case -2: mqttLog("startMqttClient", " 错误: 连接失败 (2)"); break;
case -1: mqttLog("startMqttClient", " 错误: 协议错误 (1)"); break;
case 1: mqttLog("startMqttClient", " 错误: 不支持的协议版本 (1)"); break;
case 2: mqttLog("startMqttClient", " 错误: 客户端ID无效 (2)"); break;
case 3: mqttLog("startMqttClient", " 错误: 连接不可用 (3)"); break;
case 4: mqttLog("startMqttClient", " 错误: 用户名或密码错误 (4)"); break;
case 5: mqttLog("startMqttClient", " 错误: 未授权 (5)"); break;
case -8: mqttLog("startMqttClient", " 错误: SSL配置错误 (8)"); break;
default: mqttLog("startMqttClient", QString(" 未知错误码: %1").arg(rc)); break;
}
return;
}
mqttLog("startMqttClient", "★★☆ MQTT连接成功!");
}
⚠️ 五、避坑总结
5.1 静态库链接常见坑
| 坑 | 原因 | 解决方案 |
|---|---|---|
| 符号被链接器丢弃 | 静态库中未被直接引用的函数被优化掉 | 使用 -Wl,-whole-archive |
| 链接顺序错误 | 依赖关系倒置,被依赖的库放前面了 | 被依赖的库放后面:A → B 表示A依赖B,B放后面 |
| 版本符号不匹配 | 静态库编译时无版本标签,动态库有 | 保持OpenSSL静态库和系统动态库版本一致,或用whole-archive |
| 头文件和库版本不一致 | 编译用1.1.1头文件,运行时加载1.0.x动态库 | 确保版本一致或使用静态链接 |
5.2 MQTT连接错误码速查
| rc值 | 含义 | 排查方向 |
|---|---|---|
| 0 | 成功 | - |
| -1 | 连接失败 | 检查网络、端口是否开放 |
| -3 | 连接超时 | 检查防火墙、端口是否开放 |
| -4 | 协议版本错误 | Paho与Broker版本不匹配 |
| -5 | 连接被拒绝 | Broker服务问题、端口错误 |
| -8 | 空指针参数 | 检查SSL配置、用户名密码是否有效 |
| 5 | 未授权 | 用户名或密码错误 |
5.3 字符串指针生命周期警示
cpp
// ❌ 错误:临时对象析构后指针失效
conn_opts.username = someString.toUtf8().constData();
// 等号右边:toUtf8()创建临时QByteArray
// .constData()返回临时对象内部的const char*指针
// 语句结束:临时QByteArray析构,内存被释放
// 结果:conn_opts.username指向已释放的内存!
// ✅ 正确:保持对象生命周期
QByteArray usernameUtf8 = someString.toUtf8();
conn_opts.username = usernameUtf8.constData();
// usernameUtf8是局部变量,在作用域内始终有效
// 适合在函数开头转换,函数内使用
// ✅✅ 最佳实践:存储为成员变量
m_mqttUsernameUtf8 = m_mqttUsername.toUtf8();
conn_opts.username = m_mqttUsernameUtf8.constData();
// 成员变量生命周期跟随对象,可以跨函数调用使用
5.4 常用调试命令速查
| 命令 | 功能 | 典型用法 |
|---|---|---|
strings <file> |
提取二进制文件中的字符串 | `strings libxxx.so |
nm <file> |
查看目标文件/库文件的符号表 | `nm libxxx.a |
ls -la <file> |
详细列表显示文件 | 查看文件大小、时间戳 |
readlink -f <link> |
解析符号链接的真实路径 | readlink -f libxxx.so |
file <file> |
查看文件类型 | file libxxx.so.1.0.0 |
ldd <program> |
查看程序依赖的动态库 | ldd ./myprogram |
objdump -p <file> |
查看动态库依赖 | `objdump -p libxxx.so |
5.5 验证部署是否成功checklist
📱 在板子上执行
bash
# 1. 检查文件大小(应该从1MB变成4MB)
ls -la /mnt/nandflash/lib/libMqttServer.so.1.0.0
# 期望:4308057 字节
# 2. 检查符号链接
ls -la /mnt/nandflash/lib/libMqttServer.so*
# 期望:三个符号链接 + 一个实际文件
# 3. 验证OpenSSL符号是否链接成功
strings /mnt/nandflash/lib/libMqttServer.so.1.0.0 | grep OPENSSL_sk_pop_free
# 期望:有输出(显示OPENSSL_sk_pop_free)
# 4. 检查完整符号链接链
readlink -f /mnt/nandflash/lib/libMqttServer.so
# 期望:/mnt/nandflash/lib/libMqttServer.so.1.0.0
# 5. 删除多余文件(可选)
rm /mnt/nandflash/lib/libMqttServer.so.1.0
🎓 六、知识延伸
6.1 什么是 whole-archive?
--whole-archive 是一个链接器选项,告诉链接器:"把这些静态库里的所有目标文件都给我打包进去,不要做任何符号裁剪"。
工作原理:
bash
# 不使用whole-archive
# 链接器扫描libxxx.a,只保留被引用的符号
-Wl,-Bstatic
libxxx.a
-Wl,-Bdynamic
# 使用whole-archive
# 链接器把libxxx.a里的所有目标文件都打包进去
-Wl,-whole-archive
libxxx.a # 所有符号都被保留
-Wl,-no-whole-archive
为什么OpenSSL需要whole-archive?
调用链是这样的:
应用 → Paho → SSL → Crypto → ...(很深的调用链)
链接器做直接依赖分析:
- 应用调用 Paho 的
MQTTClient_connect✅ 保留 - Paho 调用
SSLSocket_connect✅ 保留(因为Paho被直接引用) SSLSocket_connect调用SSL_CTX_new✅ 保留(因为SSLSocket被直接引用)SSL_CTX_new调用OPENSSL_sk_pop_free❌ 丢弃(间接调用,没人直接调用它)
什么时候用whole-archive?
- 静态库有很深的间接调用链(OpenSSL、各种加密库)
- 使用了插件架构,符号需要在运行时动态加载
- 需要保证所有符号都被保留(调试、符号表分析)
代价是什么?
- 最终二进制文件体积变大(可能增大几倍)
- 链接速度变慢
- 可能引入符号冲突(多个静态库定义了同一个符号)
6.2 OpenSSL版本符号(Version Script)
Linux上的动态库可以使用版本脚本(version script)来标记符号版本:
c
/* opensslsym.map */
OPENSSL_1_1_0 {
global:
*; # 导出所有符号给全局作用域
local:
*; # 其余符号仅内部可见
};
OPENSSL_1_1_0a {
global:
*;
local:
*;
} OPENSSL_1_1_0;
编译动态库时:
bash
gcc -shared -Wl,-version-script=opensslsym.map -o libcrypto.so libcrypto.o
这样 OPENSSL_sk_pop_free 就变成了 OPENSSL_sk_pop_free@OPENSSL_1_1_0。
版本符号的作用:
- ABI兼容性:不同版本的OpenSSL可以共存
- 符号隔离:程序运行时加载正确版本的符号
- 向后兼容:1.1.0编译的程序可以跑在1.1.1上
问题:
- 编译时用
no-shared选项的OpenSSL,符号表里没有版本标签 - 但系统的动态库有版本标签
- 运行时动态链接器去找带版本的符号,当然找不到
6.3 Qt 4.8.6 与 OpenSSL 的恩怨情仇
Qt 4.8.6 发布于2012年,那时候OpenSSL 1.1.x还没出来(1.1.0是2016年的事)。
Qt 4.8.6 原生支持的是 OpenSSL 1.0.x。但在实际嵌入式项目中,往往需要用更新版本的OpenSSL(安全性、性能)。
常见问题:
- Qt编译时检测到系统的OpenSSL 1.0.x,自动链接
- 项目需要用OpenSSL 1.1.x
- 头文件版本和链接的库版本不匹配
解决方案 :在项目 .pro 文件里显式指定OpenSSL路径,不使用Qt自带的:
qmake
# 指定使用自己编译的OpenSSL
INCLUDEPATH += /path/to/openssl-1.1.1w/include
LIBS += -L/path/to/openssl-1.1.1w -lssl -lcrypto
# 或者静态链接
LIBS += /path/to/openssl-1.1.1w/libssl.a /path/to/openssl-1.1.1w/libcrypto.a
6.4 嵌入式Linux上的OpenSSL多版本共存
嵌入式设备上通常会有多个OpenSSL版本共存:
bash
# 查看当前系统的OpenSSL版本
openssl version
# OpenSSL 1.1.1w 30 Sep 2020
# 查看OpenSSL动态库
ls -la /usr/lib/libssl* /lib/libssl* /usr/lib/arm-linux-gnueabi/libssl* 2>/dev/null
# 查看某个二进制依赖的OpenSSL版本
ldd /path/to/binary | grep ssl
# libssl.so.1.1 => /lib/arm-linux-gnueabi/libssl.so.1.1 (0xb6ee0000)
# 查看所有OpenSSL相关文件
find /lib /usr/lib -name "libssl*" -o -name "libcrypto*" 2>/dev/null
ldd命令详解:
| 字段 | 含义 |
|---|---|
libssl.so.1.1 |
程序需要的动态库名称 |
=> |
表示"位于" |
/lib/.../libssl.so.1.1 |
实际加载的库路径 |
(0xb6ee0000) |
库加载到内存的地址 |
📝 七、结束语
这个问题的排查过程,可以说是嵌入式工程师日常的真实写照:
"程序跑不起来?先看日志。日志没线索?加日志。日志加了一圈还是不知道为啥?恭喜你,该检查链接器配置了。"
------某不愿透露姓名的嵌入式老兵
静态库链接水很深,尤其是涉及到OpenSSL这种"基础设施级"库。链接器优化会把那些"看起来没用"的符号悄悄删掉,但偏偏那些"看起来没用"的符号,运行时可能恰恰需要。
记住 --whole-archive 这个命令,下次遇到类似的"符号明明存在却找不到"的问题,你就能少走十年弯路。
本文配套源码:关注公众号「嵌入式Linux站」,回复「MQTT」获取完整项目代码。
⚠️ 注意:本文配置参数仅供参考,实际使用时请根据你的MQTT Broker配置进行调整。尤其是证书验证选项,生产环境务必开启!
📚 附录:排查命令速查表
开发环境常用命令
bash
# 查看库文件大小和时间戳
ls -la libxxx.so.1.0.0
# 搜索二进制文件中的字符串
strings libxxx.so | grep "关键字"
strings libxxx.so | grep -E "OPENSSL|SSL|CIPHER"
# 查看符号表
nm libxxx.a | grep SYMBOL_NAME
nm -D libxxx.so | grep SYMBOL_NAME # -D只看动态符号
# 查看依赖的动态库
ldd libxxx.so
objdump -p libxxx.so | grep NEEDED
# 查看文件类型
file libxxx.so.1.0.0
# 查看符号链接链
readlink libxxx.so
readlink -f libxxx.so # 完整路径
板子(运行环境)常用命令
bash
# 查看库文件状态
ls -la /mnt/nandflash/lib/libxxx*
# 验证符号是否存在
strings /mnt/nandflash/lib/libxxx.so | grep SYMBOL
# 检查符号链接
readlink -f /mnt/nandflash/lib/libxxx.so
# 查看程序依赖
ldd /path/to/program | grep ssl
# 检查OpenSSL版本
openssl version
strings /lib/libcrypto.so.1.1 | grep "OpenSSL"
# 搜索所有OpenSSL库
find /lib /usr/lib -name "libssl*" -o -name "libcrypto*"