通過實際項目大量測試驗證,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…