深入iOS系統底層之靜態庫介紹

歐陽大哥2013發表於2019-02-13

少長鹹集,群賢畢至。--《王羲之・蘭亭集序》

目標檔案

目標檔案結構

程式設計師編寫的是原始碼,而計算機執行的則是CPU能識別的機器指令,因此必須要有一系列工具或程式來將原始碼轉化為機器指令,這個轉化的過程需要經歷編譯和連結兩個主要階段。所謂編譯就是將原始碼檔案轉化為中間的目標檔案(Object file)。目標檔案的字尾一般為.o。iOS系統的目標檔案也是一種mach-o格式的檔案,mach-o檔案的頭部結構體:struct mach_header中的filetype成員欄位用來描述當前檔案的型別,目標檔案所對應的型別是MH_OBJECT。目標檔案中的佈局結構和內容和可執行檔案中的佈局結構和內容非常相似,編譯後形成的目標檔案中的程式碼段(__TEXT Segment)中的節(__text Section) 中的內容存放的是已經被編譯為機器指令的二進位制程式碼了。下面就是一個目標檔案的佈局結構:

目標檔案結構

重定位表(Relocation table)

系統的編譯操作是針對一個個原始檔的獨立行為。通常情況下在編寫程式時會引用其他原始檔或者動態庫中定義的函式或者類方法以及全域性變數,因此在編譯階段所有的外部引用符號的地址是無法被確定的,此時生成的目標檔案中的段(Segment)中的節(Section)中的外部函式呼叫指令的運算元部分以及外部全域性變數符號的地址的值都將是0。在後續的連結過程中需要調整這些指令的運算元的值來進行重定位(Relocation),為此係統在編譯的目標檔案中的對那些有外部符號引用的節(Section)中都會建立一個重定位表(Relocation table)。這個重定位表中的每個條目會將所有需要進行重定位的指令或者資料訪問的位置資訊以及引用的外部符號的資訊記錄起來,以便在連結時進行更新處理。下面的圖表展示了這個結構:

目標檔案的重定位資訊

現假設工程中有一個原始檔test.m,其內容如下:

int testfn(NSString *str)
{
      return [str lenght];
}
複製程式碼

這個原始檔中有一個OC方法呼叫[str length],方法在編譯時會轉化為對objc_msgSend函式的呼叫,但是因為objc_msgSend函式的定義在動態庫libobjc.dylib中,因此對於原始檔test.m來說這是一個外部符號,在生成函式呼叫指令時編譯器無法確定objc_msgSend函式相對於當前指令的偏移量,因此指令中的函式呼叫無法確定運算元的值,就如上圖的呼叫指令0x00000094一樣只有操作碼而運算元被暫時設定為0。

為了在連結時能夠對所有的外部符號引用進行重定位,描述機制程式碼__text的Section結構:

//如果是64位系統則是section_64
struct section { /* for 32-bit architectures */
	char		sectname[16];	/* name of this section */
	char		segname[16];	/* segment this section goes in */
	uint32_t	addr;		/* memory address of this section */
	uint32_t	size;		/* size in bytes of this section */
	uint32_t	offset;		/* file offset of this section */
	uint32_t	align;		/* section alignment (power of 2) */
	uint32_t	reloff;		/* 重定位入口表的偏移 */
	uint32_t	nreloc;		/* 重定位的條目數量 */
	uint32_t	flags;		/* flags (section type and attributes)*/
	uint32_t	reserved1;	/* reserved (for offset or index) */
	uint32_t	reserved2;	/* reserved (for count or sizeof) */
};
複製程式碼

中的reloff和nreloc兩個欄位用來描述這個節中所有需要進行重定位的資訊。就如上面的圖例中的"Relocations Offset"和"Number of Relocations"中描述的是重定位表在檔案的0x116c的偏移處,一共有3個需要進行重定位的資訊。重定位表的條目是一個結構體:

struct relocation_info {
   int32_t	r_address;	/* offset in the section to what is being
				   relocated */
   uint32_t     r_symbolnum:24,	/* symbol index if r_extern == 1 or section
				   ordinal if r_extern == 0 */
		r_pcrel:1, 	/* was relocated pc relative already */
		r_length:2,	/* 0=byte, 1=word, 2=long, 3=quad */
		r_extern:1,	/* does not include value of sym referenced */
		r_type:4;	/* if not 0, machine specific relocation type */
};
複製程式碼

