【避坑实录】Qt 4.8.6 + Paho MQTT C客户端 + OpenSSL静态链接的血泪史

【避坑实录】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_bModuleLogEnabledg_bMqttServerLogEnabled

1.3 遇到的问题(按时间顺序)

  1. 非TLS连接正常,TLS连接失败rc=-8(SSL配置错误)
  2. undefined symbol错误OPENSSL_sk_pop_free, version OPENSSL_1_1_0
  3. 静态库符号丢失:明明链接了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.1real name(真实文件名),包含实际代码
  • .so.1soname(ABI兼容名),表示主版本号
  • .solinker 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.1libcrypto.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 问题根因分析

经过上面排查,定位到问题

  1. 我们编译用的OpenSSLopenssl-1.1.1w)是静态库 ,编译时用了 no-shared 选项
  2. 板子上的OpenSSLlibssl.so.1.1)是动态库,有版本标签
  3. 静态链接的OpenSSL符号没有版本标签
  4. 运行时动态链接器加载 libssl.so.1.1,它期望 OPENSSL_sk_pop_free@OPENSSL_1_1_0
  5. 但我们的 libMqttServer.so 里只有没有版本标签的 OPENSSL_sk_pop_free
  6. 链接器认为符号不匹配 ,所以报 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。但链接器在静态库 上只做直接依赖分析:

  1. 链接器看到 libMqttServer.so 调用了 Paho 的 MQTTClient_connect
  2. Paho 里有 SSLSocket_connect,这是直接引用,保留
  3. SSLSocket_connect 内部调用了 OpenSSL 的 SSL_connect,这也是直接引用,保留
  4. 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.proLIBS 部分:

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?

  1. 静态库有很深的间接调用链(OpenSSL、各种加密库)
  2. 使用了插件架构,符号需要在运行时动态加载
  3. 需要保证所有符号都被保留(调试、符号表分析)

代价是什么?

  1. 最终二进制文件体积变大(可能增大几倍)
  2. 链接速度变慢
  3. 可能引入符号冲突(多个静态库定义了同一个符号)

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(安全性、性能)。

常见问题

  1. Qt编译时检测到系统的OpenSSL 1.0.x,自动链接
  2. 项目需要用OpenSSL 1.1.x
  3. 头文件版本和链接的库版本不匹配

解决方案 :在项目 .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*"
相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00613 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术13 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript