安卓逆向关于SO的关键点
导出表和导入表在共享库的动态链接过程中起着关键作用。导出表定义了共享库提供给外部使用的符号,而导入表则列出了当前模块所需的外部符号。理解这两个概念有助于更好地管理和使用共享库。JNIEXPORT是一个宏,通常在 JNI 头文件中定义。它的主要作用是确保在编译共享库(如.so文件)时,方法能够被 Java 虚拟机正确识别和调用。JNIEXPORT是 JNI 中用于定义本地方法的关键宏,确保方法能够被
1.SO中的导出表,导入表是什么?
在共享库(Shared Object,通常以 .so
后缀表示)中,导出表和导入表是两个重要的概念,它们用于管理符号的可见性和链接。以下是对这两个表的详细介绍:
1. 导出表(Export Table)
导出表是一个包含了共享库中所有可以被外部使用的符号(函数、变量等)的列表。当其他程序或库需要使用这些符号时,它们会通过导出表来查找。
- 功能: 导出表允许其他模块(如动态链接的可执行文件或其他共享库)访问库中定义的符号。
- 内容: 导出表通常包含符号的名称、地址和其他信息(如符号的类型)。
- 使用场景: 当一个共享库被加载到内存中时,操作系统会使用导出表来解析外部引用。
2. 导入表(Import Table)
导入表是一个包含了当前模块所需的外部符号的列表。这些符号可能来自于其他共享库或可执行文件。
- 功能: 导入表指示当前模块需要哪些外部符号,并允许动态链接器在运行时解析这些符号。
- 内容: 导入表通常包含符号的名称和来源(即它们来自哪个共享库)。
- 使用场景: 当一个共享库被加载时,操作系统会检查导入表,加载所需的共享库,并解析符号地址。
3. 示例
假设有两个共享库:libA.so
和 libB.so
,以及一个可执行文件 main
。
- libA.so 中定义了一个函数
void funcA()
,并在导出表中列出。 - libB.so 中定义了一个函数
void funcB()
,并在导出表中列出。 main
可执行文件需要调用libA.so
和libB.so
中的函数。
在这种情况下:
- **libA.so 的导出表** 会包含
funcA
的信息。 - **libB.so 的导出表** 会包含
funcB
的信息。 - main 的导入表 会包含对
libA.so
中funcA
和libB.so
中funcB
的引用。
4. 相关工具
在 Linux 系统中,可以使用一些工具来查看共享库的导出和导入表,例如:
-
nm
: 用于列出目标文件中的符号。nm -D libA.so # 查看导出符号
-
readelf
: 用于查看 ELF 文件格式的信息,包括导入和导出表。readelf -s libA.so # 查看符号表 readelf -l libA.so # 查看段信息,包括导出和导入
-
objdump
: 用于显示对象文件的各种信息,包括符号表。objdump -T libA.so # 查看动态符号表
5. 总结
导出表和导入表在共享库的动态链接过程中起着关键作用。导出表定义了共享库提供给外部使用的符号,而导入表则列出了当前模块所需的外部符号。理解这两个概念有助于更好地管理和使用共享库。
(1)认识JNIEXPORT
JNIEXPORT
是一个宏,用于在 JNI(Java Native Interface)中定义本地方法的导出属性。它通常与 JNICALL
一起使用,以确保方法的正确调用约定。以下是对 JNIEXPORT
的详细介绍。
1. 定义
JNIEXPORT
是一个宏,通常在 JNI 头文件中定义。它的主要作用是确保在编译共享库(如 .so
文件)时,方法能够被 Java 虚拟机正确识别和调用。
2. 用法
JNIEXPORT
通常与 JNICALL
一起使用,形成 JNI 方法的完整声明。例如:
#include <jni.h>
JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
// 方法实现
}
3. 作用
- 导出符号:
JNIEXPORT
确保方法在编译时被标记为可导出符号,使得 Java 虚拟机能够找到并调用这些方法。 - 调用约定:
JNICALL
指定了调用约定,确保参数在调用时的传递方式与 Java 虚拟机的要求一致。
4. 示例
以下是一个完整的示例,展示如何使用 JNIEXPORT
和 JNICALL
定义一个 JNI 方法:
#include <jni.h>
#include <stdio.h>
// JNIEXPORT 和 JNICALL 用于定义本地方法
JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from C!\\n");
}
5. 编译和链接
在编译时,确保将 JNI 头文件包含在内,并链接到 JNI 库。编译命令示例:
gcc -shared -o libHelloWorld.so -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" HelloWorld.c
6. 总结
JNIEXPORT
是 JNI 中用于定义本地方法的关键宏,确保方法能够被 Java 虚拟机正确识别和调用。它与 JNICALL
一起使用,确保调用约定的正确性。
1. JNIEXPORT
-
定义:
JNIEXPORT
是一个宏,通常在 JNI 头文件中定义,目的是确保在不同平台上编译时正确导出函数。它通常与JNICALL
一起使用,后者用于指定调用约定。 -
用法: 通常在定义 JNI 方法时使用,例如:
JNIEXPORT void JNICALL Java_com_example_MyClass_myNativeMethod(JNIEnv *env, jobject obj) { // 本地方法的实现 }
在这个例子中,
JNIEXPORT
表示Java_com_example_MyClass_myNativeMethod
方法是一个可被 Java 虚拟机调用的本地方法。
2. JNI_OnLoad
-
定义:
JNI_OnLoad
是一个特定的 JNI 函数,当共享库(.so 文件)被加载时,Java 虚拟机会自动调用这个函数。它的主要作用是进行初始化操作。 -
函数原型:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { // 初始化代码 return JNI_VERSION_1_6; // 返回支持的 JNI 版本 }
-
参数说明:
JavaVM *vm
: 指向 Java 虚拟机的指针,可以用来获取JNIEnv
指针。void *reserved
: 保留的参数,通常不使用。
-
返回值: 返回一个
jint
类型的值,表示支持的 JNI 版本。如果返回的版本不被支持,Java 虚拟机将无法加载这个共享库。
3. 示例代码
下面是一个简单的示例,展示如何使用 JNIEXPORT
和 JNI_OnLoad
:
#include <jni.h>
#include <stdio.h>
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// 进行初始化操作
printf("JNI library loaded.\\n");
return JNI_VERSION_1_6; // 返回支持的 JNI 版本
}
JNIEXPORT void JNICALL Java_com_example_MyClass_myNativeMethod(JNIEnv *env, jobject obj) {
// 本地方法的实现
printf("Hello from native method!\\n");
}
4. 总结
JNIEXPORT
: 用于标识 JNI 方法,以确保它们在共享库中可被 Java 虚拟机调用。JNI_OnLoad
: 在共享库加载时自动调用的初始化函数,允许开发者执行必要的设置,并返回支持的 JNI 版本。
通过使用 JNIEXPORT
和 JNI_OnLoad
,你可以有效地将本地代码与 Java 代码连接起来,实现更高效的应用程序性能。
2.出现在导出表,导入表里面的函数,一般可以通过frida相关API直接得到函数地址,也可以自己计算函数地址
3.没有出现在这里的,都需要自己计算函数地址
4.要完成so的hook,都需要得到应该地址
5.java层中的native函数,被调用后会找到so中对应的函数,简单来说,就是Java调用C需要先完成函数注册,函数注册分为静态注册和动态注册
(1)静态注册
在 JNI 中,静态注册是指在 C/C++ 代码中直接注册 Java 方法,而不是通过反射动态查找。这种方式通常在库加载时进行,能够提高性能并简化方法调用。以下是关于静态注册的详细介绍和示例。
1. 静态注册的概念
静态注册允许开发者在 JNI 库中显式地定义 Java 方法与本地方法之间的映射关系。通过这种方式,JNI 不需要在运行时查找方法 ID,从而提高了性能。
2. 使用 JNIEXPORT 和 JNICALL
在 JNI 中,使用 JNIEXPORT
和 JNICALL
修饰符来定义本地方法。这两个宏确保方法能够被 JNI 正确调用。
JNIEXPORT
: 用于指示该方法是一个 JNI 导出的方法。JNICALL
: 指定调用约定,确保参数传递的方式与 Java 虚拟机的要求一致。
3. 示例代码
以下是一个简单的示例,展示如何使用静态注册在 JNI 中注册本地方法。
3.1 Java 代码
首先,定义一个 Java 类,包含一个本地方法:
// MyNativeClass.java
public class MyNativeClass {
// 声明本地方法
public native int add(int a, int b);
// 加载本地库
static {
System.loadLibrary("mynative"); // 加载名为 "mynative" 的本地库
}
}
3.2 C/C++ 代码
接下来,编写 C/C++ 代码,使用静态注册注册 add
方法:
#include <jni.h>
#include <stdio.h>
// 本地方法实现
JNIEXPORT jint JNICALL Java_MyNativeClass_add(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b; // 返回两个整数的和
}
// JNI_OnLoad 函数,用于静态注册
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // 返回错误
}
// 注册本地方法
jclass cls = (*env)->FindClass(env, "MyNativeClass");
if (cls == NULL) {
return JNI_ERR; // 返回错误
}
// 注册方法
JNINativeMethod methods[] = {
{"add", "(II)I", (void *)Java_MyNativeClass_add} // 方法名、签名和实现
};
if ((*env)->RegisterNatives(env, cls, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return JNI_ERR; // 返回错误
}
return JNI_VERSION_1_6; // 返回 JNI 版本
}
4. 代码解析
- 本地方法实现:
Java_MyNativeClass_add
是本地方法的实现,接受两个整数参数并返回它们的和。 - JNI_OnLoad: 当本地库被加载时,
JNI_OnLoad
函数会被调用。在这个函数中,我们获取JNIEnv
指针,并使用FindClass
查找 Java 类。 - 注册方法: 使用
RegisterNatives
注册本地方法。JNINativeMethod
结构体包含方法名、方法签名和实现的指针。
5. 编译和运行
要编译 C/C++ 代码并生成共享库(.so 文件),可以使用以下命令:
gcc -shared -fPIC -o libmynative.so MyNativeClass.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux"
然后在 Java 中调用本地方法:
public class Main {
public static void main(String[] args) {
MyNativeClass myNative = new MyNativeClass();
int result = myNative.add(5, 3);
System.out.println("Result: " + result); // 输出结果
}
}
6. 总结
静态注册在 JNI 中提供了一种高效的方式来注册本地方法。通过在 JNI_OnLoad
中注册方法,可以避免在运行时使用反射查找方法 ID,从而提高性能。使用 JNIEXPORT
和 JNICALL
确保方法能够被 JNI 正确调用。
(2)动态注册
动态注册是 JNI (Java Native Interface) 中的一种方法,用于在运行时将本地方法与 Java 方法进行关联。与静态注册不同,动态注册不需要在 Java 代码中提前定义本地方法,而是在本地库加载时通过 JNI API 手动注册这些方法。
1. 动态注册的概念
动态注册允许开发者在本地代码中使用 JNIEnv
指针来注册本地方法。通常在 JNI_OnLoad
函数中进行注册,这个函数在共享库被加载时自动调用。
2. 动态注册的步骤
动态注册的主要步骤包括:
- 定义本地方法:在 C/C++ 代码中实现要注册的本地方法。
- 获取
JNIEnv
指针:在JNI_OnLoad
中获取JNIEnv
指针。 - 注册方法:使用
RegisterNatives
函数将本地方法与 Java 方法进行关联。
3. 示例代码
以下是一个动态注册的示例,展示如何在 C/C++ 中实现动态注册。
3.1 Java 代码
首先,我们定义一个简单的 Java 类,包含一个要调用的本地方法:
// HelloWorld.java
public class HelloWorld {
// 声明本地方法
public native void sayHello();
// 加载本地库
static {
System.loadLibrary("HelloWorld");
}
public static void main(String[] args) {
new HelloWorld().sayHello(); // 调用本地方法
}
}
编译这个 Java 文件:
javac HelloWorld.java
3.2 C/C++ 代码
接下来,我们编写 C/C++ 代码,使用 JNI 动态注册本地方法:
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
// 本地方法的实现
void sayHello(JNIEnv *env, jobject obj) {
printf("Hello from C/C++!\\n");
}
// 动态注册本地方法
static const JNINativeMethod methods[] = {
{"sayHello", "()V", (void *)sayHello} // 方法名、签名和实现
};
// JNI_OnLoad 函数
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
// 获取 JNI 环境
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // 获取失败
}
// 查找 HelloWorld 类
jclass cls = (*env)->FindClass(env, "HelloWorld");
if (cls == NULL) {
return JNI_ERR; // 类未找到
}
// 注册本地方法
if ((*env)->RegisterNatives(env, cls, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return JNI_ERR; // 注册失败
}
return JNI_VERSION_1_6; // 返回 JNI 版本
}
// JNI_OnUnload 函数
JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {
// 清理资源(如果需要)
}
4. 代码解析
- 本地方法实现:
sayHello
是要注册的本地方法,打印一条消息。 - 方法注册数组: 使用
JNINativeMethod
结构体数组定义要注册的本地方法,包括方法名、方法签名和实现函数的指针。 - JNI_OnLoad:
- 在
JNI_OnLoad
中,首先获取JNIEnv
指针。 - 然后查找
HelloWorld
类,并使用RegisterNatives
注册本地方法。 - 如果注册成功,返回 JNI 版本;否则返回错误代码。
- 在
- JNI_OnUnload: 可选的清理函数,在共享库卸载时调用。
5. 编译和运行
要编译 C/C++ 代码,你需要链接 JNI 库。假设你将代码保存为 HelloWorld.c
,可以使用以下命令编译:
gcc -o libHelloWorld.so -shared -fPIC HelloWorld.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -L"$JAVA_HOME/lib" -ljvm
然后运行 Java 程序:
java HelloWorld
你应该能看到输出:
Hello from C/C++!
6. 总结
动态注册提供了一种灵活的方式来将本地方法与 Java 方法进行关联。通过在运行时注册本地方法,开发者可以更好地控制方法的实现和注册过程。这种方式特别适合需要根据运行时条件动态决定注册哪些方法的场景。
6.SO函数注册
1.JNI函数的静态注册
必须遵循一定的命名规则,一般是Java_包名_类名_方法名
系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用假条注册的jni函数,必然在导出表里
2.JNI函数的动态注册
通过env→RegisterNatives注册函数,以最后一次为准
3.JNINativeMethod
JNINativeMethod
是 JNI(Java Native Interface)中的一个结构体,用于在动态注册本地方法时指定本地方法的名称、签名和实现。通过这个结构体,可以在运行时将本地方法注册到 Java 类中。
1. JNINativeMethod 结构体
JNINativeMethod
的定义如下:
typedef struct {
const char *name; // 本地方法的名称
const char *signature; // 本地方法的签名
void *fnPtr; // 本地方法的指针
} JNINativeMethod;
2. 字段说明
- name: 本地方法的名称,必须与 Java 中声明的方法名称完全一致。
- signature: 方法的签名,描述了方法的参数类型和返回类型。签名的格式使用 JNI 约定,例如:
()V
表示无参数且无返回值的方法。(I)I
表示接受一个整型参数并返回一个整型值的方法。
- fnPtr: 指向实现本地方法的函数指针。该函数的参数和返回值类型必须与 Java 方法的签名匹配。
3. 动态注册本地方法的示例
以下是一个使用 JNINativeMethod
进行动态注册的完整示例。
3.1 Java 代码
首先,在 Java 中声明一个本地方法:
public class NativeLib {
// 声明本地方法
public native int add(int a, int b);
// 加载本地库
static {
System.loadLibrary("native-lib");
}
}
3.2 C/C++ 代码
接下来,在 C/C++ 中实现这个本地方法并进行动态注册:
#include <jni.h>
#include "NativeLib.h" // 生成的头文件
// 本地方法的实现
JNIEXPORT jint JNICALL Java_NativeLib_add(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b; // 返回两个整数的和
}
// JNI_OnLoad 函数,用于动态注册本地方法
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
// 定义本地方法的数组
JNINativeMethod methods[] = {
{"add", "(II)I", (void *)Java_NativeLib_add} // 注册 add 方法
};
// 查找 Java 类
jclass clazz = (*env)->FindClass(env, "NativeLib");
if (clazz == NULL) {
return -1; // 类未找到
}
// 注册本地方法
if ((*env)->RegisterNatives(env, clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return -1; // 注册失败
}
return JNI_VERSION_1_6; // 返回 JNI 版本
}
4. 关键步骤
- 实现本地方法: 实现与 Java 中声明的方法相同的 C/C++ 函数。
- JNI_OnLoad: 在这个函数中,使用
JNINativeMethod
数组定义要注册的方法,并通过RegisterNatives
注册它们。 - 查找类: 使用
FindClass
查找 Java 类,并确保成功找到。 - 注册方法: 调用
RegisterNatives
注册本地方法。
5. 总结
JNINativeMethod
是 JNI 中用于动态注册本地方法的重要结构体。通过它,开发者可以在运行时将本地实现与 Java 方法关联,从而实现更灵活的本地方法调用。
7.多个cpp文件编译成一个so
要将多个 C++ 文件编译成一个共享库(.so
文件),可以使用 Android NDK 的 CMake 或 ndk-build 工具。下面分别介绍这两种方法。
方法 1: 使用 CMake
1.1 创建 CMakeLists.txt
在你的项目目录中创建或编辑 CMakeLists.txt
文件,添加多个源文件。
cmake_minimum_required(VERSION 3.4.1)
# 设置库的名称
add_library(yourlib SHARED
file1.cpp
file2.cpp
file3.cpp
)
# 查找 JNI 库
find_library(log-lib log)
# 链接库
target_link_libraries(yourlib ${log-lib})
1.2 在 build.gradle 中配置 CMake
在你的 build.gradle
文件中,确保配置了 CMake 的使用:
android {
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
1.3 编译
在 Android Studio 中同步项目并构建,CMake 将会编译所有指定的源文件并生成一个共享库。
方法 2: 使用 ndk-build
2.1 创建 Android.mk
在你的项目目录中创建或编辑 Android.mk
文件,添加多个源文件。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := yourlib
LOCAL_SRC_FILES := file1.cpp file2.cpp file3.cpp
include $(BUILD_SHARED_LIBRARY)
2.2 创建 Application.mk(可选)
如果需要,你可以创建一个 Application.mk
文件来指定 ABI 或其他设置:
APP_ABI := arm64-v8a # 只编译 ARM64
2.3 编译
在 Android Studio 中,运行 ndk-build 命令,NDK 将会编译所有指定的源文件并生成一个共享库。
3. 总结
无论使用 CMake 还是 ndk-build,都可以轻松地将多个 C++ 文件编译成一个共享库。选择哪种方法取决于你的项目结构和个人偏好。CMake 更加灵活且适合大型项目,而 ndk-build 简单直接,适合小型项目。
8.编译多个so(了解)
要编译多个共享库(.so
文件)在 Android NDK 中,可以使用 CMake 或 ndk-build。下面分别介绍这两种方法。
方法 1: 使用 CMake
1.1 创建 CMakeLists.txt
在你的项目目录中创建或编辑 CMakeLists.txt
文件,以编译多个共享库。
cmake_minimum_required(VERSION 3.4.1)
# 编译第一个共享库
add_library(libone SHARED
libone_file1.cpp
libone_file2.cpp
)
# 编译第二个共享库
add_library(libtwo SHARED
libtwo_file1.cpp
libtwo_file2.cpp
)
# 查找 JNI 库
find_library(log-lib log)
# 链接库
target_link_libraries(libone ${log-lib})
target_link_libraries(libtwo ${log-lib})
1.2 在 build.gradle 中配置 CMake
在你的 build.gradle
文件中,确保配置了 CMake 的使用:
android {
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}
}
1.3 编译
在 Android Studio 中同步项目并构建,CMake 将会编译所有指定的源文件并生成多个共享库。
方法 2: 使用 ndk-build
2.1 创建 Android.mk
在你的项目目录中创建或编辑 Android.mk
文件,以编译多个共享库。
LOCAL_PATH := $(call my-dir)
# 编译第一个共享库
include $(CLEAR_VARS)
LOCAL_MODULE := libone
LOCAL_SRC_FILES := libone_file1.cpp libone_file2.cpp
include $(BUILD_SHARED_LIBRARY)
# 编译第二个共享库
include $(CLEAR_VARS)
LOCAL_MODULE := libtwo
LOCAL_SRC_FILES := libtwo_file1.cpp libtwo_file2.cpp
include $(BUILD_SHARED_LIBRARY)
2.2 创建 Application.mk(可选)
如果需要,可以创建一个 Application.mk
文件来指定 ABI 或其他设置:
APP_ABI := arm64-v8a # 只编译 ARM64
2.3 编译
在 Android Studio 中,运行 ndk-build 命令,NDK 将会编译所有指定的源文件并生成多个共享库。
3. 总结
无论使用 CMake 还是 ndk-build,都可以轻松地编译多个共享库。选择哪种方法取决于你的项目结构和个人偏好。CMake 更加灵活且适合大型项目,而 ndk-build 简单直接,适合小型项目。
9.so之间的相互调用(了解)
在 Android NDK 中,多个共享库(.so
文件)之间可以相互调用。要实现这一点,需要确保在编译时正确链接这些库,并在代码中使用 JNI 进行方法调用。以下是实现共享库之间相互调用的步骤。
1. 创建多个共享库
假设我们有两个共享库:libone
和 libtwo
。libone
中有一个方法需要调用 libtwo
中的方法。
1.1 libone 的 CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 编译 libtwo
add_library(libtwo SHARED
libtwo_file1.cpp
)
# 编译 libone
add_library(libone SHARED
libone_file1.cpp
)
# 查找 JNI 库
find_library(log-lib log)
# 链接 libtwo 到 libone
target_link_libraries(libone libtwo ${log-lib})
1.2 libtwo 的 CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# 编译 libtwo
add_library(libtwo SHARED
libtwo_file1.cpp
)
# 查找 JNI 库
find_library(log-lib log)
target_link_libraries(libtwo ${log-lib})
2. 实现方法
2.1 libtwo_file1.cpp
在 libtwo
中实现一个简单的方法:
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "libtwo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_yourapp_LibTwo_nativeMethod(JNIEnv *env, jobject obj) {
LOGI("This is a method from libtwo");
}
2.2 libone_file1.cpp
在 libone
中调用 libtwo
的方法:
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "libone"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_yourapp_LibOne_nativeMethod(JNIEnv *env, jobject obj) {
// 找到 libtwo 的类
jclass libtwoClass = env->FindClass("com/example/yourapp/LibTwo");
if (libtwoClass == nullptr) {
LOGI("Failed to find class LibTwo");
return;
}
// 找到 libtwo 中的方法
jmethodID methodID = env->GetStaticMethodID(libtwoClass, "nativeMethod", "()V");
if (methodID == nullptr) {
LOGI("Failed to find method nativeMethod");
return;
}
// 调用 libtwo 的方法
env->CallStaticVoidMethod(libtwoClass, methodID);
}
3. 在 Java 中使用
在 Java 中声明这两个本地方法,并加载相应的库:
public class LibOne {
static {
System.loadLibrary("libone");
}
public native void nativeMethod();
}
public class LibTwo {
static {
System.loadLibrary("libtwo");
}
public static native void nativeMethod();
}
4. 编译与运行
- 编译: 确保在
CMakeLists.txt
中正确设置了库的依赖关系,并在build.gradle
中配置 CMake。 - 运行: 在 Java 中调用
LibOne.nativeMethod()
,这将触发libone
中的方法,并间接调用libtwo
中的方法。
5. 总结
通过上述步骤,你可以在一个共享库中调用另一个共享库中的方法。确保在 CMake 中正确链接库,并使用 JNI 在 C++ 中调用其他库的方法。这样可以实现多个共享库之间的相互调用和协作。
dlopen
、dlsym
和 dlclose
在 Android NDK 中,dlopen
、dlsym
和 dlclose
是用于动态加载共享库(.so
文件)的函数。这些函数允许你在运行时加载和使用动态库中的符号。以下是这些函数的详细说明及示例。
1. 函数说明
dlopen
: 动态加载共享库并返回一个句柄。dlsym
: 根据名称获取共享库中的符号(函数或变量)。dlclose
: 关闭之前通过dlopen
打开的共享库。
2. 使用示例
以下是一个简单的示例,演示如何使用 dlopen
、dlsym
和 dlclose
来动态加载和调用一个共享库中的函数。
2.1 创建共享库
首先,创建一个共享库,例如 libmylib.so
,其中包含一个简单的函数:
// mylib.cpp
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "MyLib"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_yourapp_MyLib_nativeFunction(JNIEnv *env, jobject obj) {
LOGI("Hello from mylib!");
}
2.2 编写动态加载代码
在你的主应用中,使用 dlopen
、dlsym
和 dlclose
来加载并调用这个库:
#include <jni.h>
#include <dlfcn.h>
#include <android/log.h>
#define LOG_TAG "NativeLoader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_yourapp_MainActivity_loadNativeLib(JNIEnv *env, jobject obj) {
// 动态加载共享库
void *handle = dlopen("libmylib.so", RTLD_LAZY);
if (!handle) {
LOGI("Failed to load library: %s", dlerror());
return;
}
// 获取函数指针
typedef void (*nativeFunction_t)(JNIEnv *, jobject);
nativeFunction_t nativeFunction = (nativeFunction_t)dlsym(handle, "Java_com_example_yourapp_MyLib_nativeFunction");
const char *dlsym_error = dlerror();
if (dlsym_error) {
LOGI("Failed to find function: %s", dlsym_error);
dlclose(handle);
return;
}
// 调用函数
nativeFunction(env, obj);
// 关闭共享库
dlclose(handle);
}
3. 在 Java 中调用
在你的 Java 代码中,声明本地方法并调用它:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("yourlib"); // 确保加载包含动态加载代码的库
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 调用动态加载的本地方法
loadNativeLib();
}
public native void loadNativeLib();
}
4. 注意事项
- 库路径:
dlopen
的参数是库的名称。如果库在默认路径下(如/data/app/<package>/lib/<arch>
),可以直接使用库名。如果在其他路径,则需要提供完整路径。 - 错误处理: 使用
dlerror()
函数获取dlopen
和dlsym
的错误信息。 - 资源管理: 使用
dlclose
关闭库,释放资源。
5. 总结
通过使用 dlopen
、dlsym
和 dlclose
,你可以在 Android NDK 中动态加载和使用共享库。这种方法在需要根据条件加载不同库或实现插件机制时非常有用。
10.so路径的动态获取
在 Android 中,getPackageManager()
方法用于获取 PackageManager
实例,该实例提供了与应用程序包相关的信息和操作。你可以使用它来获取应用的元数据、安装的应用列表、权限信息等。
1. 使用 getPackageManager()
以下是如何在 Java 中使用 getPackageManager()
的示例:
1.1 获取应用信息
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
public class AppInfoUtil {
public static void printAppInfo(Context context) {
PackageManager packageManager = context.getPackageManager();
String packageName = context.getPackageName();
try {
// 获取应用的 PackageInfo
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
ApplicationInfo appInfo = packageInfo.applicationInfo;
// 打印应用信息
Log.i("AppInfo", "App Name: " + appInfo.loadLabel(packageManager));
Log.i("AppInfo", "Package Name: " + packageInfo.packageName);
Log.i("AppInfo", "Version Name: " + packageInfo.versionName);
Log.i("AppInfo", "Version Code: " + packageInfo.versionCode);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}
2. 获取已安装应用列表
你还可以使用 PackageManager
获取设备上已安装的所有应用程序的信息:
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
public class InstalledAppsUtil {
public static void listInstalledApps(Context context) {
PackageManager packageManager = context.getPackageManager();
List<PackageInfo> packages = packageManager.getInstalledPackages(0);
for (PackageInfo packageInfo : packages) {
ApplicationInfo appInfo = packageInfo.applicationInfo;
Log.i("InstalledApp", "App Name: " + appInfo.loadLabel(packageManager));
Log.i("InstalledApp", "Package Name: " + packageInfo.packageName);
}
}
}
3. 获取应用权限
你还可以使用 PackageManager
来获取应用的权限信息:
import android.content.pm.PackageManager;
public class PermissionsUtil {
public static void printAppPermissions(Context context) {
PackageManager packageManager = context.getPackageManager();
String packageName = context.getPackageName();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
String[] requestedPermissions = packageInfo.requestedPermissions;
if (requestedPermissions != null) {
for (String permission : requestedPermissions) {
Log.i("AppPermission", "Permission: " + permission);
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}
4. 总结
getPackageManager()
是一个非常有用的方法,可以帮助你获取与应用程序包相关的各种信息。通过 PackageManager
,你可以获取应用的基本信息、已安装应用的列表以及应用的权限等。这些功能在开发 Android 应用时非常重要。
在 Android 中,getPackageManager()
方法本身并不能直接获取共享库(.so
文件)的路径,但你可以结合 PackageManager
和 ApplicationInfo
来获取应用的本地库目录路径。这是因为共享库通常存放在应用的 lib
目录下。
获取 .so
文件路径的步骤
- 获取应用信息:使用
getApplicationInfo()
方法获取应用的ApplicationInfo
。 - 获取本地库目录:从
ApplicationInfo
中获取nativeLibraryDir
属性,该属性指向.so
文件所在的目录。
以下是实现的示例代码:
1. 获取本地库路径
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.util.Log;
public class NativeLibPathUtil {
public static String getNativeLibPath(Context context) {
String nativeLibPath = null;
try {
// 获取 PackageManager
PackageManager packageManager = context.getPackageManager();
// 获取应用的 ApplicationInfo
ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), 0);
// 获取本地库目录路径
nativeLibPath = appInfo.nativeLibraryDir;
} catch (PackageManager.NameNotFoundException e) {
Log.e("NativeLibPathUtil", "Failed to get application info", e);
}
return nativeLibPath;
}
}
2. 在 Activity 中使用
你可以在你的 Activity 中调用这个方法来获取 .so
文件的路径:
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取共享库路径
String nativeLibPath = NativeLibPathUtil.getNativeLibPath(this);
Log.i("NativeLibPath", "Native Library Path: " + nativeLibPath);
}
}
3. 运行时的输出
当你运行这个应用时,日志中将输出 .so
文件的路径,通常类似于:
/data/app/com.example.yourapp/lib/arm64
4. 总结
通过使用 getPackageManager()
和 ApplicationInfo
,你可以轻松获取 Android 应用中共享库(.so
文件)的路径。这在需要动态加载库或调试时非常有用。
更多推荐
所有评论(0)