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。

相关推荐
沙漠之主1 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty1 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js
Csvn2 小时前
diff 算法(虚拟 DOM Reconciliation)
前端
前端snow3 小时前
ai agent --- 文件存储
前端
Frag0ut3 小时前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
IT_陈寒4 小时前
SpringBoot自动配置坑了我一把,原来是这样绕过去的
前端·人工智能·后端
广州华水科技4 小时前
单北斗GNSS变形监测在大坝安全监测中的应用与优势
前端
zeng不错5 小时前
B站 173 个投稿活动看到眼瞎?我写了个扩展,主打一个精准打击
前端
Latchh5 小时前
前端导出的JPEG红字发糊,质量要拉到100才清楚
前端·图像处理·人工智能·计算机视觉
默_笙5 小时前
🚦 请三个 AI 考官给 RAG 打分:量化评估实战(下)
前端·javascript