嘘~ 正在从服务器偷取页面 . . .

NDK开发


项目

  1. 创建的项目 idea 已经集成 直接选择 native c++ 项目即可
  2. main 目录 有一个 CMakeLists.txtnative-lib.cpp 是对应关系
  3. 其中 cpp 是 C 层实现的算法 txt 是指导 cpp 文件如何进行编译
  4. find_library so 依赖 一般存在于系统中
  5. 创建项目的时候 包名字 尽量不要有大写 不知道为什么 出问题卡了一个小时
add_library(
             native-lib # 链接库名 生成的 so 名字

             SHARED # 生成链接库的 类型 这里是共享库

             native-lib.cpp # 编译源文件的 相对路径
             )

  1. cpp 文件中可以看到函数 自带两个参数
    1. JNIEnv* env : Env 环境 相当于 javac 交互的桥梁 通过 Env 可以调用 java 层 转换 java 数据类型
    2. jobject / jclass : 两种情况 当 native 函数 被调用的时候 谁调用了这个 函数 jobject 就是谁. 当 native 是静态方法的时候 第二个参数就是 jclass 相对的 谁调用了 这个静态方法 jclass 就指向谁
    3. 第三个参数开始 就是在 java 层定义的函数参数
  2. 在 静态注册 开头可以看到 extern "C" JNIEXPORT jstring JNICALL
    1. 因为静态注册是根据包名拼接进行查找函数 当不指定 extern "C" 就会以 C++ 的形式进行编译 C++ 会自动进行符号修饰 导致静态注册找不到函数
    2. JNIEXPORT 表示函数可见性 EXPORT 的函数将会出现在 so 导出表 也必须出现
    3. jstring 表示返回值是 java 层的 string 类型
    4. JNICALL 宏未定义
  3. 注意 在 cjava 之间的数据交互 是需要进行数据转换的
  4. 配置生成所有版本的 so 文件
defaultConfig {
    ndk {
        adiFilters "armeabi-v7a", "arm640v8a", "x86", "x86_64"
    }
}

在 C 层的 LOG 输出

#include <android/log.h> // 必要头文件

#define TAG "Ass"
#define LogD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);

// 参数对应为 日志等级 日志标签 格式化标签 参数 (这里如果是 c++ 参数 需要转换为 c 参数)
__android_log_print(ANDROID_LOG_DEBUG, "Assists", "c str : %s", hello.c_str()); // 函数内使用语句进行输出

LogD("c str : %p", clazz); // 函数内使用语句进行输出

Env JNI_OnLoad

// 一个最简洁的 JNI_OnLoad 函数 在 so 加载之前会被执行 可以在这进行 函数注册
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = nullptr; // 定义一个空指针 用于 GetEnv 的第一个 二级指针参数
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) // 判断是否成功获取 Env
        return JNI_ERR;
    return JNI_VERSION_1_6;
}

静态注册

  1. 当 函数 首次被调用的时候 就会完成 Cjava 层函数的绑定 未找到则报错
  2. 静态注册的函数 必须在 导出表 内
extern "C" JNIEXPORT jstring JNICALL
// 以下路径 必须 完全对应 java 层目录结构 指定 到函数
Java_com_AndroidUtils_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

动态注册

  1. 动态注册一般在 JNI_OnLoad 被调用时就已经注册完毕
jstring stringFromJNI_Reg(JNIEnv *env, jobject) {
    return  env->NewStringUTF("stringFromJNI_Reg");
}

// typedef struct {
//     const char* name; // 指定 Java 层函数名
//     const char* signature; // smali 形式的 参数 与 返回值
//     void*       fnPtr; // 一个指针 指向 C层函数
// } JNINativeMethod;
static const JNINativeMethod nativeMethods[] = {
        // 内部每个 都是上面的结构体
        {
                "stringFromJNI_Reg",
                "()Ljava/lang/String;",
                (jstring *) stringFromJNI_Reg
        },
};

// 一个最简洁的 JNI_OnLoad 函数 在 so 加载之前会被执行 可以在这进行 函数注册
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr; // 定义一个空指针 用于 GetEnv 的第一个 二级指针参数
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) // 判断是否成功获取 Env
        return JNI_ERR;

    // 当没有报错 成功获取到 Env 则开始动态注册
    jclass jClass = env->FindClass("com/utils/MainActivity"); // 查找需要动态绑定的类

    // 第一个参数 注册函数 所在的类
    // 第二个参数 指定方法结构体 数组 JNINativeMethod
    // 第三个参数 每个方法的大小 可以用 sizeof 计算 防止不同 位数 cpu 大小不同
    env->RegisterNatives(jClass, nativeMethods, sizeof(nativeMethods) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_6;
}
  • **注意 : ** C++ 版本的动态注册 就是对 C 版本的一个封装 隐藏掉了 第一个参数 JNIEnv 所以当 so 拖入 ida 进行反编译的时候 第一个参数 都可以忽略掉 所有函数参数位置都需要后移 一位

Jni 调用 java

JNI 创建 java 层对象

  1. 通过获取对象的 init 构造方法 new 一个对象