這個結構體在<mach-o/reloc.h>中被定義,它的結構定義我將在後續的文件中會有詳細的介紹。這裡大家只要瞭解一下這個結構中主要包括的是需要進行重定向的指令的位置,以及外部引用的符號資訊。就如上圖中展示的一樣。

簡要的說一下連結步驟所做的事情

當編譯器對所有的原始碼檔案編譯完成後,接下來的步驟就是連結了。連結的主要功能就是將所有目標檔案中的各個相同段和節的資訊依次連線起來拼裝成一個單獨的可執行檔案。同時還會將所有目標檔案中需要Relocation的部分的指令進行調整,因為此時可以知道每個引用符號的位置了。在連結時系統會分析每個目標檔案中的依賴資訊,也就是說連結成一個可執行檔案中各段各節的內容總是無依賴的目標檔案放在前面而有依賴的目標檔案放置在後面。

基地址重定向(Rebase)

在連結時還有一個重要的資訊就是新增基地址重定向(Rebase)資訊。原因是程式執行時每個引用的動態庫以及可執行檔案所載入的基地址是不一樣的。而是一個隨機的行為。而我們的原始碼或者系統實現中有很多地方儲存的是一個絕對地址值,就比如runtime中每個OC類的方法列表中的IMP部分的值就是一個函式地址指標。在對程式進行編譯連結時會為生成的可執行檔案或者動態庫指定一個預設的虛擬基地址,後續所有生成的程式碼中的絕對地址值都是基於這個虛擬基地址來構建的。我們可以在可執行檔案的mach-o檔案的名字為__TEXT的這個LC_SEGMENT或者LC_SEGMENT_64中的load command定義中找到程式載入的預設的基地址。名字為__TEXT的結構體struct segment_command中的vmaddr資料成員中的值儲存的就是程式載入的預設基地址值,一般情況下可執行程式的預設基地址都是0x100000000。

而剛才說了雖然程式生成時的基地址是固定的,但是的每次程式載入到記憶體的基地址是不一樣的,而是一個隨機值。因此程式載入的真實基地址和程式生成時的基地址值之間就有一個slide值,也就是地址差值。但是因為程式中很多地方的地址值都是以生成的虛擬基地址為基礎的,所以在程式執行載入時需要對這部分函式地址進行基地址重定向(rebase)處理。為了實現rebase的能力,可執行檔案的mach-o檔案中會構造出一個LC_DYLD_INFO或者LC_DYLD_INFO_ONLY的load command,這個load command的結構描述是一個struct dyld_info_command,詳細描述可以在<mach-o/loader.h>中看到。這個結構體中的rebase_off和rebase_size兩個欄位分別用來描述需要進行rebase的表的偏移以及需要進行rebase的數量。rebase表中記錄著所有需要進行rebase的資訊,這樣當程式在載入時就會根據預設基地址的值和真實載入的基地址值之間的slide值來調整這部分內容的值。下面就是rebase段的內容:

rebae資訊

可以看出在LC_DYLD_INFO_ONLY中不僅有需要進行rebase的地址資訊,還有弱繫結和懶載入的資訊。每個rebase條目中記錄著rebase需要進行的操作(opcode)以及需要進行rebase的地址所在的段以及段內偏移值等資訊。關於rebase的詳細資訊我將會在後面的文章中繼續介紹。這裡就不再贅述了。

靜態庫的作用

每當我們build一個工程專案時,系統總是會先將所有原始碼編譯為目標檔案,再將目標檔案連結為可執行程式。即使是我們改變其中某一個檔案中的原始碼,而其他檔案沒有改變也是如此。因此為了加快編譯速度,有些檔案將不再以原始碼的形式提供,而是可以將一部分目標檔案先集中起來形成一個靜態庫。這樣就可以對這部分檔案略過編譯而直接進行連結從而加快編譯的速度。

對於iOS系統來說因為不支援第三方以動態庫的形式整合到我們的工程中以及上傳到appstore。而第三方提供的庫因為安全和智慧財產權以及保密的特性不大可能以原始碼的形式提供給我們,而是以靜態庫的形式提供給我們。

