侧边栏壁纸
  • 累计撰写 38 篇文章
  • 累计创建 23 个标签
  • 累计收到 10 条评论

目 录CONTENT

文章目录

【Android逆向】二、NDK开发详解(基本完结)

Fup1p1
2023-01-11 / 1 评论 / 1 点赞 / 1,659 阅读 / 36,776 字 / 正在检测是否收录...

01.NDK开发详解

image-1673427059897

image-1673427245211

NDK简述

image-1673427349590
使用NDK开发的so不再具有跨平台特性,需要编译提供不同平台支持ABl:ApplicationBinary Interface。
官方文档:NDK
image-1673428031989
现在很多x86也可以通过二进制翻译器去加载arm32框架的so文件。

Android Studio里演示

新建一个native C++

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk01_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

image-1673430090945

int add(int a,int b){
    return a+b;
}
extern "C" int add1(int a,int b){
    return a+b;
}

两者IDA解析出的就完全不一样
image-1673441956659
image-1673441968658
添加extern “C” 保留了函数符号,未添加的则按C++一样name mangling。
如果是C语言编写的,没有添加extern “C”,虽然能编译,但是不能通过链接,会报错。

MainActivity

package com.example.ndk01;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import com.example.ndk01.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'ndk01' library on application startup.
    static {
        System.loadLibrary("ndk01");
    }

    private ActivityMainBinding binding;
    @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(stringFromJNI());
        MainActivity.staticfunc();

    }

    /**
     * A native method that is implemented by the 'ndk01' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
    public native void myfirstjni();
    public static native void staticfunc();
}

native-lib.cpp

#include <jni.h>
#include <string>
#include <android/log.h>
extern "C"{//注意一定要添加extern "C"
#include "testc.h"
}
int add(int a,int b){
    return a+b;
}
extern "C" int add1(int a,int b){
    return a+b;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk01_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT void JNICALL Java_com_example_ndk01_MainActivity_myfirstjni(JNIEnv* env,jobject /* this */) {

}
extern "C" JNIEXPORT void JNICALL Java_com_example_ndk01_MainActivity_staticfunc(JNIEnv* env,jclass /* this */) {
    int result=add_c(4,6);
    __android_log_print(ANDROID_LOG_INFO,"JNI","JNI->%d",result);
}

testc.c &&testc.h 以及修改在CMakeLists.txt中添加testc.c

testc.c

//
// Created by MESSI on 2023-01-11.
//
#include "testc.h"
int add_c(int a,int b){
    return a+b;
}

testc.h

//
// Created by MESSI on 2023-01-11.
//

#ifndef NDK01_TESTC_H
#define NDK01_TESTC_H

#endif //NDK01_TESTC_H
#include<stdio.h>
int add_c(int a,int b);

output

image-1673446776301

JNI类型映射

image-1673449266597

image-1673523338630

image-1673523320416

image-1673523602192

02.NDK开发提升性能

image-1673524157131

image-1673524771402

image-1674807065894

image-1674807101859

比较JNI 与 JAVA速度之间的差异

image-1674807319066

image-1674807738525

Android4.4环境下,使其运行在纯解释模式下。

image-1674807806130
JNI的速度是Java的4倍多。

Android6.0环境下,在quick模式下运行dex2oat的汇编代码

image-1674808094627
鉴于代码不是特别复杂,速度也是快了接近一倍。

java 引用数据类型

image-1674812252955

java中数组类型和JNI的数组类型

image-1674812451764

JNIEnv

image-1674812710773

Jni中字符串操作

image-1674811960994

String returnstring=testjstringapis("hello from jni");
        Log.i("Fup1p1","result"+returnstring);
extern "C" JNIEXPORT jstring JNICALL Java_com_example_ndk01_MainActivity_testjstringapis(JNIEnv* env,jobject /* this */,jstring content) {
    const char * a=env->GetStringUTFChars(content, nullptr);
    int jstring_size=env->GetStringUTFLength(content);
    if(a!= nullptr){
        __android_log_print( ANDROID_LOG_INFO,"Fup1p1","char content:%s size:%d",a,jstring_size);
    }
    env->ReleaseStringUTFChars(content,a);
    jstring result=env->NewStringUTF("Hello from jni");
    return result;
}

image-1674811922887

image-1674812642039

03.Java反射思维和NDK开发

java反射:java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意类的静态属性和方法,都能够完成对静态属性的获取和设置以及静态方法的调用;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。而对于public类型的成员变量和方法和属性都可以使用反射来进行访问。

JAVA反射的相关类

image-1674969774048

Class类

image-1674969827315

获得类中属性相关的方法

image-1674969862148

获得类中构造器相关的方法

image-1674969923326

获得类中方法相关的方法

image-1674969954667

类中其他重要的方法

image-1674970029046

Field、Method、Constructor类

image-1674970172802

image-1674970189106

image-1674970215502

image-1674970437331

代码实践

反射实现

Test类

package com.example.reflecttest;

import android.util.Log;

public class Test {
    public String flag=null;
    public Test(){
        flag="Test()";
    }
    public Test(String arg){
        flag="Test(String arg)";
    }
    public Test(String arg,int arg2){
        flag="Test(){String arg,int arg2}";
    }
    public static  String publicstaticfield="i am a public staticField";
    public  String publicNonstaticfield="i am a public non staticField";

    private static String privatestaticField="i am a private staticField";
    private String privatenonstaticField="i am a private non staticField";

    public static void publicStaticfunc(){
        Log.i("Fup1p1","i am from publicStaticfunc");
    }
    public void PublicNonstaticfunc(){
        Log.i("Fup1p1","i am from publicnonstaticfunc");
    }
    private static void privateStaticfunc(){
        Log.i("Fup1p1","i am from privateStaticfunc");
    }
    private void  privateNonstaticfunc(){
        Log.i("Fup1p1","i am from privatenonstaticfunc");
    }

}
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(stringFromJNI());
        test();
    }
