
简介面向使用GMSSL2.0进行国产密码算法开发、且需要在Windows上搭建服务端与Android客户端联调的测试人员或运维人员该压缩包提供了一套同时支持VS2015与VC6.0的完整工程方案重点解决跨版本Visual Studio下GMSSL服务端程序缺失、工程迁移与编译配置方面的常见问题。包内共43个文件核心为C源码.cpp/.h与多套工程构建文件.sln/.dsp/.dsw/.vcxproj并配有动态库.dll、静态库.lib、证书.pfx及说明文档.txt等。不同文件分工明确源码便于学习算法调用工程文件可适应VS2015与VC6.0两种环境证书和库则直接支撑SSL/TLS通信整体仅2.36MB轻量紧凑。已有263人学习下载在Android端SM2联调场景中具有一定参考价值。工程按client与server两个独立模块组织前者可对应Android客户端请求后者用于搭建Windows本地服务端附带.pfx证书和GMSSL库编译后可直接运行便于验证SM2加密通信、数字签名及双向认证等流程同时目录结构清晰省去自行配置GMSSL和证书的繁琐步骤。 做国密改造的时候最头疼的往往不是算法本身而是怎么把一个新库塞进一个老掉牙的工程里。我这次接手的任务就是把 GMSSL2.0 分别编进 VS2015 和 VC6.0 的工程一头是现代的开发环境另一头是二十多年前的编译器中间还夹着一堆 C99 语法和动态库导出的坑。这篇就完整记录一下整个过程中的思路、实操步骤和踩过的雷给同样要做国密适配的朋友一个参考。1. 项目背景与编译环境选型的纠结1.1 GMSSL2.0 到底是什么GMSSL 是 OpenSSL 的一个国内分支重点实现了 SM2、SM3、SM4 等国密算法同时保留了大部分 OpenSSL 1.0.x / 1.1.x 的接口风格。和标准 OpenSSL 相比GMSSL2.0 最核心的价值在于底层算法全部换成了国密标准上层 API 尽量兼容所以老项目迁移时不需要大面积重写业务逻辑。但问题也恰恰出在这个“尽量兼容”上。GMSSL2.0 的源码是基于较新的 C 语言规范写的工程文件默认也是为较新的 Visual Studio 版本准备的。你直接用 VS2015 打开编译大概率能跑通但如果你要把它接到一个十几年前用 VC6.0 维护的遗留系统里那就完全是另一个故事了。我这次遇到的实际场景是一套老旧的金融支付系统底层通信模块是当年用 VC6.0 写的但现在安全要求必须上国密所以 GMSSL2.0 必须能以库的形式被 VC6.0 工程调用。同时还有一个新开发的网关服务用的 VS2015要求直接静态链接 GMSSL2.0。等于说一份源码两个目标平台都得伺候好。1.2 为什么非要折腾 VS2015 和 VC6.0 两套工具链可能有人会问VC6.0 这种上古工具直接让客户换新环境不就行了但现实里很多工业控制、金融终端、嵌入式上位机的代码是几十年的存量业务逻辑复杂历史包袱重不是说换编译器就能换的。尤其是 VC6.0 工程里的 MFC 界面代码、第三方控件、ODBC 数据源配置迁移到新环境往往比重新开发还费劲。所以最现实的路径不是“替换开发工具”而是“让新库兼容老工具”。VS2015 这边比较省心因为 GMSSL2.0 官方文档里明确提到了对 VS2013/VS2015 的支持。但 VC6.0 那边就比较折磨了因为 VC6.0 的 C/C 编译器对 C99 标准的支持差到令人发指而 GMSSL2.0 源码里恰恰用了不少 C99 的写法比如for(int i0;...)这种循环内声明变量、stdint.h头文件、//行注释等。这些在 VS2015 里毫无压力在 VC6.0 里就是一堆编译错误。关键点编译一个库和你封装一个库供上层调用是两件难度完全不同的事。前者只是让源码能编过后者还要考虑头文件兼容、链接方式、运行时库匹配、调用约定等问题。我这次踩得最多的坑基本都集中在后半段。2. VS2015 环境下编译 GMSSL2.0 的完整流程2.1 环境准备与依赖工具链安装在 VS2015 下编译 GMSSL2.0并不像拿起来就能直接编有几个前置工具是必须要装的PerlGMSSL 的 Configure 脚本是用 Perl 写的没有 Perl 连配置这一步都走不了。建议装 ActivePerl 5.24 或 Strawberry Perl安装时记得把 Perl 加入 PATH 环境变量。NASM如果你要编译汇编优化模块比如 SM4 的某些加速实现需要 NASM 汇编器。如果不装可以编译纯 C 版本性能会差点但功能完整。我建议能装就装毕竟国密算法本身涉及的数学运算比较多汇编加速能省不少 CPU。VS2015 的 C 工具链包括 Windows SDK。这个一般装 VS2015 时都有但注意一定要装 C 相关的组件只装 C# 或默认组件是不够的。安装完之后建议在开始菜单里打开VS2015 x86 本机工具命令提示符或 x64取决于你要编哪种架构的库。这一步非常重要因为命令行工具里已经配好了所有环境变量避免你自己手工设置 INCLUDE、LIB、PATH 时出错。2.2 具体编译步骤与核心配置说明以编译 x86 Release 版本为例我建议在纯命令行的方式下操作比在 IDE 里点来点去要省事得多也更容易排查问题# 1. 进入源码根目录先清理环境 perl Configure VC-WIN32 no-asm --prefixD:\build\gmssl-vs2015 # 2. 编译生成 Makefile ms\do_ms.bat # 3. 编译静态库和动态库 nmake -f ms\ntdll.mak # 生成 DLL nmake -f ms\nt.mak # 生成静态库这里说一下参数选择的理由VC-WIN32表示使用 Visual C 编译 32 位版本。如果你要 64 位用VC-WIN64A。no-asm表示不使用汇编优化。如果你装了 NASM可以去掉这个参数编译速度会慢一点但运行效率更高。--prefix指定安装路径后续执行nmake install时会自动把头文件、库文件、dll 复制到这个目录。编译过程中如果报错nasm not found那就说明 NASM 没装或者 PATH 没配好。如果你根本不需要汇编版本那就直接带no-asm参数不会再报这个错。这是最省事的做法也是我在多个项目里实际验证过最稳的组合。编译时间大概是 5 到 10 分钟取决于机器性能。编完后在out32dll目录下会生成libeay32.dll、ssleay32.dllGMSSL 沿用了 OpenSSL 1.0.x 的命名在out32目录下生成libeay32.lib、ssleay32.lib静态库。2.3 VS2015 工程中配置头文件与依赖库编译完库之后工程配置又是一番功夫。在 VS2015 工程里右键项目 - 属性重点设置以下几项C/C - 常规 - 附加包含目录填入D:\build\gmssl-vs2015\include让编译器能找到openssl/ssl.h等头文件。链接器 - 常规 - 附加库目录填入D:\build\gmssl-vs2015\lib如果没有做nmake install就用out32dll或out32的绝对路径。链接器 - 输入 - 附加依赖项填入libeay32.lib;ssleay32.lib这才是真正把国密算法库链接进来。C/C - 预处理器 - 预处理器定义建议加上OPENSSL_SM2之类的宏具体以你用的 API 为准否则某些国密专用接口可能不会被头文件暴露出来。如果你的工程是 Debug 模式但 GMSSL 编的是 Release 库链接时可能会遇到_ITERATOR_DEBUG_LEVEL不匹配之类的错误。解决思路有两个要么把 GMSSL 也编一个 Debug 版本要么把你工程里 GMSSL 的附加依赖项改成 Release 库再配合/NODEFAULTLIB处理。我的经验是Debug 和 Release 分开编两个版本的库工程切换时干净利落省得后期各种诡异报错。另一个很容易忽略的问题是VS2015 默认会链接到 UCRTUniversal C Runtime而 GMSSL2.0 的库也是基于 UCRT 编译的这没问题。但当你把 DLL 部署到一台没有安装 VC 运行库的机器上时就会报缺少VCRUNTIME140.dll之类的错误。如果你是做项目交付的记得把 VC 2015 Redistributable 一起打包进去或者采用静态链接 CRT 的方式编译 GMSSL避免部署环境互相牵连。3. VC6.0 环境下编译 GMSSL2.0 的兼容性改造3.1 VC6.0 编译器太老带来的核心困境VS2015 能顺利编过的代码到 VC6.0 这里就是雪崩式的报错主要集中在三个方面C99 语法支持缺失。VC6.0 的 C 编译器基本停留在 C89/C90 标准//注释能用这个算扩展但for循环里定义变量、stdint.h头文件、snprintf、inline这些统统不支持。编译器对类型强转的警告级别不同。GMSSL2.0 源码里有大量的指针类型转换VC6.0 下经常报warning C4090或warning C4244。有些 warning 可以忽略但有些会直接升级成 error。运行时库不匹配。VC6.0 默认链接msvcrt.dll对应/MD或libcmt.lib对应/MT而 GMSSL2.0 在 VS2015 下编译时用的是新版的 UCRT。如果你拿 VS2015 编的库直接在 VC6.0 工程里链接链接器会报一堆奇怪的unresolved external symbol甚至直接提示运行时库冲突。所以在 VC6.0 下使用 GMSSL2.0最佳策略不是“用 VC6.0 编译 GMSSL2.0”而是“针对 VC6.0 编译一个兼容版本的 GMSSL2.0”也就是说你需要对源码做一些必要的裁剪和补丁。3.2 如何给 GMSSL2.0 打上 VC6.0 兼容补丁先说一个现实结论让 GMSSL2.0 的完整源码在 VC6.0 下百分之百编译通过几乎是不可能完成的任务也没有必要。因为完整的 GMSSL2.0 源码体量太大包含了很多 VC6.0 根本不支持的现代语法、模板特化、内联汇编强行移植的成本远高于收益。实际项目里更常见的做法是提取核心模块只编译你真正用到的国密算法接口。以我这次的业务为例我只需要 SM2 加解密、SM3 哈希、SM4 加解密那就只提取crypto/sm2、crypto/sm3、crypto/sm4这几个目录下的核心源文件连同必要的底层大数运算模块crypto/bn、随机数模块crypto/rand一起组成一个迷你工程用 VC6.0 编译成静态库。具体补丁工作包括把源码里的for(int i0; ...)改成int i; for(i0; ...)这一步是工作量最大的建议用脚本辅助全局替换再人工检查。新建一个stdint.h兼容头文件手动定义typedef int int32_t; typedef unsigned char uint8_t;等基础类型。删掉所有__attribute__、__declspec(dllexport)之类的非标准修饰或者用宏统一屏蔽。将snprintf替换成_snprintf这两个函数在 VC6.0 下行为有细微差别要特别注意缓冲区的边界处理。补丁打完之后在 VC6.0 里新建一个 Win32 Static Library 工程把这些源文件加进去编译成一个静态库gmssl_vc6.lib。这样上层 MFC 或 Win32 程序在链接时就完全不需要管 GMSSL 的 DLL 依赖部署也简单一个 EXE 加一个 LIB 就搞定。3.3 VC6.0 工程调用 GMSSL 头文件的注意点VC6.0 的 IDE 对标准 C 的支持比较弱用起来有几个地方必须提前规避不要直接引用openssl/ssl.h这个头文件里涉及 BIO、EVP 等一堆高级抽象VC6.0 下编译头文件本身就会报错。我的做法是只引用 CMSSL 裁剪版提供的sm2.h、sm3.h、sm4.h这些独立接口头文件。所有结构体定义以指针方式传递。VC6.0 对结构体赋值、拷贝的支持还行但对匿名结构体、联合体嵌套的解析经常出毛病。如果接口定义里用到了匿名 struct尽量改成具名 struct否则编译时会出现莫名其妙的C2061语法错误。注意函数调用约定。建议统一使用__stdcall或者默认的__cdecl不要混用。如果混用链接时会出现符号不一致的连接错误这种错误在 VC6.0 下尤其难查因为报错信息比较模糊。4. 编译过程中的高频报错排查与经验速查表4.1 两类编译器下最常见的五类编译错误我把我在这个项目里实际遇到的报错整理成了一份速查表涵盖两类编译器的常见问题方便大家直接对照解决错误现象常见原因解决方案fatal error C1083: Cannot open include file: stdint.hVC6.0 没有 stdint.h自定义兼容头文件或使用第三方提供的msinttypeserror C2065: snprintf : undeclared identifierVC6.0 不支持标准 C99 的 snprintf全局替换为_snprintferror C2143: syntax error : missing ; before type变量声明不在代码块开头把所有局部变量声明挪到函数或块的最前方LNK2005: _X already defined in msvcrt.lib运行时库混用统一所有工程的/MD或/MT设置error C2099: initializer is not a constant结构体初始化用了非编译期常量改用运行时赋值或加const这里面最坑的是第一条和最后一条。stdint.h的问题我前面已经说了补个兼容头文件就行。但C2099这种错误在 VS2015 下完全不会报只有 VC6.0 的 C 编译器才会严格到必须在编译期就能确定初始化值。这种代码在 GMSSL 源码里存在不少遇到一个改一个没有捷径。4.2 一次典型的“VC6.0 下结构体初始化”报错处理实录举个具体的例子GMSSL2.0 的 SM2 密钥结构体初始化代码大概是这样的SM2_KEY key {0};这段代码在 VS2015 下没有任何问题但在 VC6.0 下如果SM2_KEY结构体里包含了联合体union就会报error C2099: initializer is not a constant。原因在于 VC6.0 的 C 编译器对 union 成员的编译期初始化支持有限。我的解决方案是改成运行时动态初始化SM2_KEY *key (SM2_KEY *)malloc(sizeof(SM2_KEY)); if (key) { memset(key, 0, sizeof(SM2_KEY)); }这样做不仅能绕开 VC6.0 的编译器限制还能在语义上更安全——避免遗漏某些内嵌结构体的清零。配套的记得用SM2_KEY_free或手动free释放内存防止内存泄漏。4.3 动态库链接阶段的坑导出符号缺失与依赖顺序在 VS2015 工程里链接 GMSSL 的 DLL 版本时还有一个容易踩的坑链接时提示unresolved external symbol _EVP_sm3之类的错误。这个原因多半是你把libeay32.lib和ssleay32.lib的链接顺序搞反了或者某些符号需要额外指定依赖库。在我的实践中比较保险的做法是如果只用加解密和哈希只链接libeay32.lib就够用。如果需要 SSL/TLS 层的国密支持比如 SM2 证书的 TLS 握手再把ssleay32.lib加在libeay32.lib之后。如果用到zlib压缩还需要额外链接zlib.lib。顺序问题上ssleay32.lib依赖libeay32.lib所以ssleay32.lib必须在左libeay32.lib在右。VS 的链接器是从左往右解析符号的顺序反了就会出现“前面需要的符号后面还没加载”的链接错误。注意如果是在 VC6.0 工程中调用 GMSSL不建议使用动态库方式因为 VC6.0 生成的 EXE 在加载新版 UCRT 依赖的 DLL 时很容易出问题静态库方式是最省心的。5. 工程移植与代码集成的几个重要经验5.1 不同编译器下保持一致性的头文件策略头文件是整个工程移植过程中最容易出问题的环节因为 GMSSL 的头文件比较多而且相互包含关系比较复杂。我强烈建议的做法是自己再包一层“胶水头文件”比如gmssl_wrap.h里面只暴露业务需要的那几个接口底层再根据编译平台分条件引入不同头文件#ifdef _MSC_VER #if _MSC_VER 1900 #include gmssl/sm2.h #include gmssl/sm3.h #include gmssl/sm4.h #else #include compat/gmssl_vc6/sm2.h #include compat/gmssl_vc6/sm3.h #include compat/gmssl_vc6/sm4.h #endif #endif这样一来上层业务代码只需要 include 这个封装头文件即可不需要关心底层到底是 GMSSL2.0 还是裁剪版。对于甲方那边同时维护 VS2015 和 VC6.0 两套代码库的情况这种隔离方式能大幅减少条件编译的散落维护成本也低很多。5.2 数据对齐与字节序处理国密算法工程的隐藏地雷在跨编译器做国密适配时有个特别容易被忽略的问题数据对齐和字节序。VS2015 默认按 8 字节对齐VC6.0 默认按 8 字节对齐但结构体成员顺序不同可能影响实际大小。如果请求报文、响应报文里定义了结构体并要求按固定格式在网络上传输那么结构体成员之间可能会因为对齐产生 padding 空洞导致两端解析结果不一致。我当时的处理办法是所有网络传输结构体统一加上#pragma pack(1)强制一字节对齐。显式定义uint8_t、uint32_t等定宽类型避免在不同编译器下int长度不一致的问题。字节序方面国密算法标准里 SM2/SM3/SM4 的数据都是大端字节序所以从网络接收到的字节流要先转成主机序再参与运算但 GMSSL2.0 内部接口已经处理好了这一点只要你传入的指针指向的是完整的数据缓冲区即可。我当时有次 SM3 摘要值算出来和甲方给的参考结果不一致折腾了整整一个下午最后发现原因是结构体里有个int类型字段没有显式指定长度在 32 位和 64 位编译模式下实际宽度不同导致后续填充进 SM3 的数据流错位了。从那以后我对所有参与摘要或加解密运算的输入结构体一律使用定宽类型并且在实际发版前写了单测用例用标准测试向量反复校验。5.3 利用官方测试向量验证移植正确性说到测试向量这是我要重点强调的一点。GMSSL2.0 源码的test目录下其实自带了一堆标准测试向量比如 SM2 加解密、SM3 摘要的已知输入输出对。移植代码到 VC6.0 或者集成到业务工程时一定要先跑通这些测试向量不要等联调阶段才发现算法实现有问题那会非常被动。以 SM3 为例标准测试向量就是输入空字符串摘要值是66c7f0f462eeedd9d1f2d46bdc10e4e2开头的那个 32 字节值。输入abc摘要值是66c7f0f4...具体可以去国密标准文档里查。写个小程序对已知输入算一次 SM3 摘要比对这个值是否一致。一致说明你的库封装没问题不一致的话优先排查你上层调用时传参是否多传了字符串结尾的\0这类低级错误我之前也犯过。5.4 跨平台交付时库文件的版本标识与打包最后如果你也要和我一样同时交付 VS2015 和 VC6.0 两个版本的 GMSSL 库建议在库文件名或目录上做明显区分比如gmssl-vc6-static-x86.lib gmssl-vs2015-static-x86.lib gmssl-vs2015-dll-x64.lib别小看这一个命名习惯。实际项目里我曾经因为把 VC6.0 版本的静态库误当成 VS2015 的库链接导致整整排查了大半天最终发现是文件拿错了。这个低级错误如果发生在交付关键节点会非常尴尬。养成好习惯把不同编译器、不同架构、不同运行库模式/MT, /MD区分清楚能省掉大量后患。另外库的持续集成和版本管理也很重要。每次从上游拉取新代码后最好在干净的干净环境里重新编一遍并把产物连同头文件、说明文档打成一个压缩包统一放到公司内部的构件仓库里。这样项目组其他人要用的时候直接拉取分发效率极高也不会出现“你这库怎么和我编出来的不一样”的扯皮问题。6. 关于这套方案的实际落地效果这项移植工作做完后我这边最直观的感受是VC6.0 的老系统终于能跑国密 SM2/SM3/SM4 了而且完全不用改动原有的网络通信框架只是在底层加解密模块里替换了算法实现。VS2015 的新系统直接用上了完整版的 GMSSL2.0密钥协商、证书解析、TLS 国密套件全部可用性能上也明显优于 VC6.0 那个裁剪版。从工作量上看VS2015 这边的适配大概只花了两天大部分时间还在等编译。VC6.0 这边因为是老环境前前后后用了一周左右主要时间花在源码裁剪、C99 语法改写成 C89 以及结构体对齐问题的排查上。如果你们项目也有类似的“新老开发工具并存”的问题我建议尽早确定接口层设计把算法库与业务代码彻底解耦否则一旦在业务代码里散落大量#ifdef和平台判断后期的维护成本会指数级上升。这次实践也让我真正理解了一个道理在国内很多存量系统里“能不能用”和“怎么才能用上”之间的差距往往不在算法标准本身而在于工程化落地的细节。希望这份工程记录能帮到你。本文还有配套的精品资源点击获取