SO 动态库编译与加载

SO 动态库编译与加载

📚 前置知识

本配方涉及以下核心技术,建议先阅读相关章节:

本章介绍如何从零开始编译 Android SO 动态库,以及 Android 系统加载 SO 的完整流程。理解这些原理对于逆向分析 Native 层代码至关重要。

SO 文件基础

什么是 SO 文件

SO (Shared Object) 是 Linux/Android 系统的动态链接库格式,相当于 Windows 的 DLL。它采用 ELF (Executable and Linkable Format) 格式。

SO 文件结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────┐
│     ELF Header      │  文件类型、架构、入口点
├─────────────────────┤
│   Program Headers   │  段加载信息
├─────────────────────┤
│      .text          │  可执行代码
├─────────────────────┤
│      .rodata        │  只读数据(字符串常量等)
├─────────────────────┤
│      .data          │  已初始化的全局变量
├─────────────────────┤
│      .bss           │  未初始化的全局变量
├─────────────────────┤
│      .dynamic       │  动态链接信息
├─────────────────────┤
│      .dynsym        │  动态符号表
├─────────────────────┤
│      .dynstr        │  动态字符串表
├─────────────────────┤
│   Section Headers   │  节信息
└─────────────────────┘

Android 支持的 CPU 架构

ABI架构说明
arm64-v8aARMv8-A (64 位)现代 Android 设备主流
armeabi-v7aARMv7-A (32 位)老旧设备兼容
x86_64x86-64 (64 位)模拟器、部分平板
x86x86 (32 位)老旧模拟器

编译环境准备

1. 安装 Android NDK

1
2
3
4
5
6
7
8
# 方式一:通过 Android Studio SDK Manager 安装

# 方式二:命令行下载
# 下载地址:https://developer.android.com/ndk/downloads
wget https://dl.google.com/android/repository/android-ndk-r25c-linux.zip
unzip android-ndk-r25c-linux.zip
export NDK_HOME=/path/to/android-ndk-r25c
export PATH=$PATH:$NDK_HOME

2. 验证安装

1
2
3
4
5
# 检查 NDK 版本
$NDK_HOME/ndk-build --version

