导出符号介绍
Android 中的共享库(.so
文件)使用导出符号来公开其内部的函数和全局变量,使得其他库或可执行文件(如你的 App 主模块或其他 .so
)能够在运行时或链接时找到并使用它们。
导出符号是指那些被明确标记为可供外部使用的符号,当一个.so
文件被加载或链接时,只有其导出的符号才能被其他模块(App 或其他 .so
)访问调用。
导出符号作用
JNI函数调用时,当 Java/Kotlin 代码通过 System.loadLibrary("native-lib")
加载 .so
后,它需要调用用 native
关键字声明的 JNI 函数,这些 JNI 函数必须作为导出符号存在于.so
中,Java 虚拟机(JVM)才能找到并执行它们。
库间互操作:一个.so
(A)可能需要调用另一个 .so
(B)中提供的功能,B库中需要被A库调用的函数必须导出。
插件系统: 主程序动态加载插件 .so
,插件需要导出特定的入口点函数供主程序调用。
导出方式
在 Android NDK 开发中,主要通过JNIEXPORT
和 JNICALL
宏 (主要用于 JNI 函数)方式控制符号可见性。
JNIEXPORT
JNIEXPORT是一个宏定义,用于指定函数的导出方式,告诉编译器这个函数需要被导出,以便Java代码能够调用。
在Windows平台上,通常定义为__declspec(dllexport)
,而Linux/Android平台上,通常定义为 __attribute__((visibility("default")))
JNICALL
JNICALL是一个宏定义,用于指定函数的调用约定,确保C/C++函数使用正确的调用约定,以便JVM能够正确调用。
在Windows平台上,通常定义为__stdcall
, 而Linux/Android平台上,通常为空定义。
用法示例
这是 Android NDK 中最常见的方式,专门用于标记需要被 JVM 调用的 JNI 函数。
在 JNI 函数声明前使用:
JNIEXPORT jstring JNICALL
Java_com_test_symboltest_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// 实现代码
return (*env)->NewStringUTF(env, "Hello from JNI!");
}
宏特性
- 跨平台兼容性: 不同操作系统有不同的函数导出和调用约定;
- JVM集成: 确保C/C++函数能够被JVM正确识别和调用;
- 符号可见性: 确保函数符号在动态库中可见;
- 必须成对使用: 每个JNI函数都应该同时使用这两个宏;
- 函数命名规范: 通常遵循Java包名类名_方法名的格式;
- 参数约定: 第一个参数总是
JNIEnv *env
,第二个参数根据方法类型而定。
导出符号的去除
导出符号信息如果不去除,则通过函数名可以推断出应用的核心功能,暴露内部架构和设计模式等风险,但是还不能全部都去除,否则就无法去调用。
为了Android so库导出符号的安全性,如果库不涉及到调用则可以全部去除符号,如果库涉及到调用则可以将调用的符号进行保留,其余的可以去除,以下介绍下去除所有导出符号和去除部分导出符号的方式。
移除所有导出符号
- 创建个symver.txt 文件,文件内容如下所示:
{
local: *;
};
如图所示:

- 在CMakeList.txt中加入编译选项去加载symver.txt文件,如下所示:
add_library(xxxx SHARED ${SRC})
# --version-script 控制动态符号
set_target_properties(xxxx PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/symver.txt")
参考如图所示:

- 编译成功后,使用反编译工具(比如ida、ghidra)查看so库,可以看到没有任何导出符号;
移除部分导出符号
- 创建个symver.txt 文件,文件内容如下所示:
{
global:
Java_com_test_symboltest_JniTest_encrypt;
Java_com_test_symboltest_MainActivity_stringFromJNI;
local: *;
};
如图所示:

- 在CMakeList.txt中加入编译选项去加载symver.txt文件,如下所示:
# --version-script 控制动态符号
set_target_properties(xxxx PROPERTIES LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/symver.txt")
- 编译成功后,使用反编译工具(比如ida、ghidra)查看so库,可以看到设置的导出符号进行保留,没有设置的就直接去除了;
安全防范
安全问题
虽然so库的不需要的导出符号被去除了,但so库仍然有被调试、逆向和篡改的风险,比如:
被逆向风险
虽然编译生成的二进制文件逆向分析难度较高,但由于相应的反编译工具(比如ida、Ghidra)也成熟强大,依然可以反编译为类C伪代码;
被调试风险
攻击者可通过调试工具附加应用进程进行调试,在调试过程中可能暴露敏感信息(如密钥、算法逻辑);
程序被篡改风险
攻击者可以通过修改应用内存改变程序行为,绕过安全检查或实现恶意功能,导致数据泄露;
防范措施
Android so本身的格式还是属于elf格式,比如:

针对ELF格式的文件,Virbox Protector
工具在对ELF程序的保护上有着成熟的方案,可以实现对elf程序的函数级和整体保护,具体方案参考官网文档Native程序保护最佳实践。