JNI-Art下动态注册流程

与dalvik相比,art的流程只是层次更深了一点,核心的逻辑还是一样的。还有一个区别就是几乎每个版本的代码都会有点小变化。

流程分析

Art 中 register 方法的实现是在 art/runtime/jni/jni_internal.cc 中:

ini 复制代码
static jint RegisterNatives(JNIEnv* env,
                              jclass java_class,
                              const JNINativeMethod* methods,
                              jint method_count) {
    ...

    for (jint i = 0; i < method_count; ++i) {

      ...
      
      for (ObjPtr<mirror::Class> current_class = c.Get();
           current_class != nullptr;
           current_class = current_class->GetSuperClass()) {
        // Search first only comparing methods which are native.
        m = FindMethod<true>(current_class, name, sig);
        if (m != nullptr) {
          break;
        }

        // Search again comparing to all methods, to find non-native methods that match.
        m = FindMethod<false>(current_class, name, sig);
        if (m != nullptr) {
          break;
        }

        ...
      }

      

      ...

      const void* final_function_ptr = class_linker->RegisterNative(soa.Self(), m, fnPtr);
      UNUSED(final_function_ptr);
    }
    return JNI_OK;
  }

核心逻辑是使用 FindMethod 来找到Java形式的 native 方法在 JVM 中的形式。每个 class 在被 ClassLinker 加载后,类里面的方法会被描述成一个 ArtMethod 对象。这个 FindMethod 的作用就是要找到这个 ArtMethod。

找到了对应的 ArtMethod 后,进入到 class_linker 的 RegisterNative 方法中:

rust 复制代码
const void* ClassLinker::RegisterNative(
    Thread* self, ArtMethod* method, const void* native_method) {
  ...
  if (method->IsCriticalNative()) {
    ...
    if (method->GetDeclaringClass()->IsVisiblyInitialized()) {
      method->SetEntryPointFromJni(new_native_method);
    } else {
      critical_native_code_with_clinit_check_.emplace(method, new_native_method);
    }
  } else {
    method->SetEntryPointFromJni(new_native_method);
  }
  return new_native_method;
}

这个方法先处理了一些回调,然后会走到 ArtMethod 的 SetEntryPointFromJni 里面去:

scss 复制代码
void SetEntryPointFromJni(const void* entrypoint)
      REQUIRES_SHARED(Locks::mutator_lock_) {
    ...
    SetEntryPointFromJniPtrSize(entrypoint, kRuntimePointerSize);
  }

深入下去:

scss 复制代码
ALWAYS_INLINE void SetEntryPointFromJniPtrSize(const void* entrypoint, PointerSize pointer_size)
      REQUIRES_SHARED(Locks::mutator_lock_) {
    SetDataPtrSize(entrypoint, pointer_size);
  }
scss 复制代码
ALWAYS_INLINE void SetDataPtrSize(const void* data, PointerSize pointer_size)
      REQUIRES_SHARED(Locks::mutator_lock_) {
    DCHECK(IsImagePointerSize(pointer_size));
    SetNativePointer(DataOffset(pointer_size), data, pointer_size);
  }

这里的 DataOffset(pointer_size) 实际上是在计算ArtMethod的ptr_sized_fields_字段的偏移,这个字段里面的 _data 成员,我们需要将它的值替换为 jni 方法的地址。

arduino 复制代码
template<typename T>
  ALWAYS_INLINE void SetNativePointer(MemberOffset offset, T new_value, PointerSize pointer_size)
      REQUIRES_SHARED(Locks::mutator_lock_) {
    static_assert(std::is_pointer<T>::value, "T must be a pointer type");
    const auto addr = reinterpret_cast<uintptr_t>(this) + offset.Uint32Value();
    if (pointer_size == PointerSize::k32) {
      uintptr_t ptr = reinterpret_cast<uintptr_t>(new_value);
      *reinterpret_cast<uint32_t*>(addr) = dchecked_integral_cast<uint32_t>(ptr);
    } else {
      *reinterpret_cast<uint64_t*>(addr) = reinterpret_cast<uintptr_t>(new_value);
    }
  }

这里做了地址替换的操做,动态注册的核心思想就是将我们传递的方法地址存到 ArtMethod 的 ptr_sized_fields_ 成员的 _data 成员中。

初始化和终止函数

在 《LIinux-Unix系统编程手册》这本书中,提到了一个知识点。有一些加固手段就是利用了这个知识点来做防护。

当一个共享库被加载的时候,不管是自动被加载还是使用 dlopen 接口,初始化函数和终止函数都会被执行。

初始化和终止函数是使用 gcc 的 constructor 和 destructor 特性来定义的。在库被加载时需要执行的所有函数都应该定义成下面的形式:

javascript 复制代码
__attribute__((constructor(10), visibility("hidden"))) void constructor10(void) {
    LOGD("constructor10 invoked");
}

__attribute__((constructor(1), visibility("hidden"))) void constructor1(void) {
    LOGD("constructor1 invoked");
}

constructor 还可以定义优先级,数值越低,执行优先级越高。

在 constructor 和 destructor 未出现之前,用来完成共享库的初始化和终止工作是在库中创建两个函数_init()和_fini()。

javascript 复制代码
extern "C" void _init(void ) {
    LOGD("_init invoked");
}

有了 gcc 的 constructor 和 destructor 特性之后已经不建议使用_init()和_fini()函数了,因为gcc 的 constructor 和 destructor 特性允许定义多个初始化和终止函数。

so分析

我们查看一下,编译后的 so 情况:

可以看到有 init 与 init_array 两个段。使用 ida 看看,可以直接 jump 到对应的地址:

发现,_init 编译后在 ida 里面现实的是 .init_proc 函数。

.init_array 段里面居然有3个,反编译后发现第3个不是我们写的,可能是默认的一些逻辑。sub_21450 对应的是 constructor1,sub_21428 对应的是 constructor10。

相关推荐
J总裁的小芒果3 分钟前
el-form el-table 前端排序+校验+行编辑
前端·vue.js·elementui
方方怪16 分钟前
HTML之表单学习记录
前端·学习·html
熊的猫25 分钟前
ES6 中 Map 和 Set
前端·javascript·vue.js·chrome·webpack·node.js·es6
布兰妮甜31 分钟前
前端框架大比拼:React.js, Vue.js 及 Angular 的优势与适用场景探讨
前端·vue.js·react.js·前端框架·angular.js
乆夨(jiuze)33 分钟前
vue2.7.14 + vant + vue cli脚手架转vite启动运行问题记录
前端·javascript·vue.js
niech_cn1 小时前
项目启动运行npm run dev报错digital envelope routines::unsupported at new Hash
前端·npm·哈希算法
shchojj1 小时前
cuda的3DArray和TextureObject
前端
OpenTiny社区1 小时前
重磅更新!Fluent Editor 开源富文本支持 LaTeX 可编辑公式啦~
前端·ui·开源·opentiny
豆豆1 小时前
如何选择企业网站模版来搭建网站?
服务器·开发语言·前端·php·软件构建
小白菜学前端1 小时前
Threejs 材质贴图、光照和投影详解
前端·3d·three.js