public void test(){
        Test a_obj=new Test();
        Test b_obj=new Test("test");
        Test c_obj=new Test("test",2);
//方法一,通过classloader来获取。先获得classloader,再通过loadClass,对它进行加载
        try {
            Class testclazz=MainActivity.class.getClassLoader().loadClass("com.example.reflecttest.Test");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 //方法二,通过两个API,通过Class.forName获取。
        Class testclazz2;
        try {
            testclazz2=Class.forName("com.example.reflecttest.Test");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        
 //方法三,本例特殊,Test类和MainActivity类在一起,可以直接获取。
        Class testclazz3;
        testclazz3=Test.class;
        
        
        try {
        //指定域
            Field publicstaticfield=testclazz3.getDeclaredField(("publicstaticfield"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        //遍历域
        Field[] fields=testclazz3.getDeclaredFields();
        for (Field i :fields){
            Log.i("Fup1p1","getDeclaredFields->"+i);
        }
        Field[] fields1=testclazz3.getFields();
        for (Field i :fields1){
            Log.i("Fup1p1","getFields->"+i);
        }
    }

image-1674973408995
我们可以发现getDeclaredFields()与getFields()的区别。后者不能访问私有属性。

访问Public属性

            Field publicstaticfield=testclazz3.getDeclaredField(("publicstaticfield"));
            Log.i("Fup1p1","getDeclaredfield "+publicstaticfield);
            String content = (String)publicstaticfield.get(null);
            Log.i("Fup1p1","getDeclaredfield "+content);
            content=(String) publicstaticfield.get(a_obj);
            Log.i("Fup1p1","getDeclaredfield "+content);

image-1674977972409

访问私有属性

那么对于私有属性,也能get到吗?
我们换一个私有属性进行测试。
image-1674978110264
因为进行了权限检查,导致私有域不能直接访问,所以程序抛出异常,难道没有其他办法了吗?
真有!!!

            Field privatestaticField=testclazz3.getDeclaredField(("privatestaticField"));
            Log.i("Fup1p1","getDeclaredfield "+privatestaticField);
            privatestaticField.setAccessible(true);
            String content = (String)privatestaticField.get(null);
            Log.i("Fup1p1","getDeclaredfield "+content);
            content=(String) privatestaticField.get(a_obj);
            Log.i("Fup1p1","getDeclaredfield "+content);
privatestaticField.setAccessible(true);

通过上面的API,我们可以取消权限检查,使得访问成功!
image-1674978260447

修改属性

对于修改私有属性,我们只需要

privatestaticField.setAccessible(true);
privatestaticField.set(null,"Fup1p1's field");//static属性,所以obj填null

image-1674978718102

访问方法

    public void testMethod(){
        Class testclazz=Test.class;
        Method[] methods=testclazz.getDeclaredMethods();
        for(Method i:methods){
            Log.i("Fup1p1","getDeclaredMethods()-> "+i);
        }
        Method[] methods1=testclazz.getMethods();
        for(Method i:methods1){
            Log.i("Fup1p1","getMethods()-> "+i);
        }

    }

image-1674979842055
咦,getmethod()获得了更多方法,但是没有private方法。至于多出来的方法,是因为Test类继承object(虽然我们没有写),而getmethod()将继承的公有方法也一并输出了。

调用方法

对于公有方法,直接使用invoke()调用方法


        Method publicStaticfunc_method= null;
        try {
            publicStaticfunc_method = testclazz.getDeclaredMethod("publicStaticfunc");
            publicStaticfunc_method.invoke(null);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        Log.i("Fup1p1","getDeclaredMethods()-> "+publicStaticfunc_method);

对于私有方法,同之前访问Field一样,只需添加setAccessible(true)即可。

        Method privateStaticfunc_method= null;
        try {
            privateStaticfunc_method = testclazz.getDeclaredMethod("privateStaticfunc");
            privateStaticfunc_method.setAccessible(true);
            privateStaticfunc_method.invoke(null);//static方法,所以参数填null。
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        Log.i("Fup1p1","getDeclaredMethods()-> "+privateStaticfunc_method);

image-1674980639269

访问构造器

        Constructor[] constructors=testclazz.getDeclaredConstructors();
        for (Constructor i :constructors){
            Log.i("Fup1p1","getDeclaredConstructors()-> "+i);
        }

image-1674980998204

通过反射构造器创建实例

        try {
            Constructor constructor=testclazz.getConstructor(String.class,int.class);
            Object testobj=constructor.newInstance("test",888);
            Field privatefield=testclazz.getDeclaredField("privatestaticField");
            privatefield.setAccessible(true);
            String privatefiled_string= (String) privatefield.get(testobj);
            Log.i("Fup1p1","privatestaticField-> "+privatefiled_string);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }

image-1674981697526

jni层实现java反射访问公有属性

Java_com_example_reflecttest_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    jclass TestJclass=env->FindClass("com/example/reflecttest/Test");
    jfieldID publicstaticfield_jfieldID=env->GetStaticFieldID(TestJclass,"publicstaticfield", "Ljava/lang/String;");
    jobject publicstaticfield_content=env->GetStaticObjectField(TestJclass,publicstaticfield_jfieldID);
    const char* content_ptr=env->GetStringUTFChars(static_cast<jstring>(publicstaticfield_content), nullptr);
    __android_log_print(4,"Fup1p1->jni","jni->%s",content_ptr);
    return env->NewStringUTF(hello.c_str());
}

image-1674982826949

小补充

注意:一个进程JVM中只有一个JavaVM对象。VM是多执行绪(Multi-threading) ,每个JNIEnv都是不同的!特别是在不同线程,都是独自维护各自独立的JNIEnv。
image-1679055923859

image-1679056150505

image-1679056190568

image-1679056256380

image-1679056471512

image-1679056387878

04.JavaVM与JNIEnv

JavaVM

其中,JavaVM虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,该进程的所有线程都可以使用此JavaVM。

JavaEnv

JNIEnv指针是Native世界中Java环境的代表,通过该指针Native世界就可以访问Java世界的代码进行操作,其具有以下主要特点及作用:

1.JNIEnv *只对创建它的线程有效,不能跨线程传递,不同线程的JNIEnv是相互独立的;
2.通过JNIEnv *可调用Java的方法;
3.通过JNIEnv *可操作Java中的变量和对象

通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就能在不同的线程中调用Java方法了,采用这种方式的要退出线程时调用DetachCurrentThread函数来释放资源。也许你有疑问,JavaVM不是有接口 GetEnv,线程内不可以用GetEnv去获得属于自己的env呢?

只有先AttachCurrentThread到JavaVM,分配到了独立的JNIEnv之后,GetEnv第二个参数二级指针返回的env才有值。就是说JavaVM->GetEnv获取的是,此线程有效的env。JavaVM->AttachCurrentThread是向虚拟机分配线程独立的env。 所以一般在线程执行函数第一句是AttachCurrentThread,随后就能用GetEnv了。

例子

JNI_OnLoad

JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void* reserved){
    globalVM=vm;
    __android_log_print(4,"Fup1p1->jni"," JNI_OnLoad(JavaVM* vm,void* reserved) vm-> %p",vm);
    __android_log_print(4,"Fup1p1->jni","jni->%s","JNI_Onload is called");
    jint result=0;
    JNIEnv *env= nullptr;
    if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)==JNI_OK){
        __android_log_print(4,"Fup1p1->jni","jni->%s","vm->GetEnv((void**)&env,JNI_VERSION_1_6) success");
    }
    __android_log_print(4,"Fup1p1->jni","JNI env-> %p",env);
    pthread_t  thread;
    pthread_create(&thread, nullptr,threadtest, nullptr);
    pthread_join(thread, nullptr);
    result=JNI_VERSION_1_6;
    return result;
}

线程内

void *threadtest(void *args){
    for(int i=0;i<10;i++){
        __android_log_print(4,"Fup1p1->jni","jni->%s,%d","i am from threadtest",i);
    }
    JNIEnv *threadenv= nullptr;
    int result = globalVM->AttachCurrentThread(&threadenv, nullptr);//这样threadenv就能调用丰富的Api了。
    if(result==JNI_OK){
        __android_log_print(4,"Fup1p1->jni","jni->%s","GlobalVm->AttachCurrentThread");
        jstring jstring1=threadenv->NewStringUTF("threadtest jstring");
        const char* content=threadenv->GetStringUTFChars(jstring1, nullptr);
        __android_log_print(4,"Fup1p1->jni","jni->%s",content);
        threadenv->ReleaseStringUTFChars(jstring1,content);
    }
}

05.JNI新建对象、访问Java中属性、访问java方法

JNI新建对象的两种方式

image-1679404017337

extern "C" JNIEXPORT void JNICALL
Java_com_example_reflecttest_MainActivity_newObject(
        JNIEnv* env,
        jobject /* this */) {
   
    jclass Testjclass=env->FindClass("com/example/reflecttest/Test");
    jmethodID con_mid=env->GetMethodID(Testjclass,"<init>","(Ljava/lang/String;)V");//对于构造函数来说,都是固定的,为“<init>”
    
     //NewObject
    jstring arg=env->NewStringUTF("i am from jni");
    jobject  testobj=env->NewObject(Testjclass,con_mid,arg);
    if(testobj!= nullptr){
        __android_log_print(4,"Fup1p1->jni","jni->%s","New Object success");
    }
    
    //AllocObject
     jobject testobj2 =env->AllocObject(Testjclass);
    jstring arg1=env->NewStringUTF("I am from jni->AllocObject");
    env->CallNonvirtualVoidMethod(testobj2,Testjclass,con_mid,arg1);
    if(testobj2!= nullptr){
        __android_log_print(4,"Fup1p1->jni","jni->%s","AllocObject success");
    }

}

访问Java中的属性

访问静态属性

访问静态Public属性
extern "C" JNIEXPORT void JNICALL
Java_com_example_reflecttest_MainActivity_getStaticFiled(
        JNIEnv* env,
        jobject /* this */) {
    //private static
    jclass Testjclass=env->FindClass("com/example/reflecttest/Test");
    jfieldID publicStaticField_fid=env->GetStaticFieldID(Testjclass,"publicstaticfield", "Ljava/lang/String;");
    jstring publicStaticField_obj= static_cast<jstring>(env->GetStaticObjectField(Testjclass,
                                                                                  publicStaticField_fid));
    const char* punlicStaticField_content=env->GetStringUTFChars(publicStaticField_obj, nullptr);
    __android_log_print(4,"Fup1p1->jni","getStaicField_obj->%s",punlicStaticField_content);

}
访问静态私有属性

发现只把上面的public改成private,也能正常输出。这和java反射有所不同,不需要取消权限检查。

string 类型
jfieldID privateStaticField_fid=env->GetStaticFieldID(Testjclass,"privatestaticField", "Ljava/lang/String;");
    jstring privateStaticField_obj= static_cast<jstring>(env->GetStaticObjectField(Testjclass,
                                                                                  privateStaticField_fid));
    const char* privateStaticField_content=env->GetStringUTFChars(privateStaticField_obj, nullptr);
    __android_log_print(4,"Fup1p1->jni","getStaicField_obj->%s",privateStaticField_content);
int 类型

相比较string,少一步类型转换而已。

    jfieldID publicStaticField_int_fid=env->GetStaticFieldID(Testjclass,"pubicStaticField_int","I");
    jint publicStaticField_int_value=env->GetStaticIntField(Testjclass,publicStaticField_int_fid);
    __android_log_print(4,"Fup1p1->jni","getStaicField_int_value->%d",publicStaticField_int_value);
修改静态属性
jstring setjstring=env->NewStringUTF("modified by jni");
    env->SetStaticObjectField(Testjclass,publicStaticField_fid,setjstring);

    jstring publicStaticField_obj= static_cast<jstring>(env->GetStaticObjectField(Testjclass,
                                                                                  publicStaticField_fid));
    const char* punlicStaticField_content=env->GetStringUTFChars(publicStaticField_obj, nullptr);
    __android_log_print(4,"Fup1p1->jni","getStaicField_obj->%s",punlicStaticField_content);

访问非静态属性

访问非静态公开属性

由于不需要与私有一样,这里省略。

访问非静态私有属性

由于是非静态,所以需要带上参数。

Test testobj=new Test("i am from java");
        getNonStaticField(testobj);
extern "C" JNIEXPORT void JNICALL
Java_com_example_reflecttest_MainActivity_getNonStaticField(
        JNIEnv* env,
        jobject/**/ ,jobject testobj) {
    //private String
    jclass Testjclass=env->FindClass("com/example/reflecttest/Test");
    jfieldID privateField_fid=env->GetFieldID(Testjclass,"privatenonstaticField","Ljava/lang/String;");
    jstring privateFieldobj= static_cast<jstring>(env->GetObjectField(testobj, privateField_fid));
    const char *privateField_obj_content=env->GetStringUTFChars(privateFieldobj, nullptr);
    __android_log_print(4,"Fup1p1->jni","privateField_obj->%s",privateField_obj_content);
}
修改非静态属性

小问一手ChatGPT-4:

在JNI层,访问权限检查主要在Java层进行。当你在Java层通过反射访问私有属性时,需要显式关闭访问权限检查,以避免安全性异常(如 IllegalAccessException)。然而,在JNI层,访问权限检查并不会发生,因此你可以直接通过GetFieldID访问私有属性。
所以我们在编写代码的时候不需要取消权限检查。

修改private string
    jstring newString=env->NewStringUTF("Modified by jni");
    env->SetObjectField(testobj,privateField_fid,newString);//修改值

    privateFieldobj= static_cast<jstring>(env->GetObjectField(testobj, privateField_fid));
    privateField_obj_content=env->GetStringUTFChars(privateFieldobj, nullptr);//重新获取一下值
    __android_log_print(4,"Fup1p1->jni","privateField_obj_->%s",privateField_obj_content);

image-1681374136992

修改private int
    jfieldID  publicField_fid=env->GetFieldID(Testjclass,"pubicField_int","I");
    jint public_int_value=env->GetIntField(testobj,publicField_fid);
    __android_log_print(4,"Fup1p1->jni","publicField_int_obj->%d",public_int_value);

    env->SetIntField(testobj,publicField_fid,114514);
    public_int_value=env->GetIntField(testobj,publicField_fid);
    __android_log_print(4,"Fup1p1->jni","publicField_int_obj->%d",public_int_value);

image-1681375497775

访问&&修改array
在test类中先创建一个数组

构造函数中初始化数组

public class Test {
    public String flag=null;
    public int[] intarray=null;
    public Test(){
        flag="Test()";
        intarray=new int[10];
        for(int i=0;i<10;i++){
            intarray[i]=i;
        }

    }
    ......
}
访问array
jfieldID intarray_fid=env->GetFieldID(Testjclass,"intarray","[I");
    jintArray intarray_obj= static_cast<jintArray>(env->GetObjectField(testobj, intarray_fid));

    int arraylength=env->GetArrayLength(intarray_obj);//获取数组长度
    __android_log_print(4,"Fup1p1->jni","array_length->%d",arraylength);

    int* array=env->GetIntArrayElements(intarray_obj, nullptr);//获取数组的指针
    for (int i=0;i<arraylength;i++){
        __android_log_print(4,"Fup1p1->jni","array[%d]->%d",i,array[i]);
    }
修改array
 jint jint_array[arraylength];
    for (int i=0;i<arraylength;i++){
        jint_array[i]=10-i;
    }
    const jint* ptr=jint_array;
    env->SetIntArrayRegion(intarray_obj,0,arraylength,ptr);
    array=env->GetIntArrayElements(intarray_obj, nullptr);
    for (int i=0;i<arraylength;i++){
        __android_log_print(4,"Fup1p1->jni","array[%d]->%d",i,array[i]);
    }
效果(如果在java层输出,发现数组也已经被改变)

image-1681377902553

访问java方法

访问静态方法

    jclass clazz=env->FindClass("com/example/reflecttest/Test");
    jmethodID publicfuncid=env->GetStaticMethodID(clazz,"publicStaticfunc","()V");
    env->CallStaticVoidMethod(clazz,publicfuncid);

访问非静态方法

    jclass clazz=env->FindClass("com/example/reflecttest/Test");
    jmethodID methodid=env->GetMethodID(clazz,"<init>","()V");
    jmethodID privateNonstaticfuncid=env->GetMethodID(clazz,"privateNonstaticfunc","()V");
    jobject reflectobj=env->NewObject(clazz,methodid);
    env->CallVoidMethod(reflectobj,privateNonstaticfuncid);

思考:调用一个参数和返回值都是数组的方法?

    public static int[] privateStaticFunc(String[] str){
        StringBuilder retval=new StringBuilder();
        for(String i:str){
            retval.append(i);
        }
        Log.i("Fup1p1","this is privateStaticFunc"+retval.toString());
        return new int[]{0,1,2,3,4,5,6,7,8,9};
    }
 jclass clazz = env->FindClass("com/example/reflecttest/Test");
  jclass strclazz = env->FindClass("java/lang/String");
    jobjectArray strarray = env->NewObjectArray(3, strclazz, nullptr);

    //相当于 java 中 String strarray[] =new String[3];
    for (int i = 0; i < 3; i++) {
        jstring str3 = env->NewStringUTF("114514");
        env->SetObjectArrayElement(strarray, i, str3);
    }
    /* 等于for(int i=0;i<3;i++){
            strarray[i]="114514";
        }
    */
    jmethodID privateStaticFuncid=env->GetStaticMethodID(clazz,"privateStaticFunc","([Ljava/lang/String;)[I");
    jintArray  intArray= static_cast<jintArray>(env->CallStaticObjectMethod(clazz,privateStaticFuncid,
                                                                            strarray));
    int *array=env->GetIntArrayElements(intArray, nullptr);
    int arraylength=env->GetArrayLength(intArray);
    for (int i=0;i<arraylength;i++){
        __android_log_print(4,"Fup1p1->jni","array[%d]->%d",i,array[i]);
    }
    env->ReleaseIntArrayElements(intArray,array,JNI_ABORT);//释放一个整型数组在本地代码中申请的内存

06.静态注册与动调注册

JNI函数的注册主要有两种方式:静态注册和动态注册。这两者有很大的不同,也各自具有优缺点。

静态注册:

静态注册是通过在本地代码中定义一个固定格式的函数名,并与Java方法名一一对应。在Java代码中,使用native关键字声明需要本地实现的方法,同时在本地代码中实现对应的方法。

优点:

简单:静态注册的实现比较简单,只需要遵循JNI命名规范。
性能:静态注册的函数查找时间较短,性能较好。

缺点:

命名限制:静态注册需要遵循JNI命名规范,可能导致本地代码的函数名较长且可读性较差。
扩展性:如果有大量的JNI函数,静态注册会导致代码臃肿,不利于维护。

动态注册:

动态注册是在本地代码中通过一个映射表将Java方法名与本地代码中的函数名进行关联,然后在JNI_OnLoad函数中调用RegisterNatives方法进行注册。

优点:

灵活性:动态注册可以自定义函数名,不受JNI命名规范的限制。
扩展性:可以更容易地管理和维护大量的JNI函数,有利于代码组织。

缺点:

实现复杂度:动态注册的实现比静态注册复杂,需要维护映射表并在JNI_OnLoad中进行注册。
性能:动态注册的函数查找时间可能较长,相对静态注册性能较差。

总结:

静态注册和动态注册各有优缺点。静态注册适用于简单的本地方法调用,易于实现,性能较好。而动态注册适用于复杂的JNI场景,具有更好的灵活性和扩展性,但实现相对复杂,性能可能较差。在实际应用中,可以根据项目的实际需求选择合适的JNI函数注册方式。

动调注册样例

jstring test(JNIEnv *env,jobject obj,jstring a,jint b,jbyteArray c){//JNI函数
    __android_log_print(4,"Fup1p1->jni","jni->%s","Register Success!");
    return env->NewStringUTF("Register Success!");
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved){
    globalVM=vm;
    __android_log_print(4,"Fup1p1->jni","JavaVM1->%p",vm);
    JNIEnv *Env=nullptr;
    if(vm->GetEnv((void**)&Env,JNI_VERSION_1_6)==JNI_OK){
        __android_log_print(4,"Fup1p1->jni","jni->%s","Get Version Success!");
    }
    jclass mainactivityclass=Env->FindClass("com/example/ndkmultithread/MainActivity");
    JNINativeMethod methods[]={
            {"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;",(void *)test}}//映射表
            ;
    Env->RegisterNatives(mainactivityclass,methods,sizeof(methods)/sizeof(JNINativeMethod));
    return JNI_VERSION_1_6;//JNI_OnLoad要求最后一定要返回版本号

}

如果映射表中有多个重名的JNI函数,以最后一个为准,例如:

...
 {"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;",(void *)test},
  {"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;",(void *)test1}
...

那么最后还是以test1为准。

07.so之间的相互调用

方法一. CMakeList添加参数

编译到同一个so文件下的不同cpp文件之间相互调用其函数,只需要申明一下,就可以调用。
那么对于不同so文件下的,该怎么办呢?
很简单,在CMakeList中,将两个so库link一下即可。(用IDA反编译A.so,可以在导入表中找到libBtest)

target_link_libraries( # Specifies the target library.
        ndkmultithreadA
		ndkmultithreadB
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

image-1682496681739
甚至还能看到其调用的so库

方法二. 通过dlopen,dlsym,dlclose去加载其他so文件(当然可以打开一些其他目录下的,甚至系统so文件)

首先,先学会怎么获取so库的地址

获取app的so库存放地址

public String getlibpath(Context ml){
        PackageManager pm=ml.getPackageManager();
        List<PackageInfo>pkglist=pm.getInstalledPackages(0);
        Log.i("Fup1p1","pkglist"+pkglist);
        if(pkglist==null||pkglist.size()==0)return null;
        for(PackageInfo pi:pkglist){
            if(pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")&&pi.packageName.startsWith("com.example.ndkmultithread")){  //data/app为apk安装的目录
                return pi.applicationInfo.nativeLibraryDir;
            }
        }
        return null;
    }

如果需要调用

Log.i("Fup1p1",getlibpath(getApplicationContext()));  //通过getApplicationContext()来获取当前的context

so库之间的调用

创建两个so文件),然后在CMakeList里面添加进去

新建一个cpp,在CMakeList中将它添加到到xxxB.so中去。

XXXXB.so
#include<xxx>
...
extern "C" void libBtest(){
    __android_log_print(4,"Fup1p1->jni","hello from demo.cpp");
}
//我们之后需要用到符号表,由于C++存在name mangling,编译后符号会变。所以最好添加extern "C",如果不加的话,之后需要用到它的符号表时,就需要使用IDA反编译,寻找它编译后的符号名称。
stringFromJNI函数

比如我们想在xxxA.so的stringFromJNI函数中调用xxxB.so中的函数libBtest

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkmultithread_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */,jstring path) {//注意添加参数path传入so库路径
    std::string hello = "Hello from C++";
    pthread_t thread;
    pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(Mthread), nullptr);
    pthread_join(thread,nullptr);
    const char* cpath=env->GetStringUTFChars(path, nullptr);//dlopen需要传入的类型是const char *,所以我们需要调用函数来讲jstring转成char *
    __android_log_print(4,"Fup1p1->jni","cpath->%s",cpath);
    void *soinfo=dlopen(cpath,RTLD_NOW);//

    void (*ref)();  //无类型函数指针,可以自己写一两个demo玩一下。
    ref=reinterpret_cast<void(*)()>(dlsym(soinfo,"libBtest"));//C++是强类型语言,void* 与char *之间的强制类型转换则需要通过reinterpret_cast等函数
    //对于"libBtest",如果前面是extern “C”,那么直接写原函数名即可。否则要写经过name mangling之后的符号名。

    if(ref== nullptr){
        __android_log_print(4,"Fup1p1->jni","jni->%s","ref is null!");
    }
    ref();//调用libBtest
    dlclose(soinfo);//卸载
    return env->NewStringUTF(hello.c_str());

}
MainActivity
package com.example.ndkmultithread;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.ndkmultithread.databinding.ActivityMainBinding;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'ndkmultithread' library on application startup.
    static {
        System.loadLibrary("ndkmultithreadA");//只加载xxxxA.so,然后之后通过A.so中的函数去调用B.so中的函数。
    }

    private ActivityMainBinding binding;

    @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;
        String path=getlibpath(getApplicationContext())+"/libndkmultithreadB.so";//传入xxxB.so的名称!!!(注意前缀是有lib的,一开始没加,看了半天才发现...)
        tv.setText(stringFromJNI(path));
    }

    /**
     * A native method that is implemented by the 'ndkmultithread' native library,
     * which is packaged with this application.
     */
    public String getlibpath(Context ml){
        PackageManager pm=ml.getPackageManager();
        List<PackageInfo>pkglist=pm.getInstalledPackages(0);
        Log.i("Fup1p1","pkglist"+pkglist);
        if(pkglist==null||pkglist.size()==0)return null;
        for(PackageInfo pi:pkglist){
            if(pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")&&pi.packageName.startsWith("com.example.ndkmultithread")){
                return pi.applicationInfo.nativeLibraryDir;
            }
        }
        return null;
    }
    public native String stringFromJNI(String path);
}
小补充

