简介:基于 openssl-1.0.2p 的预编译开发包,面向需在 QT5.12.2 与 VS2015 环境中集成 SSL/TLS 加密通信的 C++ 开发者。该版本为 OpenSSL 1.0.2 系列最后安全更新,内置常见对称/非对称加密算法、SSL/TLS 协议及证书处理能力,编译产物以静态库和动态库形式提供,省去从源码自行编译和配置的繁琐过程。压缩包共 173 个文件,约 6.23MB,核心包含 150 个头文件(接口声明)、14 个 DLL(运行时动态库)与 4 个 LIB(静态链接库),另有少量配置和辅助文件,方便按需选择链接方式;页面显示已有 603 人学习/下载。包内按 usr、usr1 目录组织库与头文件,路径清晰,可直接引入 QT/VS 项目;配套工具可帮助快速验证 OpenSSL 配置并调整证书参数,让开发者绕开编译难题,更快获得可运行的加密能力,适用于客户端/服务端安全通信模块开发。
1. 先下载还是先编译:这份预编译库到底省掉了什么
大多数来拿 openssl-1.0.2p 编译好的静态库动态库头文件 的人,不是想研究加密算法,而是手里有个老工程正等着链接。1.0.2 系列在 Windows 上自己编译是最磨人的:先装 Perl,再配汇编工具,然后跟 nmake 的版本死磕,最后编出来的库还不一定和工程匹配;这份压缩包直接把 include、lib、dll 三样东西整理好,省掉的是最没技术含量的中间环节。它适合两类人:接手老工程、需要补一个安全补丁却不想动编译链的维护者;以及只想快速验证接口、尽快把 TLS 逻辑跑起来的开发者。反过来,如果你的目标是改 OpenSSL 源码本身,那这份包救不了你,还是得回到源码构建流程。
2. 静态库还是动态库:先从文件结构判断它怎么编出来的
OpenSSL 在 Windows 上的命名习惯和 Linux 完全不一样。1.0.2p 这个年代,动态库后缀是 .dll,而 .lib 既可能是真正的静态库,也可能是链接动态库时用的导入库。拿到压缩包后第一件事不是解压,而是想清楚一个问题:你手里这份包,到底是怎么 configure 出来的。
2.1 静态链接和动态链接的差别,落到文件上就三句话
静态库在链接时把目标代码直接复制进 exe,运行时不依赖任何同名 dll,分发时一个文件就能跑;动态库则是在链接阶段只绑一个导入库,运行时再去找同名 dll,exe 体积小,但 dll 必须在搜索路径里。落到文件名上有个容易混淆的点:静态库和导入库都叫 .lib,判断方法很简单,看它旁边有没有同名 dll。
| 形式 | 链接产物 | 运行依赖 | 典型文件 | 适合场景 |
|---|---|---|---|---|
| 静态库 | exe 内置目标代码 | 无 | libeay32.lib(体积大) | 分发单文件,不想管 dll 路径 |
| 动态库 | 链接导入库 | 需要同名 dll | libeay32.lib + libeay32.dll | 多模块共享、便于单独升级动态库 |
MSVC 下这个区分尤其重要。如果你把导入库当成静态库去链,程序编译链接都能过,一运行就提示“找不到 libeay32.dll”,这是预编译包最常见的翻车现场之一。反过来,把静态库当成导入库链,链接器会直接报 LNK2019,因为静态库文件里根本没有导出表。
2.2 从包结构反推编译时的配置:命名规则与文件清单
常见做法是包里放三个目录:include、lib、bin。include 下是 openssl 头文件目录,lib 下放 .lib,bin 下放 dll 和可能的 openssl.exe。解压后先跑一条递归清单命令,把家底摸清楚:
Get-ChildItem -Path .\openssl-1.0.2p -Recurse | Select-Object FullName这条命令的作用是递归列出包内所有文件,重点看三样东西。第一,lib 目录里是否同时存在同名 dll;第二,bin 目录下有没有 openssl.exe;第三,有没有 .pdb 调试符号文件。参数方面,-Path 指定解压根目录,-Recurse 表示包含所有子目录,Select-Object FullName 只输出完整路径,方便复制路径去配置工程。看到 libeay32.lib 和 libeay32.dll 同时在列,说明这份包走的是 shared 路线,lib 目录下的 .lib 是导入库,不是完整静态库。如果 lib 下只有 .lib,没有配套 dll,且文件体积有好几 MB 甚至更大,那才可能是 no-shared 的纯静态包。
从命名还能反推编译器。1.0.2 系列在 Windows 上的默认导出库名是 libeay32 和 ssleay32,分别对应 libcrypto 和 libssl;如果你拿到的是 libcrypto.lib 和 libssl.lib,那要么是 1.1.0 之后的版本,要么是有人手动改过 .def 导出文件。这个细节能帮你避开很多混淆,尤其当你手上同时有新旧两套库时。
2.3 选型建议:什么场景用静态,什么场景用动态
大多数预编译包对你来说就是个黑匣子,你只能通过文件结构判断它当初怎么编的,所以选型要按部署环境来定。我一般的做法是:如果是内部工具、命令行小助手,直接上静态库,省掉所有 dll 分发问题;如果是给多个业务模块共享的公共组件,或者主程序已经很大、不想每次更新都重编 exe,用动态库更合理。
运行时库的匹配也要在这里一起想好。预编译包如果当初是用 /MT 编的,你工程里就不能用 /MD,否则 LNK2038 会直接拦住你。这个问题在第五章会展开,但选型阶段就该有个意识:没有哪份预编译包是万能钥匙,先确认编译选项,再决定静态还是动态,能少踩一半坑。
3. 把库接进 VS 工程:头文件路径、链接参数与 SHA-256 最小示例
资源落地这一步,本质上是回答三个路径问题:头文件去哪找、库文件去哪找、运行时 dll 去哪找。把这三个路径配好,再填一个链接器输入名,工程就能跑起来。
3.1 目录约定:include、lib、bin 各指到哪里
VS 工程里分别对应三个配置项。打开项目属性,C/C++ 常规里的“附加包含目录”填包的 include 目录,路径写到 include 这一层,不要写到 include/openssl,因为源码里写的是#include <openssl/evp.h>,编译器会自己拼后面的 openssl 子目录。链接器常规里的“附加库目录”填 lib 目录,这个路径决定了链接器去哪找 .lib。调试配置里的“环境”可以加一条PATH=..\bin;%PATH%,让调试器启动时能找到 dll。
配置顺序有个讲究。先配包含目录,确认编译能过,再配库目录,确认链接能过,最后配 dll 路径,确认运行能过。一次性全配好再编译,一旦报错你会分不清是头文件没找到还是库没找到,调试效率很低。我习惯把这三个路径拆成三步走,每步只验证一件事。
3.2 链接器输入:老命名 libeay32.lib / ssleay32.lib 别填错
1.0.2p 的链接输入名是 libeay32.lib 和 ssleay32.lib,不是新版的 libcrypto.lib 和 libssl.lib。这个坑很隐蔽,因为网上的新教程几乎全在讲新版命名,你按新教程填进去,链接器报“无法打开文件 libcrypto.lib”,你还以为是库目录配错了。
在链接器输入里填附加依赖项时,可以带扩展名也可以不带,VS 会自动补 .lib。但如果你同时还在用某些第三方库,最好把依赖项写全名,避免多个库之间存在同名符号时被默认链接顺序坑到。只做摘要运算,链 libeay32.lib 一个就够;涉及 TLS 握手、SSL 上下文,才需要同时链 ssleay32.lib。
3.3 最小示例:SHA-256 摘要程序
用一个 20 行的程序验证整条链路是否打通。这个例子的作用是:编译过了,说明头文件路径对;链接过了,说明库文件路径和库名对;运行出结果了,说明 dll 加载成功。
#include <stdio.h> #include <openssl/evp.h> #include <openssl/opensslv.h> int main(void) { unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int len = 0; EVP_MD_CTX *ctx = EVP_MD_CTX_create(); if (ctx == NULL) { return 1; } EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); EVP_DigestUpdate(ctx, "hello openssl", 13); EVP_DigestFinal_ex(ctx, digest, &len); EVP_MD_CTX_destroy(ctx); printf("linked openssl %s\n", OPENSSL_VERSION_TEXT); for (unsigned int i = 0; i < len; i++) { printf("%02x", digest[i]); } printf("\n"); return 0; }逻辑说明:先创建摘要上下文,然后依次执行初始化、喂数据、取摘要、销毁上下文,最后打印链接进来的版本字符串和摘要的十六进制结果。参数说明:EVP_sha256()指定摘要算法,返回值直接传给EVP_DigestInit_ex;EVP_DigestUpdate第二个参数是输入数据,第三个参数是数据长度,写成 13 对应 "hello openssl" 的字节数,这个长度写错会导致摘要结果和预期不一致;EVP_MD_CTX_create和EVP_MD_CTX_destroy是 1.0.2 系的接口,如果你换用 1.1.x 头文件,这两个函数会被替换成EVP_MD_CTX_new和EVP_MD_CTX_free,编译直接报错,这本身就是版本差异的信号。
命令行编译的等价写法是:
cl /nologo /I openssl-1.0.2p\include digest_demo.c /link /LIBPATH:openssl-1.0.2p\lib libeay32.lib这段命令的作用是跳过 IDE,直接用编译器验证配置。/I指定头文件搜索路径,/LIBPATH指定库搜索路径,libeay32.lib是链接器输入。参数方面,如果你想用动态库,这里链接的仍然是 libeay32.lib,运行时才去找 libeay32.dll,不要误以为动态库要直接链 .dll 文件。
提示:如果你用这个例子编译时报“无法打开 openssl/evp.h”,先检查附加包含目录是不是写到了 include 这一层;如果报 LNK2019,才轮到检查库名和位数。
4. 1.0.2p 与新版 API 差异:老项目迁移中最常动的三个接口
很多老项目坚持用 1.0.2p,不是因为不知道它有历史包袱,而是因为换到 1.1.x 或 3.x 后,代码要动的面太大。这个章节讲清楚三个最典型的差异点,你手头的老代码在迁移时就能划定改动边界。
4.1 库名与接口的横向对比
先看一张对照表,把最常见的差异列出来,你对照自己的代码就知道要动哪里。
| 差异点 | 1.0.2p 老写法 | 新版写法 |
|---|---|---|
| 库文件命名 | libeay32.lib / ssleay32.lib | libcrypto.lib / libssl.lib |
| 全局初始化 | SSL_library_init() | OPENSSL_init_ssl(0, NULL) |
| 摘要上下文创建 | EVP_MD_CTX_create() | EVP_MD_CTX_new() |
| 摘要上下文释放 | EVP_MD_CTX_destroy() | EVP_MD_CTX_free() |
| RSA 结构体 | rsa->n 直接读字段 | RSA_get0_key(rsa, &n, NULL, NULL) |
这张表只列了最常碰面的接口。实际工程里还有更细的差异,比如 1.1.0 之后把很多结构体改成不透明类型,比如 BIO 的访问方式也变了。但以上三个点覆盖了日常迁移中 80% 的编译报错。
4.2 代码迁移:三处最常见的修改点
第一处是初始化。老代码通常在程序入口写SSL_library_init()和OpenSSL_add_all_algorithms(),新版把这两个封装进了OPENSSL_init_ssl。兼容写法用一个宏判断挡住两个版本:
#if OPENSSL_VERSION_NUMBER < 0x10100000L SSL_library_init(); OpenSSL_add_all_algorithms(); #else OPENSSL_init_ssl(0, NULL); #endif逻辑说明:OPENSSL_VERSION_NUMBER是编译期宏,来自 openssl/opensslv.h,值是一个十六进制数,高位段代表主版本,0x10100000L 对应 1.1.0 的门槛。预处理指令在编译阶段就完成分支选择,低版本走老初始化路线,高版本走新初始化路线。参数说明:OPENSSL_init_ssl的第一个参数 0 表示不启用任何额外选项,第二个参数 NULL 表示不传配置结构体,这是新版初始化最简调用方式。
第二处是摘要上下文。第三章的例子已经体现了这一点,EVP_MD_CTX_create换成EVP_MD_CTX_new,释放函数对应换成 `EV
本文还有配套的精品资源,点击获取