深刻iOS系統底層之靜態庫

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

目標文件

目標文件結構

程序員編寫的是源代碼,而計算機運行的則是CPU能識別的機器指令,所以必需要有一系列工具或程序來將源代碼轉化爲機器指令,這個轉化的過程須要經歷編譯和連接兩個主要階段。所謂編譯就是將源代碼文件轉化爲中間的目標文件(Object file)。目標文件的後綴通常爲.o。iOS系統的目標文件也是一種mach-o格式的文件,mach-o文件的頭部結構體:struct mach_header中的filetype成員字段用來描述當前文件的類型,目標文件所對應的類型是MH_OBJECT。目標文件中的佈局結構和內容和可執行文件中的佈局結構和內容很是類似,編譯後造成的目標文件中的代碼段(__TEXT Segment)中的節(__text Section) 中的內容存放的是已經被編譯爲機器指令的二進制代碼了。下面就是一個目標文件的佈局結構: linux

目標文件結構

重定位表(Relocation table)

系統的編譯操做是針對一個個源文件的獨立行爲。一般狀況下在編寫程序時會引用其餘源文件或者動態庫中定義的函數或者類方法以及全局變量,所以在編譯階段全部的外部引用符號的地址是沒法被肯定的,此時生成的目標文件中的段(Segment)中的節(Section)中的外部函數調用指令的操做數部分以及外部全局變量符號的地址的值都將是0。在後續的連接過程當中須要調整這些指令的操做數的值來進行重定位(Relocation),爲此係統在編譯的目標文件中的對那些有外部符號引用的節(Section)中都會創建一個重定位表(Relocation table)。這個重定位表中的每一個條目會將全部須要進行重定位的指令或者數據訪問的位置信息以及引用的外部符號的信息記錄起來,以便在連接時進行更新處理。下面的圖表展現了這個結構:git

目標文件的重定位信息

現假設工程中有一個源文件test.m,其內容以下:程序員

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

這個源文件中有一個OC方法調用[str length],方法在編譯時會轉化爲對objc_msgSend函數的調用,可是由於objc_msgSend函數的定義在動態庫libobjc.dylib中,所以對於源文件test.m來講這是一個外部符號,在生成函數調用指令時編譯器沒法肯定objc_msgSend函數相對於當前指令的偏移量,所以指令中的函數調用沒法肯定操做數的值,就如上圖的調用指令0x00000094同樣只有操做碼而操做數被暫時設置爲0。github

爲了在連接時可以對全部的外部符號引用進行重定位,描述機制代碼__text的Section結構:windows

//若是是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的部分的指令進行調整,由於此時能夠知道每一個引用符號的位置了。在連接時系統會分析每一個目標文件中的依賴信息,也就是說連接成一個可執行文件中各段各節的內容老是無依賴的目標文件放在前面而有依賴的目標文件放置在後面。bash