dlopen
对于dlopen,经常用到的两个参数:

RTLD_LAZY:在dlopen返回前,对于动态库中存在的未定义的变量(如外部变量extern,也可以是函数)不执行解析,就是不解析这个变量的地址。
RTLD_NOW:与上面不同,他需要在dlopen返回前,解析出每个未定义变量的地址,如果解析不出来,在dlopen会返回NULL,错误

对于无类型函数指针

#include<iostream>
using namespace std;
void s(){
	cout<<"114514";
}
 int main(void)
{
	void(*sptr)()=&s;
	sptr();
}

关于GetStringUTFChars函数
一般调用完最好都再加上一局 env->ReleaseStringUTFChars(…);来释放本地缓冲区,不然可能会导致内存泄漏,最终导致应用程序崩溃或者耗尽系统内存。

08.JNI中的内存管理

局部引用

大多数的jni函数,调用以后返回的结果都是局部引用
因此,env->NewLocalRef 基本不用
一个函数内的局部引用数量是有限制的,在早期的安卓系统中,体现的更为明显
当函数体内需要大量使用局部引用时,比如大循环中,最好及时删除不用的局部引用
可以使用 env->DeleteLocalRef 来删除局部引用

for (int i = 0; i < 3; i++) {
        jstring str3 = env->NewStringUTF("114514");
        env->SetObjectArrayElement(strarray, i, str3);
        env->DeleteLocalRef(str3);//这样每次都将局部引用的str3给删去。程序依然能正常运行。
    }

