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。

相关推荐
代码不加糖12 小时前
2026 React 面试“通关秘籍”:高频 12 问 + 深度解析(含Hooks源码思想)
前端·react.js·面试
我滴老baby12 小时前
ReAct推理模式详解让智能体学会边思考边行动
前端·react.js·前端框架
菜鸟小码13 小时前
MapReduce 核心阶段深度解析:Map 阶段与 Reduce 阶段的作用及执行流程
前端·javascript·mapreduce
步步为营DotNet13 小时前
深入剖析.NET 11 中 Semantic Kernel 于智能后端集成的创新实践
前端·.net·easyui
@大迁世界13 小时前
33.如何在 React 中使用内联样式(inline styles)?
前端·javascript·react.js·前端框架·ecmascript
CodeSheep13 小时前
DeepSeek的最新招人标准,太讽刺了。
前端·后端·程序员
不法13 小时前
vue 地图路线渲染
前端·vue.js·ubuntu
GISer_Jing13 小时前
从“工具应用”到“系统重构”:AI时代前端研发的范式转移与哲学思辨
前端·人工智能·学习
我家媳妇儿萌哒哒13 小时前
Element ui el-dialog 在一个有滚动条的页面,打开一个弹框,完了再打开一个弹框后,滚动条可以滚动,怎么限制不能滚动。
前端·vue.js·ui
得想办法娶到那个女人13 小时前
Vite + Vue 项目打包为 Electron 桌面应用 完整指南
前端·vue.js·electron