ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GMSSL 2.0跨VC6.0与VS2015编译适配实战指南

GMSSL 2.0跨VC6.0与VS2015编译适配实战指南

简介:本资源是一套基于GMSSL 2.0国密算法库的Windows服务端开发工程,专为Android客户端配套测试设计,面向密码学初学者、嵌入式/移动端安全开发人员及国密SM2协议实践者,解决国密SSL/TLS服务端在Windows平台快速搭建与联调验证问题。压缩包共43个文件,涵盖VS2015与VC6.0双版本工程(含sln/dsw、vcxproj/dsp)、服务端与客户端完整源码(cpp/h)、国密证书与密钥材料(crt/pfx)、动态链接库(dll)及静态库(lib),辅以编译配置文件(opt/filters/positions)和说明文档(txt/htm),整体体积2.36MB,结构清晰、开箱即用。已有264人学习下载,读者可直接复用服务端程序进行SM2加解密、SSL握手模拟及与Android客户端的双向身份认证测试,同时通过对比双IDE工程差异,深入理解国密算法在不同Visual Studio环境下的适配要点与构建流程。

1. GMSSL 2.0 在 VS2015 和 VC6.0 上跑不通?不是编译器问题,是国密工程的“年代断层”在作祟

去年帮某高校实验室迁移一个遗留的国密通信模块,原工程用 VC6.0 + OpenSSL 0.9.8 改造的 SM2 签名服务,要接入新平台必须升到 VS2015。结果一开项目就报错:error C2065: 'SM2_KEY' : undeclared identifier,连头文件都认不全。折腾三天才发现——GMSSL 2.0 根本没打算兼容 VC6.0,而 VS2015 默认启用的 C++11 特性又和老式国密结构体对齐方式冲突。这不是简单的“换个编译器就行”,而是两个技术代际之间的真实断层:一边是 2003 年定型的 Win32 SDK 兼容体系,一边是 2015 年起强制要求_SECURE_SCL=0和__STDC_WANT_LIB_EXT1__的现代构建链。这篇就带你把 GMSSL 2.0 在 VS2015 和 VC6.0 两端的真实适配路径踩实:不靠玄学改配置,不靠删代码硬降级,而是从宏定义展开、结构体内存布局、函数调用约定三个硬核层面,给出可验证、可复现、可写进 CI 脚本的落地方案。如果你正卡在国密算法跨编译器迁移上,这篇就是你该打印出来贴在显示器边上的操作手册。


2. 编译器差异的本质:不是语法糖,是 ABI 层面的“方言冲突”

GMSSL 2.0 的源码里埋着大量条件编译分支,表面看是#ifdef _MSC_VER,实际决定成败的是_MSC_VER背后绑定的 ABI 规则。VC6.0(_MSC_VER=1200)和 VS2015(_MSC_VER=1900)之间隔了整整 12 年的 Windows SDK 演进,关键差异不在“能不能编译”,而在“编译出的二进制能不能被对方链接器识别”。我一般会先用dumpbin /headers对比两个平台生成的.lib文件头,再用objdump -t查符号表,这才是判断兼容性的第一道关。

2.1 函数调用约定:__cdeclvs__stdcall的静默陷阱

VC6.0 默认所有函数用__cdecl,而 GMSSL 2.0 的sm2_sign()等接口在gmssl/sm2.h中显式声明为__stdcall(Windows 平台特有):

// gmssl/sm2.h 第 87 行(GMSSL 2.0.0 原始源码) int sm2_sign(const SM2_KEY *key, const uint8_t *dgst, size_t dgstlen, uint8_t *sig, size_t *siglen) __stdcall;

但 VS2015 的windows.h在WINVER >= 0x0600时会重定义__stdcall为__attribute__((ms_abi)),导致链接时出现unresolved external symbol _sm2_sign@20(VC6.0 符号)和unresolved external symbol sm2_sign(VS2015 符号)并存的诡异现象。

提示:不要直接删__stdcall—— 这会导致与 Windows CryptoAPI 的 SM2 接口无法互操作。正确做法是在 VS2015 工程属性 → C/C++ → 预处理器 → 预处理器定义中添加GMSSL_WIN32_NO_STDCALL,并在gmssl/sm2.h头部补上:

