资讯详情

资讯详情

iOS交叉编译OpenSSL 1.0.2o静态库:从Configure到lipo全攻略

简介为iOS平台预编译的OpenSSL 1.0.2o静态库内含libcrypto.a与libssl.a两个核心库分别提供加密算法和SSL/TLS协议实现面向需要深度定制安全通信功能的中高级iOS开发者。整包共79个文件以OpenSSL头文件.h为主配合两个静态库文件压缩后约6.61MB体量精简。压缩包内的include目录提供完整API接口头文件lib_universal目录则按架构聚合库文件便于在真机与模拟器之间选取或合并也方便查看和调用加密接口。静态库方式可避免运行时依赖缺失与版本冲突提升应用稳定性1.0.2o版本成熟稳定既可作为项目的加密基础组件也可作为研究SSL/TLS在iOS集成方式的参考。目前已有574人学习下载适合需要为App快速补齐HTTPS、证书校验或数据加密存储能力的开发场景。1. 想在 iOS 上拿到 OpenSSL 1.0.2o 静态库先想清楚这三件事拿到这个标题的人多半不是来学 OpenSSL而是被一个带加密需求的老 iOS 工程卡住了。工程里调用的还是 1.0.2 时代的 EVP、PEM、BIO 接口升级到 1.1.1 或 3.0 要改大量代码可是 OpenSSL 官方并不提供 iOS 平台编译好的 libcrypto.a 和 libssl.a社区里流传的包又经常混着模拟器架构真机一跑就崩。这个任务的本质不是去“下载一个库”而是用 iOS 的 SDK 和编译器把 openssl-1.0.2o 源码交叉编译成静态库再按目标架构合并。要做完这件事得同时想清楚三件事选哪个版本的源码、交给哪个 configure 目标、最后怎么用 lipo 合并。选 1.0.2o 是为了保住老接口行为交叉编译的关键是让 OpenSSL 的构建系统认得出 iOS 工具链合并则是把 armv7 和 arm64 两个独立产物合成一个 fat 静态库。把这三个点串起来自己编出的 .a 文件才会比网上随便找的靠谱。这件事适合正在维护老项目、准备把 OpenSSL 静态链接进 iOS 二进制的开发者模拟器、真机、32 位、64 位的边界会在后面逐一说清。2. 为什么是 libcrypto.a 和 libssl.a从 1.0.2o 到 iOS 静态库的选型逻辑2.1 静态库封装了哪些东西为什么 iOS 老项目都选它在把编译命令跑起来之前先把产物说清楚。libcrypto.a 是加密算法与编码解析的核心里面包括 AES、RSA、SHA、MD5、PEM 证书、DER 解析、EVP 封装这些能力libssl.a 则是 SSL/TLS 协议栈负责握手、会话恢复、证书校验、加密套件协商。两个静态库是分开的但 libssl.a 里几乎所有符号都依赖 libcrypto.a 的底层算法所以后面讲链接顺序时这一点很重要。选择静态库而不是动态库在 iOS 老项目里是一个很务实的决定。静态库在链接阶段把目标文件直接合并进可执行文件App 启动时不需要去沙盒里找动态库也就少了一类运行时报错。iOS 开发者工具的打包和签名流程对静态库也更友好依赖关系在编译期就固定不会出现主程序和库之间因为头文件版本不一致导致的结构体对齐问题。代价是每个 CPU 架构都要单独产出 .a再用 lipo 合成一个胖文件真机才能同时照顾 armv7 和 arm64。有些开发者会觉得可以直接用系统自带的加密框架比如 Security 和 CommonCrypto但对老工程来说代码里写满的是SSL_CTX_new、EVP_DigestInit_ex、PEM_read_bio_PrivateKey这类 OpenSSL 专用接口重写一遍的成本远高于编一个静态库。还有一批自研协议基于 OpenSSL 的 BIO 抽象层做 socket 封装换底层加密库等于动整个通信模块。所以从兼容性和改动范围看1.0.2o 的静态库是风险最小的选择。对比项静态库方案系统 Security接口覆盖OpenSSL 1.0.2o 全量 API仅部分算法和协议能力老代码迁移成本基本不动调用层需要重写二进制体积每个架构增加约 1 到 2 MB系统内置运行期风险编译期绑定稳定依赖系统版本行为2.2 1.0.2o 的接口特性老版本 API 和 iOS 交叉编译的对应关系1.0.2o 是 1.0.2 系列补丁版本里的一个后期包接口形状还是 1.0.2 那一套但这并不代表它落后到不可用。很多老协议比如 TLS 1.0、TLS 1.1、自定义私钥格式恰恰是在这一代 API 上写出来的。1.0.2o 修复了前面若干个小版本里的注入问题、BIO 释放、证书解析等安全问题同时没有像 1.1.0 那样改变底层结构体布局。对维护老代码的团队来说升级到 1.1.1 或 3.0 不只是改一行版本号SSL_CTX、EVP_MD_CTX的创建和销毁函数、RAND_seed的调用位置全都变了第三方库也要跟着调整。在 iOS 上编译它实际上是在用 OpenSSL 自己的Configure脚本生成 Makefile而不是靠系统工具链自动识别。1.0.2o 的 Perl 配置脚本里有一批交叉编译 target针对 32 位 iOS 的叫ios-cross针对 64 位 iOS 的叫ios64-cross。这两个 target 会使用环境变量CROSS_TOP和CROSS_SDK去拼装编译器路径和 SDK 路径。理解这一点后编译过程就不再是玄学configure 负责选择平台规则make 负责调用编译器和归档工具最后 lipo 把不同架构合并。还需要做一个关键取舍关掉汇编。iOS 真机的 armv7、arm64 上 OpenSSL 原本有汇编优化但旧版本汇编代码对现代编译器的兼容性并不好最常见的问题是字节对齐、隐式内存操作数和新版汇编器语法不匹配。用no-asm虽然会让部分算法慢一点但换来的是稳定的编译过程和可复现的产物。在客户端网络请求场景里这点性能损耗通常不值得用编译翻车去赌。同样no-shared让 configure 只生成 .a从源头上避免生成动态库后还要处理安装路径等一系列问题。关于架构iOS 设备从早期到 64 位时代App 要么只支持 arm64要么要做低版本兼容 armv7。如果只编 arm64老设备装不上如果只编 armv7新设备虽然能跑但性能差。所以最稳妥的做法是两套都编出来再用 lipo 合并。模拟器架构则单独理不要混进真机包。这就是标题里的“iOS 平台静态库”被拆成 armv7 和 arm64 两个产物的原因。还要注意 OpenSSL 1.0.2o 在 iOS 环境里可能会引用 zlib 的可选接口。如果你的代码里用到压缩相关 BIO链接时就要补-lz。这一点在后面的链接章节会再遇到。到这里可以下结论选 1.0.2o 是为了保住接口选静态库是为了压缩运行期风险configure 里加ios-cross/ios64-cross、no-asm、no-shared则是保证 iOS 真机能稳定编出libcrypto.a和libssl.a的三个前提。3. 命令行交叉编出 armv7/arm64 的 libcrypto.a、libssl.a从 Configure 到 lipo3.1 准备编译环境和目录先把 SDK 路径喂给编译器开始之前先确认开发机已经装了 iOS SDK 和命令行工具。这个过程不会自动完成需要你手动把 SDK 路径变成 OpenSSL 能理解的环境变量。下面这组命令在 macOS 终端里执行核心思路是把每个架构的编译目录分开避免头文件和目标文件互相污染。export BUILD_ROOT$HOME/openssl-ios-1.0.2o mkdir -p $BUILD_ROOT/armv7 $BUILD_ROOT/arm64 \ $BUILD_ROOT/include/openssl $BUILD_ROOT/lib cd $BUILD_ROOT tar zxf openssl-1.0.2o.tar.gz mv openssl-1.0.2o openssl-src这里把源码包解压后改名成openssl-src是为了后续多个架构共用同一份源码。虽然可以在同一个目录里反复make clean但 1.0.2o 生成的头文件在切换架构时容易残留所以用源码目录保持不变、构建输出目录分别隔离的方式最稳。BUILD_ROOT下面的四个目录分别放 32 位产物、64 位产物、公共头文件和最终合并出的 fat 库。接下来导出 iOS 真机 SDK 路径export SDK_ROOT$(xcrun --sdk iphoneos --show-sdk-path) export CROSS_TOP${SDK_ROOT%SDKs/*} export CROSS_SDK${SDK_ROOT##*SDKs/} echo SDK_ROOT$SDK_ROOT echo CROSS_TOP$CROSS_TOP echo CROSS_SDK$CROSS_SDK这里的CROSS_TOP被 OpenSSL 用来定位Developer/Platforms/iPhoneOS.platform/Developer而CROSS_SDK是具体的 SDK 目录名比如iPhoneOS.sdk。用这种 Shell 参数展开的方式从xcrun输出里自动提取路径比把某个固定的开发者工具安装路径写死在脚本里更可靠。执行后建议先核对三行输出如果CROSS_TOP是空或者路径明显不对后面的编译基本都会失败。3.2 用 Configure 生成 Makefile架构、编译器、安装目录一次说清路径准备好后先编译 64 位真机架构。进入源码目录执行 Configure 指定目标。OpenSSL 1.0.2o 有一个 64 位 iOS 专用目标叫ios64-cross它会根据CROSS_TOP和CROSS_SDK拼出编译命令。cd $BUILD_ROOT/openssl-src ./Configure ios64-cross \ --openssldir$BUILD_ROOT/arm64 \ no-asm no-shared enable-deprecated make clean参数说明--openssldir不只是安装目录还会影响后续某些路径判断所以一定要指到对应架构的输出目录no-asm关掉汇编优化规避旧汇编代码与新版 clang 的兼容问题no-shared让构建系统只生成静态库enable-deprecated保留 1.0.2 里已经被标记为废弃的接口老代码通常需要。Configure 完成后生成 Makefile下面执行编译和安装make -j4 make install这里的make -j4是四任务并发编译机器核心更多可以改-j8。make install在 OpenSSL 里并不是装到系统目录而是把产物复制到--openssldir指定的位置所以这一步完成后$BUILD_ROOT/arm64/lib/下会出现libcrypto.a和libssl.a$BUILD_ROOT/arm64/include/openssl/下会出现头文件。可以用下方命令确认文件存在ls -l $BUILD_ROOT/arm64/lib/libcrypto.a ls -l $BUILD_ROOT/arm64/lib/libssl.a正常情况下两个 .a 文件应该在几 MB 到十几 MB 之间。如果看到文件很小基本是编译过程中没有生成有效目标代码需要回头看 configure 是否有报错。有些新版 clang 会被 1.0.2o 的配置脚本判定为未知版本但一般只会打印警告不会中断编译。提示如果 make 时发现编译器没有带-isysroot可以在 configure 前手动导出CC例如把路径换成你本机实际的工具链位置再追加-arch arm64 -isysroot $SDK_ROOT。3.3 在两个架构下分别 make并把头文件和静态库收拢到统一目录64 位编完后开始编 32 位架构。这一步最容易犯的错误是不清理就直接重新 configure。OpenSSL 的 Makefile 不会自动感知架构切换上一轮留下的.o文件会干扰新构建所以必须先make clean。cd $BUILD_ROOT/openssl-src make clean ./Configure ios-cross \ --openssldir$BUILD_ROOT/armv7 \ no-asm no-shared enable-deprecated make -j4 make install这里把 target 从ios64-cross换成ios-cross对应 32 位 armv7。--openssldir指向$BUILD_ROOT/armv7这样 armv7 的库文件和头文件会单独落在自己的目录里不会覆盖 arm64 的产物。ios-cross和ios64-cross的差异集中在Configure里定义的编译器参数和架构标记不需要手工修改源码。两个架构都编完以后需要把对外导出用的头文件统一到一个目录。虽然两个架构生成的公共头在逻辑上应该一致但实际编译时会根据平台生成opensslconf.h这类配置头不要同时拷贝两份。这里用 arm64 的版本作为统一头文件来源mkdir -p $BUILD_ROOT/include/openssl cp $BUILD_ROOT/arm64/include/openssl/*.h $BUILD_ROOT/include/openssl/执行完后$BUILD_ROOT/include/openssl就是后续给 iOS 工程用的完整头文件目录。如果后面链接时报出跟结构体或宏相关的问题可以用 armv7 的头文件再覆盖一次并对比差异但我遇到的大多数情况是 arm64 头文件可以直接统一使用。3.4 用 lipo 合并成通用静态库再用符号表做自检两个架构的静态库都准备好后用 macOS 自带的lipo把它们合并成一个 fat 文件。lipo 只是二进制打包不会重新编译目标代码所以单个架构的 .a 必须是完整可用的。lipo -create \ $BUILD_ROOT/arm64/lib/libcrypto.a \ $BUILD_ROOT/armv7/lib/libcrypto.a \ -output $BUILD_ROOT/lib/libcrypto.a lipo -create \ $BUILD_ROOT/arm64/lib/libssl.a \ $BUILD_ROOT/armv7/lib/libssl.a \ -output $BUILD_ROOT/lib/libssl.a lipo -info $BUILD_ROOT/lib/libcrypto.a lipo -info $BUILD_ROOT/lib/libssl.alipo -info输出里应该同时出现armv7和arm64。如果只看到一个架构说明某个目录下的 .a 没有真正产出或者-create时路径写错。这一步是后面排错的第一个检查点。除了看架构建议再用nm检查关键符号是否在最终库里nm $BUILD_ROOT/lib/libssl.a | grep SSL_library_init | head nm $BUILD_ROOT/lib/libcrypto.a | grep EVP_md5 | headSSL_library_init来自 libssl.aEVP_md5来自 libcrypto.a。如果 grep 没有输出说明对应库是空的或者架构没对齐这时候回头检查编译日志比继续往下集成更省时间。到这里$BUILD_ROOT/lib下的两个 .a 就是可以直接拖进 iOS 工程的最终产物。4. 把静态库接进 iOS 工程导入、链接参数和最小验证 demo4.1 同时导入 libcrypto.a、libssl.a 和头文件工程设置别遗漏拿到两个 .a 之后把它们放进 iOS 工程并不是简单拖进去就结束。静态库不像 framework 那样自带头文件必须手动配搜索路径。常见做法是在 Target 的 Build Phases 里把libcrypto.a和libssl.a加到 Link Binary With Libraries同时把$BUILD_ROOT/include这个目录填进工程的 Header Search Path。具体配置时路径建议用相对工程的变量比如把 include 目录复制到$(PROJECT_DIR)/openssl-1.0.2o/include然后在 Header Search Path 里写$(PROJECT_DIR)/openssl-1.0.2o/include。不要写绝对路径否则换机器或者 CI 环境后会找不到。头文件引用方式保持#include openssl/evp.h这要求 Header Search Path 指向的是 include 的上一层也就是能同时看到 openssl 子目录的位置。这里还有一个容易漏的点OpenSSL 1.0.2o 在 iOS 上可能引用 zlib 库。如果最终链接报错说找不到inflate或deflate相关符号就需要在链接配置里补上-lz。这个依赖和 BIO 里的压缩滤波器有关即使你的代码没直接调用某些 OpenSSL 模块也会引用。4.2 Other Linker Flags 怎么写-lssl -lcrypto 的坑与正确顺序如果你不想把 .a 文件逐个拖进 Build Phases另一个常见做法是只配置 Library Search Path然后在 Other Linker Flags 里写库名。两种方式二选一不能混用否则会出现重复符号。推荐用这种方式# Build Settings - Other Linker Flags按顺序填 -lssl -lcrypto -lz参数说明-lssl让链接器在 Library Search Path 里找 libssl.a-lcrypto找 libcrypto.a-lz补上 zlib 依赖。顺序不能反因为 libssl.a 依赖 libcrypto.a链接器对符号的解析是从后往前的如果把-lcrypto写在前面会遇到_EVP_*找不到的情况。在这里Library Search Path 需要设置为$BUILD_ROOT/lib也就是存放合并后 .a 文件的目录。如果已经把两个 .a 拖进了 Link Binary With Libraries那 Other Linker Flags 里就不应该再出现-lssl -lcrypto否则同一份符号会被重复引入。我一般会优先用 Library Search Path 加 Linker Flags 的方式这样以后换库版本只改一个路径不用再去工程面板里增删文件。4.3 用 OpenSSL 的 EVP 接口写一个最小 demo 验证静态库真被链接进去了很多工程把库配置好之后编译通过就以为万事大吉直到运行到具体加密逻辑才发现链接的不是目标库。为了避免这个情况先写一个最小的 C 函数直接调用 1.0.2o 的核心接口。在工程里新建一个smoke.c内容如下#include openssl/evp.h #include string.h int openssl_smoke_test(void) { unsigned char digest[EVP_MAX_MD_SIZE]; unsigned int digest_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, ios-openssl, 11); EVP_DigestFinal_ex(ctx, digest, digest_len); EVP_MD_CTX_destroy(ctx); return digest_len 32 ? 0 : -1; }逻辑说明EVP_DigestInit_ex、EVP_sha256、EVP_DigestUpdate这些符号都在 libcrypto.a 里如果链接配置正确这段代码能顺利编译。EVP_MD_CTX_create和EVP_MD_CTX_destroy是 1.0.2 时代的 API在 1.1.1 里被改名成EVP_MD_CTX_new和EVP_MD_CTX_free所以这个函数还能顺便验证你拿到的确实是 1.0.2o而不是系统里残留的其他版本。编译后用file命令看库格式也能确认是不是真的静态库file $BUILD_ROOT/lib/libcrypto.a如果输出是current ar archive说明是静态库如果输出是动态库格式说明之前在 configure 时漏掉了no-shared需要重新编译。把openssl_smoke_test在 App 启动早期调用一次返回 0 就说明静态库已经真正编进来了后续排错可以排除链接问题。5. iOS 静态库编译与链接期的常见问题排查现象、原因、解决5.1 编译阶段SDK 路径配置错、Configure 目标选错**现象**执行./Configure ios64-cross时报Invalid configuration或者 make 时提示找不到编译器路径。这通常是CROSS_TOP和CROSS_SDK没有正确导出或者 configure 命令是在源码目录之外执行的。OpenSSL 的 configure 脚本对参数比较敏感它需要在源码根目录下运行并且能够解析到包含 SDK 信息的目录结构。如果 CROSS_TOP 配成了包含SDKs/本身的上一级make 里拼出来的 clang 路径就是错的。解决先跑xcrun --sdk iphoneos --show-sdk-path确认输出的是真实存在的 SDK 路径。然后检查CROSS_TOP${SDK_ROOT%SDKs/*}这个写法会去掉SDKs/之后的部分但如果 SDK 路径本身带了尾斜杠截断结果也会不对。可以把三行 echo 放进编译脚本开头每次编译前先看一次输出。另一个容易忽略的点是 1.0.2o 的 configure 脚本会把目标平台名打印出来如果发现打印的是 host 而不是 ios说明 configure 参数顺序错了重新按./Configure ios64-cross --openssldir...的格式执行。5.2 链接阶段Undefined symbols、依赖顺序错误、重复符号**现象**真机链接时报Undefined symbols for architecture arm64: _EVP_DigestInit_ex但头文件已经能正常 include。原因很直接链接器没有找到 libcrypto.a。这里有两种可能一是 Library Search Path 没有指向合并后 .a 所在目录二是 Other Linker Flags 里写了-lssl但漏了-lcrypto。因为_EVP_DigestInit_ex在 libcrypto.a 里缺失它基本可以断定 crypto 库没被链接。解决确认LIBRARY_SEARCH_PATHS指向$(PROJECT_DIR)/openssl-lib这类实际目录并在 Linker Flags 里补上-lcrypto。不要只把 .a 拖进 Link Binary With Libraries 却不再设置 Library Search Path如果你手动填了-lssl -lcrypto链接器是在 Search Path 里找的跟拖进去的文件关系不大。**现象**工程里同时配置了 .a 文件和 Linker Flags编译报duplicate symbol _SSL_library_init。原因就是重复导入了同一个静态库。在 Build Phases 里拖入 .a又在 Other Linker Flags 写了-lssl -lcrypto链接器会拿到两份符号。解决只保留一种方式。我一般建议只保留 Library Search Path 加-lssl -lcrypto -lz的方式因为这样改版本号时只需要换目录里的 .a 文件工程文件基本不用动。5.3 验证阶段架构不对、Bitcode 报错、头文件冲突**现象**编译链接都通过了但运行到加密函数时直接崩溃或者启动时报image not found。这种情况大多是最终 .a 里混入了模拟器架构。lipo 只是把多个架构拼在一起不会自动判断哪个属于真机如果你不小心把 x86_64 的库也放到同一目录链接器会在主二进制里留下错误平台的 load command。解决对最终库执行lipo -info真机包必须只有armv7和arm64如果需要模拟器调试单独维护一个包含x86_64的版本不要和真机版合并。**现象**Xcode 的 Build Settings 里开启了 Bitcode 编译选项链接时报bitcode bundle could not be generated because ... was built without bitcode。原因是在编译 OpenSSL 时没有启用 Bitcode而后期的 Xcode 版本在链接阶段会尝试回填 Bitcode 信息。解决有两个方向如果 App 不需要上架到要求 Bitcode 的场景直接把 Target 的 Enable Bitcode 设为 NO这是最省事的做法如果必须保留 Bitcode就需要在编译 OpenSSL 时导出CFLAGS-fembed-bitcode然后重新执行第三章的完整流程。注意CFLAGS要在 configure 之前导出并且两个架构都要重新编。**现象**头文件报conflicting types for EVP_MD_CTX_create或者接口声明跟库不对劲。这通常是 include 目录里混入了其他版本的 OpenSSL 头文件。老工程的机器上可能还残留了/usr/local/opt/openssl之类的头文件目录Header Search Path 的搜索顺序不对时编译器先命中了错误版本。解决统一使用第三章收拢到$BUILD_ROOT/include的头文件并在 Header Search Path 里把它排在所有系统搜索路径前面。排查时也可以用-H参数查看编译器的实际 include 路径一眼就能看出openssl/evp.h来自哪里。6. 进阶把整个编译过程封装成一条脚本顺便给静态库做瘦身和可追溯校验如果只编译一次手敲命令没关系但 OpenSSL 这种库经常会因为换机器、换 SDK 版本而需要重编。我建议把第三章的流程固化成一个构建脚本用变量控制架构列表和输出目录以后换版本时只需要改版本号和 target 名。# build-ios-lib.sh 的简化骨架 ARCHSarmv7 arm64 for arch in $ARCHS; do if [ $arch arm64 ]; then TARGETios64-cross; else TARGETios-cross; fi ./Configure $TARGET --openssldir$PWD/dist/$arch \ no-asm no-shared enable-deprecated make -j4 make install make clean done lipo -create dist/armv7/lib/*.a dist/arm64/lib/*.a -output lib/libcrypto.a这个骨架还没有处理 SDK 变量导出实际使用时要先把CROSS_TOP和CROSS_SDK传进来。架构列表放在变量里后续如果模拟器也要用加一个x86_64分支即可。注意make clean必须放在每次架构编译之后否则下一次 configure 会被上一次的 .o 文件污染。瘦身方面等编译完成后可以用strip -S去掉本地符号静态库的二进制体积能缩小一部分但不要用strip -x去掉全部符号那会把导出符号表也清理掉链接时反而报 Undefined symbols。如果你明确用不到 SSLv3、不压缩、不使用某些 PSK 套件可以加no-ssl3、no-psk等 configure 选项来缩小编译范围但这些选项只能用在确定没有运行路径的代码里否则运行期会报不支持的算法。最后是可追溯性。静态库不像源码可以直接 diff所以我会在编译完成后对两个 .a 计算一句摘要并连同构建时间、SDK 路径、configure 参数一起写进持续集成日志。下次出问题先对比摘要就知道产物是不是同一份能省掉大半天排错时间。我第一次做这个编译时觉得make clean多此一举直接在同一目录里连续编 armv7 和 arm64结果第二个版本的符号表里全是上一个架构的机器码lipo 也救不回来。从那以后编译目录、头文件目录、最终产物全部用变量钉在脚本里再也不靠手记。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →