- 什么是靜態(tài)鏈接
- 如何實現(xiàn)靜態(tài)鏈接
- 靜態(tài)鏈接的優(yōu)點與缺點
- 什么是動態(tài)鏈接
- 如何實現(xiàn)動態(tài)鏈接
- 動態(tài)鏈接的優(yōu)點與缺點
- SO文件格式簡析
- 根據(jù)SO文件格式進行靜態(tài)反編譯
靜態(tài)鏈接
一段代碼從文本編輯器上產生到最終能夠在機器上運行,經歷了非常多的階段,概括而言,至少包含了以下幾個階段:
- 編譯: 編譯器通過詞法分析,語法分析,語義分析等,將一段代碼翻譯成匯編語言
- 匯編:將匯編語言翻譯成機器指令
- 鏈接:解決符號之間的重定位問題
- 裝載:將可執(zhí)行文件加載到內存
靜態(tài)鏈接就是在裝載之前,就完成所有的符號引用的一種鏈接方式。靜態(tài)鏈接的處理過程分為2個步驟:
- 空間與地址的分配。掃描所有的目標文件,合并相似段,收集當中所有的符號信息。
- 符號解析與重定位。調整代碼位置。

如何可得,在完成靜態(tài)鏈接之后,可執(zhí)行文件中代碼段、數(shù)據(jù)段等的虛擬地址已經確定,即當此可執(zhí)行文件被載入到內存后,代碼段的起始位置就在0000000000400400的位置。
靜態(tài)鏈接的優(yōu)缺點
- 優(yōu)點: 簡單
- 缺點:
- 浪費內存空間。在多進程的操作系統(tǒng)下,同一時間,內存中可能存在多個相同的公共庫函數(shù)。
- 程序的開發(fā)與發(fā)布流程受模塊制約。 只要有一個模塊更新,那么就需要重新編譯打包整個代碼。
為了解決以上2個問題,就誕生了動態(tài)鏈接。
動態(tài)鏈接
基本思想就是將對符號的重定位推遲到程序運行時才進行。
只要推遲到運行時進行符號的重定位,就能解決靜態(tài)鏈接的兩個缺點。
對于第一個缺點:在運行時重定位,如果在運行過程中調用了公共庫函數(shù)或者其他模塊的函數(shù),系統(tǒng)只需要在內存中維護一份公共庫代碼即可,只要將不同應用程序對公共庫函數(shù)的調用地址設置成相同即可。
對于第二個缺點:理論上只要將需要替換的模塊更新,無需將整個應用程序打包。
動態(tài)鏈接的實現(xiàn)
對于靜態(tài)鏈接來說,系統(tǒng)只需要加載一個文件(可執(zhí)行文件)到內存即可,但是在動態(tài)鏈接下,系統(tǒng)需要映射一個主程序和多個動態(tài)鏈接模塊,因此,相比于靜態(tài)鏈接,動態(tài)鏈接使得內存的空間分布更加復雜。
不同模塊在內存中的裝載位置一定要保證不一樣。
裝載時重定位
對于每一個模塊,對于代碼中符號的絕對引用,都需要加上一個基地址偏移量。(比如一個模塊中存在一個絕對地址的引用A,它假定的是模塊可以被加載到0x1000的地方,但是系統(tǒng)在加載該位置已經被其他模塊占用了,系統(tǒng)選擇將它加載到0x4000的地方,那么對絕對地址A的引用就需要被修改為A+0x3000,即修改代碼)。
但是它也有缺點,因為不同進程可能使用同一個模塊,但是對于不同進程,代碼段是不能共享的,因此,多個進程還是沒有辦法共享一個模塊。因為不能修改模塊當中的代碼,一旦一個進程將共享模塊的絕對地址修改了,其他進程此時調用就一定會報錯。
解決這個問題也有辦法,在Windows中使用的就是裝載時重定位,但是在Linux下使用的是另外一種方式。
地址無關代碼
一個模塊的代碼部分是共享的,但是數(shù)據(jù)部分是每個進程一個副本的。因此,地址無關代碼的基本思想就是將代碼段中的絕對地址引用剝離出來放到數(shù)據(jù)段中,以保證代碼指令不變。在Android系統(tǒng)下進行SO文件的編譯,默認就是產生地址無關代碼。
因此程序執(zhí)行的流程就變成了:當模塊A調用模塊B的某個方法的時候,會從模塊A的數(shù)據(jù)段部分找到模塊B中函數(shù)地址,然后進行函數(shù)調用。
動態(tài)鏈接的優(yōu)點與缺點
優(yōu)點: 解決了靜態(tài)鏈接的缺陷,更適應現(xiàn)代的大規(guī)模的軟件開發(fā)
缺點:1. 結構復雜 2.引入了安全問題,這也是我們能夠進行PLT HOOK的基礎
SO文件格式
ELF頭表
ELF頭表記錄了ELF文件的基本信息,包括魔數(shù),目標文件類型(可執(zhí)行文件,共享庫文件或者目標文件),文件的目標體系結構,程序入口地址(共享庫文件為此值為0),然后是section表大小和數(shù)目,程序頭表的大小和數(shù)目,分別對應的是鏈接視圖和執(zhí)行視圖。
Section表
Section表記錄了每一個Section的基本信息,名稱,類型,字節(jié)數(shù),虛擬地址偏移和文件偏移。文件偏移指的是在ELF文件中,Section距離ELF文件起始位置的字節(jié)數(shù),而虛擬地址偏移指的是當此section被加載到內存中后,該Section距離ELF起始位置的字節(jié)數(shù)。由于有些section只存在于文件中,而不會被系統(tǒng)加載到內存中,因此虛擬地址偏移可能為0.
程序頭表
程序頭表是裝載視圖下,系統(tǒng)進行segment解析的入口,它給出了每一個segment的類型,文件偏移,虛擬地址偏移和對齊等。通過對程序頭表的遍歷,我們可以得到ELF文件所有的segment。
重定位表
靜態(tài)鏈接下需要對符號的引用進行重定位,并且ELF文件中對符號的引用可能出現(xiàn)在代碼段,也可能出現(xiàn)在數(shù)據(jù)段,因此重定位表分為了代碼段重定位表和數(shù)據(jù)段重定位表,分別記錄引用的符號名和所在的偏移地址。因此,重定位表的格式就是記錄符號名和重定位地址的數(shù)組。
.dynamic段
.dynamic段是動態(tài)鏈接中最重要的段,它記錄了和動態(tài)鏈接有關的段的類型,地址或者數(shù)值。指向了與動態(tài)鏈接相關的段
.got段
位于數(shù)據(jù)區(qū),就是上文所述為了實現(xiàn)位置無關代碼將對絕對地址的引用抽離出來存放的集合
.plt段
在介紹.rel.plt段之前,對比靜態(tài)鏈接與動態(tài)鏈接我們需要知道.plt段的作用。我們會發(fā)現(xiàn),動態(tài)鏈接將所有的重定位操作延遲到加載時處理,那么就難以避免的會降低程序執(zhí)行的效率,試想,如果有1000個對外部模塊的函數(shù)引用,動態(tài)鏈接器就需要先解決這1000個函數(shù)引用,然后才開始執(zhí)行程序。為此,鏈接器為了提升效率,采取了這樣一種策略:僅當函數(shù)被調用時,才會喚醒動態(tài)鏈接器解決重定位問題。
.plt段就是為了實現(xiàn)這種策略增加的段。增加了.plt段之后,代碼段中的地址就不再指向.got段而是指向了.plt段,再由.plt段指向.got段,具體過程如下:
.plt段是包含了若干數(shù)目的代碼片段組成的段,代碼段中的地址指向對應函數(shù)的.plt代碼段,代碼片段的第一行代碼就是間接調用.got表中對應函數(shù)地址,但是此時,.got表中的地址指向的是.plt中代碼片段的第二行代碼,而第二行以后的代碼作用就是調用動態(tài)鏈接器處理.got表中的地址。當再次調用此函數(shù)時,.plt中代碼段第一行代碼就可以正確的跳入函數(shù)地址執(zhí)行相應的函數(shù)了。
但是,在Android平臺下,由于兼容性的限制,并沒有采用這種"延遲加載"的特性,所以一開始加載,.got表中的地址就是真實的函數(shù)調用地址。但是,Android下的ELF文件仍然保留了.plt表這種結構。
根據(jù)SO文件結構進行靜態(tài)反編譯
靜態(tài)反編譯是指對未載入內存控件的SO文件進行的反編譯,在這種情況下,主要是針對SO文件的鏈接視圖進行關鍵參數(shù)上的修改,使得靜態(tài)反編譯工具不能夠正常的工作。
根據(jù)SO文件的格式,在ELF頭表中會表明Section表的位置和Section表格的大小。絕大部分靜態(tài)反編譯工具是依據(jù)SO文件的鏈接視圖還原出SO文件內容的,那么在這種情況下,我們只需要修改ELF頭表中與SECTION表相關的2個參數(shù),就能使得工具反編譯SO文件失效。但是SO文件在系統(tǒng)載入內存之后,是使用的執(zhí)行視圖對SO文件進行解析,因此不會影響SO文件運行時。