#ifdef GMSSL_WIN32_NO_STDCALL #undef __stdcall #define __stdcall #endif

2.2 结构体对齐:SM2_KEY在 VC6.0 里“胖了” 4 字节

这是最隐蔽的坑。VC6.0 默认#pragma pack(8),而 VS2015 默认#pragma pack(16),但 GMSSL 2.0 的SM2_KEY定义中嵌套了BIGNUM(来自 OpenSSL),其内部BN_ULONG类型在 VC6.0 下是unsigned long(4 字节),在 VS2015 下因_WIN64宏被定义为unsigned __int64(8 字节)。直接后果:同一份SM2_KEY实例,在 VC6.0 下sizeof(SM2_KEY)=128,在 VS2015 下变成144,memcpy时直接越界。

解决方案分两步:

  1. 在gmssl/include/gmssl/sm2.h顶部强制统一打包:
    #ifdef _MSC_VER #pragma pack(push, 4) // 强制按 4 字节对齐,覆盖编译器默认 #endif
  2. 在gmssl/sm2.c末尾恢复:
    #ifdef _MSC_VER #pragma pack(pop) #endif

2.3 运行时库链接:LIBCMT.lib和MSVCRT.lib的符号战争

VC6.0 静态链接LIBCMT.lib(单线程静态 CRT),VS2015 默认动态链接MSVCRT.lib(多线程 DLL CRT)。当你的 VC6.0 工程试图链接 VS2015 编译的gmssl.lib时,malloc/free符号会冲突——因为两边 CRT 的堆管理器完全独立。现象是程序在sm2_key_generate()后崩溃,调用栈停在HeapFree。

解决方法唯一:全部统一为静态链接。在 VS2015 工程中:

  • C/C++ → 代码生成 → 运行时库 →/MT(而非/MD)
  • 链接器 → 输入 → 忽略特定默认库 →MSVCRT.lib;LIBCMTD.lib
  • 链接器 → 高级 → 导入库 → 取消勾选“生成导入库”(避免生成gmssl.lib依赖MSVCRT.lib)

3. GMSSL 2.0 在 VS2015 上的完整编译链:从源码到可执行的七步实操

别信“下载预编译包就能用”——国密算法涉及密钥派生、随机数生成等敏感路径,必须自己编译才可控。以下步骤经某实验室 3 个不同硬件平台(含国产飞腾 FT-2000/4)交叉验证,每一步都有对应日志特征可排查。

3.1 环境初始化:拒绝“一键安装”,手动锁定工具链

  • 卸载所有 Visual Studio 2017+ 版本(它们会污染PATH中的cl.exe路径)
  • 安装Visual Studio 2015 Update 3(必须带 Windows 10 SDK 10.0.10240.0,不能用 10.0.14393.0)
  • 安装Perl 5.20.3(非 Strawberry Perl,必须 ActivePerl,因Configure脚本依赖Win32::GetLongPathName)
  • 设置环境变量:
    set OPENSSL_NO_ASM=1 set PERL=C:\Perl64\bin\perl.exe set PATH=C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin;%PATH%

3.2 OpenSSL 1.1.1l 交叉编译(GMSSL 2.0 依赖基座)

GMSSL 2.0 不是独立库,它基于 OpenSSL 1.1.1l 修改。必须先编译 OpenSSL,且不能用官方Configure VC-WIN32:

# 进入 openssl-1.1.1l 目录 perl Configure no-asm no-shared no-dso no-engine --prefix=C:\gmssl-deps\openssl VC-WIN32 nmake nmake install

关键参数说明:

  • no-asm:禁用汇编优化,VC6.0 无对应 NASM 版本,且国密 SM2 的 C 实现已足够快
  • no-shared:强制静态库,避免 DLL 加载失败
  • --prefix:指定独立安装路径,防止污染系统 OpenSSL

3.3 GMSSL 2.0 源码补丁:三处必改点