jclass cls = env->FindClass("com/utils/Test");
jmethodID methodId = env->GetMethodID(cls, "<init>", "()V");
jobject obj = env->NewObject(cls, methodId);
LogD("C << %p", obj)
  1. 另外一种方式 创建 对象
jclass cls = env->FindClass("com/utils/Test");
jmethodID methodId = env->GetMethodID(cls,"<init>", "(Ljava/lang/String;)V");
jobject obj = env->AllocObject(cls); // 分配类内存空间 并没有进行实例化
jstring c_str = env->NewStringUTF("c_str"); // 因为上面调用一个需要参数的 构造函数 所以这 new 一个 java 字符串
// 这里调用父类方法 进行实例化 最终实例化的对象就是在上面分配的 obj 空间内
env->CallNonvirtualVoidMethod(obj,cls,methodId,c_str);

LogD("C << %p", obj)

JNI 操作 java 属性

// 首先还是获取类对象
jclass cls = env->FindClass("com/utils/Test");
// 然后获取 Field ID
jfieldID fieldId = env->GetStaticFieldID(cls,"name", "Ljava/lang/String;");
// 获取 java 层字段
jstring j_str = static_cast<jstring>(env->GetStaticObjectField(cls, fieldId));

// 设置 指定字段的值
env->SetStaticObjectField(cls,fieldId,env->NewStringUTF("C value"));

// 强转为 C 层类型
char* c_str = const_cast<char *>(env->GetStringUTFChars(j_str, nullptr));
LogD("C << %s", c_str)

JNI 调用 java 方法

  1. 大体上调用流程都一样
  2. 只要分辨返回值 参数
// 调用静态方法 	无参

// 首先还是获取类对象
jclass cls = env->FindClass("com/utils/Test");
// 找到方法 ID
jmethodID methodId = env->GetStaticMethodID(cls,"staticMethods","()V");
// 调用指定方法
env->CallStaticVoidMethod(cls,methodId);
// 调用实例方法 	传参

// 首先还是获取类对象
jclass cls = env->FindClass("com/utils/Test");
// 找到方法 id
jmethodID methodID = env->GetMethodID(cls, "MethodsArgs", "(Ljava/lang/String;)V");

// 因为是实例方法 所以需要 new 出来一个对象
// 获取 init
jmethodID JInitID = env->GetMethodID(cls, "<init>", "()V");
// 实例化一个对象
jobject obj = env->NewObject(cls, JInitID);

// 调用实例方法
env->CallVoidMethod(obj, methodID, env->NewStringUTF("C value"));
  • 可以看到有 CallVoidMethodV CallVoidMethodA CallVoidMethod 三种调用方式
  • CallVoidMethod 内部其实就是 CallVoidMethodV 的封装 后面的可变参数 在内部进行了处理 就不要自己操作
  • CallVoidMethodA 同理 不过 最终处理完毕的是一个 指针 再进行调用
  • **平时 直接 使用 CallVoidMethod 即可 **

JNI 调用 java 层父类方法

  1. 前面 创建 对象的时候已经使用过了 env->CallNonvirtualVoidMethod
  2. 所有 带有 Nonvirtualenv 函数 都是 调用父类方法 参数相同

全局引用

jclass GlobalRefCls;

extern "C" JNIEXPORT jstring JNICALL
Java_com_utils_MainActivity_stringFromJNI(JNIEnv *env, jobject) {

    // 首先还是获取类对象 (通过这种方式获取的类 都是局部引用 在函数执行完毕之后 就会被释放)
    // 就算你把定义 cls 的代码放在全局也不行
    // 如果希望 跨函数使用 需要套一层 并且 定义 放在 全局
    jclass cls = env->FindClass("com/utils/Test");
    // 将 cls 变为全局引用
    // 以下 cls 就可以在其他函数中使用
    GlobalRefCls = static_cast<jclass>(env->NewGlobalRef(cls));

    // 当需要释放的时候就要调用
    env->DeleteGlobalRef(GlobalRefCls);

    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
  • 这里的释放不只是 全局引用需要释放
  • 获取到的 指针类型 都需要释放 nullptr 注意这个类型的释放

initinitarray

  1. 两个函数的执行优先级比 JniOnLoadinit > initarray > JniOnLoad
  2. 一般加固都会把 so 解密放在这两个内 initarray 可以有多个函数 执行顺序也可以不一样
  3. 一般执行到 JniOnLoad 的时候 so 就已经是解密状态了
// 函数名字可以随便起
// 如果 constructor 后面没有跟函数 那么就是按顺序执行 initArray 函数
// 在内部 跟上数值 ( 推荐 大于 100 的是数) 这个值越小越早执行
// 如果没有指定数字 会在最后执行
// 在 so 逆向的时候 已经排好顺序了
__attribute__ ((constructor(201))) void initArray2(){
    LogD("initArray2 call")
}
__attribute__ ((constructor(200))) void initArray1(){
    LogD("initArray1 call")
}

// 在编译之后 名字就变成 .init_proc 了
extern "C" void _init(){
    LogD("_init call")
}

onCreate native

  1. 其实就是通过 重新 定义一个 onCreate 增加 native
  2. 然后 动态绑定 在 c 层实现 java 层的全部流程 即可
  3. 相当于前面所有流程的结合使用

文章作者: 林木木
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 林木木 !
评论
  目录