ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android ARM64 OpenSSL动态库编译与JNI集成指南

Android ARM64 OpenSSL动态库编译与JNI集成指南 简介面向Android ARM64-v8a架构的预编译OpenSSL加密库专供使用NDK进行Android原生开发的工程师使用。库内包含完整头文件与动态链接库能直接补齐TLS/SSL协议、RSA/ECC加解密、数字签名、证书解析等核心密码学能力使开发者无需再从源码交叉编译OpenSSL。整个资源包共111个文件以106个头文件和2个C源文件为主头文件覆盖EVP、EC、X509、SSL等常用子模块另附demo示例与工程辅助文件压缩包仅319KB非常轻量便于直接嵌入工程或随项目分发。目录以openssl_arm64-v8a为根include与lib分层清晰lib下提供专用于arm64-v8a的libcrypto.so与libssl.so兼容Android 5.0API 21及以上系统。适合需要实现HTTPS通信、安全存储、密钥交换或国密扩展的Android原生层开发者借助该库可避免自行编译OpenSSL的繁琐把更多精力投入上层安全功能设计与业务集成。目前已有17人学习下载中高级NDK工程师可直接用于原生安全模块的快速搭建。 最近把一个用 JNI 做网络解析的 Android 模块从 32 位迁到 ARM64第一个卡住的就是 OpenSSL。OpenSSL 这东西在桌面环境里一条apt install就完了但到了 Android 上不能那么干系统自带的 libcrypto 是 BoringSSL 的定制版本API 暴露和 OpenSSL 有差异尤其 targetSdk 调高之后很多接口已经不再开放。于是只能自己编一份 Android 平台 ARM64 架构专用的 OpenSSL 加密库头文件、动态链接库一起整理出来。跑通之后回头看真正麻烦的其实不是编译而是后面的 so 拷贝、CMake 链接和运行时加载很多坑不实际踩一遍根本想不到。1. 为什么不能直接使用现成的 OpenSSL 库1.1 Android ABI 隔离与工具链差异Android 的 native 动态库不是随便拿一个 gcc 编出来就能跑的。手机里的系统库用的是 bionic libc不是桌面 Linux 的 glibc动态链接器路径、符号解析规则、线程局部存储的实现都不同。从 PC 或服务器上拷一份libssl.so到手机最常见的结局就是启动时直接dlopen failed甚至报Invalid ELF header这种看起来完全说不通的错。ARM64 对应的是 Android 的arm64-v8aABI指令集是 AArch64和旧设备的armeabi-v7a不兼容。Android 进程启动时会按 APK 里存在的 native library 目录匹配设备 ABI加载期间只要出现一个 ABI 对不上的 so整个 library 组都可能加载失败。所以我一开始就把目标定得很明确只编arm64-v8a的 OpenSSL不去做那种幻想一个 so 通吃所有手机的东西。1.2 动态链接库为什么比静态库更合适标题里特意强调“动态链接库”这一点在实际集成里是有讲究的。OpenSSL 默认编出来可能是一堆.a静态库静态库链接进自己的 so 之后如果 App 里有多个 native 模块都用了 OpenSSL内存里就可能存在两份不同版本的 OpenSSL 代码全局状态互相不认。比如一个模块用SSL_CTX_new创建的上下文另一个模块尝试读取轻则行为诡异重则直接崩溃。动态库libssl.so和libcrypto.so则可以让多个模块共享同一份实现。调试阶段也可以单独替换 so不用重新编译整个 App。不过动态库也不是没代价麻烦全在依赖关系上libssl.so依赖libcrypto.so拷贝的时候不能只拷贝一个运行时缺了依赖照样起不来。我后来的处理方案是动态库为主但每个 ABI 目录里的版本化文件也一并放进去宁可多占一点空间也不让运行时到处找依赖。1.3 ARM64 专用与多 ABI 打包的关系“ARM64 专用”听起来像是偷懒其实是合理的产物策略。Google Play 老早就要求新应用和更新必须包含 64 位 native 库对现在的新项目来说arm64-v8a 是默认优先级最高的 ABI。只编 arm64-v8a 可以减少 APK 体积也能避开 32 位兼容层里那些奇奇怪怪的问题。调试的时候例外。如果你用的模拟器是 x86_64那手头最好还是再编一份 x86_64 的库否则只能一直用真机调试。这个后面会讲到怎么用脚本批量扩展核心思路不复杂。2. 编译前的版本选择和工具链准备2.1 OpenSSL 源码、NDK 与 Perl 的版本搭配编译 OpenSSL for Android 需要三样东西OpenSSL 源码、Android NDK、Perl。源码一定要从 openssl 官网下载去 source 目录拿.tar.gz不要下那种别人打包好的预编译 binary。以目前常用的 OpenSSL 3.0 系列为例很多老工程还锁在 1.1.1但新工程我更建议直接上 3.0TLS 能力、API 稳定性和维护周期都更好。下面的操作我用 3.0 系列来演示如果你用的是 1.1.1w流程一样只是产出的 so 文件名后缀从.so.3变成.so.1.1后面处理依赖时要留意。NDK 我推荐 r25 或更新的版本比如25.2.9519653。r25 之后工具链统一走 llvm不再有老的 gcc 残留OpenSSL 配置脚本识别起来也更干净。为什么不能装一个 NDK 永久通用因为 OpenSSL 的Configure脚本在检测交叉编译环境时会读取ANDROID_NDK_ROOT这个变量如果你装的是 r17 那种老版本编译器路径和可用的 API level 可能都不一样编出来的东西在现在的 Android 系统上会有兼容隐患。Perl 在 macOS 和 Linux 上一般自带Windows 上需要装 Strawberry Perl。OpenSSL 的整个配置脚本都是 Perl 写的没有 Per 就卡死在第一步。2.2 配置交叉编译环境变量的关键点编译前最核心的一步是设置环境变量。不要只设一个ANDROID_HOMEOpenSSL 的脚本要认的是ANDROID_NDK_ROOT而且必须指向包含toolchains/llvm/prebuilt的那一层目录。export ANDROID_NDK_ROOT$HOME/Android/Sdk/ndk/25.2.9519653 export PATH$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH如果你在 macOS 上prebuilt 目录名是darwin-x86_64Apple Silicon 机器上如果 NDK 还没有对应工具链Rosetta 跑 x86_64 版本也能用。Windows 上则要选windows-x86_64。很多人在这一步路径写错导致 Configure 脚本自动找到宿主机 gcc 去编最后编出来一个 x86_64 的 so放到 Android 模拟器上还是能跑真机上直接崩。验证环境是否生效可以先在终端里敲一下which aarch64-linux-android21-clang能输出路径基本就说明 NDK 工具链已经在 PATH 里了。2.3 宿主平台差异macOS、Linux、Windows编译 OpenSSL for Android 在三种主流宿主平台上都能完成区别主要是路径分隔符和 Perl 的安装方式。Linux 上最顺手包管理器直接装 perl 和 makeNDK 路径随便放。macOS 只要确认自带 Perl 可用且 NDK 工具链能被 Rosetta 正确执行。Windows 上建议用 cmd 或 PowerShell 设置环境变量不要顺手打开 WSL 就觉得完事了WSL 里编译出来的 so 虽然也能用但路径映射容易让--prefix目标变得混乱。有一点需要提前说清楚NDK 在 Windows 上目前主要提供 x86_64 版本如果你用的是 Windows ARM64 开发机最好还是跑 x64 模拟环境来编译避免因为工具链不匹配造成莫名其妙的 “无法定位程序输入点” 类错误。这类问题在 Windows 上尤其经典本质是依赖库版本或者架构不对和 Android 端dlopen failed的排查思路是一样的。3. ARM64 版 OpenSSL 的完整编译过程3.1 Configure 命令的参数逐项说明进入到 OpenSSL 源码目录之后运行下面的命令./Configure android-arm64 -D__ANDROID_API__21 shared --prefix$PWD/dist/arm64-v8a逐项拆开看android-arm64是 OpenSSL 官方内置的 Android AArch64 交叉编译 target它知道怎么去调用 NDK 里的aarch64-linux-android21-clang。-D__ANDROID_API__21表示以 Android 5.0 的 API 级别为目标。API level 21 是 64 位 native 库可用的最低门槛如果 minSdk 更高比如 26可以对应改成 26。shared是关键它让 OpenSSL 生成动态链接库而不是默认的静态库。--prefix指定最终安装目录所有头文件和 so 都会归拢到这里。如果你在编译 1.1.1 老版本命令基本一样区别是它可能会要求你额外设置ANDROID_NDK_ROOT而不是ANDROID_NDK_HOME。如果 Configure 阶段报unknown option先去确认一下你是不是下载成了 Windows 预编译 binary源码目录里应该能看到Configure这个无后缀文件。3.2 编译、安装头文件与动态库Configure 没问题之后直接编译make -j8 make install_swinstall_sw只安装软件相关内容不装文档和多余的示例能省一点时间。安装完成后dist/arm64-v8a/lib下会看到libssl.so、libcrypto.so以及带版本号的libssl.so.3、libcrypto.so.3。include/openssl下则是全部的 OpenSSL 头文件。这里必须先解释一个容易踩的坑。编译出来的libssl.so本身可能只是个符号链接真实文件是libssl.so.3。Android 打包 APK 不支持符号链接所以不能直接把软链接丢进 jniLibs。同时System.loadLibrary(ssl)找的是libssl.so而其他 so 的依赖关系里记录的可能是libssl.so.3所以最稳妥的做法是把真实文件复制成不带版本号的名字同时也把带版本号的文件一起放进 jniLibs这样 loadLibrary 和依赖解析都能找到文件。空间会多占一点但换来的是运行时不迷路。3.3 想同时支持 x86_64 模拟器怎么办只做 ARM64 的话真机调试没问题但模拟器大多跑在 x86_64 上。为了调试方便可以写一个简单循环把常见 ABI 一起编出来for abi in arm64-v8a armeabi-v7a x86_64; do case $abi in arm64-v8a) targetandroid-arm64 ;; armeabi-v7a) targetandroid-arm ;; x86_64) targetandroid-x86_64 ;; esac make distclean /dev/null 21 ./Configure $target -D__ANDROID_API__21 shared --prefix$PWD/dist/$abi make -j8 make install_sw done注意每个 ABI 编译前先make distclean否则上一次生成的.so和对象文件会污染下一次构建。如果 minSdk 定了 21armeabi-v7a也可以用-D__ANDROID_API__21不必为这个老 ABI 单独降级。编译完成后用file验证产物file dist/arm64-v8a/lib/libssl.so # 正常会输出 ELF 64-bit LSB shared object, ARM aarch64这时再把dist/arm64-v8a/include整个拷到工程里头文件和动态链接库就算齐了。4. 在 Android Studio 中集成编译产物4.1 把 so 和头文件放进正确的位置Android Studio 项目里native 库的默认目录是app/src/main/jniLibs/abi/。对 arm64-v8a 来说就是app/src/main/jniLibs/arm64-v8a/。把编译得到的libssl.so、libssl.so.3、libcrypto.so、libcrypto.so.3都放进去头文件则放到app/src/main/cpp/openssl/include/下。有人习惯把 so 放在src/main/cpp/libs然后在 gradle 里指定 jniLibs 路径也可以但没必要增加自定义。默认 jniLibs 目录能被 Android Gradle Plugin 自动识别并打包出错概率最低。如果你不希望 APK 里出现多余 ABI在app/build.gradle里加一行android { defaultConfig { ndk { abiFilters arm64-v8a } } }这个过滤只对 jniLibs 和外部 native 构建生效记得在最终 release 包确认一下。4.2 通过 CMake 链接头文件与动态库在CMakeLists.txt里需要同时做三件事声明 so 为 imported 库、让 native 代码能找到 OpenSSL 头文件、最后链接进自己的动态库。示例cmake_minimum_required(VERSION 3.18) project(openssl_demo) include_directories(${CMAKE_SOURCE_DIR}/openssl/include) add_library(ssl SHARED IMPORTED) set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libssl.so) add_library(crypto SHARED IMPORTED) set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libcrypto.so) add_library(native-lib SHARED native-lib.cpp) target_link_libraries(native-lib ssl crypto log)这里IMPORTED_LOCATION不要写成固定路径${ANDROID_ABI} 会自动替换成当前构建的 ABI。链接顺序也值得一提先 libssl 后 libcrypto因为 libssl 依赖 libcrypto某些老版本链接器如果顺序反了会报 undefined reference虽然 LLVM 下不一定必现但按依赖顺序写更稳。4.3 用 JNI 调用 OpenSSL 的最小示例native 代码里调用 OpenSSL 之前先确认 include 路径没问题。一个典型的错误是明明已经放好了头文件却依然报openssl/evp.h: No such file or directory。这种情况多半是 CMake 里include_directories写错了相对路径或者在android配置里没有打开externalNativeBuild。一个最简单的 JNI 函数可以这样写#include jni.h #include openssl/rand.h extern C JNIEXPORT jbyteArray JNICALL Java_com_example_demo_NativeLib_randomBytes(JNIEnv *env, jobject, jint len) { unsigned char buf[32]; if (RAND_bytes(buf, sizeof(buf)) ! 1) { return nullptr; } jbyteArray result env-NewByteArray(sizeof(buf)); env-SetByteArrayRegion(result, 0, sizeof(buf), reinterpret_castconst jbyte *(buf)); return result; }RAND_bytes的返回值必须检查返回 0 说明随机数生成失败常见原因是 openssl 初始化阶段没有正确加载随机种子。对 Android 来说系统级的/dev/urandom是可用熵源一般不会出问题但如果你在模拟器上遇到随机数调用卡住或返回 0先看看是不是只拷贝了libssl.so忘了libcrypto.so。Java/Kotlin 侧加载顺序建议显式控制init { System.loadLibrary(crypto) System.loadLibrary(ssl) System.loadLibrary(native-lib) }先加载 libcrypto 再加载 libssl最后加载自己的库能在第一时间暴露依赖缺失而不是等到真正调用 OpenSSL 函数时才崩。5. 集成过程中的常见问题与排查记录5.1 dlopen 报错找不到 libssl.so.3这是最经典的问题几乎每个自己编译 OpenSSL 的人都会遇到。现象是运行到加载 native 库时崩溃日志里写着dlopen failed: library libssl.so.3 not found原因通常不是没放libssl.so而是只放了无版本号的文件或者只放了libssl.so的软链接。编译出来的 so 内部记录的 SONAME 是libssl.so.3当另一个 so 依赖它时系统找的是这个带版本号的名字而不是libssl.so。解决办法就是第 3.2 节说的把libssl.so.3、libcrypto.so.3以及无版本号的libssl.so、libcrypto.so全部放进 jniLibs。如果你用 1.1.1 版本文件名中3换成1.1逻辑一样。5.2 include 路径导致头文件缺失或错版编译 native 代码时最常见两类头文件问题。一类是找不到openssl/ssl.h那是 CMake include path 配置不对。另一类更隐蔽编译第三方 C 库时它自动检测到的 OpenSSL 头文件版本和你实际链接的 so 版本不一致比如日志出现checking openssl header version... 100020bf (openssl 1.0.2k)说明它拿到的是系统头文件或者某个旧目录里的头文件并不是你在 jniLibs 旁边放的那份。解决这类问题的根本思路是保证“头文件版本 链接库版本”。一个项目里尽量不要同时存在多份 OpenSSL include 目录。如果确实要支持多个版本就把头文件放在各自独立的路径下并且编译命令里控制-I搜索顺序让自己那份 OpenSSL include 排在系统路径之前。我见过有人为了绕过版本检查直接去改opensslv.h这种方法非常危险OpenSSL 很多公开结构体的大小和布局跟版本强相关头文件版本和库版本不一致短期能编译过运行起来随机崩溃都查不到原因。5.3 TLS 握手时报 unexpected eof while readingerror:0a000126:SSL routines::unexpected eof while reading是很多人在刚接上自己编译的 OpenSSL 后遇到的第一个运行时报错。这个错本身不一定是库有问题更多是 TLS 握手交互过程中对端没有按正常流程结束连接直接被 TCP 层断开。排查顺序我先看协议版本。如果服务端只支持 TLS 1.2而你代码里把最小版本设成了 TLS 1.3就会握手失败。其次看证书链和 SNIAndroid 上如果证书加载路径不对握手也会在某个阶段被服务端掐断。最后抓一把日志看是不是中间代理设备在搞鬼。总而言之这个问题和 so 版本关系不大但很容易被前几个排查步骤误导到编译问题上。5.4 证书文件读不到先看 Android 分区存储OpenSSL 常用SSL_CTX_load_verify_locations加载 CA 证书。在 Android 上如果直接传一个/storage/emulated/0/xxx/ca.pem路径尤其是 targetSdk 30 或更高时很可能读不到。这不是 OpenSSL 自身的问题是 Android 分区存储和 FileProvider 限制了应用直接访问共享存储。可靠的做法是把证书放进assets或res/raw运行时拷到应用私有目录再传给 OpenSSL。也可以直接走SSL_CTX_set_cert_store把证书内容在内存里构建成X509_STORE。总之不要让 OpenSSL 去直接访问共享存储路径否则在部分国产 ROM 上表现还会更不稳定。6. 最后说几句落地心得编译 OpenSSL for Android 这个事技术上不复杂但细节非常多。我在实际项目里吃过几次亏之后现在处理这类 native 库会坚持两条习惯。第一BuildConfig 里记录 OpenSSL 的库版本。在 native 代码初始化时通过OPENSSL_VERSION_TEXT宏输出日志崩溃现场看到OpenSSL 3.0.x和 include 目录里的版本能对上能省下很多排查时间。第二构建产物一定要做 ABI 校验。不要觉得编完就万事大吉每次拷贝 so 到 jniLibs 后先unzip -l看一下 APK 里的 native library 列表确认arm64-v8a/libssl.so和libcrypto.so都在并且没有混入 x86 的产物。这一步虽然机械但在团队协作时能挡住不少低级问题。如果你只是内部工具使用静态库也不是不行但那就要保证整个 App 只有一个 OpenSSL 消费者。一旦有多个模块都要用动态库加全套版本化文件的方案会省心得多。这个项目做完之后我对 OpenSSL 的构建流程算是有了一套固定流程下次再接到类似需求直接跑脚本验证产物就行。本文还有配套的精品资源点击获取
返回列表