本篇將用下面例子分析:ios
// a.c 文件
extern int global_var;
void func(int a);
int main() {
int a = 100;
func(a+global_var);
return 0;
}
=========================
// b.c 文件
int global_var = 1;
void func(int a) {
global_var = a;
}
=========================
//生成a.o b.o
xcrun -sdk iphoneos clang -c a.c b.c -target arm64-apple-ios12.2
// a.o和b.o連接成可執行文件ab
xcrun -sdk iphoneos clang a.o b.o -o ab -target arm64-apple-ios12.2
複製代碼
請注意,生成的a.o和b.o目標文件,都是基於arm64。a.o和b.o目標文件經過靜態連接後生成可執行文件ab。(因爲基於arm64,其實連接過程,也有動態連接庫libSystem.B.dylib(系統庫)參與,但本文忽略動態連接的參與,只討論靜態連接。要是基於X86,就不會有動態庫的參與。後一篇文章專門再討論動態連接)程序員
這裏先介紹兩個概念:模塊和符號。數組
靜態連接:輸入多個目標文件,輸出一個文件(通常是可執行文件)。這個過程當中,把多個目標文件裏相同性質的段合併到一塊兒。好比:上面a.o和b.o目標文件合併成可執行文件ab。合併過程是a.o裏面的代碼段和b.o裏面的代碼段一塊兒合併成ab裏面的代碼段,數據段同理,兩個目標文件裏面的數據段一塊兒合併成ab裏的數據段...緩存
掃描全部的輸入目標文件,而且得到他們各個段的長度、屬性和位置,將輸入目標文件中的符號表(下面詳細講解)中全部的符號定義和符號引用收集起來(就是收集函數和變量的定義與引用),統一放到一個全局符號表。這一步中,連接器可以得到全部的輸入目標文件的段的長度,將它們合併,計算出輸出文件中各個段合併後的長度和位置,並創建映射關係。bash
使用上面第一步收集到的信息,讀取輸入文件中段的數據、重定位信息,而且進行符號解析和重定位,調整代碼中的地址等。app
a模塊使用了global_var和func兩個符號,那是怎麼知道這兩個符號的地址呢?iphone
在a.o目標文件中:模塊化
global_var(地址0)和func(地址0x2c,這條指令自己地址)都是假地址。編譯器暫時用0x0和0x2c替代着,把真正地址計算工做留給連接器。經過前面的空間與地址分配能夠得知,連接器在完成地址與空間分配後,就能夠肯定全部符號的虛擬地址了。那麼連接器就能夠根據符號的地址對每一個須要重定位的指令進行地址修正。在連接後的ab可執行文件中:函數
能夠看到global_var(地址0x100008000,指向data段,值爲1)和func(地址0x100007f90,指向func函數地址)都是真正的地址。連接器是怎麼知道a模塊裏哪些指令要被調整,這些指令如何調整。事實上a.o裏,有一個重定位表,專門保存這些與重定位相關的信息。並且每一個section的section_64的header的reloff(重定位表裏的偏移)和nreloc(幾個須要重定位的符號),讓連接器知道a模塊的哪一個section裏的指令須要調整。post
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_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; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
複製代碼
重定位表能夠認爲是一個數組,數組裏的元素爲結構體relocation_info。
//定義在<mach-o/reloc.h>裏
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 */
};
複製代碼
每一個參數都有註釋,r_address和r_length足夠讓咱們知道要重定位的字節了;r_symbolnum(當爲外部符號)是符號表的index。其它參數可先無論。
例如a.o中,重定位表記錄符號_func和_global_var,兩個符號須要重定位。而且給出了兩個符號在代碼段的位置,和指向符號表的index,連接時候(a.o裏面有這兩符號的引用,而後b.o裏面有這兩符號的定義,一塊兒合併到全局符號表裏),在全局符號表裏,能夠找到這兩個符號的虛擬內存位置和其它信息(見下面符號表),就能夠完成重定位工做了。上面說r_symbolnum(當爲外部符號)是符號表的index,咱們這裏再給你們介紹一個加載命令:符號表
//定義在<mach-o/loader.h>中
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
複製代碼
上篇文章咱們說過,加載命令的前兩個參數都是cmd和cmdsize。符號表加載命令的symoff和nsyms告訴了連接器符號表的位置(偏移)和個數;stroff和strsize告訴字符串表的位置和大小。
符號表也是一個數組,裏面元素是結構體nlist_64
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
複製代碼
n_un歷史緣由,忽略;n_strx字符串表的index,能夠找到符號對應的字符串;n_sect第幾個section;n_valuen符號的地址值。其它先無論,要是有興趣,能夠去頭文件<mach-o/nlist.h>查看。
從普通程序員的角度看,爲何要連接,由於一個模塊(a模塊)可能引用了其它模塊(b模塊)的符號,因此須要把全部模塊(目標文件)連接在一塊兒。重定位就是:連接器會去查找由全部輸入的目標文件的符號表組成的全局符號表,找到相應的符號後進行重定位。其中有2個常見的錯誤:
一個靜態庫能夠簡單當作一組目標文件的集合,即多個目標文件通過壓縮打包後造成的一個文件。
靜態庫連接:是指本身的模塊與靜態庫裏的某個模塊(用到的某個目標文件,或多個目標文件)連接成可執行文件。其實和靜態連接概念同樣,只是這裏,咱們這裏取了靜態庫裏的某個/多個目標文件與咱們本身的目標文件一塊兒做爲輸入。
靜態庫通常包含多個目標文件,一個目標文件可能只有一個函數。由於連接器在連接靜態庫的時候是以目標文件爲單位的。假如咱們把全部函數放在一個目標文件裏,那咱們可能只用到一個函數,確把不少沒用的函數一塊兒連接到可執行文件裏。