基地址重定向(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的詳細信息我將會在後面的文章中繼續介紹。這裏就再也不贅述了。

靜態庫代碼連接規則

應用程序連接的過程最開始是以主程序工程中的全部目標文件爲單位進行的,不管這個工程中的目標文件中的代碼是否有被引用或者被調用都會連接進可執行程序中。在連接的過程當中,若是發現某個符號沒有在主程序工程中被定義,那麼就會去導入的動態庫文件或者靜態庫文件中查找。若是符號在動態庫中被定義那麼就會爲動態庫的中的符號(這裏假設符號就是某個函數) 生成stub代碼而且將引用信息放入導入符號表以便在後續程序運行時動態的加載真實的函數地址。而若是發現符號在靜態庫中被定義那麼就會按以下的規則進行處理:

  • 默認狀況下是以靜態庫中的目標文件爲單位進行連接的,只要某個目標文件中定義的符號被主程序引用,則這個目標文件中的全部代碼都會連接到可執行程序中去。若是這個目標文件中又引用了其餘目標文件中定義的符號則連接會進行遞歸處理。若是靜態庫中某個目標文件中的代碼沒有被任何其餘地方引用則這個目標文件將不會連接到可執行程序中去。
  • OC類的方法列表的構建是在編譯階段完成的,可是對其中的方法調用都是在運行時動態肯定的,因此在代碼中的任何對靜態庫中定義的OC類的方法調用都不會被認爲是對符號的引用,都不會產生連接行爲。除非在代碼中引用了這個OC類自己纔會產生連接行爲,此時會把靜態庫中定義的全部OC類的方法都連接到可執行程序中(由於OC類的方法列表在編譯階段已經構建完成)。也就是說靜態庫中的OC類定義的方法要麼就所有都連接進可執行程序中,要麼就一個方法也不會被連接

假設某個靜態庫中定義了一個名字爲CA的OC類:

//類中定義了2個方法。
@interface CA:NSObject
-(void)fn1
-(void)fn2;
@end

//假如在同一個文件中還定義了CB類
@interface CB:NSObject

@end

複製代碼

假設主程序中有兩處會使用到靜態庫中定義的CA類的地方:

//雖然這裏CA做爲一個參數,而且裏面調用了對應的方法,可是在連接時仍然不會將CA類連接進來,由於這個是一個運行時的間接方法調用過程。
void foo1(CA *p)
{
    [p fn1];
}

//假設沒有foo2這個函數則CA類中的代碼是不會連接進可執行程序中的。
void foo2()
{
    //只有明確的使用CA類來建立對象時,才代表是對CA類的引用。這樣纔會將CA類中的全部方法都連接進可執行程序中,這裏雖然沒有調用fn2可是fn2的實現也會被連接進去。
     CA *p = [CA new];
     [p fn1];
}

void main()
{
    foo1(nil);
    foo2();   
}

複製代碼

由於CB和CA類在同一個.m文件中實現,因此即便CB類沒有被引用,可是根據上述的按文件爲單位的連接規則,CB類仍然會被連接到可執行程序中,除非CB類和CA類不在同一個文件中實現。

  • 靜態庫中的任意OC類所定義的分類方法默認狀況下都不會連接到可執行程序中,即便這個方法在主程序中調用了也是如此(這也就是爲何當咱們調用靜態庫中某個類的分類方法時總會報方法找不到的異常,緣由上面的OC類方法調用都是運行時被肯定而不是編譯時就被肯定)。 除非是在主程序工程中的Other Linker Flags 中添加 -ObjC 選項。這個選項的意思是會把全部靜態庫中定義的OC類的方法都連接到可執行程序中去,而無論這個類是否有被引用,也無論方法是不是分類方法。

  • 若是靜態庫中定義的C語言函數沒有被任何地方引用則這個函數將不會被連接到可執行程序中去。而若是相同文件中其餘符號被引用則根據以文件爲單爲的連接規則即便這個函數沒有被引用也會連接到可執行程序中去。

  • 若是靜態庫中定義的C++類的的某個普通的成員函數沒有被任何地方引用則這個成員函數將不會被連接到可執行程序中去。若是這是一個虛函數則只要這個類被引用則即便這個虛函數沒有被引用也會連接到可執行程序中去,由於虛函數須要在編譯時參與虛表的構建。而若是相同文件中其餘符號被引用則根據以文件爲單爲的連接規則即便這個文件中C++類中定義的成員函數沒有被引用也會連接到可執行程序中去。

  • 若是靜態庫中某文件定義的Swift類沒有被任何地方引用則不會連接到可執行程序中,若是類自己或者類中的方法被引用則類中定義的全部方法都會連接到可執行程序中去。對於Swift類定義的extension中的擴展方法而言若是擴展方法是和類方法定義在同一個文件,則一旦類被引用則擴展方法也會被連接到可執行程序中。若是擴展方法定義在不一樣的文件中,則只有擴展方法被調用時纔會被連接進可執行程序。

Swift類的方法調用並非和OC類的方法調用在運行時才決定的,而是採用了相似C++虛函數的機制來實現多態的功能以及和虛函數的調用機制類似,而Swift中對extension的實現則是直接採用函數地址調用,也就是說extension中定義的函數就和普通的C函數很是類似。

  • 若是咱們在Other Linker Flags中添加**-all_load選項,則主程序工程會把全部靜態庫中的全部代碼所有連接到可執行程序中去,而無論代碼是以何種語言實現的代碼,以及無論代碼是否被引用和調用。若是咱們只想對某個靜態庫中的全部代碼進行所有連接處理,則能夠在Other Linker Flags中添加-force_load 靜態庫路徑**來實現。

這也就是爲何當咱們調用在靜態庫中定義的分類方法時若是不使用-Objc或者-all_load選項時,會出現方法不被識別的調用異常了。可是這兩個選項的另一個問題是無論靜態庫中的類是否被引用都會將代碼連接到可執行程序中去,從而增長了可執行程序的尺寸。

  • 咱們能夠在主程序工程的項目中將DEAD_CODE_STRIPPING(Dead Code Stripping) 開關開啓,用來優化可執行程序中的代碼。須要注意的是這個開關是在代碼連接完成後的優化行爲。當這個開關被打開時連接器會刪除可執行程序中全部沒有被調用的C函數以及C++中的普通成員函數。可是不會刪除沒有被調用到的OC類的成員方法,以及Swift類的成員方法,以及C++類中的虛函數。在XCODE中這個開關默認是開啓的。

從上面的規則中能夠看出採用靜態庫的形式進行連接能夠減小可執行文件的尺寸。有的時候咱們的應用可能會引用一些第三方的靜態庫,而這些第三方的靜態庫的尺寸很是的龐大(好比地圖類的SDK,可能有好幾百兆)。但是當應用最後生成的可執行文件卻不是那麼的大。如今的應用每每都集成了不少的功能,尤爲是一些大型應用的尺寸都已經達到好幾百M了,這麼大尺寸的應用下載安裝的時間每每很長,並且還會消耗用戶的網絡流量,甚至會影響程序的啓動時間。

靜態庫的做用

每當咱們build一個工程項目時,系統老是會先將全部源代碼編譯爲目標文件,再將目標文件連接爲可執行程序。即便是咱們改變其中某一個文件中的源代碼,而其餘文件沒有改變也是如此。所以爲了加快編譯速度,有些文件將再也不以源代碼的形式提供,而是能夠將一部分目標文件先集中起來造成一個靜態庫。這樣就能夠對這部分文件略過編譯而直接進行連接從而加快編譯的速度。

對於iOS系統來講由於不支持第三方以動態庫的形式集成到咱們的工程中以及上傳到appstore。而第三方提供的庫由於安全和知識產權以及保密的特性不大可能以源代碼的形式提供給咱們,而是以靜態庫的形式提供給咱們。

可見靜態庫的做用主要是爲了加快編譯速度、進行模塊劃分、以及代碼安全的功能。靜態庫是一個編譯產生的結果,而動態庫則是編譯連接產生的結果。靜態庫的組成實際上是一個個目標文件。下面就是靜態庫和普通源代碼參與編譯和鏈接的流程圖,從流程圖中能夠看出靜態庫存在的做用和意義:

靜態庫參與連接的流程

靜態庫文件結構

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

靜態庫的文件格式並非mach-o文件格式的一部分。可是目前大部分操做系統中靜態庫的文件格式和生成標準都很是的類似。由於在iOS系統中能夠支持x64和arm兩種體系結構,所以iOS系統中的靜態庫文件中還能夠同時支持多種體系結構的目標文件的集合,咱們稱這種靜態庫文件之爲fat格式的靜態庫文件。下面分別展現的單體系結構下的靜態庫文件佈局結構和多體系結構下的靜態庫文件佈局結構:

靜態庫文件佈局結構

1.靜態庫文件簽名

正如大部分文件的開頭老是有一個所謂的magic標識同樣,單體系結構靜態庫文件的開頭也有一個8字節長度的字符串簽名:!\n。這個簽名是全部檔案文件(archive file)的通用頭部簽名。所以你能夠經過讀取文件的前8個字節的內容來和「!\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…

靜態庫中的一個應用場景

靜態庫的目標文件中的relocation信息是保存的外部符號的引用信息,那麼咱們能夠對目標文件的這部分信息進行修改,使得在不改變源代碼的狀況下實現原生對函數A的調用改成對函數B的調用!一個很是有意思的應用就是咱們能夠改動全部對objc_msgSend的調用!來實現對OC方法調用的HOOK處理。至於爲何要對靜態庫中的目標文件修改的緣由是XCODE對源代碼的編譯和連接是一體的咱們沒法在編譯以後和連接以前插入腳原本修改目標文件中的內容。可是靜態庫中的內容則是咱們能夠任意預先去修改的。

參考

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

👉【返回目錄


歡迎你們訪問個人github地址

相關文章
相關標籤/搜索