要是以爲寫得還能夠,看了有收穫。不要吝嗇👍,記得給一個👍呀html
動態連接要比靜態連接複雜多了,我要是直接分析MachO文件動態連接的具體實現,會讓讀者知其然不知其因此然。因此本文分紅2部分,第一部分先講理論知識,基本解答了以下幾個問題:ios
掌握了這些理論知識,再看第二部分講MachO文件動態連接的具體實現,就容易理解蘋果這麼作的緣由了。千萬不要跳過第一部分,否則你會以爲生澀難懂,看了理論,第二部分其實很簡單的。程序員
把程序的模塊分割開來,不是經過靜態連接在一塊兒,並且推遲到程序運行時候連接在一塊兒。數組
好比微信用到UIKit系統庫,等到咱們點擊微信App,微信開始運行以前去連接依賴的UIKit,連接完成再運行App。那微信和淘寶是否是不須要在包裏有UIKit,UIKit只需存一份在手機裏,等App快運行時候,發現依賴UIKit,而後把UIKit加載到內存裏,連接在一塊兒。假如UIKit已經存在內存了,是否是直接連接就能夠了。這個就作到了磁盤和內存裏,都只有一份UIKit。一樣的,升級也很是簡單了,UIKit的bug解決了,直接在手機裏存放新的UIKit,覆蓋舊的,下次App運行時候,就加載這個新的UIKit了。這個連接和靜態連接的工做原理很是相像,也是符號解析、地址重定位等。緩存
名稱解析:bash
靜態連接和動態連接都是把程序分割成一個個獨立的模塊,可是靜態連接是運行前就用ld連接器連接成一個完整的程序;動態連接是程序主模塊被加載時候,對應的Mach-O文件裏有dyld加載命令,經過這個dyld而後去找依賴的dylib(Mach-O有動態連接庫加載命令),把dylib加載到內存(若是對應的dylib不在內存),而後將程序中全部未決議的符號綁定到相應的 dylib中,並進行重定位工做。dyld和dylib加載命令以下:微信
//dyld加載命令
struct dylinker_command {
uint32_t cmd; /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
LC_DYLD_ENVIRONMENT */
uint32_t cmdsize; /* includes pathname string */
union lc_str name; /* dynamic linker's path name */ }; //在dyld加載命令中,offset爲sizeof(cmd)+sizeof(cmdsize)+sizeof(offset)=12; ptr表示dyld的路徑。表示偏移12位置是dyld的路徑 //在加載命令中,假若有字符串,那都用lc_str表示,lc_str僅僅告訴去相對於加載命令頭部多少的偏移位置取字符串,這個字符串都是放在加載命令結構體最後。 union lc_str { uint32_t offset; /* offset to the string */ #ifndef __LP64__ char *ptr; /* pointer to the string */ #endif ====================================== //dylib加載命令 struct dylib_command { uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB, LC_REEXPORT_DYLIB */ uint32_t cmdsize; /* includes pathname string */ struct dylib dylib; /* the library identification */ }; //name 放在加載命令最後,lc_str是告訴偏移位置。加載命令是4字節倍數,字符串填充後,不知足這要求,填充0來知足4字節倍數。 struct dylib { union lc_str name; /* library's path name 名字*/
uint32_t timestamp; /* library's build time stamp 構建的時間戳*/ uint32_t current_version; /* library's current version number 版本號*/
uint32_t compatibility_version; /* library's compatibility vers number 兼容的版本號*/ }; }; 複製代碼
//print.c 文件
#include <stdio.h>
char *global_var = "global_var";
void print(char *str)
{
printf("wkk:%s\n", str);
}
=======================================
//main.c 文件
void print(char *str);
extern char *global_var;
int main()
{
print(global_var);
return 0;
}
=========================================
//1. 編譯main.c
xcrun -sdk iphoneos clang -c main.c -o main.o -target arm64-apple-ios12.2
//2. 編譯print.c 成動態庫libPrint.dylib
xcrun -sdk iphoneos clang -fPIC -shared print.c -o libPrint.dylib -target arm64-apple-ios12.2
//3. 連接main.o 和 libPrint.dylib 成可執行文件main
xcrun -sdk iphoneos clang main.o -o main -L . -l Print -target arm64-apple-ios12.2
-target arm64-apple-ios12.2 ==> 運行的目標版本號iOS12.2
-l Print ==> 連接libPrint.dylib
-L . ==> libPrint.dylib在當前路徑尋找(.表明當前路徑)
複製代碼
上面說過動態連接跟靜態連接區別就是連接時機推遲到程序被加載時候,可是上面第三步將目標文件main.o連接成可執行文件時候,仍是用到了動態庫libPrint.dylib了。app
經過靜態連接,咱們知道main.o目標文件裏面,不知道global_var和print兩個符號的地址。而libPrint.dylib裏面有這兩個符號,因此咱們連接時候,用到libPrint.dylib,讓連接器知道這兩符號是來自dylib,只須要給這兩符號作個標記就能夠了,而不是此刻進行綁定和重定位(靜態連接此刻就要綁定和重定位)。獲得的main可執行文件知道這兩個符號是來自dylib,作了標記。等到main被加載時候,再把這兩符號綁定到libPrint.dylib裏,並進行重定位。iphone
如圖,main可執行文件把這兩個符號標記來自libPrint.dylib,可是沒有解析符號的地址。 ide
dylib在編譯時候,是不知道本身在進程中的虛擬內存地址的。由於dylib能夠被多個進程共享,好比進程1能夠在空閒地址0x1000-0x2000放共享對象a,可是進程2的0x1000-0x2000已經被主模塊佔用了,只有空閒地址0x3000-0x4000能夠放這個共享對象a。
因此共享對象a裏面有一個函數,在進程1中的虛擬內存地址是0x10f4,在進程2中的虛擬內存地址就成了0x30f4。那是否是機器指令就不能包含絕地地址了(動態庫代碼段全部進程共享;可修改的數據段,每一個進程有一個副本,私有的)。
爲了解決dylib的代碼段能被共享,PIC(地址無關代碼)技術就產生了。PIC原理很簡單,就是把指令中那些須要被修改的部分分離出來,跟數據部分放在一塊兒,這樣指令部分就能夠保持不變,而數據部分是每一個進程都有一個副本。
dylib須要被修改的部分(對地址的引用),按照是否跨模塊分爲兩類,引用方式又能夠分兩類:函數調用和數據訪問。這樣就分紅了4類:
因爲調用者和被調用者都在同一個模塊裏,它們之間的相對位置不變。因而有了相對尋址,用相對尋址就能夠作到是地址無關代碼。
給出相對於當前地址的偏移地址,二者相加就能夠獲得尋找的地址。
上圖中講了arm64裏的bl跳轉指令,比較簡單。爲了講解模塊內部的數據訪,我這裏再講一個比較難的指令adr和adrp指令,也是一個相對尋址指令。講以前,你們想下,爲何要有相對尋址?有兩個緣由:
adr指令是能夠尋找+/- 1MB的相對地址;adrp指令能夠尋找+/-4GB的相對地址。
immhi(immediate value high 當即數高位)和immlo(immediate value low當即數低位)一塊兒是21位,1位是符號位(往前/後跳),剩下20位表示1MB(2的10次方=1KB,2的20次方=1MB...)。當即數(offset)+PC(base)=目標地址。
adrp相似於adr,但它將12個較低位歸零並相對於當前PC頁面偏移。因此咱們能夠尋找+/-4GB的相對地址,代價是adrp指令後面,要跟着add指令去設置較低的12位。
adrp指令將21位當即數左移12位,將其和PC(程序計數器)相加,最後較低12位清零,而後將結果寫入通用寄存器。這容許計算4KB對齊的存儲區域的地址。 結合add指令,設置較低12位,能夠計算或訪問當前PC的±4GB範圍內的任何地址。
模塊外部的數據訪問的目標地址,要等到模塊被裝載時才決定,例如上面的動態連接🌰,main函數(能夠看成是主模塊的一個函數)訪問外部的global_var全局變量。global_var被定義在libPrint.dylib模塊,要等這個模塊被裝載了,而後連接器才決定global_val目標地址。前面提到了PIC基本思想就是把跟地址相關的部分放到數據段裏面。mach-o文件的數據段有一個got section(got:Global Offset Table 全局偏移表),當代碼須要引用該全局變量時,能夠經過got中相對應的項間接引用。
例以下圖,訪問global_var時,地址是0x100008000,說明global_var的真實地址存放在地址0x100008000裏面(注意:global_var真實地址不是0x100008000,而是放在0x100008000裏面。想下C語言中的指針)。連接器在裝載模塊時候會查找每一個外部變量所在的地址,而後填充got中的各個項,確保got裏面(下圖的綠框)存放的地址是正確的。got在數據段,因此它能夠在模塊裝載時被修改,而且每一個進程均可以有獨立的副本,相互不受影響。
模塊外部的函數調用,跟上面同樣的,也是間接尋址,此時got裏面存放的是模塊外部的函數地址。
經過上面,能夠看到模塊內部的函數訪問和數據訪問,都是用相對尋址作到PIC;模塊外部的函數訪問和數據訪問,都是用間接尋址作到PIC。
延遲綁定基本思想跟iOS的objc_msgSend基本同樣的,都是第一次調用函數時候,去查找函數的地址。而不是程序啓動時候,先把全部地址查找好。
模塊外部的函數和數據訪問,都是經過got來間接尋址的。程序被加載時候,動態連接要進行一次連接工做,好比加載依賴的模塊,修改got裏面的地址(符號查找、地址重定位)等工做,減慢了程序的啓動速度。好比咱們引入了Foundation動態庫,就必定會使用裏面的所有函數嗎?確定不是的,那咱們能夠相似objc_msgSend,等第一次調用時候,再去查找函數的地址。(got在數據段,程序運行期間可修改,因此第一次調用後,把函數的真實地址填入便可。objc_msgSend是第一次調用後,把函數地址放入cache裏,加速查找。)
//定義在<mach-o/loader.h>中
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */
... 這裏省略了好多暫時不需關心的字段(這個命令太多字段)
uint32_t indirectsymoff; /* file offset to the indirect symbol table 指向間接符號表位置*/
uint32_t nindirectsyms; /* number of indirect symbol table entries 間接符號表裏元素的個數*/
.... 省略
};
複製代碼
dysymtab_command可稱爲間接符號表(Indirect Symbol Table),能夠看作指向一個數組,裏面元素是整型數字。例如dysymtab[0]值爲2,意義:間接符號表第0項對應的符號在符號表第2項中(不清楚符號表:見上篇文章靜態連接)。
前面講模塊間的函數調用和數據訪問,都是經過got間接尋址,而後又講到延遲綁定。
具體到macho文件,由於模塊間的數據訪問不多(模塊間還提供不少全局變量給其它模塊用,那耦合度太大了,因此這樣的狀況不多見),因此外部數據地址,都是放到got(也稱Non-Lazy Symbol Pointers)數據段,非惰性的,動態連接階段,就尋找好全部數據符號的地址;而模塊間函數調用就太頻繁了,就用了延遲綁定技術,將外部函數地址都放在la_symbol_ptr(Lasy Symbol Pointers)數據段,惰性的,程序第一次調用到這個函數,才尋址函數地址,而後將地址寫入到這個數據段。下面經過上面的動態連接🌰來分析:
上圖,從main函數中,看到訪問的global_var在got數據段。在程序裝載時候,就重定位got裏面的地址,但是重定位global_var時候,至少得知道兩個信息(一、這是什麼符號;二、這個符號來自哪一個模塊),才能找到global_var的地址,修改got。
在上面咱們已經知道了,符號表裏描述了每一個符號的信息(包括外部符號來自哪一個模塊),因此咱們須要知道global_var對應符號表的index。在MachO文件結構分析最後,講了section_64,裏面有一個字段reserved1。
struct section_64 { /* for 64-bit architectures */
...
uint32_t reserved1; /* reserved (for offset or index) */
...
};
複製代碼
在got數據段的section_64裏,這個reserved1表示got裏面的符號在間接符號表(IndirectSymbolTable)的起始index,而後根據間接符號表含義。可獲得
value = IndirectSymbolTable[got.section_64.reserved1];
symbolTable[value] 就是got數據段的第一個符號。
symbolTable[value+1] 就是got數據段的第二個符號。
...依次類推
//從got的section_64能夠找到got數據段裏面元素對應的符號
複製代碼
模塊間的函數調用,是一個很頻繁的操做。具體到macho的動態連接中,將外部函數地址都放在la_symbol_ptr(Lasy Symbol Pointers)數據段,惰性的,程序第一次調用到這個函數,才尋址函數地址,而後將地址寫入到這個數據段。用上面一樣的方法,在la_symbol_ptr數據段的section_64,先找到reserved1,而後三步找到這個函數符號是什麼,來自哪一個模塊,可是程序加載時候不重定位。下面咱們以上面的動態連接🌰來分析:分析一下第一次調用時候,是如何尋址到函數地址的。
有木有發現其實訪問模塊外部(能夠簡單理解主模塊,訪問dylib)的變量和函數,爲了作到PIC,都是把變量和函數的地址放到數據段,由於數據段可修改。只是寫入變量和函數地址的時機不一樣,變量是動態連接時候寫入,主要不多訪問模塊外部變量,對程序啓動速度影響小。而函數是第一次調用時候,纔去尋址,寫入地址。到這裏,應該很好理解fishhook爲啥能夠修改模塊外部的函數/變量的地址,外部函數/變量的地址都放在數據段啊,數據段原本就是能夠修改的。若是咱們不理解動態連接,覺得函數地址在代碼段,那就很難理解fishhook是什麼黑魔法了。下一篇咱們將好好分析一下fishhook。