局部引用相关的其他函数

env->EnsureLocalCapacity(num) 判断是否有足够的局部引用可以使用,足够则返回0

if(env->EnsureLocalCapacity(100)==0){
        for (int i = 0; i < 3; i++) {
            jstring str3 = env->NewStringUTF("114514");
            env->SetObjectArrayElement(strarray, i, str3);
            env->DeleteLocalRef(str3);
            sleep(1);//日志输出速度跟不上代码执行速度,不加sleep(1),log只会输出两次。而实际上代码循环了三次。
             __android_log_print(4,"Fup1p1->jni","EnsureLocalCapacity");
        }
    }

需大量使用局部引用时,手动删除太过麻烦,可使用以下两个函数来批量管理局部引用
env->PushLocalFrame(num)
env->PopLocalFrame(nullptr)

    env->PushLocalFrame(100);
    if(env->EnsureLocalCapacity(100)==0){
        for (int i = 0; i < 3; i++) {
            jstring str3 = env->NewStringUTF("114514");
            env->SetObjectArrayElement(strarray, i, str3);
            sleep(1);
            __android_log_print(4,"Fup1p1->jni","EnsureLocalCapacity");
        }
    }
    env->PopLocalFrame(nullptr);

全局引用

在jni开发中,需要跨函数使用变量时,直接定义全局变量是没用的
需要使用以下两个方法,来创建和删除全局引用
env->NewGlobalRef
env->DeleteGlobalRef
比如我们之前的例子中经常用到jclass clazz = env->FindClass(“com/example/reflecttest/Test”);
我们能否把它定义成全局引用呢?
直接定义为全局变量,然后直接引用,会发现程序能通过编译,但是运行就会崩溃。
这时候我们就需要用到NewGlobalRef函数