可見靜態庫的作用主要是為了加快編譯速度、進行模組劃分、以及程式碼安全的功能。靜態庫是一個編譯產生的結果,而動態庫則是編譯連結產生的結果。靜態庫的組成其實是一個個目標檔案。下面就是靜態庫和普通原始碼參與編譯和連線的流程圖,從流程圖中可以看出靜態庫存在的作用和意義:

靜態庫參與連結的流程

靜態庫檔案結構

靜態庫是由檔案頭標誌加符號表加目標檔案集合組成的一個檔案。可見靜態庫檔案是一個檔案的集合檔案。靜態庫在unix/linux中一般以.a結尾,而在windows中一般以.lib結尾。靜態庫檔案是一種檔案檔案(archive file),檔案檔案的格式並沒有形成統一的標準。

靜態庫的檔案格式並不是mach-o檔案格式的一部分。但是目前大部分作業系統中靜態庫的檔案格式和生成標準都非常的相似。因為在iOS系統中可以支援x64和arm兩種體系結構,因此iOS系統中的靜態庫檔案中還可以同時支援多種體系結構的目標檔案的集合,我們稱這種靜態庫檔案之為fat格式的靜態庫檔案。下面分別展示的單體系結構下的靜態庫檔案佈局結構和多體系結構下的靜態庫檔案佈局結構:

靜態庫檔案佈局結構

1.靜態庫檔案簽名

正如大部分檔案的開頭總是有一個所謂的magic標識一樣,單體系結構靜態庫檔案的開頭也有一個8位元組長度的字串簽名:!<arch>\n。這個簽名是所有檔案檔案(archive file)的通用頭部簽名。因此你可以通過讀取檔案的前8個位元組的內容來和“!<arch>\n”進行比較判斷來確認是否是一個有效的靜態庫。需要注意的是這裡的\n是一個換行的轉義字元。

2.符號表頭結構

靜態庫檔案的第二部分就是一個符號表頭結構。其實符號表也是可以單獨成為一個檔案的。因此符號表頭結構其實就是用來對符號表進行描述的結構體。這是一個變長的結構體,結構體定義如下:

struct symtab_header
{
   char identifier[16];       //符號表的標識
   char timestamp[12];       //符號表生成的時間戳, 這裡用數字字串來表示從1970.1.1到現在的毫秒數。
   char ownerid[6];             //符號表檔案的所有者標識
   char groupid[6];             //符號表檔案的組標識
   char mode[8];                 //符號表檔案的讀寫模式
   char size[10];                  //符號表的尺寸,用字串形式表示的尺寸。
   char end[2];                    //頭部結束標誌。
   char name[0];                 //可選的符號表檔名稱。
};
複製程式碼

符號表頭結構體中所有的資料成員都是字串型別,觀察結構體的資料成員有很多是和檔案屬性關聯的,比如時間戳、所有者、所屬的組、以及讀寫模式。這樣定義的作用是當我們把靜態庫中的符號表資訊單獨提取出一個檔案時可以設定提取出來檔案的預設屬性,同時這些資訊也用來描述生成這個靜態庫的符號表檔案的資訊。 符號表頭結構中的identifier和name兩個資料成員都可以用來描述符號表的名字。name部分則是可選的。當identifier為正常的字串時則identifier欄位用來描述符號表的名字。而當identifier中的內容為一個特殊值: “#1/長度” 時則表明name部分是用來描述符號表名稱的。name的長度則由identifier中指定的長度決定。比如當某個identifier中的內容為:“#1/20”時則表明符號表的名稱存放在name欄位中,並且名字的長度為20個字元。一般情況符號表的名稱都是固定為:“__.SYMDEF”或者為"__.SYMDEF_64",並且儲存在name欄位中。

3.符號表

靜態庫中的符號表中儲存的是所有目標檔案中的符號表資訊的集合。我們知道在程式連結時需要讀取目標檔案中的符號表資訊才能決定其他目標檔案中引用的符號資訊是否真實存在,當其他目標檔案引用的符號資訊不存在或者找不到時就會報經典的符號資訊不存在的錯誤:

Undefined symbols for architecture arm64:
  "_fn", referenced from:
      -[ViewController viewDidLoad] in ViewController.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

複製程式碼

