以前看了不少的關於延遲綁定的文章,對stub、stub_helper、la_symbol_ptr這些概念有了必定的認識,知道對於外部定義的函數調用,首次調用須要在運行時期間藉助stub_helper來動態尋找到函數調用的地址,而後存儲到la_symbol_ptr段的數據段中。第二次訪問的時候,就直接調到該函數調用的地址就能夠了。可是要是問到具體的細節仍是隻知其一;不知其二,今天就準備本身動手把它弄懂弄透。xcode
在咱們的程序進行編譯時,一些外部定義的函數(系統庫或者App自身的動態庫中包含的函數)調用是沒法在編譯器肯定它的調用地址的,由於依賴的動態庫在運行環境中加載的地址是不肯定的。固然,這裏說的外部定義函數一版是指C/C++的靜態調用,若是是OC的方法其實不須要延遲綁定。由於OC的方法調用都是依賴於msg_send來動態搜索類中的方法列表類來找獲取調用地址。因此即便調用了外部定義的OC方法,只要這個OC類被加載了,就能夠經過OC運行時進行調用。 在編譯過程當中,外部定義的C方法會在二進制中進行記錄,這些須要進行延遲綁定的符號都被記錄在二進制文件中的Dynamic Loader Info -> Lazy Binding Info 中,經過 MachOView 咱們能夠觀察二進制裏的段信息,以下圖1所示。 sass
圖中的printf是在app中調用的方法,這個方法的定義確定是由系統庫實現的,那麼在程序編譯過程當中,在連接階段能夠知道printf這個方法是在libSystem.B.dylib這個系統庫中實現的,那麼在程序運行的時候咱們就須要從libSystem.B.dylib中尋找printf的函數調用地址。而在程序運行期間,系統庫的加載地址是不會改變的,printf的調用地址相對於libSystem.B.dylib的相對地址也是不變的,這就意味着printf的調用地址在程序的運行期間都不會改變,這個行爲只須要進行一次。這個過程就叫做延遲綁定(lazy bind)。bash
爲了探究這個過程,寫了一個Demo,在main函數裏調用了兩次printf的方法。app
#import <UIKit/UIKit.h>
int main(int argc, char * argv[]) {
printf("1");
printf("2");
}
複製代碼
咱們要追蹤的就是printf這個函數是如何調用的。在個人arm64真機設備上進行調試。首先在printf("1")處打一個斷點,經過Debug->Debug Workflow->Always Show Disassembly將代碼切換至彙編,方便咱們觀察調用地址。如圖2所示。dom
迴歸到printf的調用過程,咱們經過LLDB命令breakpoint set -a 0x10464a624在printf的樁函數處打一個斷點,讓程序繼續運行,跳轉到了圖5所示的樁函數。函數
這裏只有3行代碼,nop表明一個空操做,真正有意義的是後兩句。第二句代碼的意思是講當前的PC與當即數0x5a10相加,獲得的結果是一個地址,將這個地址存儲的數讀入到x16寄存器。第三句代碼就是跳轉到x16存儲的地址處。因此咱們要關心的就是這個地址是從哪裏來的,它裏面存儲的數據(br最終跳轉的地址)又表明着什麼呢?佈局
0x10464a624 <+0>: nop // 空操做,忽略
0x10464a628 <+4>: ldr x16, #0x5a10 // x16 = *(pc + 0x5a10) = *(0x10464a628 + 0x5a10) = *(0x104650038)
0x10464a62c <+8>: br x16 // 跳轉至x16存儲的地址處
複製代碼
經過計算咱們知道,這個地址是0x104650038,經過LLDB命令咱們跟蹤這個地址的相關信息,如圖6所示:ui
如圖9所示,這裏面有做用的就是前三行,第一行是將0x10464a6a4這個地址中存儲的數據讀入到w16寄存器中,而將0x10464a6a4這個地址正好是第三行,它實際上存儲的就是一個硬編碼數0xb9,因此此時w16 = 0xb9。而第二行直接將當前的程序跳轉到了0x10464a630這個地址。那麼咱們追蹤的地址就轉換成了0x10464a630,而w16中存儲的0xb9表明的意義也是咱們關心的(很明顯這是一個傳參)。在0x10464a630打個斷點,繼續運行程序。 編碼
如圖10所示 spa
0x10464a630: adr x17, #0x6e38 // pc + 0x6e38 = 0x104651468寫入x17寄存器,對應的無偏移地址是[0x000000010000d468] (__DATA.__data + 0),對應的是_dyld_private這個符號的值,如圖11所示
0x10464a634: nop // 空操做
0x10464a638: stp x16, x17, [sp, #-0x10]! // 移動SP棧指針,將存儲在x16和x17中數據入棧
0x10464a63c: nop // 空操做
0x10464a640: ldr x16, #0x19c0 // 0x000000019f3dfb44: dyld_stub_binder調用地址寫入x16寄存器中
0x10464a644: br x16 // 跳轉至dyld_stub_binder方法處(libdyld.dylib.__TEXT.__text + 11568)
複製代碼
dyld_stub_binder就是dyld庫中進行延遲綁定的方法,咱們調用這個方法進行了兩個傳參,一個是x16也就是以前存儲的0xb9,還有一個是x17中存儲的_dyld_private。那麼問題來了,x16參數攜帶的究竟是什麼參數呢?試想,若是咱們調用dyld_stub_binder方法來進行綁定,起碼要告訴函數咱們要尋找哪一個方法的調用地址(這裏是printf),那既然第二個參數不是,那第一個參數就必定跟這個信息有關。其實0xb9是表示的是printf在Dynamic Loader Info -> Lazy Binding info -> Actions段的偏移,能夠參考圖1。0x10351 - 0x10298 = 0xb9。咱們能夠看到在這個數據段裏不只記錄了符號的名稱_printf,並且還記錄了這個符號所在的動態庫名稱libSystem.B.dylib,經過這兩個信息dyld就能夠去對應的動態庫尋找對應的符號,從而將地址返回,回寫到printf對應的la_symbol_ptr數據段中。到MachOView中觀察stub_helper數據段,咱們可以更加清晰的發現這個數據段之間的內在聯繫。最上面是一段通用綁定的代碼,其目的就是經過調用dyld_stub_binder來獲取符號的地址。而下面的數據段,每3行一組,其做用是當作一箇中間的跳板,每一個符號的la_symbol_ptr在初始化時都指向一個這樣的跳板,這個跳板就是提供對應符號的信息(例如本例中經過提供0xb9這個偏移給dyld_stub_binder提供符號名、動態庫名信息),每一個跳板最終仍是會調用通用綁定方法來實現最終的綁定。
第二次運行printf,在讀取la_symbol_ptr的時候,咱們發現不須要再進行綁定了,0x000000019f24c978就是printf的調用地址,直接跳轉這個地址就是調用了printf。
咱們上面的流程已經把如何尋找printf的符號的過程說清楚了,可是有一點還未涉及的問題
猜測:經過二進制的中的Dynamic Symbol Table能夠獲取到printf這個符號對應的信息,其中就包括其調用地址在la_symbol_ptr段的位置,這樣就可以進行數據的回寫。以下圖:
猜測:在lazy_bind_info中並無找到dyld_stub_binder這個符號的信息,也就是說,它並非經過延遲綁定機制肯定的地址。在Dynamic Symbol Table中看到,與printf不一樣,其關聯地址是在__DATA_CONST,__got這個數據段,而這個數據段如圖16所示,是一個Non-Lazy Symbol Pointers。因此dyld_stub_binder這個符號的地址必定是在二進制加載的時候就已經肯定了,而不是經過後期的延遲綁定
延遲綁定本是一個很是常見的技術點,可是在不一樣的平臺可能會存在不一樣的實現方式。以前常常看一些大V的文章,可是不少時候都是說的只知其一;不知其二,真正上手操做的時候仍是可以學到很多的新東西,對知識點的融會貫通很是有幫助。上面的問題也僅僅是本人現階段的一個理解,若是有什麼問題還請批評指正。