jclass clazz1;//先定义一个全局变量
JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void* reserved){
    jint result=0;
    JNIEnv *env= nullptr;
    if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)==JNI_OK){
        __android_log_print(4,"Fup1p1->jni","jni->%s","vm->GetEnv((void**)&env,JNI_VERSION_1_6) success");
    }
    jclass tempclazz=env->FindClass("com/example/reflecttest/Test");
    clazz1= static_cast<jclass>(env->NewGlobalRef(tempclazz));//创建全局引用
    result=JNI_VERSION_1_6;
    return result;
}

弱全局引用

与全局引用基本相同,区别是弱全局引用有可能会被回收
env->NewWeakGlobalRef
env->DeleteWeakGlobalRef

总结

全局引用通常用于跨越多个本地方法调用来访问Java对象。当你需要在不同的本地方法中共享Java对象时,可以使用全局引用。

弱全局引用适用于那些希望引用Java对象但不阻止垃圾回收的场景,比如缓存。通过使用弱全局引用,你可以确保在内存紧张时,Java虚拟机可以回收不再需要的对象。

09.子线程中加载类

方法一. 在子线程中,findClass可以直接获取系统类

前面有例子,就不举例了,要注意的就是要先通过AttachCurrentThread获取env。

方法二.在主线程中获取类,使用全局引用来传递到子线程中