那麼既然目標檔案中都有符號表資訊,為什麼還要在靜態庫的開頭來構造一段靜態庫內所有目標檔案匯出的符號資訊呢?答案就是為了加快連結的速度,因為每次都從目標檔案中去讀取符號資訊肯定會比單獨從靜態庫中一處讀取符號資訊要慢很多。

符號表的結構體也是一個可變長度的結構體其定義如下:

struct symtab
{
     int size;    //符號表條目的尺寸。注意這裡是整個符號表條目陣列的尺寸,而不是條目的數量。
     struct ranlib[0];  //符號表條目陣列,如果是64位的則是ranlib_64
};
複製程式碼

結構體struct ranlib的定義可以在<mach-o/ranlib.h>中找到。這個結構體的定義如下:

struct	ranlib {
    union {
	uint32_t	ran_strx;	 //符號名稱在下面的字串表中的開始偏移位置。
#ifndef __LP64__
	char		*ran_name;	/* symbol defined by */
#endif
    } ran_un;
    uint32_t		ran_off;	//符號歸屬的目標檔案頭結構的偏移。
};
複製程式碼

每個符號條目由兩部分組成:一個ran_strx是指定符號在下面字串表中的開始偏移的位置。一個ran_off則是指定這個符號是在哪個目標檔案中被定義,這個值是對應目標檔案的目標頭結構在靜態庫中的偏移量值。因此可以通過這個值快速的定義到符號所在的目標檔案。

4.字串表

靜態庫裡面的字串表是專門用來為符號表服務的。字串表跟在符號表的後面,最開始的4個位元組儲存的是字串表的長度,而後面跟隨的就是以\0結尾的字串陣列列表。字串表的結構定義如下:

struct stringtab
{
    int size;     //字串表的尺寸
    char strings[0];   //字串表的內容,每個字串以\0分隔。
};
複製程式碼

5.目標檔案頭結構

目標檔案頭結構用來描述後面跟隨的目標檔案的資訊。它的結構的定義和符號表頭結構是一模一樣的。這裡就不再贅述了。

6.目標檔案

目標檔案是一個mach-o格式的檔案,在上面關於目標檔案的介紹中有大體介紹目標檔案的格式,要想了解更多關於目標檔案的格式資訊請參考一些相關的mach-o格式介紹的文件,以及後續我也會在相關的文章中進行詳細介紹。

因為在靜態庫中是目標檔案的集合,因此每個靜態庫檔案中都會有非常多的目標檔案頭結構和目標檔案。下面就是一個靜態庫檔案結構的例子:

靜態庫檔案結構例項

7.Fat靜態庫頭結構

靜態庫檔案中可能只有一個體繫結構的庫,可能包括多個體繫結構的庫的集合,就比如第三方提供給我們的靜態庫可能會有模擬器版本和真機版本。因此靜態庫也是可以支援多體系結構的,當一個靜態庫中包含有多種體系結構的內容時,在靜態庫檔案的開頭將是一個Fat靜態庫的頭結構,而不是以"!\n"開頭了。而是一個如下定義的結構體:

struct fat_header {
	uint32_t	magic;		/* FAT_MAGIC or FAT_MAGIC_64 */
	uint32_t	nfat_arch;	/* number of structs that follow */
};
複製程式碼

這個結構體的定義可以在<mach-o/fat.h>中找到,可以看出無論是靜態庫還是可執行檔案,當檔案中包含多個體繫結構的程式碼時,檔案的開頭都是一個fat_header的結構體。結構體後面跟隨著多個體繫結構的描述資訊。

8.體系結構頭

體系結構頭資訊描述具體的體系結構的資訊,這個結構體的定義如下:

//如果是64位系統則是fat_arch_64
  struct fat_arch {
	cpu_type_t	cputype;	/* cpu specifier (int) */
	cpu_subtype_t	cpusubtype;	/* machine specifier (int) */
	uint32_t	offset;		/* file offset to this object file */
	uint32_t	size;		/* size of this object file */
	uint32_t	align;		/* alignment as a power of 2 */
};
複製程式碼

這個結構體的定義也可以在<mach-o/fat.h>中找到,可以很清楚的看到結構體中有描述具體的CPU的型別,以及對於的內容的偏移offset和size。對於靜態庫來說每個fat_arch的offset位置就是一個單體系結構的靜態庫的檔案的內容,而可執行檔案來說offset位置指定的就是可執行檔案的image內容。