# 检查可用的工具链
ls $NDK_HOME/toolchains/llvm/prebuilt/*/bin/

3. 独立工具链(可选)

1
2
3
4
5
# 创建独立工具链(适合命令行编译)
$NDK_HOME/build/tools/make_standalone_toolchain.py \
    --arch arm64 \
    --api 21 \
    --install-dir /path/to/toolchain

使用 NDK 编译 SO

方式一:ndk-build

1. 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
project/
├── jni/
│   ├── Android.mk
│   ├── Application.mk
│   └── native.c
└── libs/
    ├── arm64-v8a/
    │   └── libnative.so
    └── armeabi-v7a/
        └── libnative.so

2. Android.mk

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

# 模块名称(生成 lib<名称>.so)
LOCAL_MODULE := native

# 源文件
LOCAL_SRC_FILES := native.c

# 依赖的系统库
LOCAL_LDLIBS := -llog -lz

# 编译选项
LOCAL_CFLAGS := -Wall -O2

# 构建动态库
include $(BUILD_SHARED_LIBRARY)

3. Application.mk

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 目标 ABI
APP_ABI := arm64-v8a armeabi-v7a

# 最低 API 级别
APP_PLATFORM := android-21

# C++ STL 库
APP_STL := c++_shared

# 优化级别
APP_OPTIM := release

4. native.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <jni.h>
#include <string.h>
#include <android/log.h>

#define LOG_TAG "NativeLib"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

// 导出函数
JNIEXPORT jstring JNICALL
Java_com_example_app_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
    LOGI("stringFromJNI called");
    return (*env)->NewStringUTF(env, "Hello from C!");
}

// 初始化函数(可选)
__attribute__((constructor))
void lib_init() {
    LOGI("Library loaded!");
}

// 卸载函数(可选)
__attribute__((destructor))
void lib_fini() {
    LOGI("Library unloaded!");
}

5. 编译

1
2
3
4
5
6
7
8
cd project/
$NDK_HOME/ndk-build

# 清理
$NDK_HOME/ndk-build clean

# 指定 verbose 输出
$NDK_HOME/ndk-build V=1

方式二:命令行直接编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 设置编译器
export CC=$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang

# 编译为目标文件
$CC -c -fPIC -o native.o native.c

# 链接为 SO
$CC -shared -o libnative.so native.o -llog

# 查看导出符号
$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm -D libnative.so

使用 CMake 构建

1. CMakeLists.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
cmake_minimum_required(VERSION 3.10.2)

project("nativelib")

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)

# 添加编译选项
add_compile_options(-Wall -O2)

# 定义库
add_library(
    native-lib
    SHARED
    src/native.cpp
    src/crypto.cpp
    src/utils.cpp
)

# 查找系统库
find_library(log-lib log)
find_library(z-lib z)

# 包含头文件目录
target_include_directories(native-lib PRIVATE
    ${CMAKE_SOURCE_DIR}/include
)

# 链接库
target_link_libraries(native-lib
    ${log-lib}
    ${z-lib}
)

# 添加预处理器定义
target_compile_definitions(native-lib PRIVATE
    DEBUG_MODE=1
)

2. 使用 CMake 编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 创建构建目录
mkdir build && cd build

# 配置(指定 NDK 工具链)
cmake \
    -DCMAKE_TOOLCHAIN_FILE=$NDK_HOME/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_PLATFORM=android-21 \
    ..

# 编译
cmake --build .

# 或者使用 make
make -j4

3. Gradle 集成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
android {
    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++17 -frtti -fexceptions"
                arguments "-DANDROID_STL=c++_shared"
            }
        }
        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a'
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

SO 加载流程分析

Android SO 加载流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java: System.loadLibrary("native")
Runtime.loadLibrary0()
ClassLoader.findLibrary()  ──→  查找 libnative.so 路径
Runtime.nativeLoad()
dlopen("libnative.so")  ──→  系统加载器
         ├──→ 解析 ELF 头
         ├──→ 映射 PT_LOAD 段到内存
         ├──→ 处理依赖库(递归加载)
         ├──→ 执行重定位
         └──→ 调用 .init_array / constructor
JNI_OnLoad() ──→ 如果存在,执行初始化

关键函数

函数说明
System.loadLibrary()Java 层加载库
dlopen()Native 层打开动态库
dlsym()查找符号地址
dlclose()关闭动态库
JNI_OnLoad()JNI 初始化回调
JNI_OnUnload()JNI 卸载回调

库搜索路径

1
2
3
4
5
6
7
8
// 应用私有库目录
/data/app/<package>/lib/<abi>/

// 系统库目录
/system/lib64/  (64位)
/system/lib/    (32位)
/vendor/lib64/
/vendor/lib/

手动加载 SO

Java 层加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class NativeLoader {

    // 方式一:从默认路径加载
    static {
        System.loadLibrary("native");  // 加载 libnative.so
    }

    // 方式二:从绝对路径加载
    public static void loadFromPath(String path) {
        System.load(path);  // 如: /data/local/tmp/libnative.so
    }

    // 方式三:自定义 ClassLoader
    public static void loadWithClassLoader(String libName) {
        try {
            // 获取应用的 ClassLoader
            ClassLoader loader = NativeLoader.class.getClassLoader();

            // 使用反射调用 findLibrary
            Method findLibrary = ClassLoader.class.getDeclaredMethod(
                "findLibrary", String.class);
            findLibrary.setAccessible(true);

            String libPath = (String) findLibrary.invoke(loader, libName);
            if (libPath != null) {
                System.load(libPath);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Native 层加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <dlfcn.h>
#include <android/log.h>

#define LOG_TAG "DynamicLoader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

// 动态加载 SO 并调用函数
void load_and_call(const char* lib_path, const char* func_name) {
    // 打开动态库
    void* handle = dlopen(lib_path, RTLD_NOW);
    if (!handle) {
        LOGI("dlopen failed: %s", dlerror());
        return;
    }

    // 查找函数符号
    typedef int (*func_ptr)(int, int);
    func_ptr func = (func_ptr)dlsym(handle, func_name);

    if (!func) {
        LOGI("dlsym failed: %s", dlerror());
        dlclose(handle);
        return;
    }

    // 调用函数
    int result = func(10, 20);
    LOGI("Function result: %d", result);

    // 关闭库
    dlclose(handle);
}

// 示例:加载加密库
void load_crypto_lib() {
    void* handle = dlopen("libcrypto.so", RTLD_LAZY);
    if (handle) {
        // 获取加密函数
        void* encrypt_func = dlsym(handle, "AES_encrypt");
        if (encrypt_func) {
            LOGI("Found AES_encrypt at %p", encrypt_func);
        }
    }
}

调试与分析技巧

1. 查看 SO 信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 查看 ELF 头信息
readelf -h libnative.so

# 查看程序头(加载段)
readelf -l libnative.so

# 查看节头
readelf -S libnative.so

# 查看动态符号表
readelf -s libnative.so

# 查看依赖库
readelf -d libnative.so | grep NEEDED

# 查看导出函数
nm -D libnative.so | grep " T "

2. Frida 动态分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 监控 dlopen
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
  onEnter: function (args) {
    this.path = args[0].readCString();
    console.log("[dlopen]", this.path);
  },
  onLeave: function (retval) {
    console.log("[dlopen] handle:", retval);
  },
});

// 监控 dlsym
Interceptor.attach(Module.findExportByName(null, "dlsym"), {
  onEnter: function (args) {
    this.symbol = args[1].readCString();
    console.log("[dlsym]", this.symbol);
  },
  onLeave: function (retval) {
    console.log("[dlsym] address:", retval);
  },
});

// 枚举已加载的模块
Process.enumerateModules().forEach(function (module) {
  if (module.name.indexOf("native") !== -1) {
    console.log(module.name, module.base, module.size);
  }
});

// 枚举模块导出函数
Module.enumerateExports("libnative.so").forEach(function (exp) {
  console.log(exp.type, exp.name, exp.address);
});

3. GDB 调试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 连接到进程
adb forward tcp:1234 tcp:1234
gdbserver :1234 --attach <pid>

# 本地 GDB 连接
$NDK_HOME/prebuilt/linux-x86_64/bin/gdb
(gdb) target remote :1234
(gdb) set solib-search-path /path/to/symbols

# 在函数上设置断点
(gdb) break Java_com_example_app_MainActivity_stringFromJNI
(gdb) continue

# 查看内存
(gdb) x/20x $pc

4. 常见问题排查

问题原因解决方法
UnsatisfiedLinkError找不到库或符号检查库路径和函数签名
dlopen failed: library not found依赖库缺失检查 readelf -d 输出
text relocations代码段重定位编译时添加 -fPIC
cannot locate symbol符号未导出检查函数是否标记为导出

相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信