jobject classloader;//定义一个全局变量
JNIEXPORT jint JNI_OnLoad(JavaVM *vm,void *reserved){
    globalVM=vm;
    __android_log_print(4,"Fup1p1->jni","JavaVM1->%p",vm);
    JNIEnv *Env=nullptr;
    if(vm->GetEnv((void**)&Env,JNI_VERSION_1_6)==JNI_OK){
        __android_log_print(4,"Fup1p1->jni","jni->%s","Get Version Success!");
    }
    //下面一段相当于MainActivity.class.getClassLoader()
    
    jclass mainactivityclass=Env->FindClass("com/example/ndkmultithread/MainActivity");
    jclass classclazz=Env->FindClass("java/lang/Class");
    jmethodID getClassLoaderid= Env->GetMethodID(classclazz,"getClassLoader", "()Ljava/lang/ClassLoader;");
    jobject tempclassloaderobj=Env->CallObjectMethod(mainactivityclass,getClassLoaderid);
    classloader=Env->NewGlobalRef(tempclassloaderobj);//设置为全局引用

    return JNI_VERSION_1_6;

}

方法三.在主线程中获取正确的ClassLoader,在子线程中去加载类

3.1 在Java中,可以先获取类字节码,然后使用getClassLoader()来获取

Demo.class.getClassLoader()
new Demo().getClass().getClassLoader()
Class.forName(…).getClassLoader()

