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。

相关推荐
用户059540174463 小时前
把AI对话记忆存储测试从手工改成Playwright+pytest,覆盖率从20%提到96%,回归时间缩短90%
前端·css
kyriewen4 小时前
别再这样写TypeScript了——Code Review中最常见的8个反模式
前端·javascript·typescript
名字还没想好☜4 小时前
Next.js ‘use client‘ 到底加在哪:Server/Client Components 边界与常见报错
开发语言·前端·javascript·react·next.js
午安~婉4 小时前
GitHub Token/ GitHub Stats统计显示图异常
前端·github·vercel·github token
码云之上4 小时前
AI Agent 工程化总览篇:从 Prompt 到 Harness
前端·人工智能
2501_926978334 小时前
以说明书 DNA 为模板——完整 AGI 的结构图景
前端·人工智能·经验分享·笔记·ai写作
IT_陈寒5 小时前
Vite静态资源引用这个坑我踩得有点疼
前端·人工智能·后端
程序员黑豆5 小时前
鸿蒙应用开发:AttributeModifier 使用教程
前端·harmonyos
codeniu6 小时前
TRAE Work 实战 | 从"有个想法"到线上Demo,我全程只靠对话就完成了
前端·trae
妙码生花6 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十):增加管理员角色组管理
前端·后端·ai编程