如何通過Mach-O實現static函式的動態呼叫(續)
一、前言
首先先跟大家說個抱歉,在上篇 ofollow,noindex">《如何通過Mach-O實現static函式的動態呼叫》 中由於調研不夠嚴謹,沒考慮到Xcode在打包時會將符號表strip的情況(在這裡要感謝@walreal的及時指正)。由於正式版APP中不存在符號表,因此導致整個方案不可行。為了解決這個問題,特地左思右想換了個實現方案。
二、為什麼會strip符號表
為什麼會剔除掉符號表?如果剔除了符號表,那麼程式執行時如何才能找到地址呢?檢視walreal的留言及自身驗證得知,實際上Mach-O儲存了動態連結的符號表,靜態連結的符號表已經被剝離到.dSYM檔案中。從這點就可以看出,靜態連結的函式實際上是不需要符號的,因為一旦編譯完成後,地址即已確定,不能確定地址的才會保留符號表。因此可以說,符號表只是程式設計師的除錯工具,並不完全具備函式呼叫的對映意義。保留符號表不但沒有意義而且會造成安全隱患和應用包的增大。
三、.dSYM
大家都知道,在接入bugly等崩潰統計工具時,會要求開發者上傳dSYM檔案。有了符號表,無論是通過xcrun atos 命令還是dwarfdump 命令,我們都能根據地址獲取到相應的符號。dSYM實際上是一個壓縮包,其內部包含了一個Mach-O檔案,這是一個胖二進位制檔案,為了方便檢視,首先通過lipo命令將其按架構拆分
lipo xxx.app.dSYM/Contents/Resources/DWARF/xxx -thin arm64 -output xxx_arm64
拆分完成後,我們可以清楚地看到這個檔案內容。

image.png
在檔案的符號表中,我們可以看到靜態函式確實儲存在符號表中

image.png
除了符號表外,還有一些debug相關的secion也儲存在dSYM中。也就是說之前的方案是從當前應用獲取符號表,現在可能需要從外界獲取到符號表後,將獲取到的函式地址以字串的形式下發到應用中,而不是下發檔名和符號名。
三、實現
首先將拆分後的檔案直接載入到記憶體中,為了讀取檔案中的符號表,我建立了一個Mac應用,整個應用主要是讀取檔案並獲取符號資訊。在有了前一篇文章的基礎後,在記憶體中載入檔案的難度並不大,很容易就可以獲取到該檔案的符號表和字串表
載入檔案並獲取檔案header
//載入檔案並獲取檔案header NSString *path = @"/Users/a58/Desktop/xxx_armv7"; NSData *data = [NSData dataWithContentsOfFile:path]; void *pt = (void*)[data bytes]; mach_header_t *header = (mach_header_t*)pt;
獲取符號表LoadCommand
//獲取符號表LoadCommand pt = pt+sizeof(mach_header_t); for (uint i = 0; i < header->ncmds; i++, pt += cur_seg_cmd->cmdsize) { cur_seg_cmd = (struct segment_command *)pt; if (cur_seg_cmd->cmd == LC_SYMTAB) { symtab_cmd = (struct symtab_command*)cur_seg_cmd; } }
獲取符號表和字串表
//獲取符號表和字串表 struct nlist *symtab = (struct nlist *)((uintptr_t)header+symtab_cmd->symoff); char *strtab = (char *)((uintptr_t)header + symtab_cmd->stroff);
之後的流程與前文一致。但是,經過檢視符號表發現,dSYM符號表中並沒有儲存任何檔案相關的資訊,在前文中,我們通過匹配檔案來定位這個符號到底是不是我們所要的檔案中的符號(因為static 在不同的檔案中符號相同,需要區別同名符號是否是我們所指檔案中的符號),這就意味著我們獲取到的地址可能並不是我們所需要的地址。比如,在58中,某符號在專案中共存在48處,因此僅靠符號名無法準確獲取到地址。
四、debug_info
使用過apple 的crash 分析工具的同學可能會清除,在獲取崩潰後apple能幫你自動定位到crash發生的檔案類甚至精確到某行程式碼。這是因為在dSYM檔案中,存在著相關除錯資訊,

image.png
雖然在MachOView工具中我們看不出什麼,但是通過
dwarfdump --debug-info /Users/a58/Desktop/OS2JS_Demo.app.dSYM/Contents/Resources/DWARF/OS2JS_Demo
命令檢視其符號化後的形式,
//隨意擷取的一個函式 0x00001ac5:TAG_subprogram [47] * AT_low_pc( 0x00000001000063f0 )//函式起始地址 AT_high_pc( 0x00000014 ) AT_frame_base( reg31 ) AT_object_pointer( {0x00001ade} ) AT_name( "-[AppDelegate .cxx_destruct]" ) AT_decl_file( "/Users/a58/Desktop/OS2JS_Demo/OS2JS_Demo/AppDelegate.m" )//檔案資訊 AT_decl_line( 27 ) AT_prototyped( true ) AT_artificial( true ) AT_APPLE_optimized( true )
因此可以斷定,我們所需要的檔案資訊其實都在debug_info section中。起初,我的第一想法是獲取到符號地址後,反查地址所對應的debug_info檔案資訊,從而判斷該符號地址是否是我需要的地址。但是很遺憾,直接解析debug_info的二進位制資料對我來說是十分複雜的,這需要對DWARF格式資料有相當的瞭解才行。因此偷懶藉助dwarfdump來實現地址獲取,通過
dwarfdump /Users/a58/Desktop/MachOtest.app.dSYM/Contents/Resources/DWARF/MachOtest --name=xxx
可以獲取到指定變數/函式名的debug資訊,資訊中包括檔案資訊、變數偏移地址等。

image.png
檔案資訊與符號表的對照關係如下:

image.png
這個方式比我們遍歷符號表省力太多,我們不需要關注符號規則,只需要一個命令即可獲取到地址。獲取到函式地址後,下發到應用中,在應用內計算出其真實執行時的地址即可。
為了驗證可行性,我寫了個demo,在demo中,我定義了一個static 變數,一個static函式
static int s_idata = 100; static int printData(){ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:[NSString stringWithFormat:@"%d",s_idata] delegate:[UIApplication sharedApplication].delegate cancelButtonTitle:nil otherButtonTitles:@"OK", nil]; [alert show]; return s_idata; }
然後通過動態輸入地址,修改s_idata的值,並動態呼叫printData。dSYM檔案分析如下:

image.png
輸入動態引數,模仿指令碼呼叫,將s_idata修改為0xc8(200),並動態呼叫static 函式

IMG_0057.PNG
s_idata值被修改,並且函式正確執行

IMG_185E3EE6D9F9-1.jpeg
校驗計算過程,執行地址計算正確

IMG_F74FC70EEB38-1.jpeg