一. NDK简介
1.1 为什么把代码放到so中
- c历史悠久,有很多现成代码使用
- c的效率比java高
- c相对java更难反编译
1.2 什么是JNI
Java Native Interface 从java1.1开始jni标准成为java平台的一部分,允许java和其他语言代码交互
1.3 什么是NDK
交叉编译工具链https://developer.android.com/ndk/guides
NDK: 原生开发套件
CMake: 用于指导构建so
LLDB: Android Studio 用于调试原生代码的调试程序
1.4 ABI与指令集
不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口 (ABI)
1.5 在so中接触到的东西
jni调用
系统库函数
加密算法
魔改算法
系统调用
自定义算法
so的加固和混淆
so的dump 内存中的so是解密后的
so的修复 修复dump下来的so
so的文件结构
自定义linker
二. 第一个NDK项目
2.1 NDK项目与Java项目的区别
在New > Project > Native C++创建NDK项目
-
NDK开发流程
-
Java代码中存在加载so和声明使用到的so函数
java层需要加载so文件 (对应so文件名为libnativetest.so)
1
2
3
|
static {
System.loadLibrary("nativetest");
}
|
使用native关键字声明native函数,具体实现在native中
1
|
public native String stringFromJNI();
|
-
build.gradle中添加部分代码用于支持ndk开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
android {
......
defaultConfig {
......
//指定cmake设置,c++标准
externalNativeBuild {
cmake{
cppFlags "-std=c++11"
}
}
// 指定编译的so文件支持的平台,默认4种
ndk{
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
externalNativeBuild {
//指定cmakelists文件路径和cmake版本
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
......
}
|
-
main/cpp/目录下的CMakeLists.txt和cpp文件
CMakeLists.txt用于指导编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 编译so的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
# 项目名称
project("nativetest")
# 设置so的名字,类型,源文件
add_library(${CMAKE_PROJECT_NAME}
SHARED
native-lib.cpp)
# 链接相关依赖
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
|
native-lib.cpp中有具体的代码实现java层声明的函数
1
2
3
4
5
6
7
8
9
10
|
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativetest_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
|
三. NDK基础知识
3.1 静态注册
3.1.1 java代码
- native关键字用于声明该函数为native层函数
- static{ System.loadLibrary(“libname”); }用于加载动态库文件
- 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
|
package com.example.nativetest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.example.nativetest1.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
public native String stringFromJNI();//java层声明JNI方法
static {
System.loadLibrary("nativetest1");//导入库文件
}
private ActivityMainBinding binding;//注册绑定类
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);//调用父类方法
binding = ActivityMainBinding.inflate(getLayoutInflater());//绑定布局
setContentView(binding.getRoot());//将根视图作为主视图
TextView tv = binding.sampleText;//通过binding类直接获取组件
tv.setText(stringFromJNI());//调用native方法
}
}
|
3.1.2 cpp代码
1
2
3
4
5
6
7
8
9
10
|
#include <jni.h>
#include <string>
//静态注册需要保证以C形式导出 且函数名=Java_PackageName_ClassName_MethodName才能正常被加载
extern "C" JNIEXPORT jstring JNICALL//指定以c方式编译 指定为JNI导出函数 返回值类型 调用方式
Java_com_example_nativetest_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());//将c字符串转换为java字符串返回
}
|
-
extern “C"保证以C语言命名规则导出函数,防止c++的名称粉碎影响函数绑定
-
native函数命名规则
函数名=Java_PackageName_ClassName_MethodName,加载库文件后会自动根据函数名进行绑定
-
JNIEnv jobject/jclass
和java层对接的native函数都有JNIEnv* env和jobject参数
jclass是java层对象, jobject是native函数对应java层方法所属的java层对象实例
当native函数的声明改为static时使用jclass,因为静态方法可以通过类直接调用,无需创建对象实例
-
NewstringUTF
java层数据和native层数据不互通,如果native的数据最后要转到java层处理就需要NewstringUTF进行类型转换NewstringUTF可以成为一个hook点
-
JNIIMPORT / JNICALL / JNIEXPORT
JNIIMPORT和JNICALL宏未指定内容
JNIEXPORT宏添加可见性属性为默认(可见)
1
2
3
|
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL
|
3.1.3 layout代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
|
3.1.4 逆向分析结果
根据函数名直接找到JNI函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
__int64 __fastcall Java_com_example_nativetest1_MainActivity_stringFromJNI(_JNIEnv *a1)
{
const char *v1; // rax
__int64 v3; // [rsp+18h] [rbp-48h]
char v4[24]; // [rsp+40h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
std::string::basic_string<decltype(nullptr)>(v4, "Hello from C++");
v1 = (const char *)sub_20CC0((__int64)v4);
v3 = _JNIEnv::NewStringUTF(a1, v1);
std::string::~string(v4);
return v3;
}
|
恢复符号后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
__int64 __fastcall Java_com_example_nativetest1_MainActivity_stringFromJNI(_JNIEnv *env)
{
const char *v1; // rax
__int64 v3; // [rsp+18h] [rbp-48h]
char str[24]; // [rsp+40h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
std::string::basic_string<decltype(nullptr)>(str, "Hello from C++");
v1 = (const char *)strTocstr((__int64)str);
v3 = _JNIEnv::NewStringUTF(env, v1);
std::string::~string(str);
return v3;
}
|
3.2 动态注册
动态注册的函数名称无需遵守静态注册的函数名称规则
并且不要求函数是导出类型,安全性高于静态注册
3.2.1 java代码
xml布局代码同上,java代码额外声明add和sub函数用于测试动态注册
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
|
package com.example.nativetest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import com.example.nativetest1.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
public native String stringFromJNI();
public native int add(int x,int y);
public native int sub(int x,int y);
static {
System.loadLibrary("nativetest");
}
private ActivityMainBinding binding;//注册绑定类
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());//绑定布局
setContentView(binding.getRoot());//将根视图作为主视图
TextView tv = binding.sampleText;//通过binding类直接获取组件
Log.d("add",""+add(51,5));
tv.setText(stringFromJNI()+add(7,8));
}
}
|
3.2.2 动态注册相关知识
RegisterNatives
该函数用于注册native函数,用于将java层方法和native层函数绑定,声明如下
1
2
3
4
|
//clazz:指定的类,即 native 方法所属的类
//methods:方法数组
//nMethods:方法数组的长度,即有多少个动态注册的导出函数
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
|
JNINativeMethod
该结构体用于保存java层函数和native层函数映射关系
1
2
3
4
5
|
typedef struct {
const char* name; // java层的方法名
const char* signature; // 方法签名,与函数参数类型和返回值类型有关,例如 ()Ljava/lang/String;
void* fnPtr; // native层的函数指针
} JNINativeMethod;
|
方法签名signature
方法签名用于描述方法的参数和返回值类型,基本格式如下
例如有以下函数,他们的方法签名对应关系如下
1
2
3
|
void func();//"()V"
jint add(jint x,jint y);//"(II)I"
jstring stringFromJNI();//"()Ljava/lang/String;" 注意String后要跟;
|
签名规则
-
基本类型参照类型和签名字符映射表
-
引用类型以"L"开头,以”;“结尾,中间用”/“隔开包及类名,其对应的C函数的参数类型为jobject()
例如:Ljava/lang/String; 、Ljava/net/Socket; 等
注意: String类对应C类型为jstring而非jobject
-
如果java函数位于一个内部类,则用$作为类名间的分隔符
例如:“Landroid/os/FileUtils$FileStatus;”
JNI数据类型
Java类型 |
Native类型 |
签名字符 |
描述 |
基本类型 |
|
|
|
void |
void |
V |
|
boolean |
jboolean |
Z |
C/C++8位整型 |
byte |
jbyte |
B |
C/C++带符号的8位整型 |
char |
jchar |
C |
C/C++无符号的16位整型 |
short |
jshort |
S |
C/C++带符号的16位整型 |
int |
jint |
I |
C/C++带符号的32位整型 |
long |
jlong |
J |
C/C++带符号的64位整型 |
float |
jfloat |
F |
C/C++32位浮点型 |
double |
jdouble |
D |
C/C++64位浮点型 |
Object |
jobject |
Ljava/lang/Object; |
任何Java对象,或者没有对应java类型的对象 |
Class |
jclass |
Ljava/lang/Class; |
Class对象 |
String |
jstring |
Ljava/lang/String; |
字符串对象 |
数组类型 |
|
|
|
Object[] |
jobjectArray |
|
任何对象的数组 |
boolean[] |
jbooleanArray |
[Z |
布尔型数组 |
byte[] |
jbyteArray |
[B |
比特型数组 |
char[] |
jcharArray |
[C |
字符型数组 |
short[] |
jshortArray |
[S |
短整型数组 |
int[] |
jintArray |
[I |
整型数组 |
long[] |
jlongArray |
[J |
长整型数组 |
float[] |
jfloatArray |
[F |
浮点型数组 |
double[] |
jdoubleArray |
[D |
双浮点型数组 |
3.2.3 cpp代码
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
|
#include <jni.h>
#include <string>
//native函数对应的java方法所属类
static const char *ClassName="com/example/nativetest1/MainActivity";
jint add(JNIEnv* env,jobject obj,jint x,jint y){
return x+y;
}
jint sub(JNIEnv* env,jobject obj,jint x,jint y){
return x-y;
}
//定义本地方法数组 建立映射关系
//JNINativeMethod结构体成员为函数名,函数签名,函数指针
static JNINativeMethod methods[]={
{"add","(II)I",(void*)add},
{"sub","(II)I",(void*)sub}
};
//编写加载注册方法
jint JNI_OnLoad(JavaVM* vm,void* reserved){
JNIEnv* env=NULL;//获取JNIEnv对象
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK)
return -1;
//获取对应java方法所属的类对象
jclass clazz=env->FindClass(ClassName);
//调用RegisterNatives注册方法
if(clazz){
env->RegisterNatives(clazz,methods,sizeof(methods)/sizeof(methods[0]));
return JNI_VERSION_1_6;//注意必须返回JNI版本
}
else
return -1;
}
|
注意: JNI_OnLoad函数最后一定要返回JNI_VERSION才能成功执行,否则程序会崩溃
使用registerNativeMethods方法的作用:
-
改变丑陋的长方法名
-
提高效率
当Java类别透过VM呼叫到Native函数时,通常是依靠VM去动态寻找.so中的Native函数(因此它们才需要特定规则的命名格式)
如果某方法需要连续呼叫很多次,则每次都要寻找一遍,使用RegisterNatives将本地函数向VM进行登记,可以让其更快找到函数
-
运行时动态调整Native函数与Java函数之间的映射关系
通过多次调用registerNativeMethods()方法,并传入不同的映射表参数即可实现
3.2.4 逆向分析结果
恢复符号后
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
|
__int64 __fastcall JNI_OnLoad(_JavaVM *vm)
{
__int64 clazz; // [rsp+10h] [rbp-30h]
_JNIEnv *env; // [rsp+30h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
env = 0LL;
if ( (unsigned int)_JavaVM::GetEnv(vm, (void **)&env, 0x10006) )// vm &env JNI_VERSION
{
return (unsigned int)-1;
}
else
{
clazz = _JNIEnv::FindClass(env, className);
if ( clazz )
{
_JNIEnv::RegisterNatives(env, clazz, methods, 2LL);
return 65542;
}
else
{
return (unsigned int)-1;
}
}
}
|
其中methods数组如下:
第一个是Java层函数名 第二个是方法签名 第三个是函数地址

跟进查看函数
1
2
3
4
|
__int64 __fastcall add(__int64 env, __int64 obj, int x, int y)
{
return (unsigned int)(y + x);
}
|
3.3 JNI_OnLoad
so中各种函数的执行顺序: init->init_array->JNI_OnLoad
JNI_OnLoad格式如下
1
2
3
4
5
6
7
8
|
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGD("GetEnv failed");
return -1;
}
return JNI_VERSION_1_6;//共有1_1 1_2 1_4 1_6这4个版本
}
|
注意:
- so中可以不定义JNI_OnLoad,定义后在so被加载时自动执行
- 必须返回JNI版本号,如果so中没有提供JNI_OnLoad,VM默认该so使用最老的JNI 1.1版本
- so被卸载时会调用JNI_UnLoad
1
|
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);
|
3.4 JavaVM
JavaVM是JVM虚拟机在JNI中的表示,一个JVM中只有一个JavaVM实例,这个实例是线程共享的。
通过JNIEnv可以获取一个Java虚拟机实例,vm用于存放返回的虚拟机指针实例
1
|
jint GetJavaVM(JNIEnv *env, JavaVM **vm);//获取成功时返回0
|
定义如下
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
|
//C语言版JavaVM定义
struct JNIInvokeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);//用于主线程中获取env
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);//用于子线程中获取env
};
//C++版本JavaVM是C版本的封装,逆向时看到的都是C版本
struct _JavaVM {
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }//c++传递this指针省略了C版的JavaVM*参数
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
|
JavaVM的获取方式:
-
JNI_OnLoad的第一个参数
-
JNI_OnUnload的第一个参数
-
env->GetJavaVM
3.5 JNIEnv
JNIEnv一般是是由虚拟机传入,且与线程相关的变量,也就是说线程A不能使用线程B的JNIEnv。
作为一个结构体,它定义了JNI的相关操作函数,其定义如下:
C版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
struct JNINativeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
void* reserved3;
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
jsize);
jclass (*FindClass)(JNIEnv*, const char*);
jmethodID (*FromReflectedMethod)(JNIEnv*, jobject);
jfieldID (*FromReflectedField)(JNIEnv*, jobject);
/* spec doesn't show jboolean parameter */
jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean);
jclass (*GetSuperclass)(JNIEnv*, jclass);
jboolean (*IsAssignableFrom)(JNIEnv*, jclass, jclass);
......
}
|
C++版本,对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
26
|
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
jsize bufLen)
{ return functions->DefineClass(this, name, loader, buf, bufLen); }
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
......
#endif
}
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
#else
typedef const struct JNINativeInterface* JNIEnv;
#endif
|
_JNIEnv中定义了一个functions变量,指向JNINativeInterface该结构聚合了所有JNI函数指针
所以可以通过env调用其中的函数
1
2
|
env->FindClass("java/lang/String");//c++写法
(*env)->FindClass(env, "java/lang/String") //c写法
|
JNIEnv的获取方式
- 函数静态/动态注册时的第一个参数
- 主线程JavaVM->GetEnv()
- 子线程JavaVM->AttachCurrentThread()
3.6 NativeLog
相当于java层的log.d()函数
使用宏定义进行封装,支持格式化字符串
LOGD(…)宏定义中,省略号表示是可变参数宏,可接收任意数量参数
__VA_ARGS__则是可变参数占位符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "glass"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativetest_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
LOGD("LOGD test%d",1);//
LOGI("LOGI test%d,%d",1,2);
LOGE("LOGE test%d,%d,%d",1,2,3);
return env->NewStringUTF(hello.c_str());
}
|
3.7 NDK多线程
在native层使用pthread_create()创建一个线程,函数声明如下
1
|
int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
|
参数1是指向pthread的指针,也是线程id
参数2是线程属性
参数3是线程执行的函数
参数4是函数参数
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
|
#include <jni.h>
#include <string>
#include <android/log.h>
#define TAG "glass"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
void myThread(){
LOGD("Hello,myThread!")
pthread_exit(0);//退出线程
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativetest_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
pthread_t pthread;
//线程id 线程属性 执行的函数 函数参数
//默认线程属性为joinable,主线程结束时子线程结束
pthread_create(&pthread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);
//使用join可以阻塞主线程直到子线程结束
pthread_join(pthread, nullptr);
return env->NewStringUTF(hello.c_str());
}
|
3.8 so的相关知识
so中,在导出表,导入表的函数一般可以通过frida的api直接获得函数地址
不在导出表/导入表/符号表的函数需要手动计算地址
3.8.1 多个cpp文件编译成一个so
- 创建cpp源文件,编写代码
- 修改CMakeLists.txt,添加源文件
默认源文件为native-lib.cpp,创建test.cpp文件并添加
1
2
3
4
5
|
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp
test.cpp
)
|
3.8.2 编译多个so
通过cmakelists中使用add_library()添加新的so,注意需要target_link_libraries()进行链接
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
|
cmake_minimum_required(VERSION 3.22.1)
project("nativetest")
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp
)
add_library(
test
SHARED
test.cpp
)
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log)
target_link_libraries(
test
android
log
)
|
3.8.3 动态获取so的路径
每次安装apk时,/data/app/<app_path>/<package_name>/lib/中<app_path>是随机的
所以需要动态获取app的路径,以下代码获取app的so所在目录,具体的so需要使用目录+名称获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public String getPath(Context context){
PackageManager packageManager=context.getPackageManager();//获取包管理器
List<PackageInfo> packageInfoList=packageManager.getInstalledPackages(0);//获取已安装的包列表
if(packageInfoList==null||packageInfoList.size()==0)
return null;
//遍历列表,寻找目标app
for(PackageInfo packageInfo:packageInfoList){
//用户app在/data/app/内,用于过滤系统app,包名用于确定目标app
if(packageInfo.applicationInfo.nativeLibraryDir.startsWith("/data/app/")
&&packageInfo.packageName.startsWith("com.example.app")){
return packageInfo.applicationInfo.nativeLibraryDir;
}
}
return null;
}
|
3.8.4 so之间的相互调用
- 导入dlfcn.h,使用dlopen函数
- 修改cmakelists的链接选项实现
四. JNI相关操作
4.1 创建java对象
4.1.1 NewObject创建对象
调用java层对象的构造函数,创建新的对象直接使用
1
2
3
4
5
|
jclass clazz=env->FindClass("com/example/nativetest/MainActivity");//获取java类
//参数为jclass,方法名,方法签名 <init>表示构造函数
jmethodID jmethodId=env->GetMethodID(clazz,"<init>","()V");//获取方法id
jobject reflectObj=env->NewObject(clazz,jmethodId);//通过类和方法id,调用构造方法创建对象
LOGD("ReflectObject %p",reflectObj);//使用对象
|
4.1.2 AllocObject创建对象
给对象分配内存初始化,但需要初始化对象再使用
1
2
3
4
5
|
jclass clazz = env->FindClass("com/example/nativetest/MainActivity");
jmethodID methodID2 = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;I)V");
jobject ReflectDemoObj2 = env->AllocObject(clazz);
jstring jstr = env->NewStringUTF("from jni str");
env->CallNonvirtualVoidMethod(ReflectDemoObj2, clazz, methodID2, jstr, 100);
|
4.2 访问java属性
4.2.1获取静态字段
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
|
//1. 获取静态字段id
jclass clazz = env->FindClass("com/example/nativetest/MainActivity");
//java类,字段名,字段类型
jfieldID privateStaticStringField =
env->GetStaticFieldID(clazz, "privateStaticStringField", "Ljava/lang/String;");
// 根据字段类型选择不同GetStaticxxxField函数 此处java的String为引用型,使用Object
// env->GetStaticBooleanField();
// env->GetStaticIntField();
// env->GetStaticShortField();
// env->GetStaticByteField();
// env->GetStaticCharField();
// env->GetStaticFloatField();
// env->GetStaticDoubleField();
// env->GetStaticLongField();
// env->GetStaticObjectField();
//2. 获取字段并进行类型转换
jstring privateStaticString =
static_cast<jstring>(env->GetStaticObjectField(clazz, privateStaticStringField));
//jstring在java虚拟机,需要通过env获取对应c字符串使用
const char* privatecstr = env->GetStringUTFChars(privateStaticString, nullptr);
LOGD("privateStaticString: %s", privatecstr);
//使用完后需要释放,否则会导致出错
env->ReleaseStringUTFChars(privateStaticString, privatecstr);
|
4.2.2 获取实例字段
获取实例字段前必须拥有实例对象,可以通过java层传递或者native层创建
不能直接从native层获取java层实例
1
2
3
4
5
6
7
8
|
jclass clazz = env->FindClass("com/example/nativetest/MainActivity");
//创建实例并通过实例获取字段
jmethodID methodID = env->GetMethodID(clazz, "<init>", "()V");
jobject instance = env->NewObject(clazz, methodID);
//GetxxxFiled方法
jstring publicString = static_cast<jstring>(env->GetObjectField(instance, publicStringField));
LOGD("privateStaticString: %s", privatecstr);
env->ReleaseStringUTFChars(privateStaticString, privatecstr);
|
4.2.3 设置字段
1
2
|
//对象,字段id,值
env->SetObjectField(ReflectDemoObj, privateStringFieldID, env->NewStringUTF("glass"));
|
1
2
3
4
5
6
7
8
9
10
11
|
jfieldID privateStringFieldID = env->GetFieldID(clazz, "privateStringField", "Ljava/lang/String;");
jstring privateString = static_cast<jstring>(env->GetObjectField(ReflectDemoObj, privateStringFieldID));
const char* privateCstr = env->GetStringUTFChars(privateString, nullptr);
LOGD("privateStringField old: %s", privateCstr);
env->ReleaseStringUTFChars(privateString, privateCstr);
//使用SetObjectField修改字段并重新打印
env->SetObjectField(ReflectDemoObj, privateStringFieldID, env->NewStringUTF("glass"));
privateString = static_cast<jstring>(env->GetObjectField(ReflectDemoObj, privateStringFieldID));
privateCstr = env->GetStringUTFChars(privateString, nullptr);
LOGD("privateStringField new: %s", privateCstr);
env->ReleaseStringUTFChars(privateString, privateCstr);
|
4.3 访问java数组
java层定义如下
1
2
|
byte[] byteArray;
public native String stringFromJNI();
|
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
|
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativetest_MainActivity_stringFromJNI(JNIEnv* env,jobject ReflectDemoObj) {
std::string hello = "Hello from C++";
//1. 获取java的byteArray对象
jclass clazz=env->FindClass("com/example/nativetest/MainActivity");
//获取Java对象的class和byte数组的字段ID
jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray", "[B");
//通过GetObjectField获取byte数组对象,并且通过GetArrayLength获取byte数组的长度。
jbyteArray byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID));
int byteArrayLength = env->GetArrayLength(byteArray);
//2. 修改byteArray内容
//在C++中创建一个char类型的数组cByteArray,并且为其赋值。
char cByteArray[10];
for(int i = 0; i < 10; i++){
cByteArray[i] = static_cast<char>(100 - i);
}
//将cByteArray中的元素转换成jbyte类型的数组。
const jbyte *java_array = reinterpret_cast<const jbyte *>(cByteArray);
//使用SetByteArrayRegion函数将java数组元素设置为cByteArray中的元素。
env->SetByteArrayRegion(byteArray, 0, byteArrayLength, java_array);
//3. 判断修改结果
//再次获取Java对象的byte数组,并且获取byte数组的元素。
byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID));
byteArrayLength = env->GetArrayLength(byteArray);
//使用GetByteArrayElements函数将byte数组的元素转换到C++中的char数组CBytes中。
char* CBytes = reinterpret_cast<char *>(env->GetByteArrayElements(byteArray, nullptr));
//遍历CBytes数组元素,输出数组内容。
for(int i = 0; i < byteArrayLength; i++) {
LOGD("CArray: %d", CBytes[i]);
}
//释放CBytes数组元素,将其转移回byte数组中。
env->ReleaseByteArrayElements(byteArray, (jbyte*)CBytes, 0);
return env->NewStringUTF(hello.c_str());
}
|
4.4 访问java方法
4.4.1 调用静态方法
获取类和方法id后直接调用即可
1
2
3
4
|
jclass clazz=env->FindClass("com/example/nativetest/MainActivity");
jmethodID publicStaticFuncId=env->GetStaticMethodID(clazz,"publicStaticFunc","()V");
env->CallStaticVoidMethod(clazz, publicStaticFuncId);
// CallStaticxxxMethod
|
4.4.2 调用实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//获取类和方法id
jclass clazz=env->FindClass("com/example/nativetest/MainActivity");
jmethodID privateFuncID =env->GetMethodID(clazz,"privateFunc","(Ljava/lang/String;I)Ljava/lang/String;");
//创建参数并调用
jstring str1 = env->NewStringUTF("this is from JNI");
jstring retval_jstring = static_cast<jstring>(env->CallObjectMethod(ReflectDemoObj, privateFuncID, str1, 1000));
//处理返回值
const char* retval_cstr = env->GetStringUTFChars(retval_jstring, nullptr);
LOGD("privateStaticString: %s", retval_cstr);
env->ReleaseStringUTFChars(retval_jstring, retval_cstr);
// env->CallBooleanMethod();
// env->CallVoidMethod();
// env->CallByteMethod();
// env->CallShortMethod();
// env->CallIntMethod();
// env->CallCharMethod();
// env->CallDoubleMethod();
// env->CallLongMethod();
// env->CallFloatMethod();
// env->CallObjectMethod();
|
4.4.3 CallVoidMethodA
CallVoidMethod底层调用的是CallVoidMethodV
区别在于CallVoidMethod会帮我们封装参数,而CallVoidMethodV需要我们自己封装参数(使用较少)
1
2
3
4
5
6
7
8
9
10
11
|
void CallVoidMethod(jobject obj, jmethodID methodID, ...)
{
va_list args;
va_start(args, methodID);
functions->CallVoidMethodV(this, obj, methodID, args);
va_end(args);
}
void CallVoidMethodV(jobject obj, jmethodID methodID, va_list args)
{ functions->CallVoidMethodV(this, obj, methodID, args); }
void CallVoidMethodA(jobject obj, jmethodID methodID, const jvalue* args)
{ functions->CallVoidMethodA(this, obj, methodID, args); }
|
对于CallVoidMethodA,它的参数是jvalue*,而jvalue是一个联合体,可以存储任意类型
1
2
3
4
5
6
7
8
9
10
11
|
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
|
调用CallVoidMethodA并通过jvalue传递参数
1
2
3
4
5
6
7
8
9
10
|
jmethodID privateFuncID =
env->GetMethodID(clazz,"privateFunc","(Ljava/lang/String;I)Ljava/lang/String;");
jstring str2 = env->NewStringUTF("this is from JNI2");
jvalue args[2];
args[0].l = str2;
args[1].i = 1000;
jstring retval = static_cast<jstring>(env->CallObjectMethodA(ReflectDemoObj, privateFuncID, args));
const char* cpp_retval = env->GetStringUTFChars(retval, nullptr);
LOGD("cpp_retval: %s", cpp_retval);
env->ReleaseStringUTFChars(retval, cpp_retval);
|
4.5 访问java父类方法
通过env->CallNonvirtualXXXMethod()可以调用父类方法
activity类的onCreate方法中必定调用父类的onCreate,否则程序崩溃
1
2
3
4
|
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
|
可以修改为native方法
1
2
|
@Override
protected native void onCreate(Bundle savedInstanceState);
|
1
2
3
4
5
6
7
8
9
|
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativetest_MainActivity_onCreate(
JNIEnv *env, jobject thiz,jobject saved_instance_state) {
jclass AppCompatActivityClazz=env->FindClass("androidx/fragment/app/FragmentActivity");//获取父类
jmethodID onCreateID=env->GetMethodID(AppCompatActivityClazz,"onCreate", "(Landroid/os/Bundle;)V");//获取onCreate方法
env->CallNonvirtualVoidMethod(thiz,AppCompatActivityClazz,onCreateID,saved_instance_state);//调用父类onCreate()
}
//CallNonvirtualXXXMethod根据不同返回值类型也有多个版本
|
4.6 内存管理
4.6.1 局部引用
局部引用只能在函数体内部使用,不能跨函数使用,即使定义为全局变量也不行
-
大多数的jni函数返回结果都是局部引用
因此一般不用env->NewLocalRef()创建局部引用
-
函数体内部的局部引用数量有限
如果需要大量使用需要及时调用env->DeleteLocalRef()删除局部引用
-
当函数体内需要大量使用局部引用时, 可以使用 env->DeleteLocalRef 删除局部引用
-
其他函数
env->EnsureLocalcapacity(num) 判断是否有num个局部引用可以使用,足够则返回0
以下两个函数用于批量管理局部引用
env->PushLocalFrame(num)
env->PopLocalFrame(nullptr)
1
2
3
4
5
6
7
8
9
10
11
|
env->PushLocalFrame(100);//开辟100个局部引用空间
if(env->EnsureLocalCapacity(100) == 0) {
for(int i = 0; i < 3; i++){
jstring tempString = env->NewStringUTF("glass");
env->SetObjectArrayElement(_jstringArray, i, tempString);
//env->DeleteLocalRef(tempString);
sleep(1);//防止执行太快导致log失效
LOGD("env->EnsureLocalCapacity");
}
}
env->PopLocalFrame(nullptr);//释放空间
|
4.6.2 全局引用
当需要跨函数使用引用时,可以定义全局变量并设置全局引用
使用env->NewGlobalRef 和 env->DeleteGlobalRef 创建和删除全局引用
1
2
3
|
jobject tempClassLoaderObj = env->CallObjectMethod(MainActivityClazz, getClassLoaderID);//获取局部引用
ClassLoaderObj = env->NewGlobalRef(tempClassLoaderObj);//设置为全局引用
env->DeleteGlobalRef(ClassLoaderObj);//删除全局引用
|
4.6.3 弱全局引用
与全局引用基本相同,但可能被回收,关键函数如下:
env->NewWeakGlobalRef
env->DeleteWeakGlobalRef
4.7 子线程中获取java类
-
子线程中可以直接获取系统类,但不能获取用户类
1
2
3
4
|
jclass ClassLoaderClazz = env->FindClass("java/lang/ClassLoader");
LOGD("myThread ClassLoaderClazz: %p", ClassLoaderClazz);
jclass StringClasszz=env->FindClass("java/lang/String");
LOGD("myThread StringClasszz: %p", StringClasszz);
|
-
主线程中获取类,设置为全局引用传递给子线程(常用)
1
2
3
4
5
|
jclass MainActivityClazz = env->FindClass("com/example/javaandso/MainActivity");
globalClass= static_cast<jclass>(env->NewGlobalRef(MainActivityClazz));
//子线程中调用
LOGD("myThread globalClass: %p", globalClass);
|
-
在主线程中获取ClassLoader,在子线程中去加载类(麻烦,不推荐)
4.8 init和initarray
so加载后,函数的执行顺序为: init>initarray>JNI_OnLoad
一般在init和initarray中执行so和字符串解密,用于后续JNI_OnLoad进行绑定
4.8.1 init
使用方法如下,名称必须为_init()
1
2
3
|
extern "C" void _init(){
LOGD("_init ");
}
|
_init在ida反编译后为.init_proc
4.8.2 initarray
使用_attribute_ ((constructor(priority)))修饰符修饰的函数会在initarray中执行
规则如下:
- 优先执行指定了优先级的函数,再执行未指定优先级的函数
- priority值越小的优先级越高,最好从100以后开始
- 未指定优先级的函数的执行顺序按照定义的先后顺序
故下列函数的执行顺序为 test3>test4>test1>test5>test2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
__attribute__ ((constructor(101))) void initArrayTest3(){
LOGD("initArrayTest3");
}
__attribute__ ((constructor)) void initArrayTest5(){
LOGD("initArrayTest5");
}
__attribute__ ((constructor(303))) void initArrayTest1(){
LOGD("initArrayTest1 ");
}
__attribute__ ((constructor)) void initArrayTest2(){
LOGD("initArrayTest2 ");
}
__attribute__ ((constructor(202))) void initArrayTest4(){
LOGD("initArrayTest4");
}
|
另外可以使用visibility(“hidden”)属性去除函数符号
1
2
3
|
__attribute__ ((constructor, visibility("hidden"))) void initArrayTest6(){
LOGD("initArrayTest6");
}
|
initarray反编译后在.init_array段中
4.9 Java代码Native化
以java层的MainActivity.onCreate()方法为例
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
|
package com.example.nativetest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.example.nativetest.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("nativetest");
}
private ActivityMainBinding binding;
@Override
protected native void onCreate(Bundle savedInstanceState);
// @Override
// protected void onCreate(Bundle savedInstanceState) {
// super.onCreate(savedInstanceState);
// binding = ActivityMainBinding.inflate(getLayoutInflater());
// setContentView(binding.getRoot());
//
// // Example of a call to a native method
// TextView tv = binding.sampleText;
// tv.setText("Hello,World");
// }
}
|
cpp代码-静态注册版
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
|
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativetest_MainActivity_onCreate(
JNIEnv *env, jobject thiz,jobject saved_instance_state) {
//1. 获取java的activity对象
jclass mainActivity=env->GetObjectClass(thiz);
//2. 调用父类方法 super.onCreate()
jclass superActivity=env->GetSuperclass(mainActivity);//直接获取父类
//jclass superActivity=env->FindClass("androidx/fragment/app/FragmentActivity");//手动寻找父类
jmethodID onCreateID=env->GetMethodID(superActivity, "onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz, superActivity, onCreateID, saved_instance_state);//调用父类方法
//调用方法时,需要传入调用的类实例,方法所属根类,方法id,参数
//3. 获取binding并调用inflate方法 ActivityMainBinding.inflate(getLayoutInflater())
//getLayoutInflater()
jclass Activity=env->FindClass("android/app/Activity");
jmethodID getLayoutInflater=env->GetMethodID(Activity,"getLayoutInflater", "()Landroid/view/LayoutInflater;");
jobject layoutInflater=env->CallObjectMethod(thiz,getLayoutInflater);
//bingding=ActivityMainBinding.inflate(getLayoutInflater())
jclass ActivityMainBinding=env->FindClass("com/example/nativetest/databinding/ActivityMainBinding");//此处ide找不到,因为binding类此时不存在,在编译时生成
jmethodID inflate=env->GetStaticMethodID(ActivityMainBinding,"inflate","(Landroid/view/LayoutInflater;)Lcom/example/nativetest/databinding/ActivityMainBinding;");
jobject binding=env->CallStaticObjectMethod(ActivityMainBinding,inflate,layoutInflater);
//4. 使用setContentView绑定布局
jclass AppCompatActivity=env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID setContentView=env->GetMethodID(AppCompatActivity,"setContentView", "(Landroid/view/View;)V");
//binding.getRoot()
jmethodID getRoot=env->GetMethodID(ActivityMainBinding,"getRoot","()Landroidx/constraintlayout/widget/ConstraintLayout;");
jobject layout=env->CallObjectMethod(binding,getRoot);
//setContentView(binding.getRoot())
env->CallVoidMethod(thiz,setContentView,layout);
//5. 创建TextView并设置内容 TextView.setText()
jclass TextView=env->FindClass("android/widget/TextView");
jfieldID sampleText=env->GetFieldID(ActivityMainBinding,"sampleText","Landroid/widget/TextView;");
jobject textView=env->GetObjectField(binding,sampleText);
jmethodID setText=env->GetMethodID(TextView, "setText", "(Ljava/lang/CharSequence;)V");
env->CallVoidMethod(textView,setText,env->NewStringUTF("Hello,Native!"));
}
|
cpp代码-动态注册版
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
|
#include <jni.h>
#include <string>
void func(JNIEnv *env, jobject thiz,jobject saved_instance_state){
......
//内容同上Java_com_example_nativetest_MainActivity_onCreate
......
}
JNINativeMethod methods[]={
{"onCreate", "(Landroid/os/Bundle;)V",(void*)func}
};
jint JNI_OnLoad(JavaVM* vm,void* reserved){
const char* ClassName="com/example/nativetest/MainActivity";
//1. 获取JNIEnv对象
JNIEnv* env=NULL;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK)
return -1;
//2. 获取对应java方法所属的类对象,并调用RegisterNatives注册方法
jclass clazz=env->FindClass(ClassName);
if(clazz){
env->RegisterNatives(clazz,methods,sizeof(methods)/sizeof(methods[0]));
return JNI_VERSION_1_6;//注意必须返回JNI版本
}
else
return -1;
}
|
反汇编效果如下
JNI_OnLoad

func

五. References
- Android官方NDK指南
- Android开发学习笔记——NDK开发
- Android JNI编程—JNI基础
- [原创]《安卓逆向这档事》十二、大佬帮我分析一下
- Android JNI学习(三)——Java与Native相互调用