与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。