3.2 在jni的主线程中获取ClassLoader

3.3 在jni的子线程中使用loadClass

//MainActivity中
public static void func(){
        Log.i("Fup1p1","You have got it!");
    }
void Mthread() {
    JNIEnv *env= nullptr;
    int result = globalVM->AttachCurrentThread(&env, nullptr);
    
    jclass loadclass=env->FindClass("java/lang/ClassLoader");
    jmethodID loadclassmethodid=env->GetMethodID(loadclass,"loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    jclass MainActivityclazz=static_cast<jclass>(env->CallObjectMethod(classloader,loadclassmethodid,env->NewStringUTF("com.example.ndkmultithread.MainActivity")));
    //MainActivity.class.getClassLoader().loadClass();
    
    
    jmethodID fun=env->GetStaticMethodID(MainActivityclazz,"func","()V");
    env->CallStaticVoidMethod(MainActivityclazz,fun);
    pthread_exit(0);
}

10.init与initarray以及onCreatre Native化

so在执行JNI_Onload之前,还会执行两个构造函数init、initarray
so加固、so中字符串加密等等,一般会把相关代码放到这里

init的使用

extern “C” void _init(){		//函数名必须为_init
   ......
}

IDA中怎么找到这个函数呢?查看导出表,搜_init,搜不到?搜一搜.init_proc。为什么,不是加了extern “C”吗?
问了一手ChatGPT4:编译器为了避免潜在的命名冲突,可能会给函数名加上特定的前缀或后缀。
┓( ´∀` )┏

initarray的使用

__attribute__ ((constructor)) void initArrayTest1(){ ... }
__attribute__ ((constructor(200))) void initArrayTest2(){ ... }
__attribute__ ((constructor(101))) void initArrayTest3(){ ... }
__attribute__ ((constructor, visibility("hidden"))) void initArrayTest4(){ ... }//visibility("hidden"),这样函数名就隐藏了(符号表被去除了),IDA的导出表就看不见这个函数了。
constructor后面的值,较小的先执行,最好从100以后开始用
如果constructor后面没有跟值,那么按定义的顺序,从上往下执行

onCreate Native化

有些安全开发的喜欢这样搞。
自己改写了一遍,快累死了…

    @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;
        String path=getlibpath(getApplicationContext())+"/libndkmultithreadB.so";
        Log.i("Fup1p1",stringFromJNI2("114514",100,"114514".getBytes()));
        tv.setText(stringFromJNI(path));
    }

Native化,前方高能

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndkmultithread_MainActivity_onCreate(JNIEnv *env, jobject thiz,
                                                      jobject saved_instance_state) {


    //        super.onCreate(savedInstanceState);
     jclass clazz=env->FindClass("androidx/fragment/app/FragmentActivity");
    jmethodID oncreateid=env->GetMethodID(clazz,"onCreate", "(Landroid/os/Bundle;)V");
    env->CallNonvirtualVoidMethod(thiz,clazz,oncreateid,saved_instance_state);
//-----------------------------------------------------------------------------------------------------------------------------------------------------------

    jclass Activityclazz=env->FindClass("android/app/Activity");
    jmethodID getLayoutInflaterid =env->GetMethodID(Activityclazz,"getLayoutInflater", "()Landroid/view/LayoutInflater;");
    jobject  LayoutInflater=env->CallObjectMethod(thiz,getLayoutInflaterid);

    jclass ActivityMainBindingclazz= env->FindClass("com/example/ndkmultithread/databinding/ActivityMainBinding");//这个类是编译后产生的,与页面绑定的
    jmethodID inflateid =env->GetStaticMethodID(ActivityMainBindingclazz,"inflate","(Landroid/view/LayoutInflater;)Lcom/example/ndkmultithread/databinding/ActivityMainBinding;");
    jobject ActivityMainBindingobj= env->CallStaticObjectMethod(ActivityMainBindingclazz,inflateid,LayoutInflater);

//        binding = ActivityMainBinding.inflate(getLayoutInflater());
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
    jmethodID getrootid=env->GetMethodID(ActivityMainBindingclazz,"getRoot","()Landroidx/constraintlayout/widget/ConstraintLayout;");
    jobject ConstraintLayoutobj = env->CallObjectMethod(ActivityMainBindingobj,getrootid);


    jclass AppCompatActivityclazz=env->FindClass("androidx/appcompat/app/AppCompatActivity");
    jmethodID setContentViewid=env->GetMethodID(AppCompatActivityclazz,"setContentView", "(Landroid/view/View;)V");
    env->CallVoidMethod(thiz,setContentViewid,ConstraintLayoutobj);

//        setContentView(binding.getRoot());
//-----------------------------------------------------------------------------------------------------------------------------------------------------------

//        TextView tv = binding.sampleText;
    jfieldID sampleTextid=env->GetFieldID(ActivityMainBindingclazz,"sampleText","Landroid/widget/TextView;");
    jobject tv=env->GetObjectField(ActivityMainBindingobj,sampleTextid);

//-----------------------------------------------------------------------------------------------------------------------------------------------------------

//        String path=getlibpath(getApplicationContext())+"/libndkmultithreadB.so";
        jclass getApplicationContextclazz=env->FindClass("android/content/ContextWrapper");
        jmethodID getApplicationContextid=env->GetMethodID(getApplicationContextclazz,"getApplicationContext","()Landroid/content/Context;");
        jobject getApplicationContextobj=env->CallObjectMethod(thiz,getApplicationContextid);

        jclass getlibpathclazz=env->FindClass("com/example/ndkmultithread/MainActivity");
         const char* path_0= "/libndkmultithreadB.so";
        jmethodID getlibpathid=env->GetMethodID(getlibpathclazz,"getlibpath", "(Landroid/content/Context;)Ljava/lang/String;");
        const char * path_1= env->GetStringUTFChars(static_cast<jstring>(env->CallObjectMethod(thiz, getlibpathid,
                                                                                               getApplicationContextobj)),
                                                    nullptr);
        char *path;
        strcat(path, path_1);
        strcat(path,path_0);


   __android_log_print(4,"Fup1p1","fullgetlibpath result is %s",path);


//-----------------------------------------------------------------------------------------------------------------------------------------------------------
//        Log.i("Fup1p1",stringFromJNI2("114514",100,"114514".getBytes()));

    jclass mainactivityclass=env->FindClass("com/example/ndkmultithread/MainActivity");
    //   jclass mainactivityclass=env->GetObjectClass(thiz);  两个都行
    jmethodID stringFromJNI2id=env->GetMethodID(mainactivityclass,"stringFromJNI2", "(Ljava/lang/String;I[B)Ljava/lang/String;");
    jstring arg1=env->NewStringUTF("114514");
    jint arg2=100;
    jbyteArray arg3=env->NewByteArray(7);
    env->SetByteArrayRegion(arg3, 0, 7, reinterpret_cast<const jbyte *>("114514"));
    const char * log_i= env->GetStringUTFChars(static_cast<jstring>(env->CallObjectMethod(thiz, stringFromJNI2id, arg1, arg2,
                                                                                          arg3)),
                                               nullptr);
    __android_log_print(4,"Fup1p1","stringFromJNI2 result is %s",log_i);
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
    jmethodID stringFromJNIid=env->GetMethodID(mainactivityclass,"stringFromJNI", "(Ljava/lang/String;)Ljava/lang/String;");
    jobject stringFromJNIobj= env->CallObjectMethod(thiz,stringFromJNIid,env->NewStringUTF(path));

    jclass setTextclazz=env->FindClass("android/widget/TextView");
    jmethodID setTextid=env->GetMethodID(setTextclazz,"setText", "(Ljava/lang/CharSequence;)V");

     env->CallVoidMethod(tv,setTextid,stringFromJNIobj);
//        tv.setText(stringFromJNI(path));
}
1

评论区