通过实际项目大量测试验证,FastHook表现出了远超YAHFA的优异稳定性。用户反馈未出现Hook引起的稳定性问题、压力测试也未发生Hook引起的稳定问题。之因此FastHook拥有优异的稳定性,除了框架实现原理的优越性以外,还得益于FastHook出色的细节处理。java
本文将经过FastHook实现原理优越性与一些出色的细节处理来解释为什么FastHook拥有优异的稳定性,最后对比YAHFA框架。android
若是你还未了解FastHook,请移步FastHook——一种高效稳定、简洁易用的Android Hook框架。 FastHook相较YAHFA框架原理上最大的优点、也是最大的亮点即是:不须要备份原方法!不须要备份原方法!不须要备份原方法!git
科学上有一个著名的“奥卡姆剃刀定律”,什么意思呢?若是一个现象有两个或者多个不一样的理论解释,那么选最简单的那个。作Hook框架,也能够用剃刀定律来作指导:实现相同的功能,选对系统状态改动最小的。github
“备份原方法”是一种隐患颇多的方式,引起了诸如方法解析出错、Moving GC空指针等问题。尽管其余框架经过一些手段来提升稳定性,好比保证方法不被再次解析、检查Moving GC是否移动了原方法相关对象等,可是这些都不是理论安全的,就像地上有个坑,你不去补上,而是让人不要去踩。安全
反观FastHook,Hook时对系统原有状态的改变是最小的。bash
简而言之,FastHook就是用Hook方法hook原方法,原方法hook Forward方法来实现最小改动hook。完美地从实现层面解决了YAHFA框架不能解决的问题,并且无需作一些其余操做,YAHFA框架都须要一些其余的操做来提升稳定性,而FastHook不须要作任何其余处理,更简洁、更优雅。微信
若是你看过YAHFA框架代码,你会发现没有一个框架作了JIT状态检查。JIT状态检查的目的是为了保证hook的安全性,但这也不是理论安全的,也没法作到理论安全。这是为何呢?框架
若是原方法未编译则须要进行手动JIT编译。那么问题来了,何时编译才是安全的呢。下面列举出全部可能出现的情景:异步
上述4中情景,其中二、3是不安全的。若是要保证手动JIT编译的安全性,必须作到如下两点:post
如今来看看FastHook究竟是怎么处理的
int CheckJitState(JNIEnv *env, jclass clazz, jobject target_method) {
void *art_method = (void *)(*env)->FromReflectedMethod(env, target_method);
//添加kAccCompileDontBother,禁止JIT、AOT编译
AddArtMethodAccessFlag(art_method, kAccCompileDontBother);
uint32_t hotness_count = GetArtMethodHotnessCount(art_method);
if(hotness_count >= kHotMethodThreshold) {
//hotness_count >= hot_threshold,确定就不是1了,看看是二、三、4中的哪个
long entry_point = (long)GetArtMethodEntryPoint(art_method);
if((void *)entry_point == art_quick_to_interpreter_bridge_) {
void *profiling = GetArtMethodProfilingInfo(art_method);
void *save_entry_point = GetProfilingSaveEntryPoint(profiling);
if(save_entry_point) {
//JIT垃圾回收会改变方法EntryPoint,虽然方法已经编译了,可是EntryPoint也多是art_quick_to_interpreter_bridge
return kCompile;
}else {
//JIT状态保存在profiling中,经过其来判断是不是正在编译,若是不是多是正在等待或者已经编译失败。
bool being_compiled = GetProfilingCompileState(profiling);
if(being_compiled) {
return kCompiling;
}else {
return kCompilingOrFailed;
}
}
}
return kCompile;
}else {
//hotness_count < hot_threshold,多是1,也多是2,即将进入编译等待队列,统一加一个增量,若是此时大于hot_threshold,就认为是2,反之是1
uint32_t assumed_hotness_count = hotness_count + kHotMethodMaxCount;
if(assumed_hotness_count > kHotMethodThreshold) {
return kCompiling;
}
}
return kNone;
}
复制代码
class ProfilingInfo {
private:
ProfilingInfo(ArtMethod* method, const std::vector<uint32_t>& entries);
// Number of instructions we are profiling in the ArtMethod.
const uint32_t number_of_inline_caches_;
// Method this profiling info is for.
// Not 'const' as JVMTI introduces obsolete methods that we implement by creating new ArtMethods.
// See JitCodeCache::MoveObsoleteMethod.
ArtMethod* method_;
// Whether the ArtMethod is currently being compiled. This flag
// is implicitly guarded by the JIT code cache lock.
// TODO: Make the JIT code cache lock global.
bool is_method_being_compiled_;
bool is_osr_method_being_compiled_;
// When the compiler inlines the method associated to this ProfilingInfo,
// it updates this counter so that the GC does not try to clear the inline caches.
uint16_t current_inline_uses_;
// Entry point of the corresponding ArtMethod, while the JIT code cache
// is poking for the liveness of compiled code.
const void* saved_entry_point_;
// Dynamically allocated array of size `number_of_inline_caches_`.
InlineCache cache_[0];
};
复制代码
若是只是简单用entry point与解释入口比较来判断,经过3.1的分析可知这是不完备的。
JIT垃圾回收会改变entry point为解释入口,必须作进一步判断是否为JIT编译方法。FastHook的作法很简单,判断hotness_count是否小于hot_threshold,若是其小于hot_threshold,那确定还未被JIT编译,所以能够断定其须要进行手动JIT编译。
而且,这一步是在JIT检查成功基础上进行的,能够不用担忧JIT状态的影响。
bool IsCompiled(JNIEnv *env, jclass clazz, jobject method) {
bool ret = false;
void *art_method = (void *)(*env)->FromReflectedMethod(env, method);
void *method_entry = (void *)ReadPointer((unsigned char *)art_method + kArtMethodQuickCodeOffset);
int hotness_count = GetArtMethodHotnessCount(art_method);
if(method_entry != art_quick_to_interpreter_bridge_)
ret = true;
if(!ret && hotness_count >= kHotMethodThreshold)
ret = true;
return ret;
}
复制代码
当一个java方法进入JNI时,线程状态由runnable状态变为native状态,返回java前恢复为runable状态。而JIT编译方法会将参数thread的状态转变为runnable状态。
最开始在手动JIT编译方法时不作其余处理。可是后来项目上有反馈,有几率出现crash,出现的位置正好是编译完成后返回java的地方,异常缘由是线程状态错误。 FastHook以前的解决方案是:新建native线程用于JIT编译,避免当前线程编译。这时出现了新的问题,如何获取native线程的thread对象?
经过研究android代码发现,art获取线程thread对象是经过TLS来获取的,thread存储在TLS固定位置。但实际上,这种方案虽然解决了crash的问题,但也致使了新的问题:线程错误地等待。
究其原因,都是线程状态异常引发的,所以根治的方法即是恢复线程状态。经过研究Thread代码发现,线程状态是一个union结构体StateAndFlags,保存在thread对象里,所以能够经过偏移的方式来访问。
static inline void *CurrentThread() {
return __get_tls()[kTLSSlotArtThreadSelf];
}
#if defined(__aarch64__)
# define __get_tls() ({ void** __val; __asm__("mrs %0, tpidr_el0" : "=r"(__val)); __val; })
#elif defined(__arm__)
# define __get_tls() ({ void** __val; __asm__("mrc p15, 0, %0, c13, c0, 3" : "=r"(__val)); __val; })
#endif
复制代码
class Thread {
union PACKED(4) StateAndFlags {
struct PACKED(4) {
volatile uint16_t flags;
volatile uint16_t state;
} as_struct;
AtomicInteger as_atomic_int;
volatile int32_t as_int;
};
struct PACKED(4) tls_32bit_sized_values {
typedef uint32_t bool32_t;
union StateAndFlags state_and_flags;
int suspend_count GUARDED_BY(Locks::thread_suspend_count_lock_);
int debug_suspend_count GUARDED_BY(Locks::thread_suspend_count_lock_);
uint32_t thin_lock_thread_id;
uint32_t tid;
const bool32_t daemon;
bool32_t throwing_OutOfMemoryError;
uint32_t no_thread_suspension;
uint32_t thread_exit_check_count;
bool32_t handling_signal_;
bool32_t is_transitioning_to_runnable;
bool32_t ready_for_debug_invoke;
bool32_t debug_method_entry_;
bool32_t is_gc_marking;
Atomic<bool32_t> interrupted;
bool32_t weak_ref_access_enabled;
uint32_t disable_thread_flip_count;
int user_code_suspend_count GUARDED_BY(Locks::thread_suspend_count_lock_);
} tls32_;
复制代码
bool CompileMethod(JNIEnv *env, jclass clazz, jobject method) {
bool ret = false;
void *art_method = (void *)(*env)->FromReflectedMethod(env, method);
void *thread = CurrentThread();
int old_flag_and_state = ReadInt32(thread);
ret = jit_compile_method_(jit_compiler_handle_, art_method, thread, false);
memcpy(thread,&old_flag_and_state,4);
return ret;
}
复制代码
Inline模式下须要注入代码,那么就必须确保被覆盖的指令不包含pc相关的指令。 这是为何呢?pc寄存器存储的是当前执行的指令,若是以pc寄存器来作寻址就跟当前地址息息相关了,若是咱们覆盖的指令包含pc相关的指令,那么寻址将出错。
须要注意的是,Thumb2有16位和32位两种指令,所以对于Thumb2指令集还需额外判断指令类型。
static inline bool IsThumb32(uint16_t inst, bool little_end) {
if(little_end) {
return ((inst & 0xe000) == 0xe000 && (inst & 0x1800) != 0x0000);
}
return ((inst & 0x00e0) == 0x00e0 && (inst & 0x0018) != 0x0000);
}
复制代码
static inline bool HasThumb16PcRelatedInst(uint16_t inst) {
uint16_t mask_b1 = 0xf000;
uint16_t op_b1 = 0xd000;
uint16_t mask_b2_adr_ldr = 0xf800;
uint16_t op_b2 = 0xe000;
uint16_t op_adr = 0xa000;
uint16_t op_ldr = 0x4800;
uint16_t mask_bx = 0xfff8;
uint16_t op_bx = 0x4778;
uint16_t mask_add_mov = 0xff78;
uint16_t op_add = 0x4478;
uint16_t op_mov = 0x4678;
uint16_t mask_cb = 0xf500;
uint16_t op_cb = 0xb100;
if((inst & mask_b1) == op_b1)
return true;
if((inst * mask_b2_adr_ldr) == op_b2 || (inst * mask_b2_adr_ldr) == op_adr || (inst * mask_b2_adr_ldr) == op_ldr)
return true;
if((inst & mask_bx) == op_bx)
return true;
if((inst & mask_add_mov) == op_add || (inst & mask_add_mov) == op_mov)
return true;
if((inst & mask_cb) == op_cb)
return true;
return false;
}
复制代码
static inline bool HasThumb32PcRelatedInst(uint32_t inst) {
uint32_t mask_b = 0xf800d000;
uint32_t op_blx = 0xf000c000;
uint32_t op_bl = 0xf000d000;
uint32_t op_b1 = 0xf0008000;
uint32_t op_b2 = 0xf0009000;
uint32_t mask_adr = 0xfbff8000;
uint32_t op_adr1 = 0xf2af0000;
uint32_t op_adr2 = 0xf20f0000;
uint32_t mask_ldr = 0xff7f0000;
uint32_t op_ldr = 0xf85f0000;
uint32_t mask_tb = 0xffff00f0;
uint32_t op_tbb = 0xe8df0000;
uint32_t op_tbh = 0xe8df0010;
if((inst & mask_b) == op_blx || (inst & mask_b) == op_bl || (inst & mask_b) == op_b1 || (inst & mask_b) == op_b2)
return true;
if((inst & mask_adr) == op_adr1 || (inst & mask_adr) == op_adr2)
return true;
if((inst & mask_ldr) == op_ldr)
return true;
if((inst & mask_tb) == op_tbb || (inst & mask_tb) == op_tbh)
return true;
return false;
}
复制代码
static inline bool HasArm64PcRelatedInst(uint32_t inst) {
uint32_t mask_b = 0xfc000000;
uint32_t op_b = 0x14000000;
uint32_t op_bl = 0x94000000;
uint32_t mask_bc = 0xff000010;
uint32_t op_bc = 0x54000000;
uint32_t mask_cb = 0x7f000000;
uint32_t op_cbz = 0x34000000;
uint32_t op_cbnz = 0x35000000;
uint32_t mask_tb = 0x7f000000;
uint32_t op_tbz = 0x36000000;
uint32_t op_tbnz = 0x37000000;
uint32_t mask_ldr = 0xbf000000;
uint32_t op_ldr = 0x18000000;
uint32_t mask_adr = 0x9f000000;
uint32_t op_adr = 0x10000000;
uint32_t op_adrp = 0x90000000;
if((inst & mask_b) == op_b || (inst & mask_b) == op_bl)
return true;
if((inst & mask_bc) == op_bc)
return true;
if((inst & mask_cb) == op_cbz || (inst & mask_cb) == op_cbnz)
return true;
if((inst & mask_tb) == op_tbz || (inst & mask_tb) == op_tbnz)
return true;
if((inst & mask_ldr) == op_ldr)
return true;
if((inst & mask_adr) == op_adr || (inst & mask_adr) == op_adrp)
return true;
return false;
}
复制代码
主要是几类指令:
而Thumb2须要特别注意,由于其有16位和32位两种模式,而跳转指令长度是8字节,若是固定复制8字节,有可能会把指令截断,例如4-2-4,最后4字节指令将会被截断,所以须要作判断,以肯定须要复制8字节仍是10字节
int original_prologue_len = 0;
while(original_prologue_len < jump_trampoline_len) {
if(IsThumb32(ReadInt16((unsigned char *)target_code + original_prologue_len),IsLittleEnd())) {
original_prologue_len += 4;
}else {
original_prologue_len += 2;
}
}
复制代码
Inline模式下,须要向目标方法代码段注入一段跳转指令,而代码段是不可写。通常解决方案是使用mprotect修改访问权限。
而从实际项目测试来看,mprotect多是无效的。mprotect执行成功了,可是仍是出现了SEGV_ACCERR。
FastHook的解决方案是先捕获出错信号,再使用mprotect修改访问权限。若是修改无效,则一直会修改直到生效为止。指令注入后恢复默认信号处理。捕获信号处理以后,再无crash的反馈。
void SignalHandle(int signal, siginfo_t *info, void *reserved) {
ucontext_t* context = (ucontext_t*)reserved;
void *addr = (void *)context->uc_mcontext.fault_address;
if(sigaction_info_->addr == addr) {
void *target_code = sigaction_info_->addr;
int len = sigaction_info_->len;
long page_size = sysconf(_SC_PAGESIZE);
unsigned alignment = (unsigned)((unsigned long long)target_code % page_size);
int ret = mprotect((void *) (target_code - alignment), (size_t) (alignment + len),
PROT_READ | PROT_WRITE | PROT_EXEC);
}
}
复制代码
sigaction_info_->addr = target_code;
sigaction_info_->len = original_prologue_len;
if(current_handler_ == NULL) {
default_handler_ = (struct sigaction *)malloc(sizeof(struct sigaction));
current_handler_ = (struct sigaction *)malloc(sizeof(struct sigaction));
memset(default_handler_, 0, sizeof(sigaction));
memset(current_handler_, 0, sizeof(sigaction));
current_handler_->sa_sigaction = SignalHandle;
current_handler_->sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, current_handler_, default_handler_);
}else {
sigaction(SIGSEGV, current_handler_, NULL);
}
memcpy(target_code, jump_trampoline, jump_trampoline_len);
sigaction_info_->addr = NULL;
sigaction_info_->len = 0;
sigaction(SIGSEGV, default_handler_, NULL);
复制代码
在得到写权限以后,注入的时候必须保证没有其余线程同时读须要注入的区域,否则将致使未知错误。
能够利用art暂停所用线程和恢复全部线程的接口来实现。FastHook并无采用这种方式,stop the world这种方式过重了,对性能有损耗。
FastHook是怎么作的呢?很简单,强制须要注入的方法解释执行,注入完成后恢复。即保证了注入安全,也没有任何性能损失。
memcpy((unsigned char *) art_target_method + kArtMethodQuickCodeOffset,&art_quick_to_interpreter_bridge_,pointer_size_);
memcpy(target_code, jump_trampoline, jump_trampoline_len);
memcpy((unsigned char *) art_target_method + kArtMethodQuickCodeOffset,&target_entry,pointer_size_);
复制代码
EntryPoint替换模式要求原方法以解释模式执行,而JIT垃圾回收会更改方法entry point为解释执行入口,当方法即将进入解释执行时会从新设置为原来的入口,这会致使什么问题呢?
java方法有两种执行模式,一种执行dex字节码,一种执行机器码,art所以须要知道机器码与dex字节码的映射关系,例如执行一条机器码,它对应哪一条dex字节码。而这些映射须要方法entry point做为基址来计算,此时entry point已经被替换,会得出错误的结果。
所以,若是监测到上述状况,须要修改save_entry_point为解释执行入口,防止执行JIT编译的机器码。
if(art_forward_method) {
memcpy((unsigned char *) target_trampoline + hook_trampoline_target_index, &art_target_method, pointer_size_);
memcpy((unsigned char *) target_trampoline + target_trampoline_target_entry_index, &target_entry, pointer_size_);
if(kTLSSlotArtThreadSelf) {
uint32_t hotness_count = GetArtMethodHotnessCount(art_target_method);
if(hotness_count >= kHotMethodThreshold) {
void *profiling = GetArtMethodProfilingInfo(art_target_method);
void *save_entry_point = GetProfilingSaveEntryPoint(profiling);
if(save_entry_point) {
SetProfilingSaveEntryPoint(profiling,art_quick_to_interpreter_bridge_);
}
}
}
}
复制代码
框架 | 备份原方法 | 性能 | JIT状态检查 | EntryPoint检查(JIT) | 线程状态恢复 | 指令检查 | mprotect失效处理 | 注入安全 | 防止内联 | 防止backup/forword内联 |
---|---|---|---|---|---|---|---|---|---|---|
YAHFA | 是 | 高 | 否 | - | - | - | - | 否 | 否 | 否 |
FastHook | 否 | 高 | 是 | 是 | 是 | 是 | 是 | 是(高效) | JIT内联 | 是 |
从上述对比能够看出,FastHook与YAHFA框架的本质区别是不备份原方法,在细节上的处理也比YAHFA要严谨、高效,其余框架在细节处理上都有所欠缺。
因为项目缘由,主要维护arm平台,其余平台暂时不支持,后续再计划加入,目前主要关注arm平台的稳定性。若是有兴趣,对稳定性有要求的朋友,欢迎使用,本项目长期维护。
FastHook:github.com/turing-tech…