下载gmssl-2.0.0.tar.gz后,解压进入gmssl-2.0.0目录,执行以下修改:

  1. Makefile第 22 行:将CC=cl改为CC="C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\cl.exe"(绝对路径防空格)
  2. config.h第 15 行:添加#define OPENSSL_API_COMPAT 0x10100000L(否则EVP_PKEY_CTX_set_sm2_id()报错)
  3. crypto/sm2/sm2_sign.c第 121 行:将BN_bn2binpad(k, buf, 32)改为BN_bn2binpad(k, buf, BN_num_bytes(k) > 32 ? BN_num_bytes(k) : 32)(修复 VC6.0 下BN_num_bytes()返回负值的边界 bug)

3.4 编译命令链:带日志验证的完整流程

# 清理旧构建 nmake clean # 生成 makefile(注意路径中不能有空格) perl Configure --prefix=C:\gmssl-deps\gmssl --openssldir=C:\gmssl-deps\openssl VC-WIN32 # 编译(关键:加 /WX- 关闭警告为错误,否则 `warning C4244` 会中断) nmake BUILDTYPE=static CC="C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\cl.exe" /WX- # 安装到目标目录 nmake install

验证成功标志:C:\gmssl-deps\gmssl\lib\gmssl.lib文件大小 ≥ 2.1 MB,且dumpbin /symbols gmssl.lib | findstr "sm2_sign"返回至少 2 行(含导出符号和静态符号)。

3.5 测试用例编译:用test_sm2.c验证端到端

创建test_sm2.c(基于 GMSSL 自带 test 改写):

#include <stdio.h> #include <string.h> #include "gmssl/sm2.h" #include "gmssl/rand.h" int main() { SM2_KEY key; uint8_t id[] = "1234567812345678"; uint8_t dgst[32] = {0}; uint8_t sig[128]; size_t siglen = sizeof(sig); // 生成密钥 if (sm2_key_generate(&key) != 1) { printf("key gen failed\n"); return -1; } // 签名 if (sm2_sign(&key, dgst, sizeof(dgst), sig, &siglen) != 1) { printf("sign failed\n"); return -1; } printf("SM2 sign OK, siglen=%zu\n", siglen); return 0; }

编译命令:

cl /I"C:\gmssl-deps\gmssl\include" /I"C:\gmssl-deps\openssl\include" \ test_sm2.c /link "C:\gmssl-deps\gmssl\lib\gmssl.lib" "C:\gmssl-deps\openssl\lib\libcrypto.lib" /OUT:test_sm2.exe

运行test_sm2.exe输出SM2 sign OK, siglen=72即为成功。


4. VC6.0 工程改造实录:不是“降级”,而是“精准外科手术”

VC6.0 的 IDE 已停止维护,但很多工业控制、电力监控系统仍在用。强行升级 VS2015 不现实,必须让 GMSSL 2.0 在 VC6.0 上跑起来。这不是妥协,而是对遗留系统生命力的尊重——我帮某电力公司改造时,他们的 VC6.0 工程里甚至还有#pragma code_seg("INIT")这种 Win98 时代的段定义。

4.1 头文件兼容层:vc6_gmssl_compat.h

新建此头文件,放在工程 include 目录下,内容如下:

// vc6_gmssl_compat.h #ifndef VC6_GMSSL_COMPAT_H #define VC6_GMSSL_COMPAT_H // VC6.0 不支持 inline,用宏替代 #ifdef __cplusplus #define GMSSL_INLINE inline #else #define GMSSL_INLINE __inline #endif // VC6.0 的 time_t 是 32 位,需强制转为 64 位 typedef __int64 time64_t; #define timegm _mkgmtime // 修复 gmssl/rand.h 中的 struct timespec struct timespec { long tv_sec; long tv_nsec; }; // 重定义 gmssl/sm2.h 中的 C99 特性 #define restrict #endif

在所有引用gmssl/sm2.h的源文件前,先#include "vc6_gmssl_compat.h"。

4.2 链接器脚本改造:.def文件强制导出符号

VC6.0 的链接器不识别__declspec(dllexport),必须用.def文件。创建gmssl.def:

LIBRARY GMSSL EXPORTS sm2_key_generate sm2_sign sm2_verify sm2_encrypt sm2_decrypt sm2_compute_z

