简介面向 Qt5.12.2 与 VS2015 开发者的 OpenSSL 1.0.2p 预编译成品包直接提供静态库、动态库与配套头文件省去在 Windows 下自行配置 nmake、指定编译器路径与编译选项的繁琐过程可用于为桌面应用补上 HTTPS、SMTPS 等加密通信能力。压缩包共 173 个文件、约 6.23MB其中 150 个 .h 头文件涵盖 ssl、rsa 等接口声明14 个 .dll 与 4 个 .lib 分别对应运行期加载与编译期链接另有 openssl.cnf 配置文件及少量可执行工具usr 与 usr1 两套目录按 include、lib 结构组织便于按项目需要挑选链接方式。目前已有 605 人学习下载。对于不想在编译环节反复试错的开发者可直接对照目录取用库与头文件快速完成工程包含路径、库目录与链接器选项的配置把精力留给业务逻辑与联调验证。1. 老项目里最难解决的一环OpenSSL 1.0.2p 预编译包能解决哪类场景前阵子帮人排查一个 2015 年前后的上位机程序源码里一行#include openssl/rsa.h就让整条构建流程卡了三天。开发机只有旧版 IDEPerl 是老版本汇编器版本又对不上照着构建文档敲到配置脚本那一步就报错换了两台机器结果一样。这种时候一份 openssl-1.0.2p 编译好的静态库、动态库加头文件包直接从折腾构建环境降到配三个路径。它真正适配的场景很具体老编译器需要补加密能力、对接方只认 TLS 1.0/1.1 的老接口、离线内网工具不方便现场编库。但它是 1.0.2 分支API 跟 1.1.1 差一大截照着新教程抄代码必然失败所以下面按我实际拆包的顺序写先验收再接入。2. 解压后先别写代码目录、命名与版本号三处对账拿到 rar 的第一反应往往是双击解压然后立刻#include这一步最容易出问题。预编译包最常见的失败不是代码写错而是头文件、导入库、DLL 三者不同源——头文件是 1.0.2p 的libeay32.lib却是别人从 1.0.2d 里抠出来的编译器不报错链接器不报错运行起来在某个EVP_*调用上直接崩溃。所以拆包后的前十分钟我一般全部花在核对文件上。2.1 include、.lib、.dll 三种文件分别在哪个阶段生效先把包里的东西按生效阶段分堆比按文件夹分堆有用得多。路径/文件形态生效阶段说明include/openssl/*.h头文件编译期提供函数声明、结构体定义与OPENSSL_VERSION_NUMBER宏libeay32.lib导入库或静态库链接期crypto 模块摘要、加解密、大数、随机数ssleay32.lib导入库或静态库链接期ssl 模块握手、会话、证书链libeay32.dll/ssleay32.dll动态库运行期只有动态链接方式才需要随 exe 一起部署openssl.exe部分包带命令行工具自测/运维生成自签证书、验证算法可用性、看版本openssl.cnf部分包带配置文件运行期命令行工具和部分SSL_CTX默认行为会读它这里有个细节值得记一下看到libeay32和ssleay32这两个名字基本可以断定是 1.0.x 及更早的产物libeay来自早期作者 Eric A. Young 的命名遗留。1.1.0 之后官方把两个库合并重命名为libcrypto和libssl并且把结构体全部改成不透明指针。所以你在网上搜到的教程里如果出现libcrypto.lib跟手里这个包是两套东西配置项不能照抄。还有一个容易误判的点.lib后缀既可能是导入库也可能是静态库名字完全一样。区分办法很直接——导入库只有几十 KB里面基本只有一个跳转桩静态库通常是几 MB包含一堆目标文件。REM 列出 lib 里包含的目标文件出现大量 .obj 就是静态库 lib /list libeay32.lib | findstr /i .obj REM 导入库几乎没有目标文件或者只有一个 dll 的桩 lib /list libeay32.lib如果lib /list输出了一长串cryptlib.obj、mem.obj、aes_core.obj之类那这就是静态库链接之后不需要再带 DLL如果几乎没输出那它是导入库运行时必须有对应的 DLL 在场。这一步判断错后面部署阶段一定会出现我这台机器能跑客户机器上闪退的经典事故。2.2 用 dumpbin 和版本号把这份包验明正身include/openssl/opensslv.h里定义了OPENSSL_VERSION_NUMBER和OPENSSL_VERSION_TEXT前者是编译期常量后者是字符串。而运行期库内部也保存了一份数字版本1.0.2 里通过SSLeay()取出来。这两个值必须相等不相等就说明头文件和库不是同一份源码编出来的。/* version_check.c —— 十行代码完成头文件/库一致性体检 */ #include stdio.h #include openssl/opensslv.h #include openssl/crypto.h int main(void) { unsigned long hdr (unsigned long)OPENSSL_VERSION_NUMBER; /* 编译期来自头文件 */ unsigned long bin (unsigned long)SSLeay(); /* 运行期来自 .lib/.dll */ printf(header : 0x%08lx %s\n, hdr, OPENSSL_VERSION_TEXT); printf(binary : 0x%08lx %s\n, bin, SSLeay_version(SSLEAY_VERSION)); if (hdr ! bin) { printf([MISMATCH] 头文件与库不同源别往下走了\n); return 1; } printf([OK] 版本一致\n); return 0; }SSLeay()在 1.0.2 里返回的就是打包进库的数字版本SSLeay_version(SSLEAY_VERSION)返回完整版本字符串。注意 1.1.0 之后这两个函数被改名成OpenSSL_version_num()和OpenSSL_version()所以这段代码在 1.1.1 的头文件下编不过——这不是 bug是版本边界。参数上没什么可调的SSLEAY_VERSION换成SSLEAY_CFLAGS或SSLEAY_DIR可以看到编译选项和构建目录排查这个库到底开了哪些开关时挺有用。顺带用dumpbin查一下位数和导出符号这两条命令在没装 VS 的机器上可能找不到需要在开发者命令提示符里跑# 看目标机器类型14C 是 x868664 是 x64 dumpbin /headers libeay32.lib | findstr /i machine # 看导出符号是否存在动态库方式 dumpbin /exports libeay32.dll | findstr /i SSLeay_version RAND_bytes EVP_EncryptInit_ex # 看这个 lib 默认依赖哪个运行库 dumpbin /directives libeay32.lib | findstr /i DEFAULTLIB/directives这一条特别关键输出里如果出现DEFAULTLIB:MSVCRT说明是/MD编的出现DEFAULTLIB:LIBCMT说明是/MT编的。这个信息直接决定你工程里运行库选项怎么选选错了会撞上LNK2038后面第 5 章会专门讲。2.3 位数、运行库、调用约定三项必须一致预编译包最容易出错的地方就在这三个维度上而且报错信息往往指向别处让人排查半天。维度怎么查不匹配时的典型症状位数 x86/x64dumpbin /headers里的 machine 字段LNK1112或运行时报0xc000007b运行库 /MD 与 /MTdumpbin /directives里的 DEFAULTLIBLNK2038、LNK2005、堆破坏随机崩调用约定dumpbin /exports看导出名有无修饰链接能过一调用就栈失衡崩调用约定这一项1.0.2 的 Windows 官方构建默认是__cdecl导出符号名不带后缀。有些第三方重新打包时会改成__stdcall导出名变成_RAND_bytes8这种形式。你如果看到带的修饰名说明整个工程的函数指针声明都得跟着改不然调用瞬间栈就乱了。我的做法是拿到包先跑一遍dumpbin /exports确认导出名是干净的RAND_bytes而不是_RAND_bytes8再决定能不能直接用。3. 工程接入五项设置、三个系统库和一个五分钟验证程序核对完之后进入接入环节。这一步我不想写成打开属性页点这里的流水账因为不同 IDE 版本路径不一样写清楚为什么改更有价值。下面先用命令行把整条链路跑通再映射回 IDE 的属性页这样无论你用什么版本的开发环境都能对上号。3.1 真正要改的五个地方命令行编译能一次把包含目录、库目录、依赖库三件事说清楚cl /nologo /EHsc /MD /W3 ^ /IE:\pkg\openssl-1.0.2p\include ^ version_check.c ^ /Fe:version_check.exe ^ /link /LIBPATH:E:\pkg\openssl-1.0.2p\lib\x64 ^ libeay32.lib ssleay32.lib ^ ws2_32.lib crypt32.lib user32.lib gdi32.lib advapi32.lib逐项拆开看。/I指向头文件根目录注意是include这一层不是include/openssl因为源码里写的是#include openssl/rsa.h预处理器会自己拼上openssl/这一段。/LIBPATH指向具体位数的子目录x64和x86千万别混。/MD是运行库选项必须跟库的编译方式一致前面dumpbin /directives查出来是什么就填什么。libeay32.lib和ssleay32.lib是两个必填项只加前者的话一旦代码里碰到SSL_CTX_new之类的 SSL 接口就会报未解析符号。后面那五个系统库是 Windows 上的隐式依赖ws2_32.lib提供 socketcrypt32.lib提供系统证书存储访问user32.lib和gdi32.lib在部分随机数熵源采集路径上会被用到advapi32.lib涉及注册表和权限查询。静态链接时这几个库是硬需求动态链接时虽然 DLL 内部已经链接好了但某些情况下仍然要补加上不会有坏处。映射到 IDE 属性页就是五处C/C 的附加包含目录、链接器的附加库目录、链接器的附加依赖项、调试选项卡里的环境变量PATH把 DLL 目录塞进去省得手动拷贝、以及 C/C 代码生成里的运行库选项。第五项最容易被忘也最容易引发第 5 章要讲的那类崩溃。3.2 链接顺序与静态库的符号解析链接器解析符号是单向扫描的顺序错了就报未解析。规则很简单调用方在前被调用方在后。你自己的main.obj依赖libeay32.liblibeay32.lib又依赖ws2_32.lib所以顺序是main.obj → libeay32.lib → ssleay32.lib → 系统库。REM 顺序错了会报 LNK2019虽然符号明明在库里 cl main.c /link libeay32.lib main.obj REM 错误lib 写在了 obj 前面 REM 正确写法交给编译器驱动它会按依赖排序 cl main.c /link libeay32.lib ssleay32.lib ws2_32.lib crypt32.lib如果确实遇到循环依赖比如两个静态库互相引用可以在链接命令行里把库名重复写一遍或者在 IDE 里勾选链接器 → 常规 → 使用库依赖项输入。这个开关会让链接器反复扫描直到符号稳定代价是链接时间变长几千个符号的工程大概多几秒可以接受。另外提一句NODEFAULTLIB。有些老工程为了减小体积会在链接选项里塞/NODEFAULTLIB:MSVCRT这在用动态库方式时可能没事但静态链接 OpenSSL 时会直接炸——静态库里有一堆对 CRT 的引用被你排掉了就全变成未解析符号。看到LNK2019里出现_memcpy、_malloc这类最基础的符号先回头看看是不是自己排掉了默认库。3.3 最小验证程序版本 一条 SHA-256接入完成不能只看编译通过得跑一次真实的加密运算才算数。下面这段代码同时验证三件事库能加载、EVP 抽象层能用、摘要结果正确。/* smoke_test.c —— 编译通过只是及格跑出正确摘要才算接入成功 */ #include stdio.h #include string.h #include openssl/evp.h #include openssl/err.h #include openssl/crypto.h static void dump_hex(const unsigned char *buf, unsigned int len) { unsigned int i; for (i 0; i len; i) printf(%02x, buf[i]); printf(\n); } int main(void) { unsigned char md[EVP_MAX_MD_SIZE]; unsigned int md_len 0; EVP_MD_CTX *ctx; /* 1.0.2 必须显式初始化算法表和错误字符串表 */ OpenSSL_add_all_digests(); ERR_load_crypto_strings(); ctx EVP_MD_CTX_create(); /* 1.0.2 的写法1.1.0 起改为 EVP_MD_CTX_new */ if (ctx NULL) { printf(ctx alloc failed\n); return 1; } if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) ! 1) goto fail; if (EVP_DigestUpdate(ctx, abc, 3) ! 1) goto fail; if (EVP_DigestFinal_ex(ctx, md, md_len) ! 1) goto fail; dump_hex(md, md_len); /* 期望ba7816bf8f01cfea414140de5dae2223... */ printf(%s\n, SSLeay_version(SSLEAY_VERSION)); EVP_MD_CTX_destroy(ctx); ERR_free_strings(); return 0; fail: printf(openssl error: %s\n, ERR_error_string(ERR_get_error(), NULL)); EVP_MD_CTX_destroy(ctx); return 1; }EVP_MD_CTX_create和EVP_MD_CTX_destroy是 1.0.2 的接口1.1.0 之后换成了_new和_free两者语义一样只是改名。EVP_MAX_MD_SIZE是宏64 字节够放下最长的摘要输出。md_len是出参实际长度由算法决定SHA-256 固定 32 字节不要硬编码。把这段跑通说明头文件、导入库、DLL 三者是配套的运行库也对得上。我一般还会故意把libeay32.dll改名跑一次确认报的是找不到 DLL而不是别的错这样部署清单心里就有数了。4. 常用接口落地摘要、AES-CBC、RSA 与随机数在 1.0.2 里的写法接入验证过了接下来是真正要用到的功能。1.0.2 的 API 风格跟 1.1.1 差别很大尤其是要不要手动初始化和结构体能不能直接访问这两点照着新文档写必然失败。下面按使用频率从高到低排每个都给完整可编译的片段。4.1 摘要与全局初始化1.0.2 必须先加载算法表1.0.2 有一个 1.1.0 之后被取消的设计算法表需要手动加载。如果不调用OpenSSL_add_all_algorithms()用EVP_get_digestbyname(sha256)按名字查算法会返回 NULL而直接传EVP_sha256()反而没事——因为后者是编译期就绑定的常量。这个不一致性坑过很多人。#include openssl/evp.h #include openssl/err.h void crypto_global_init(void) { OpenSSL_add_all_algorithms(); /* 加载全部算法最省事 */ /* 或者按需加载体积敏感时用 OpenSSL_add_all_digests(); OpenSSL_add_all_ciphers(); */ ERR_load_crypto_strings(); /* 让 ERR_error_string 能输出可读文本 */ } void crypto_global_cleanup(void) { EVP_cleanup(); ERR_free_strings(); }OpenSSL_add_all_algorithms()是个宏内部同时加载摘要和对称算法如果只用到哈希用OpenSSL_add_all_digests()可以减少内存占用但实际差别不大。ERR_load_crypto_strings()一定要调否则出错时ERR_error_string返回的是 error:1408F10B:lib(20):func(143):reason(267) 这种纯数字排查时只能猜。多线程场景下1.0.2 还需要注册锁回调否则并发调用会出现随机崩溃。这个话题在第 5 章会展开这里只需要知道它是 1.0.2 特有的要求。4.2 AES-256-CBCIV、填充和错误栈对称加密是使用频率最高的功能也是最容易写错的地方。1.0.2 的EVP_CIPHER_CTX已经可以通过_new和_free动态管理这点跟 1.1.0 一致但EVP_CIPHER_CTX_init和EVP_CIPHER_CTX_cleanup那套老写法仍然有效。#include string.h #include openssl/evp.h #include openssl/rand.h #include openssl/err.h /* 返回密文长度失败返回 -1out 缓冲区需 inlen 32 */ int aes256_cbc_encrypt(const unsigned char *key, /* 32 字节 */ const unsigned char *iv, /* 16 字节 */ const unsigned char *in, int inlen, unsigned char *out) { EVP_CIPHER_CTX *ctx EVP_CIPHER_CTX_new(); int len 0, total 0; if (ctx NULL) return -1; if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv) ! 1) goto fail; if (EVP_EncryptUpdate(ctx, out, len, in, inlen) ! 1) goto fail; total len; if (EVP_EncryptFinal_ex(ctx, out total, len) ! 1) goto fail; total len; EVP_CIPHER_CTX_free(ctx); return total; fail: printf(aes encrypt failed: %s\n, ERR_error_string(ERR_get_error(), NULL)); EVP_CIPHER_CTX_free(ctx); return -1; }几个参数必须对上EVP_aes_256_cbc()要求 key 恰好 32 字节、iv 恰好 16 字节传错长度不会报错但结果不对——这是最隐蔽的一类 bug。可以用EVP_CIPHER_key_length(EVP_aes_256_cbc())在运行期查一次跟自己的常量比一下比事后对着密文发呆强。输出缓冲区要预留inlen 32。CBC 是分组加密PKCS#7 填充最多加一整块密文长度是(inlen / 16 1) * 16加上 32 字节余量足够不做这个预留就是典型的栈溢出。解密方向对称把EVP_EncryptInit_ex换成EVP_DecryptInit_exUpdate/Final同理。注意EVP_DecryptFinal_ex返回值必须检查它返回 0 说明填充校验失败可能是密钥错、也可能是密文被篡改。这一步不检查就等于放弃了完整性验证。4.3 RSA 密钥生成与 PEM 读写1.0.2 里RSA是个完全公开的结构体你可以直接读rsa-n、rsa-e这在 1.1.0 之后被彻底封死改成了不透明指针。这个差异导致同一份代码在两个版本间完全不兼容迁移时是主要工作量。#include openssl/rsa.h #include openssl/pem.h #include openssl/bn.h int rsa_gen_and_save(const char *priv_path, const char *pub_path) { RSA *rsa NULL; BIGNUM *e NULL; BIO *bp NULL; int ok -1; rsa RSA_new(); e BN_new(); if (!rsa || !e) goto done; BN_set_word(e, RSA_F4); /* 65537标准公钥指数 */ if (RSA_generate_key_ex(rsa, 2048, e, NULL) ! 1) goto done; /* 私钥PKCS#1 格式PEM 编码 */ bp BIO_new_file(priv_path, w); if (!bp) goto done; if (PEM_write_bio_RSAPrivateKey(bp, rsa, NULL, NULL, 0, NULL, NULL) ! 1) goto done; BIO_free(bp); bp NULL; /* 公钥SubjectPublicKeyInfo 格式带算法标识 */ bp BIO_new_file(pub_path, w); if (!bp) goto done; if (PEM_write_bio_RSA_PUBKEY(bp, rsa) ! 1) goto done; ok 0; done: if (bp) BIO_free(bp); if (e) BN_free(e); if (rsa) RSA_free(rsa); return ok; }RSA_F4就是 65537 的宏定义别用 3虽然数学上能用但现代实现都会拒绝。RSA_generate_key_ex的第四参数是进度回调传 NULL 表示不关心进度2048 位密钥在普通机器上大概几十毫秒到几百毫秒可以接受同步阻塞。PEM_write_bio_RSAPrivateKey的第三个参数是加密算法传 NULL 表示私钥不加密。生产环境当然应该传EVP_aes_256_cbc()并给密码但要注意这样产出的私钥文件某些老工具可能读不了——这是兼容性和安全性的取舍得看对接方。公钥那一步要用PEM_write_bio_RSA_PUBKEY而不是PEM_write_bio_RSAPublicKey。前者输出的是标准 SPKI 格式以-----BEGIN PUBLIC KEY-----开头后者是 PKCS#1 格式以-----BEGIN RSA PUBLIC KEY-----开头。这两个格式长得像但互不兼容把后者给对接方对方八成会回一句公钥解析失败。4.4 随机数与 Base64两个被低估的基础件随机数是所有密钥材料的源头用错地方比加密算法选错更致命。1.0.2 里正确接口是RAND_bytes它在无法获取足够熵时返回 0 而不是给个弱随机返回值必须检查。#include openssl/rand.h #include openssl/evp.h int gen_token(unsigned char *buf, int len) { if (RAND_bytes(buf, len) ! 1) { /* 返回 1 才算成功 */ return -1; } return 0; } /* Base64 编码out 需要 4 * ((len 2) / 3) 1 字节 */ int b64_encode(const unsigned char *in, int len, char *out) { int n EVP_EncodeBlock((unsigned char *)out, in, len); if (n 0) return -1; out[n] \0; return n; } /* Base64 解码返回值含填充长度要自己修正 */ int b64_decode(const char *in, int len, unsigned char *out) { int n EVP_DecodeBlock(out, (const unsigned char *)in, len); if (n 0) return -1; while (len 0 in[len - 1] ) { n--; len--; } /* 减掉填充 */ return n; }RAND_pseudo_bytes不要用它在 1.1.0 里被标记为废弃语义上也弱于RAND_bytes。EVP_DecodeBlock的返回值包含了填充对应的字节数不减掉的话解出来的缓冲区尾部会多出零字节做二进制协议解析时会莫名其妙失败——这个坑我在解析一个老协议时踩过查了两小时才发现是解码长度没修正。5. 避坑排查五个把构建和运行卡死的现场这一章全是真实踩过的。每一条按现象 → 原因 → 解决写你遇到类似症状可以直接对照。5.1 链接期的两个死结第一条LNK2019无法解析的外部符号但符号明明在库里。现象代码里调RSA_generate_key_ex编译通过链接报error LNK2019: 无法解析的外部符号 _RSA_generate_key_ex。用dumpbin /exports查libeay32.dll那个符号清清楚楚在那儿。原因通常有两个。一是位数不匹配工程是 x64库目录却指到了lib/x86此时链接器报的是LNK1112: 模块计算机类型x86与目标计算机类型x64冲突但如果库里恰好有同名符号的 32 位版本报错会退化成LNK2019。二是只加了libeay32.lib而这个符号其实在ssleay32.lib里——虽然RSA_*属于 crypto但某些重新打包的版本会做符号拆分边界没那么干净。解决先跑dumpbin /headers libeay32.lib | findstr machine确认位数再跑dumpbin /exports libeay32.dll或dumpbin /symbols静态库用这个确认符号归属最后检查附加依赖项里两个.lib是不是都在。三步走下来基本能定位。第二条LNK2038运行库不匹配或者LNK2005符号重复定义。现象一堆LNK2005: _malloc 已经在 libcmt.lib 中定义或者LNK2038: 检测到RuntimeLibrary的不匹配项: 值MT_StaticRelease不匹配值MD_DynamicRelease。原因静态库是用/MT编的你的工程用/MD两套 CRT 同时被拉进来符号自然重复。反过来也一样。这个错误在混用第三方库时特别常见因为预编译包往往只提供一种运行库配置。解决先用dumpbin /directives libeay32.lib | findstr DEFAULTLIB查清库用的是什么。输出MSVCRT就是/MD输出LIBCMT就是/MT。然后统一工程设置属性页 → C/C → 代码生成 → 运行库改成一致的值。如果工程里其他依赖库坚持另一种配置那就只能重新编一份 OpenSSL或者改用动态库方式——导入库没有 CRT 依赖这个坑自动绕过。这也是我后来优先选动态链接的原因之一。5.2 运行期的三个冷箭第三条编译链接全过双击 exe 报0xc000007b。现象开发机上跑得好好的拷到测试机上立刻弹出应用程序无法正常启动 (0xc000007b)。原因0xc000007b是STATUS_INVALID_IMAGE_FORMAT翻译成人话就是加载了一个位数不对的 DLL。32 位 exe 加载了 64 位的libeay32.dll或者反过来。另一种可能是 DLL 根本没找到但 Windows 的加载器在搜索路径里翻到了一个同名的旧版本。解决用dumpbin /dependents your.exe列出依赖再用dumpbin /headers your.exe | findstr machine确认 exe 位数然后逐个核对依赖 DLL 的位数。部署时把libeay32.dll、ssleay32.dll放在 exe 同目录这是加载器优先级最高的位置比塞PATH和System32都可靠。千万别往System32里放那是最经典的装了新版把全机器老程序搞崩的操作。第四条单线程测试全绿多线程跑一段时间随机崩。现象压测跑几分钟,进程收到访问违例崩栈的位置每次都不一样有时候在ERR_get_error里有时候在CRYPTO_malloc里。原因1.0.2 不是默认线程安全的。它内部有一堆全局表错误队列、算法表、内存分配钩子并发访问需要外部提供锁。1.1.0 之后官方内置了线程 ID 和锁管理所以这个问题在新版本里消失了导致很多从 1.1.1 迁移过来的代码不知道要处理。解决在程序启动、创建任何线程之前注册回调。#include openssl/crypto.h #include windows.h static CRITICAL_SECTION *g_locks; static void locking_cb(int mode, int n, const char *file, int line) { (void)file; (void)line; if (mode CRYPTO_LOCK) EnterCriticalSection(g_locks[n]); else LeaveCriticalSection(g_locks[n]); } static void threadid_cb(CRYPTO_THREADID *id) { CRYPTO_THREADID_set_numeric(id, (unsigned long)GetCurrentThreadId()); } void openssl_thread_setup(void) { int i, n CRYPTO p a hrefhttps://download.csdn.net/download/weixin_38911698/21931203 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
阅读完成 · 觉得有帮助?