上面就是我要介紹的關於靜態庫檔案結構的所有內容了。通過上面的介紹我想你應該對靜態庫的作用和其檔案佈局結構有了更進一步的瞭解。我們可以通過XCODE工程來生成一個靜態庫檔案,我們還可以通過lipo命令來構造一個多體系結構的靜態庫。(其實瞭解了靜態庫的檔案結構後我們就很容易自己編寫出一個lipo命令出來了!)

靜態庫的一些操作命令。

對於靜態庫檔案通常情況下我們可以藉助lipo命令在構建多體系結構的靜態庫,還可以通過ar命令來構建和顯示一個靜態庫中的檔案,以及提取這些檔案,或則將某個目標檔案從靜態庫中刪除,以及將某個目標檔案新增到靜態庫中。另外你還可以用nm命令來檢視一個靜態庫中的所有符號資訊。

lipo命令使用入口blog.csdn.net/SoaringLee_… ar命令使用入口: www.cnblogs.com/woxinyijiu/… nm命令使用入口: www.jianshu.com/p/6d5147347…

靜態庫中的兩個應用場景

☞場景1: 當你頭疼於你的程式的尺寸而需要刪減一些無用程式碼時,那麼對於刪除靜態庫中多餘的程式碼是一個不錯的選擇,你需要做的就是通過ar命令將靜態庫中的目標檔案逐個刪除,然後再做連結,直到應用不報連結錯誤為止。

☞場景2: 既然目標檔案中的relocation資訊是儲存的外部符號的引用資訊,那麼我們可以對目標檔案的這部分資訊進行修改,使得在不改變原始碼的情況下實現原生對函式A的呼叫改為對函式B的呼叫!一個非常有意思的應用就是我們可以改動所有對objc_msgSend的呼叫!來實現對OC方法呼叫的HOOK處理。至於為什麼要對靜態庫中的目標檔案修改的原因是XCODE對原始碼的編譯和連結是一體的我們無法在編譯之後和連結之前插入指令碼來修改目標檔案中的內容。但是靜態庫中的內容則是我們可以任意預先去修改的。

參考

1.本文對靜態庫結構的介紹主要是來自於machOView的原始碼。 2.en.wikipedia.org/wiki/Ar_(Un…

目錄

1.深入iOS系統底層之組合語言

2.深入iOS系統底層之指令集介紹

3.深入iOS系統底層之XCODE對彙編的支援介紹

4.深入iOS系統底層之CPU暫存器介紹

5.深入iOS系統底層之機器指令介紹

6.深入iOS系統底層之賦值指令介紹

7.深入iOS系統底層之函式呼叫介紹

8.深入iOS系統底層之其他常用指令介紹

9.深入iOS系統底層之函式棧介紹

10.深入iOS系統底層之函式棧(二)介紹

11.深入iOS系統底層之不定引數函式實現原理介紹

12.深入iOS系統底層之在高階語言中嵌入組合語言介紹

13.深入iOS系統底層之常見的彙編程式碼片段介紹

14.深入iOS系統底層之OC中的各種屬性以及修飾的實現介紹

15.深入iOS系統底層之ABI介紹

16.深入iOS系統底層之編譯連結過程介紹

17.深入iOS系統底層之可執行檔案結構介紹

18.深入iOS系統底層之MACH-O檔案格式介紹

19.深入iOS系統底層之映像檔案操作API介紹

20.深入iOS系統底層之知名load command結構介紹

21.深入iOS系統底層之程式載入過程介紹

22.深入iOS系統底層之靜態庫介紹

23.深入iOS系統底層之動態庫介紹

24.深入iOS系統底層之framework介紹

25.深入iOS系統底層之基地址介紹

26.深入iOS系統底層之模組內函式呼叫介紹

27.深入iOS系統底層之模組間函式呼叫介紹

28.深入iOS系統底層之機器指令動態構造介紹

29.深入iOS系統底層之crash解決方法介紹

30.深入iOS系統底層之常用工具和命令的實現原理介紹

31.深入iOS系統底層之真實的OC類記憶體結構介紹


歡迎大家訪問我的github地址

相關文章