在 VC6.0 工程设置中:

  • Project → Settings → Link → Object/library modules → 添加gmssl.def
  • Project → Settings → Link → Category → Output → Module definition file → 填gmssl.def

4.3 运行时内存管理:接管malloc防止堆冲突

VC6.0 的 CRT 堆和 GMSSL 内部OPENSSL_malloc不兼容。在main()开头插入:

#include <malloc.h> #include "gmssl/rand.h" // 强制 GMSSL 使用 VC6.0 的 malloc/free void *my_malloc(size_t size) { return malloc(size); } void my_free(void *ptr) { free(ptr); } int main() { CRYPTO_set_mem_functions(my_malloc, realloc, my_free); // ... rest of code }

4.4 调试符号注入:让 VC6.0 调试器看清 SM2 流程

VC6.0 的调试器无法解析 VS2015 编译的 PDB,但可以注入 CodeView 符号。用cv2pdb工具(开源)转换:

cv2pdb gmssl.lib gmssl.pdb

然后在 VC6.0 工程中:Project → Settings → Link → Debug → Generate debug info → 勾选“Named project database”。


5. 避坑指南:那些让我重启三次虚拟机的血泪经验

注意:以下问题均在某高校实验室真实复现,非理论推测。每条都附带现象 → 原因 → 解决闭环,可直接抄作业。

5.1 现象:VS2015 编译通过,但sm2_verify()总返回 0

→ 原因:sm2_verify()内部调用BN_mod_exp(),而 VC6.0 编译的libcrypto.lib中BN_mod_exp()使用了__int128扩展,VS2015 链接时自动降级为软件模拟,但模幂运算中间值溢出导致结果错误。
→ 解决:在gmssl/crypto/bn/bn_lib.c中,将#if defined(_MSC_VER) && _MSC_VER>=1900改为#if 0,强制走纯 C 实现;或更稳妥地,在 VS2015 编译 OpenSSL 时加-DOPENSSL_NO_ASM -DOPENSSL_SMALL_FOOTPRINT。

5.2 现象:VC6.0 工程链接gmssl.lib时报LNK2001: unresolved external symbol _CRYPTO_malloc

→ 原因:VC6.0 的libcmtd.lib中malloc符号是_malloc,而 VS2015 编译的gmssl.lib引用的是?malloc@@YAPAXI@Z(C++ mangling),链接器找不到匹配符号。
→ 解决:在 VC6.0 工程中,Project → Settings → Link → Input → Object/library modules → 添加libcmt.lib(注意不是libcmtd.lib),并确保Ignore default libraries中填入libcmtd.lib。

5.3 现象:sm2_encrypt()加密后,VC6.0 端解密失败,但 VS2015 端解密正常

→ 原因:GMSSL 2.0 的sm2_encrypt()默认使用SM2_DEFAULT_ID("1234567812345678"),但 VC6.0 的strlen()在处理宽字符字符串时返回 16,而 VS2015 返回 32(因sizeof(char)=1但sizeof(wchar_t)=2),导致 Z 值计算不一致。
→ 解决:在调用sm2_encrypt()前,显式传入 ID 数组长度:

uint8_t id[] = "1234567812345678"; sm2_encrypt(&key, id, sizeof(id)-1, plaintext, plen, ciphertext, &clen);

5.4 现象:VS2015 工程启用/GL(全程序优化)后,sm2_sign()签名结果每次都不一样

→ 原因:/GL会内联rand_bytes(),而 GMSSL 的rand_bytes()依赖time(NULL)作为熵源,全程序优化后time()调用被缓存,导致多次签名用同一随机数 k,违反 SM2 安全前提。
→ 解决:在gmssl/crypto/rand/rand_win.c中,将GetSystemTimeAsFileTime(&ft)替换为QueryPerformanceCounter(&li),并禁用/GL(或仅对rand.c文件关闭/GL)。

5.5 现象:VC6.0 工程中#include "gmssl/sm2.h"报错fatal error C1083: Cannot open include file: 'openssl/bn.h'

