
1. 从一条编译错误开始项目里多出来的两个幽灵库前段时间做一个上位机项目需要对接国密通信网关服务端用的是基于国密算法的 SSL 协议所以客户端这边必须直接集成国密 SSL 库。我选了 GmSSL它是国内开源社区维护很好的国密实现API 与 OpenSSL 高度兼容这意味着我可以继续用EVP_DigestInit_ex、EVP_SealInit这套写习惯的接口只是底层算法从 RSA/SHA256 换成 SM2/SM3/SM4 而已改造量比想象中小很多。开发环境是 Windows 10 Qt 5.15.2 (msvc2019_64) Visual Studio 2019 编译套件。GmSSL 源码我克隆下来很快编译通过库文件也出来了但一到 Qt 项目里链接问题就来了报错信息里反复出现两个不存在的库——-lssl-1_1-x64和-lcrypto-1_1-x64。Qt Creator 的编译输出面板里除了找不到符号的报错还跟着一行很怪的提示error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid...后面被截断了看得人一头雾水。这篇文章就是记录我整个排查过程的。如果你也在用 Qt 集成 GmSSL 或者其他 OpenSSL 衍生库卡在-lssl-1_1-x64 -lcrypto-1_1-x64这类的链接错误上那这篇应该能帮你省下一两天时间。文章不只写解决方案更会讲清楚 qmake 在 Windows 下到底怎么处理库名、这些库文件从哪来、以及dependent 某某路径这种报错的真实含义。1.1 完整报错现场先照着我这个现象查一下当时 Qt Creator 里报错大概是这样的:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid... :-1: error: LNK1120: 8 个无法解析的外部命令 :-1: error: LNK2019: 无法解析的外部符号 SM2_do_encrypt :-1: error: LNK2019: 无法解析的外部符号 EVP_sm3第一行那个qtwid后面被截断我一开始以为是 qtwidgets 模块出了问题在网上搜了一圈也是各种说法有人说重装 Qt有人说加QT webenginewidgets。我把 WebEngine 模块加上之后报错确实变了但-lssl-1_1-x64 -lcrypto-1_1-x64相关的链接错误还在说明它俩根本不是一个层面的问题。这个dependent报错是 MSVC 链接器在解析项目里某个模块的依赖项时回退到 include 目录下找头文件导致的提示后面我单独讲。真正卡死我的是 LNK2019 那一串无法解析的外部符号 EVP_sm3。这说明 GmSSL 的库文件实际上已经参与链接了或者说至少被 Qt 项目认识了但里面找不到这些符号。一个最常见的原因就是——你链接进来的那个 lib根本不是我编译出来的那个 lib。1.2 这两个库名到底是谁定义的-lssl-1_1-x64和-lcrypto-1_1-x64并不是 gmssl 的专属命名它来自 OpenSSL 1.1.1 在 Windows 上的 DLL/导入库命名规范。OpenSSL 在 Windows 上用 Perl Configure 编译时默认输出库名带版本号和架构比如libssl-1_1-x64.dll/libssl-1_1-x64.liblibcrypto-1_1-x64.dll/libcrypto-1_1-x64.lib对应到 MinGW 环境还会生成libssl-1_1-x64.dll.a这样的链接文件。qmake 或者 CMake 里的-lssl-1_1-x64实际是在告诉链接器去目标库目录里找ssl-1_1-x64.lib或libssl-1_1-x64.lib。GmSSL 在兼容 OpenSSL 1.1 ABI 的版本客户端源码停留在 OpenSSL 1.1.1 线里编译产物同样沿用了这套命名。所以你如果在网上搜到一篇教程让你在.pro文件里写LIBS -L$$PWD/../GmSSL/lib -lssl-1_1-x64 -lcrypto-1_1-x64那它默认你编译出来的库就是这个命名。问题就在于如果你用的 GmSSL 不是我说的这个分支或者你编译时选了静态库模式又或者你用的是 CMake 构建而非 Configurenmake那实际产出的文件名可能完全不同。用-l去链接一个名字不存在的库MSVC 不会直接报找不到文件它会报一堆无法解析的外部符号因为 qmake 会把这个-l参数翻译成ssl-1_1-x64.lib如果找不到对应的.lib链接器有时候会继续尝试把目标文件名当库名最终给你一串更让人困惑的依赖错误。所以第一步不是改代码而是去你的 GmSSL 编译输出目录看看真实的库文件到底叫什么。2. GmSSL 在 MSVC 下编译的完整记录从源码到 .lib我在这个环节折腾了两遍第一遍用的是相对比较老的 GmSSL 分支第二遍是直接拉的主干。这里把能稳定跑通的过程完整写一遍包括你容易忽略的细节。2.1 环境准备VS2019 Perl NASM先说明Windows 下编译 GmSSL 不是打开 Visual Studio 点两下就行。它的构建体系沿袭自 OpenSSL必须满足三个前置条件工具用途我的建议Visual Studio 2019/2022C 编译器和链接器必须安装使用 C 的桌面开发工作负载Perl调用 Configure 脚本生成 MakefileStrawberry Perl装完要能直接在命令行执行perl -vNASM汇编优化模块SM4 等算法的优化下载后把 nasm.exe 所在目录加入 PATH这里有个坑不要用 Git 自带的那套 MSYS Bash 去跑perl Configure也不要用 WSL。必须在 Windows 的 cmd 里先进入x64 Native Tools Command Prompt for VS 2019再执行命令。否则编译器检测会出问题生成的 Makefile 可能错误地选成 32 位或者使用 MinGW。我当时的验证顺序where cl where perl where nasm三个命令都有输出才继续下一步。特别是cl这个命令必须在 VS 的开发命令行里才有普通 cmd 里是没有的。2.2 编译方式选择Configurenmake 还是 CMakeGmSSL 官方仓库现在支持两种构建方式。如果你需要的是和标题一致的-lssl-1_1-x64 -lcrypto-1_1-x64动态库建议走 Configurenmake 路线perl Configure VC-WIN64A no-asm --prefixD:\libs\GmSSL\x64 nmake nmake install依赖项没有额外问题nmake跑完大约五到十分钟取决于机器性能。--prefix指定安装目录后面 Qt 集成时直接指向安装出来的 include 和 lib 目录比在源码目录里翻东西要干净得多。如果你更习惯 CMake也可以这样mkdir build cd build cmake -G Visual Studio 16 2019 -A x64 -DCMAKE_INSTALL_PREFIXD:\libs\GmSSL\x64 .. cmake --build . --config Release cmake --install .两种方式都试过之后我的体会是CMake 路线比较适合你只需要静态库或者打算在 Visual Studio 工程里加 GmSSL 源码的情况如果主要场景是 Qt qmake还是 Configurenmake 更省心因为它生成产物的命名规则最贴近各类教程里的-lssl-1_1-x64 -lcrypto-1_1-x64。注意我上面写了no-asm。其实可以去掉这个参数把 NASM 的优化用上但我第一次带着 asm 编译时遇到一个 NASM 版本太新的问题报了一堆指令错误本着尽快跑通的原则直接no-asm了。对纯软件实现的 SM2/SM3/SM4 来说性能损失在一般上位机场景里完全可以接受。如果你追求性能再试试升级/降级 NASM 后去掉no-asm。2.3 编译产物清单与命名规则nmake install之后在D:\libs\GmSSL\x64下你会看到bin\ libcrypto-1_1-x64.dll libssl-1_1-x64.dll include\ openssl\ evp.h ssl.h sm2.h ... lib\ libcrypto-1_1-x64.lib libssl-1_1-x64.lib libcrypto.lib - 符号链接/拷贝 libssl.lib - 符号链接/拷贝关键点在于真正的动态链接库是带版本号的libssl-1_1-x64.dll和libcrypto-1_1-x64.dll而libssl.lib和libcrypto.lib是为了兼容老项目链接方式的导入库名字。有些教程写LIBS -lssl -lcrypto在 Windows 上 MSVC 也能找到因为libcrypto.lib确实存在。但如果你在两个 lib 都没有libssl-1_1-x64.lib的情况下写-lssl-1_1-x64那就等于给链接器报了个假路径。所以看到这里你应该理解了网上流传的.pro写法本身没问题前提是编译 GmSSL 时用的就是 OpenSSL 1.1 兼容命名。我自己第一次失败是因为拉到了一个以 CMake 为主构建方式的更新版本输出库名变成了libgmssl.dll/gmssl.lib之类的自定义名字。后来切换到 Configure 方式名字才对上。3. Qt 5.15.2 项目集成 GmSSL 的标准姿势把 GmSSL 编译成想要的库文件之后集成进 Qt 工程本来应该是简单事但因为 Qt 的 shadow build、MSVC 调试运行时、DLL 搜索路径这几个因素叠在一起很容易出幺蛾子。我按正确顺序做一遍。3.1 .pro 文件里的三处修改假设 GmSSL 安装在D:\libs\GmSSL\x64我的.pro文件里最终长这样# GmSSL 头文件 INCLUDEPATH D:/libs/GmSSL/x64/include # 第三方库目录 LIBS -L$$quote(D:/libs/GmSSL/x64/lib) -lssl-1_1-x64 -lcrypto-1_1-x64三处关键点头文件路径指向include不是include/openssl。因为代码里写的是#include openssl/evp.h编译器会自动在 include 下找openssl/子目录。库目录用$$quote()包一层防止路径里有空格时被 qmake 拆成多段参数。虽然我这里的路径没有空格但老一辈经验建议最好养成习惯。-lssl-1_1-x64和-lcrypto-1_1-x64的书写顺序先写 ssl 再写 crypto因为 ssl 库内部依赖 crypto。如果顺序反了MSVC 偶尔会因为链接顺序问题报告一些奇怪的 LNK2005/LNK2019。如果只在代码里用了EVP_sm3、SM2_do_encrypt这类底层接口其实链接时只需要-lcrypto-1_1-x64就够了ssl 库可以不加。但为了后续可能用SSL_CTX_new(TLS_server_method())这类 SSL 会话 API我两个都加了反正也不冲突。3.2 Debug/Release 混用引发重影问题这又是一个隐蔽的坑。Qt Creator 默认会为每个项目管理 Debug 和 Release 两套构建目录而.pro里的LIBS是不区分构建套件的。如果你的 GmSSL 编译时只编译了 Release 版那么 Debug 模式下链接器会用同一个 Release 导入库去链接你的代码——本来这不至于报错但因为 MSVC 的 Debug 运行库/MDd和 Release 运行库/MD不同GmSSL 的 DLL 内部用的是 Release 运行库你的 Qt Debug 程序如果也同时链接了带调试信息的 C 运行库就有可能出现LNK4098警告或者运行时莫名其妙的堆损坏。最典型的现象是编译能通过但程序一启动就崩调用了 SM2 加密函数后内存检察报错。这不是你代码写错了而是 Debug 程序链接了 Release 版库。我当时的处理方式简单粗暴直接用 Release 构建 Qt 程序并把 GmSSL 也编译成 Release。如果你确实需要 Debug 调试那最好把 GmSSL 也编译一份 Debug 版输出文件名可能变成libssl-1_1-x64d.lib、libcrypto-1_1-x64d.lib并用一个.pri文件按CONFIG(debug, debug|release)分支切换库名。下面这个片段可以抄CONFIG(debug, debug|release) { LIBS -L$$quote(D:/libs/GmSSL/x64/debug/lib) -lssl-1_1-x64d -lcrypto-1_1-x64d } else { LIBS -L$$quote(D:/libs/GmSSL/x64/release/lib) -lssl-1_1-x64 -lcrypto-1_1-x64 }3.3 运行时 DLL 的部署方式链接成功只是第一步跑起来还需要把libssl-1_1-x64.dll和libcrypto-1_1-x64.dll放到可执行文件能找得到的地方。Windows 下 DLL 搜索顺序是从 exe 所在目录开始的所以我建议用构建后的拷贝步骤而不是去改系统环境变量 PATH改 PATH 会影响所有程序而且换机器就失效。Qt Creator 里可以在.pro文件末尾加一个自定义步骤CONFIG(release, debug|release): { QMAKE_POST_LINK $$quote(cmd /c copy /y D:\\libs\\GmSSL\\x64\\bin\\*.dll $$OUT_PWD\\debug\\) }当然这只是临时的正式部署建议用下面第 5 节里的批处理脚本把 DLL、Qt 运行库、依赖文件一起打包。4. 排查 -lssl-1_1-x64 -lcrypto-1_1-x64 链接错误的全链路如果你不是照着我前面的步骤从零编译而是接手了一个已有工程报错已经出现那排查链路比重新编译更重要。我把当时的每一步和背后的原理写清楚。4.1 qmake 是如何翻译 -l 参数的在 qmake 里写-lfooMSVC 的链接器最终收到的是foo.lib或者libfoo.lib取决于具体实现。这个转换过程不是 qmake 自发完成的而是通过QMAKE_LIBS和QMAKE_LFLAGS驱动的。简单说-L指定的是链接器搜索目录-l指定的是库名搜索路径加库名拼起来就是最后的链接输入。这个机制带来的一个隐蔽问题如果你在LIBS里既写了-L又写了-l但-L的路径写错或目录不存在链接器并不会马上报找不到路径它会静默跳过然后继续用从环境变量LIB里继承的搜索路径去找。而当它在一个错误的地方找到一个同名的旧版库时你看到的就不是文件不存在而是符号找不到。所以排查时先确认你的D:\libs\GmSSL\x64\lib目录下确确实实存在libssl-1_1-x64.lib和libcrypto-1_1-x64.lib这两个文件。如果不存在检查编译产物命名。如果存在再看下一步。4.2 dependent ............\qt\5.15.2\msvc2019_64\include\qtwid是什么鬼这条报错我在网上搜到很多复制粘贴讨论但能讲清楚的不多。它本质上不是 qmake 的错误而是 MSVC 链接器在解析某个.lib的依赖项时输出的提示。.lib文件除了包含目标文件的内容还记录了它依赖的其他.dll或者其他.lib。当 MSVC 链接器发现你链接的某个库依赖 Qt 的模块比如 qtwidgets.lib它回去找这些模块对应的头文件目录最终拼出这条相对路径。换句话说这条dependent报错和 GmSSL 本身没有直接关系反倒是你的 Qt 项目里某个模块的链接配置有问题。最常见的情况是你的.pro里写了QT webenginewidgets但安装的 Qt 5.15.2 二进制包里没有带 WebEngineQt 官方从 5.15 起把 WebEngine 单独分发新装环境很容易缺这个模块。于是 MSVC 在解析依赖时跑到 include 目录去找qtwebenginewidgets相关的头文件层层上溯生成那条看起来莫名其妙的dependent路径。我那次就是误加了QT webenginewidgets后才出现这个提示。把这一行删掉dependent报错就消失了。如果你确实需要 WebEngine那你得用 Qt 在线安装器把 WebEngine 模块补上或者在.pro里去掉相关代码。4.3 用 dumpbin 定位到真正的根因dependent报错消失后剩下的才是主角LNK2019 无法解析的外部符号 EVP_sm3。这时候我用了 Visual Studio 自带的dumpbin工具在 x64 Native Tools Command Prompt 里执行dumpbin /exports D:\libs\GmSSL\x64\bin\libcrypto-1_1-x64.dll | findstr /i sm3/exports会列出 DLL 全部导出符号。如果输出里看不到EVP_sm3则说明这个 DLL 不是你要找的那个版本如果能看到再把.lib拉出来看dumpbin /linkermember D:\libs\GmSSL\x64\lib\libcrypto-1_1-x64.lib | findstr /i EVP_sm3我最后定位到的问题是这样的编译 GmSSL 时机器上已经安装了一份系统级 OpenSSL 1.1.1GmSSL 的 Configure 脚本优先级比 NASM 低一层在检测系统库时把 OpenSSL 的头文件和导入库解析进去了导致编译出来的 libcrypto 对象文件里的符号名带上了 OpenSSL 的 namespace 修饰而 Qt 工程里我用的调用符号名又是从 GmSSL 头文件里来的。两者之间有一批符号对不上。说白了就是头文件版本和库文件版本不匹配。解决方式很朴素让 Qt 工程只使用D:\libs\GmSSL\x64\include的头文件并且保证这个 include 路径在 Qt 自带 include 之前。怎么保证顺序在.pro里把INCLUDEPATH写在所有QT 模块之后qmake 会按书写顺序从左到右排列 include 搜索路径。我自己当时还做了个更绝的验证暂时把系统 OpenSSL 的 include 路径从环境变量里移除只留 GmSSL 的编译立刻通过。5. 修复后的工程配置与国密算法验证问题定位清楚后修复只需要几处改动。这里把最终的工程配置和一键验证代码放出来。5.1 最终改好的 .pro 文件去掉多余的 webenginewidgets 模块去掉系统 OpenSSL 干扰最终如下QT core gui widgets network CONFIG c11 TARGET GmSSLTest TEMPLATE app INCLUDEPATH D:/libs/GmSSL/x64/include SOURCES main.cpp LIBS -L$$quote(D:/libs/GmSSL/x64/lib) -lssl-1_1-x64 -lcrypto-1_1-x64几个细节再次强调INCLUDEPATH放在QT 之后确保覆盖系统库头文件。不要写QT webenginewidgets除非必要。LIBS只写一次避免重复-lcrypto-1_1-x64导致符号重定义。5.2 SM2/SM3/SM4 最小验证代码不改动项目框架直接用控制台或者 QDebug 输出验证库是否可用。下面是我在main.cpp里写的最小验证覆盖三个核心国密算法#include QCoreApplication #include QDebug #include openssl/evp.h #include openssl/sm2.h #include openssl/rand.h #include cstring int main(int argc, char* argv[]) { QCoreApplication a(argc, argv); // SM3 摘要 unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digestLen 0; EVP_MD_CTX* mdCtx EVP_MD_CTX_new(); EVP_DigestInit_ex(mdCtx, EVP_sm3(), nullptr); EVP_DigestUpdate(mdCtx, hello gmssl, 11); EVP_DigestFinal_ex(mdCtx, digest, digestLen); EVP_MD_CTX_free(mdCtx); qDebug() SM3: QByteArray((char*)digest, (int)digestLen).toHex(); // SM2 密钥对生成 EVP_PKEY_CTX* keyCtx EVP_PKEY_CTX_new_id(EVP_PKEY_SM2, nullptr); EVP_PKEY* sm2Key nullptr; EVP_PKEY_keygen_init(keyCtx); EVP_PKEY_keygen(keyCtx, sm2Key); EVP_PKEY_CTX_free(keyCtx); qDebug() SM2 keygen: OK; // SM4 CBC 加解密 unsigned char key[16] { 0 }; unsigned char iv[16] { 0 }; RAND_bytes(key, sizeof(key)); RAND_bytes(iv, sizeof(iv)); unsigned char plaintext[] GmSSL SM4 test; unsigned char ciphertext[64] { 0 }; unsigned char decrypted[64] { 0 }; int len1 0, len2 0, len3 0; EVP_CIPHER_CTX* sm4Ctx EVP_CIPHER_CTX_new(); EVP_EncryptInit_ex(sm4Ctx, EVP_sm4_cbc(), nullptr, key, iv); EVP_EncryptUpdate(sm4Ctx, ciphertext, len1, plaintext, (int)strlen((char*)plaintext)); EVP_EncryptFinal_ex(sm4Ctx, ciphertext len1, len2); EVP_CIPHER_CTX_free(sm4Ctx); EVP_CIPHER_CTX* sm4DecCtx EVP_CIPHER_CTX_new(); EVP_DecryptInit_ex(sm4DecCtx, EVP_sm4_cbc(), nullptr, key, iv); EVP_DecryptUpdate(sm4DecCtx, decrypted, len1, ciphertext, len1 len2); EVP_DecryptFinal_ex(sm4DecCtx, decrypted len1, len3); EVP_CIPHER_CTX_free(sm4DecCtx); qDebug() SM4 decrypt: QByteArray((char*)decrypted, len1 len3); return 0; }编译运行后如果能看到 SM3 摘要、SM2 keygen: OK、SM4 decrypt 正常输出就说明 Qt GmSSL 的链接完全打通了。有一个小提示EVP_PKEY_SM2是 GmSSL 扩展的定值如果头文件版本和你链接的库版本不匹配这里可能直接编译报错说没有定义。刚好可以作为验证头库匹配的试金石。5.3 一键部署 .dll 的批处理链接通过后部署阶段还需要把两个 DLL 放到 exe 旁。我写了一个很简单的批处理每次构建完自动执行echo off set BUILD_DIR%1 if %BUILD_DIR% set BUILD_DIRdebug copy /y D:\libs\GmSSL\x64\bin\libssl-1_1-x64.dll %BUILD_DIR% copy /y D:\libs\GmSSL\x64\bin\libcrypto-1_1-x64.dll %BUILD_DIR% windeployqt --release %BUILD_DIR%\GmSSLTest.exe也可以直接在 Qt Creator 的构建步骤里添加一个自定义处理步骤命令填cmd /c deploy.bat debug这样每次编译完成自动部署省得手动拷贝。6. 这次踩坑留给我的一条通用排查思路经历了这次问题我最大的收获不是知道怎么链接 GmSSL而是总结出一套 Windows MSVC qmake 下链接第三方库的通用排查顺序以后碰到类似-lxxx-yyy的问题基本半小时内能定位。6.1 MSVC 链接第三方库的三板斧第一板斧确认库文件真实存在名字一个字符不差。libssl-1_1-x64.lib和ssl-1_1-x64.lib可能同时存在但哪个是你链接器实际找到的在.pro里把LIBS改成绝对路径直接指定到libssl-1_1-x64.lib绕过 qmake 和链接器的名字解析往往能立刻暴露问题。第二板斧用 dumpbin 检查符号和依赖。/exports看 DLL 导出了什么/linkermember看导入库里的符号集合/dependents看 DLL 依赖哪些其他 DLL。这三个命令组合起来足以应付 90% 的为什么符号找不到问题。第三板斧排除环境变量 LIB 的干扰。MSVC 链接器会额外读取系统环境变量LIB里的默认库路径。如果你机器上装过多个版本的 OpenSSL/GmSSLLIB里可能残留旧路径。在 VS 开发命令行里执行echo %LIB%能看到全部默认搜索路径必要时在.pro里用!win32-msvc之类的条件判断或者干脆在项目属性里重写库目录顺序。这三板斧其实是个递进关系先确保文件存在再看符号对不对最后排查是不是被环境变量带偏了。我这次刚好三个都踩了一遍。6.2 当 GmSSL 和 OpenSSL 同时出现在项目里这里值得单独说一下。很多项目不是从零引入 GmSSL而是原本就有 OpenSSL 依赖比如 Qt 的 HTTPS 模块、某些网络库底层就链接了 OpenSSL。这种情况下libcrypto-1_1-x64.dll可能在系统里存在两份甚至更多。MSVC 的链接器在按顺序搜索LIB路径时如果先找到 OpenSSL 的库后找到 GmSSL 的库就会发生符号冲突或静默链接了错误的版本。我的建议是统一用 GmSSL 替代 OpenSSL让所有代码都通过openssl/xxx.h接口调用。因为 GmSSL 的 API 本来就是 OpenSSL 兼容的你的SSL_CTX_new、EVP_*系列代码根本不用改。替代之后整个进程里只有一份 crypto/ssl 实现避免两个库内部的同名全局变量互相污染。如果实在无法替换至少要做到链接顺序上把 GmSSL 放在 OpenSSL 前面运行时把 GmSSL 的 DLL 放到 exe 目录优先于系统目录绝不要同时链接两套导入库进同一个可执行文件。6.3 最后给后来者的一句话这次把 Qt 5.15.2、GmSSL、MSVC 三者的编译集成链路完整走了一遍踩了不少坑但也让我彻底搞懂了 Windows 下动态链接库从命名到解析的完整机制。以后再看到-lssl-1_1-x64 -lcrypto-1_1-x64这种参数我第一反应就是去翻编译产物目录看文件名、看导出符号而不是在.pro里瞎改。如果你正在做国产化适配或者等保合规相关项目需要在 Qt 里集成国密算法建议从一开始就把 GmSSL 的编译脚本、库文件目录、部署脚本一起纳入版本管理并且写清楚是哪个分支、哪个编译选项得到的产物。这样换一台电脑重建环境时才不会重新踩一遍我今天踩过的坑。