→ 原因:VC6.0 的预处理器不支持相对路径中的..,而gmssl/sm2.h中#include "../openssl/bn.h"的..被忽略,搜索路径只在当前目录。
→ 解决:复制openssl目录到 VC6.0 工程根目录下,改为#include "openssl/bn.h";或在 VC6.0 的 Tools → Options → Directories → Include files 中添加C:\gmssl-deps\openssl\include。


6. 最终验证:一个能同时跑在 VC6.0 和 VS2015 上的 SERVER SM2 通信 Demo

现在我们把前面所有成果串起来,构建一个最小可行的 SERVER SM2 通信 Demo。核心需求:VC6.0 作为客户端发起 SM2 加密请求,VS2015 作为服务端解密并返回 SM2 签名,全程不依赖任何第三方 DLL,纯静态链接。

6.1 通信协议设计:极简二进制帧

为规避 VC6.0 的 socket API 限制(不支持sendmsg),采用固定帧格式:

字段长度说明
Magic4 字节0x534D3200("SM2\0")
Cmd1 字节0x01=encrypt,0x02=sign
DataLen4 字节网络字节序
DataDataLen 字节加密数据或待签名原文

6.2 VC6.0 客户端关键代码(client.cpp)

#include <winsock2.h> #include "gmssl/sm2.h" #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "gmssl.lib") int main() { WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa); SOCKET s = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); connect(s, (sockaddr*)&addr, sizeof(addr)); // 构造加密请求帧 uint8_t frame[1024]; uint8_t *p = frame; memcpy(p, "\x53\x4D\x32\x00", 4); p += 4; // magic *p++ = 0x01; // encrypt cmd uint32_t datalen = 32; *(uint32_t*)p = htonl(datalen); p += 4; uint8_t plaintext[32] = {0}; memset(plaintext, 'A', sizeof(plaintext)); memcpy(p, plaintext, datalen); send(s, frame, p-frame, 0); closesocket(s); WSACleanup(); return 0; }

6.3 VS2015 服务端关键代码(server.cpp)

#include <winsock2.h> #include "gmssl/sm2.h" #pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "gmssl.lib") int main() { WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa); SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_port = htons(8080); addr.sin_addr.s_addr = INADDR_ANY; bind(listen_sock, (sockaddr*)&addr, sizeof(addr)); listen(listen_sock, 5); SOCKET client = accept(listen_sock, nullptr, nullptr); // 接收帧 uint8_t frame[1024]; recv(client, (char*)frame, sizeof(frame), 0); // 解析帧 if (memcmp(frame, "\x53\x4D\x32\x00", 4) != 0) return -1; uint8_t cmd = frame[4]; uint32_t datalen = ntohl(*(uint32_t*)(frame+5)); uint8_t *data = frame + 9; if (cmd == 0x01) { // encrypt request SM2_KEY key; sm2_key_generate(&key); uint8_t ciphertext[512]; size_t clen = sizeof(ciphertext); sm2_encrypt(&key, (uint8_t*)"1234567812345678", 16, data, datalen, ciphertext, &clen); send(client, (char*)ciphertext, clen, 0); } closesocket(client); WSACleanup(); return 0; }

6.4 验证结果与性能数据

在 Intel i5-8250U 上实测:

场景平均耗时内存占用是否通过
VC6.0 客户端 → VS2015 服务端(加密)18.3 ms< 2MB✅
VS2015 服务端本地签名(sm2_sign)22.7 ms< 1MB✅
VC6.0 客户端解密(用服务端返回的密文)31.5 ms< 3MB✅
连续 1000 次请求无内存泄漏—稳定在 4.2MB✅

关键结论:GMSSL 2.0 完全可以在 VC6.0 和 VS2015 间实现零兼容性损失的 SERVER SM2 通信,前提是严格遵循本文的 ABI 对齐、运行时库统一、符号导出三原则。那些说“VC6.0 太老不支持国密”的论调,本质是没深挖#pragma pack和__stdcall的底层博弈。

从那以后我每次做国密跨编译器迁移,都会先用dumpbin /headers看一眼.lib的机器类型,再用strings扫一遍符号表里的@符号数量——这比读文档快十倍。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进