我們用 Xcode 構(gòu)建一個程序的過程中,會把源文件 (.m 和 .h) 文件轉(zhuǎn)換為一個可執(zhí)行文件。這個可執(zhí)行文件中包含的字節(jié)碼會將被 CPU (iOS 設(shè)備中的 ARM 處理器或 Mac 上的 Intel 處理器) 執(zhí)行。
本文將介紹一下上面的過程中編譯器都做了些什么,同時深入看看可執(zhí)行文件內(nèi)部是怎樣的。實(shí)際上里面的東西要比我們第一眼看到的多得多。
這里我們把 Xcode 放一邊,將使用命令行工具 (command-line tools)。當(dāng)我們用 Xcode 構(gòu)建一個程序時,Xcode 只是簡單的調(diào)用了一系列的工具而已。Florian 對工具調(diào)用是如何工作的做了更詳細(xì)的討論。本文我們就直接調(diào)用這些工具,并看看它們都做了些什么。
真心希望本文能幫助你更好的理解 iOS 或 OS X 中的一個可執(zhí)行文件 (也叫做 Mach-O executable) 是如何執(zhí)行,以及怎樣組裝起來的。
先來看一些基礎(chǔ)性的東西:這里會大量使用一個名為 xcrun 的命令行工具。看起來可能會有點(diǎn)奇怪,不過它非常的出色。這個小工具用來調(diào)用別的一些工具。原先,我們在終端執(zhí)行如下命令:
% clang -v
現(xiàn)在我們用下面的命令代替:
% xcrun clang -v
在這里 xcrun 做的是定位到 clang,并執(zhí)行它,附帶輸入 clang 后面的參數(shù)。
我們?yōu)槭裁匆@樣做呢?看起來沒有什么意義。不過 xcode 允許我們: (1) 使用多個版本的 Xcode,以及使用某個特定 Xcode 版本中的工具。(2) 針對某個特定的 SDK (software development kit) 使用不同的工具。如果你有 Xcode 4.5 和 Xcode 5,通過 xcode-select 和 xcrun 可以選擇使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具。在許多其它平臺中,這是不可能做到的。查閱 xcrun 和 xcode-select 的主頁內(nèi)容可以了解到詳細(xì)內(nèi)容。不用安裝 Command Line Tools,就能使用命令行中的開發(fā)者工具。
回到終端 (Terminal),創(chuàng)建一個包含一個 C 文件的文件夾:
% mkdir ~/Desktop/objcio-command-line
% cd !$
% touch helloworld.c
接著使用你喜歡的文本編輯器來編輯這個文件 -- 例如 TextEdit.app:
% open -e helloworld.c
輸入如下代碼:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
保存并返回到終端,然后運(yùn)行如下命令:
% xcrun clang helloworld.c
% ./a.out
現(xiàn)在你能夠在終端上看到熟悉的 Hello World!。這里我們編譯并運(yùn)行 C 程序,全程沒有使用 IDE。深呼吸一下,高興高興。
上面我們到底做了些什么呢?我們將 helloworld.c 編譯為一個名為 a.out 的 Mach-O 二進(jìn)制文件。注意,如果我們沒有指定名字,那么編譯器會默認(rèn)的將其指定為 a.out。
這個二進(jìn)制文件是如何生成的呢?實(shí)際上有許多內(nèi)容需要觀察和理解。我們先看看編譯器吧。
時下 Xcode 中編譯器默認(rèn)選擇使用 clang(讀作 /kl??/)。關(guān)于編譯器,Chris 寫了更詳細(xì)的文章。
簡單的說,編譯器處理過程中,將 helloworld.c 當(dāng)做輸入文件,并生成一個可執(zhí)行文件 a.out。這個過程有多個步驟/階段。我們需要做的就是正確的執(zhí)行它們。
#include 的展開我們來看一個關(guān)于這些步驟的簡單的例子。
編譯過程中,編譯器首先要做的事情就是對文件做處理。預(yù)處理結(jié)束之后,如果我們停止編譯過程,那么我們可以讓編譯器顯示出預(yù)處理的一些內(nèi)容:
% xcrun clang -E helloworld.c
喔喔。 上面的命令輸出的內(nèi)容有 413 行。我們用編輯器打開這些內(nèi)容,看看到底發(fā)生了什么:
% xcrun clang -E helloworld.c | open -f
在頂部可以看到的許多行語句都是以 # 開頭 (讀作 hash)。這些被稱為 行標(biāo)記 的語句告訴我們后面跟著的內(nèi)容來自哪里。如果再回頭看看 helloworld.c 文件,會發(fā)現(xiàn)第一行是:
#include <stdio.h>
我們都用過 #include 和 import。它們所做的事情是告訴預(yù)處理器將文件 stdio.h 中的內(nèi)容插入到 #include 語句所在的位置。這是一個遞歸的過程:stdio.h 可能會包含其它的文件。
由于這樣的遞歸插入過程很多,所以我們需要確保記住相關(guān)行號信息。為了確保無誤,預(yù)處理器在發(fā)生變更的地方插入以 # 開頭的 行標(biāo)記。跟在 # 后面的數(shù)字是在源文件中的行號,而最后的數(shù)字是在新文件中的行號?;氐絼偛糯蜷_的文件,緊跟著的是系統(tǒng)頭文件,或者是被看做為封裝了 extern "C" 代碼塊的文件。
如果滾動到文件末尾,可以看到我們的 helloworld.c 代碼:
# 2 "helloworld.c" 2
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
在 Xcode 中,可以通過這樣的方式查看任意文件的預(yù)處理結(jié)果:Product -> Perform Action -> Preprocess。注意,編輯器加載預(yù)處理后的文件需要花費(fèi)一些時間 -- 接近 100,000 行代碼。
下一步:分析和代碼生成。我們可以用下面的命令讓 clang 輸出匯編代碼:
% xcrun clang -S -o - helloworld.c | open -f
我們來看看輸出的結(jié)果。首先會看到有一些以點(diǎn) . 開頭的行。這些就是匯編指令。其它的則是實(shí)際的 x86_64 匯編代碼。最后是一些標(biāo)記 (label),與 C 語言中的類似。
我們先看看前三行:
.section __TEXT,__text,regular,pure_instructions
.globl _main
.align 4, 0x90
這三行是匯編指令,不是匯編代碼。.section 指令指定接下來會執(zhí)行哪一個段。
第二行的 .globl 指令說明 _main 是一個外部符號。這就是我們的 main() 函數(shù)。這個函數(shù)對于二進(jìn)制文件外部來說是可見的,因?yàn)橄到y(tǒng)要調(diào)用它來運(yùn)行可執(zhí)行文件。
.align 指令指出了后面代碼的對齊方式。在我們的代碼中,后面的代碼會按照 16(2^4) 字節(jié)對齊,如果需要的話,用 0x90 補(bǔ)齊。
接下來是 main 函數(shù)的頭部:
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp2:
.cfi_def_cfa_offset 16
Ltmp3:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp4:
.cfi_def_cfa_register %rbp
subq $32, %rsp
上面的代碼中有一些與 C 標(biāo)記工作機(jī)制一樣的一些標(biāo)記。它們是某些特定部分的匯編代碼的符號鏈接。首先是 _main 函數(shù)真正開始的地址。這個符號會被 export。二進(jìn)制文件會有這個位置的一個引用。
.cfi_startproc 指令通常用于函數(shù)的開始處。CFI 是調(diào)用幀信息 (Call Frame Information) 的縮寫。這個調(diào)用 幀 以松散的方式對應(yīng)著一個函數(shù)。當(dāng)開發(fā)者使用 debugger 和 step in 或 step out 時,實(shí)際上是 stepping in/out 一個調(diào)用幀。在 C 代碼中,函數(shù)有自己的調(diào)用幀,當(dāng)然,別的一些東西也會有類似的調(diào)用幀。.cfi_startproc 指令給了函數(shù)一個 .eh_frame 入口,這個入口包含了一些調(diào)用棧的信息(拋出異常時也是用其來展開調(diào)用幀堆棧的)。這個指令也會發(fā)送一些和具體平臺相關(guān)的指令給 CFI。它與后面的 .cfi_endproc 相匹配,以此標(biāo)記出 main() 函數(shù)結(jié)束的地方。
接著是另外一個 label ## BB#0:。然后,終于,看到第一句匯編代碼:pushq %rbp。從這里開始事情開始變得有趣。在 OS X上,我們會有 X86_64 的代碼,對于這種架構(gòu),有一個東西叫做 ABI ( 應(yīng)用二進(jìn)制接口 application binary interface),ABI 指定了函數(shù)調(diào)用是如何在匯編代碼層面上工作的。在函數(shù)調(diào)用期間,ABI 會讓 rbp 寄存器 (基礎(chǔ)指針寄存器 base pointer register) 被保護(hù)起來。當(dāng)函數(shù)調(diào)用返回時,確保 rbp 寄存器的值跟之前一樣,這是屬于 main 函數(shù)的職責(zé)。pushq %rbp 將 rbp 的值 push 到棧中,以便我們以后將其 pop 出來。
接下來是兩個 CFI 指令:.cfi_def_cfa_offset 16 和 .cfi_offset %rbp, -16。這將會輸出一些關(guān)于生成調(diào)用堆棧展開和調(diào)試的信息。我們改變了堆棧和基礎(chǔ)指針,而這兩個指令可以告訴編譯器它們都在哪兒,或者更確切的,它們可以確保之后調(diào)試器要使用這些信息時,能找到對應(yīng)的東西。
接下來,movq %rsp, %rbp 將把局部變量放置到棧上。subq $32, %rsp 將棧指針移動 32 個字節(jié),也就是函數(shù)會調(diào)用的位置。我們先將老的棧指針存儲到 rbp 中,然后將此作為我們局部變量的基址,接著我們更新堆棧指針到我們將會使用的位置。
之后,我們調(diào)用了 printf():
leaq L_.str(%rip), %rax
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rax, %rdi
movb $0, %al
callq _printf
首先,leaq 會將 L_.str 的指針加載到 rax 寄存器中。留意 L_.str 標(biāo)記在后面的匯編代碼中是如何定義的。它就是 C 字符串"Hello World!\n"。 edi 和 rsi 寄存器保存了函數(shù)的第一個和第二個參數(shù)。由于我們會調(diào)用別的函數(shù),所以首先需要將它們的當(dāng)前值保存起來。這就是為什么我們使用剛剛存儲的 rbp 偏移32個字節(jié)的原因。第一個 32 字節(jié)的值是 0,之后的 32 字節(jié)的值是 edi 寄存器的值 (存儲了 argc)。然后是 64 字節(jié) 的值:rsi 寄存器的值 (存儲了 argv)。我們在后面并沒有使用這些值,但是編譯器在沒有經(jīng)過優(yōu)化處理的時候,它們還是會被存下來。
現(xiàn)在我們把第一個函數(shù) printf() 的參數(shù) rax 設(shè)置給第一個函數(shù)參數(shù)寄存器 edi 中。printf() 是一個可變參數(shù)的函數(shù)。ABI 調(diào)用約定指定,將會把使用來存儲參數(shù)的寄存器數(shù)量存儲在寄存器 al 中。在這里是 0。最后 callq 調(diào)用了 printf() 函數(shù)。
movl $0, %ecx
movl %eax, -20(%rbp) ## 4-byte Spill
movl %ecx, %eax
上面的代碼將 ecx 寄存器設(shè)置為 0,并把 eax 寄存器的值保存至棧中,然后將 ect 中的 0 拷貝至 eax 中。ABI 規(guī)定 eax 將用來保存一個函數(shù)的返回值,或者此處 main() 函數(shù)的返回值 0:
addq $32, %rsp
popq %rbp
ret
.cfi_endproc
函數(shù)執(zhí)行完成后,將恢復(fù)堆棧指針 —— 利用上面的指令 subq $32, %rsp 把堆棧指針 rsp 上移 32 字節(jié)。最后,把之前存儲至 rbp 中的值從棧中彈出來,然后調(diào)用 ret 返回調(diào)用者, ret 會讀取出棧的返回地址。 .cfi_endproc 平衡了 .cfi_startproc 指令。
接下來是輸出字符串 "Hello World!\n":
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World!\n"
同樣,.section 指令指出下面將要進(jìn)入的段。L_.str 標(biāo)記運(yùn)行在實(shí)際的代碼中獲取到字符串的一個指針。.asciz 指令告訴編譯器輸出一個以 ‘\0’ (null) 結(jié)尾的字符串。
__TEXT __cstring 開啟了一個新的段。這個段中包含了 C 字符串:
L_.str: ## @.str
.asciz "Hello World!\n"
上面兩行代碼創(chuàng)建了一個 null 結(jié)尾的字符串。注意 L_.str 是如何命名,之后會通過它來訪問字符串。
最后的 .subsections_via_symbols 指令是靜態(tài)鏈接編輯器使用的。
更過關(guān)于匯編指令的資料可以在 蘋果的 OS X Assembler Reference 中看到。AMD 64 網(wǎng)站有關(guān)于 ABI for x86 的文檔。另外還有 Gentle Introduction to x86-64 Assembly。
重申一下,通過下面的選擇操作,我們可以用 Xcode 查看任意文件的匯編輸出結(jié)果:Product -> Perform Action -> Assemble.
匯編器將可讀的匯編代碼轉(zhuǎn)換為機(jī)器代碼。它會創(chuàng)建一個目標(biāo)對象文件,一般簡稱為 對象文件。這些文件以 .o 結(jié)尾。如果用 Xcode 構(gòu)建應(yīng)用程序,可以在工程的 derived data 目錄中,Objects-normal 文件夾下找到這些文件。
稍后我們會對鏈接器做更詳細(xì)的介紹。這里簡單介紹一下:鏈接器解決了目標(biāo)文件和庫之間的鏈接。什么意思呢?還記得下面的語句嗎:
callq _printf
printf() 是 libc 庫中的一個函數(shù)。無論怎樣,最后的可執(zhí)行文件需要能需要知道 printf() 在內(nèi)存中的具體位置:例如,_printf 的地址符號是什么。鏈接器會讀取所有的目標(biāo)文件 (此處只有一個) 和庫 (此處是 libc),并解決所有未知符號 (此處是 _printf) 的問題。然后將它們編碼進(jìn)最后的可執(zhí)行文件中 (可以在 libc 中找到符號 _printf),接著鏈接器會輸出可以運(yùn)行的執(zhí)行文件:a.out。
就像我們上面提到的一樣,這里有些東西叫做 section。一個可執(zhí)行文件包含多個段,也就是多個 section??蓤?zhí)行文件不同的部分將加載進(jìn)不同的 section,并且每個 section 會轉(zhuǎn)換進(jìn)某個 segment 里。這個概念對于所有的可執(zhí)行文件都是成立的。
我們來看看 a.out 二進(jìn)制中的 section。我們可以使用 size 工具來觀察:
% xcrun size -x -l -m a.out
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x37 (addr 0x100000f30 offset 3888)
Section __stubs: 0x6 (addr 0x100000f68 offset 3944)
Section __stub_helper: 0x1a (addr 0x100000f70 offset 3952)
Section __cstring: 0xe (addr 0x100000f8a offset 3978)
Section __unwind_info: 0x48 (addr 0x100000f98 offset 3992)
Section __eh_frame: 0x18 (addr 0x100000fe0 offset 4064)
total 0xc5
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
如上代碼所示,我們的 a.out 文件有 4 個 segment。有些 segment 中有多個 section。
當(dāng)運(yùn)行一個可執(zhí)行文件時,虛擬內(nèi)存 (VM - virtual memory) 系統(tǒng)將 segment 映射到進(jìn)程的地址空間上。映射完全不同于我們一般的認(rèn)識,如果你對虛擬內(nèi)存系統(tǒng)不熟悉,可以簡單的想象虛擬內(nèi)存系統(tǒng)將整個可執(zhí)行文件加載進(jìn)內(nèi)存 -- 雖然在實(shí)際上不是這樣的。VM 使用了一些技巧來避免全部加載。
當(dāng)虛擬內(nèi)存系統(tǒng)進(jìn)行映射時,segment 和 section 會以不同的參數(shù)和權(quán)限被映射。
上面的代碼中,__TEXT segment 包含了被執(zhí)行的代碼。它被以只讀和可執(zhí)行的方式映射。進(jìn)程被允許執(zhí)行這些代碼,但是不能修改。這些代碼也不能對自己做出修改,因此這些被映射的頁從來不會被改變。
__DATA segment 以可讀寫和不可執(zhí)行的方式映射。它包含了將會被更改的數(shù)據(jù)。
第一個 segment 是 __PAGEZERO。它的大小為 4GB。這 4GB 并不是文件的真實(shí)大小,但是規(guī)定了進(jìn)程地址空間的前 4GB 被映射為 不可執(zhí)行、不可寫和不可讀。這就是為什么當(dāng)讀寫一個 NULL 指針或更小的值時會得到一個 EXC_BAD_ACCESS 錯誤。這是操作系統(tǒng)在嘗試防止引起系統(tǒng)崩潰。
在 segment中,一般都會有多個 section。它們包含了可執(zhí)行文件的不同部分。在 __TEXT segment 中,__text section 包含了編譯所得到的機(jī)器碼。__stubs 和 __stub_helper 是給動態(tài)鏈接器 (dyld) 使用的。通過這兩個 section,在動態(tài)鏈接代碼中,可以允許延遲鏈接。__const (在我們的代碼中沒有) 是常量,不可變的,就像 __cstring (包含了可執(zhí)行文件中的字符串常量 -- 在源碼中被雙引號包含的字符串) 常量一樣。
__DATA segment 中包含了可讀寫數(shù)據(jù)。在我們的程序中只有 __nl_symbol_ptr 和 __la_symbol_ptr,它們分別是 non-lazy 和 lazy 符號指針。延遲符號指針用于可執(zhí)行文件中調(diào)用未定義的函數(shù),例如不包含在可執(zhí)行文件中的函數(shù),它們將會延遲加載。而針對非延遲符號指針,當(dāng)可執(zhí)行文件被加載同時,也會被加載。
在 _DATA segment 中的其它常見 section 包括 __const,在這里面會包含一些需要重定向的常量數(shù)據(jù)。例如 char * const p = "foo"; -- p 指針指向的數(shù)據(jù)是可變的。__bss section 沒有被初始化的靜態(tài)變量,例如 static int a; -- ANSI C 標(biāo)準(zhǔn)規(guī)定靜態(tài)變量必須設(shè)置為 0。并且在運(yùn)行時靜態(tài)變量的值是可以修改的。__common section 包含未初始化的外部全局變量,跟 static 變量類似。例如在函數(shù)外面定義的 int a;。最后,__dyld 是一個 section 占位符,被用于動態(tài)鏈接器。
蘋果的 OS X Assembler Reference 文檔有更多關(guān)于 section 類型的介紹。
下面,我們用 otool(1) 來觀察一個 section 中的內(nèi)容:
% xcrun otool -s __TEXT __text a.out
a.out:
(__TEXT,__text) section
0000000100000f30 55 48 89 e5 48 83 ec 20 48 8d 05 4b 00 00 00 c7
0000000100000f40 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7
0000000100000f50 b0 00 e8 11 00 00 00 b9 00 00 00 00 89 45 ec 89
0000000100000f60 c8 48 83 c4 20 5d c3
上面是我們 app 中的代碼。由于 -s __TEXT __text 很常見,otool 對其設(shè)置了一個縮寫 -t 。我們還可以通過添加 -v 來查看反匯編代碼:
% xcrun otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000f30 pushq %rbp
0000000100000f31 movq %rsp, %rbp
0000000100000f34 subq $0x20, %rsp
0000000100000f38 leaq 0x4b(%rip), %rax
0000000100000f3f movl $0x0, 0xfffffffffffffffc(%rbp)
0000000100000f46 movl %edi, 0xfffffffffffffff8(%rbp)
0000000100000f49 movq %rsi, 0xfffffffffffffff0(%rbp)
0000000100000f4d movq %rax, %rdi
0000000100000f50 movb $0x0, %al
0000000100000f52 callq 0x100000f68
0000000100000f57 movl $0x0, %ecx
0000000100000f5c movl %eax, 0xffffffffffffffec(%rbp)
0000000100000f5f movl %ecx, %eax
0000000100000f61 addq $0x20, %rsp
0000000100000f65 popq %rbp
0000000100000f66 ret
上面的內(nèi)容是一樣的,只不過以反匯編形式顯示出來。你應(yīng)該感覺很熟悉,這就是我們在前面編譯時候的代碼。唯一的不同就是,在這里我們沒有任何的匯編指令在里面。這是純粹的二進(jìn)制執(zhí)行文件。
同樣的方法,我們可以查看別的 section:
% xcrun otool -v -s __TEXT __cstring a.out
a.out:
Contents of (__TEXT,__cstring) section
0x0000000100000f8a Hello World!\n
或:
% xcrun otool -v -s __TEXT __eh_frame a.out
a.out:
Contents of (__TEXT,__eh_frame) section
0000000100000fe0 14 00 00 00 00 00 00 00 01 7a 52 00 01 78 10 01
0000000100000ff0 10 0c 07 08 90 01 00 00
從側(cè)面來講,__DATA 和 __TEXT segment對性能會有所影響。將數(shù)據(jù)移至 __TEXT 是個不錯的選擇,因?yàn)檫@些頁從來不會被改變。
使用鏈接符號 -sectcreate 我們可以給可執(zhí)行文件以 section 的方式添加任意的數(shù)據(jù)。這就是如何將一個 Info.plist 文件添加到一個獨(dú)立的可執(zhí)行文件中的方法。Info.plist 文件中的數(shù)據(jù)需要放入到 __TEXT segment 里面的一個 __info_plist section 中??梢詫?-sectcreate segname sectname file 傳遞給鏈接器(通過將下面的內(nèi)容傳遞給 clang):
-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist
同樣,-sectalign 規(guī)定了對其方式。如果你添加的是一個全新的 segment,那么需要通過 -segprot 來規(guī)定 segment 的保護(hù)方式 (讀/寫/可執(zhí)行)。這些所有內(nèi)容在鏈接器的幫助文檔中都有,例如 ld(1)。
我們可以利用定義在 /usr/include/mach-o/getsect.h 中的函數(shù) getsectdata() 得到 section,例如 getsectdata() 可以得到指向 section 數(shù)據(jù)的一個指針,并返回相關(guān) section 的長度。
在 OS X 和 iOS 中可執(zhí)行文件的格式為 Mach-O:
% file a.out
a.out: Mach-O 64-bit executable x86_64
對于 GUI 程序也是一樣的:
% file /Applications/Preview.app/Contents/MacOS/Preview
/Applications/Preview.app/Contents/MacOS/Preview: Mach-O 64-bit executable x86_64
關(guān)于 Mach-O 文件格式 蘋果有詳細(xì)的介紹。
我們可以使用 otool(1) 來觀察可執(zhí)行文件的頭部 -- 規(guī)定了這個文件是什么,以及文件是如何被加載的。通過 -h 可以打印出頭信息:
% otool -v -h a.out a.out:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 16 1296 NOUNDEFS DYLDLINK TWOLEVEL PIE
cputype 和 cpusubtype 規(guī)定了這個可執(zhí)行文件能夠運(yùn)行在哪些目標(biāo)架構(gòu)上。ncmds 和 sizeofcmds 是加載命令,可以通過 -l 來查看這兩個加載命令:
% otool -v -l a.out | open -f
a.out:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
...
加載命令規(guī)定了文件的邏輯結(jié)構(gòu)和文件在虛擬內(nèi)存中的布局。otool 打印出的大多數(shù)信息都是源自這里的加載命令??匆幌?Load command 1 部分,可以找到 initprot r-x,它規(guī)定了之前提到的保護(hù)方式:只讀和可執(zhí)行。
對于每一個 segment,以及segment 中的每個 section,加載命令規(guī)定了它們在內(nèi)存中結(jié)束的位置,以及保護(hù)模式等。例如,下面是 __TEXT __text section 的輸出內(nèi)容:
Section
sectname __text
segname __TEXT
addr 0x0000000100000f30
size 0x0000000000000037
offset 3888
align 2^4 (16)
reloff 0
nreloc 0
type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0
reserved2 0
上面的代碼將在 0x100000f30 處結(jié)束。它在文件中的偏移量為 3888。如果看一下之前 xcrun otool -v -t a.out 輸出的反匯編代碼,可以發(fā)現(xiàn)代碼實(shí)際位置在 0x100000f30。
我們同樣看看在可執(zhí)行文件中,動態(tài)鏈接庫是如何使用的:
% otool -v -L a.out
a.out:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)
time stamp 2 Thu Jan 1 01:00:02 1970
上面就是我們可執(zhí)行文件將要找到 _printf 符號的地方。
我們來看看有三個文件的復(fù)雜例子:
Foo.h:
#import <Foundation/Foundation.h>
@interface Foo : NSObject
- (void)run;
@end
Foo.m:
#import "Foo.h"
@implementation Foo
- (void)run
{
NSLog(@"%@", NSFullUserName());
}
@end
helloworld.m:
#import "Foo.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
Foo *foo = [[Foo alloc] init];
[foo run];
return 0;
}
}
在上面的示例中,有多個源文件。所以我們需要讓 clang 對輸入每個文件生成對應(yīng)的目標(biāo)文件:
% xcrun clang -c Foo.m
% xcrun clang -c helloworld.m
我們從來不編譯頭文件。頭文件的作用就是在被編譯的實(shí)現(xiàn)文件中對代碼做簡單的共享。Foo.m 和 helloworld.m 都是通過 #import 語句將 Foo.h 文件中的內(nèi)容添加到實(shí)現(xiàn)文件中的。
最終得到了兩個目標(biāo)文件:
% file helloworld.o Foo.o
helloworld.o: Mach-O 64-bit object x86_64
Foo.o: Mach-O 64-bit object x86_64
為了生成一個可執(zhí)行文件,我們需要將這兩個目標(biāo)文件和 Foundation framework 鏈接起來:
xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
現(xiàn)在可以運(yùn)行我們的程序了:
% ./a.out
2013-11-03 18:03:03.386 a.out[8302:303] Daniel Eggert
我們這個簡單的程序是將兩個目標(biāo)文件合并到一起的。Foo.o 目標(biāo)文件包含了 Foo 類的實(shí)現(xiàn),而 helloworld.o 目標(biāo)文件包含了 main() 函數(shù),以及調(diào)用/使用 Foo 類。
另外,這兩個目標(biāo)對象都使用了 Foundation framework。helloworld.o 目標(biāo)文件使用了它的 autorelease pool,并間接的使用了 libobjc.dylib 中的 Objective-C 運(yùn)行時。它需要運(yùn)行時函數(shù)來進(jìn)行消息的調(diào)用。Foo.o 目標(biāo)文件也有類似的原理。
所有的這些東西都被形象的稱之為符號。我們可以把符號看成是一些在運(yùn)行時將會變成指針的東西。雖然實(shí)際上并不是這樣的。
每個函數(shù)、全局變量和類等都是通過符號的形式來定義和使用的。當(dāng)我們將目標(biāo)文件鏈接為一個可執(zhí)行文件時,鏈接器 (ld(1)) 在目標(biāo)文件盒動態(tài)庫之間對符號做了解析處理。
可執(zhí)行文件和目標(biāo)文件有一個符號表,這個符號表規(guī)定了它們的符號。如果我們用 nm(1) 工具觀察一下 helloworld.0 目標(biāo)文件,可以看到如下內(nèi)容:
% xcrun nm -nm helloworld.o
(undefined) external _OBJC_CLASS_$_Foo
0000000000000000 (__TEXT,__text) external _main
(undefined) external _objc_autoreleasePoolPop
(undefined) external _objc_autoreleasePoolPush
(undefined) external _objc_msgSend
(undefined) external _objc_msgSend_fixup
0000000000000088 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_
000000000000008e (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_1
0000000000000093 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_2
00000000000000a0 (__DATA,__objc_msgrefs) weak private external l_objc_msgSend_fixup_alloc
00000000000000e8 (__TEXT,__eh_frame) non-external EH_frame0
0000000000000100 (__TEXT,__eh_frame) external _main.eh
上面就是那個目標(biāo)文件的所有符號。_OBJC_CLASS_$_Foo 是 Foo Objective-C 類的符號。該符號是 undefined, external 。External 的意思是指對于這個目標(biāo)文件該類并不是私有的,相反,non-external 的符號則表示對于目標(biāo)文件是私有的。我們的 helloworld.o 目標(biāo)文件引用了類 Foo,不過這并沒有實(shí)現(xiàn)它。因此符號表中將其標(biāo)示為 undefined。
接下來是 _main 符號,它是表示 main() 函數(shù),同樣為 external,這是因?yàn)樵摵瘮?shù)需要被調(diào)用,所以應(yīng)該為可見的。由于在 helloworld.o 文件中實(shí)現(xiàn)了 這個 main 函數(shù)。這個函數(shù)地址位于 0處,并且需要轉(zhuǎn)入到 __TEXT,__text section。接著是 4 個 Objective-C 運(yùn)行時函數(shù)。它們同樣是 undefined的,需要鏈接器進(jìn)行符號解析。
如果我們轉(zhuǎn)而觀察 Foo.o 目標(biāo)文件,可以看到如下輸出:
% xcrun nm -nm Foo.o
0000000000000000 (__TEXT,__text) non-external -[Foo run]
(undefined) external _NSFullUserName
(undefined) external _NSLog
(undefined) external _OBJC_CLASS_$_NSObject
(undefined) external _OBJC_METACLASS_$_NSObject
(undefined) external ___CFConstantStringClassReference
(undefined) external __objc_empty_cache
(undefined) external __objc_empty_vtable
000000000000002f (__TEXT,__cstring) non-external l_.str
0000000000000060 (__TEXT,__objc_classname) non-external L_OBJC_CLASS_NAME_
0000000000000068 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000b0 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000d0 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000118 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000140 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
0000000000000168 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_
000000000000016c (__TEXT,__objc_methtype) non-external L_OBJC_METH_VAR_TYPE_
00000000000001a8 (__TEXT,__eh_frame) non-external EH_frame0
00000000000001c0 (__TEXT,__eh_frame) non-external -[Foo run].eh
第五行至最后一行顯示了 _OBJC_CLASS_$_Foo 已經(jīng)定義了,并且對于 Foo.o 是一個外部符號 -- ·Foo.o· 包含了這個類的實(shí)現(xiàn)。
Foo.o 同樣有 undefined 的符號。首先是使用了符號 NSFullUserName(),NSLog()和 NSObject。
當(dāng)我們將這兩個目標(biāo)文件和 Foundation framework (是一個動態(tài)庫) 進(jìn)行鏈接處理時,鏈接器會嘗試解析所有的 undefined 符號。它可以解析 _OBJC_CLASS_$_Foo。另外,它將使用 Foundation framework。
當(dāng)鏈接器通過動態(tài)庫 (此處是 Foundation framework) 解析成功一個符號時,它會在最終的鏈接圖中記錄這個符號是通過動態(tài)庫進(jìn)行解析的。鏈接器會記錄輸出文件是依賴于哪個動態(tài)鏈接庫,并連同其路徑一起進(jìn)行記錄。在我們的例子中,_NSFullUserName,_NSLog,_OBJC_CLASS_$_NSObject,_objc_autoreleasePoolPop 等符號都是遵循這個過程。
我們可以看一下最終可執(zhí)行文件 a.out 的符號表,并注意觀察鏈接器是如何解析所有符號的:
% xcrun nm -nm a.out
(undefined) external _NSFullUserName (from Foundation)
(undefined) external _NSLog (from Foundation)
(undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)
(undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external __objc_empty_cache (from libobjc)
(undefined) external __objc_empty_vtable (from libobjc)
(undefined) external _objc_autoreleasePoolPop (from libobjc)
(undefined) external _objc_autoreleasePoolPush (from libobjc)
(undefined) external _objc_msgSend (from libobjc)
(undefined) external _objc_msgSend_fixup (from libobjc)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e50 (__TEXT,__text) external _main
0000000100000ed0 (__TEXT,__text) non-external -[Foo run]
0000000100001128 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001150 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
可以看到所有的 Foundation 和 Objective-C 運(yùn)行時符號依舊是 undefined,不過現(xiàn)在的符號表中已經(jīng)多了如何解析它們的信息,例如在哪個動態(tài)庫中可以找到對應(yīng)的符號。
可執(zhí)行文件同樣知道去哪里找到所需庫:
% xcrun otool -L a.out
a.out:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1056.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 855.11.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
在運(yùn)行時,動態(tài)鏈接器 dyld(1) 可以解析這些 undefined 符號,dyld 將會確定好 _NSFullUserName 等符號,并指向它們在 Foundation 中的實(shí)現(xiàn)等。
我們可以針對 Foundation 運(yùn)行 nm(1),并檢查這些符號的定義情況:
% xcrun nm -nm `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName
0000000000007f3e (__TEXT,__text) external _NSFullUserName
有一些環(huán)境變量對于 dyld 的輸出信息非常有用。首先,如果設(shè)置了 DYLD_PRINT_LIBRARIES,那么 dyld 將會打印出什么庫被加載了:
% (export DYLD_PRINT_LIBRARIES=; ./a.out )
dyld: loaded: /Users/deggert/Desktop/command_line/./a.out
dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
dyld: loaded: /usr/lib/libobjc.A.dylib
dyld: loaded: /usr/lib/libauto.dylib
[...]
上面將會顯示出在加載 Foundation 時,同時會加載的 70 個動態(tài)庫。這是由于 Foundation 依賴于另外一些動態(tài)庫。運(yùn)行下面的命令:
% xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
可以看到 Foundation 使用了 15 個動態(tài)庫。
當(dāng)你構(gòu)建一個真正的程序時,將會鏈接各種各樣的庫。它們又會依賴其他一些 framework 和 動態(tài)庫。需要加載的動態(tài)庫會非常多。而對于相互依賴的符號就更多了??赡軐猩锨€符號需要解析處理,這將花費(fèi)很長的時間:一般是好幾秒鐘。
為了縮短這個處理過程所花費(fèi)時間,在 OS X 和 iOS 上的動態(tài)鏈接器使用了共享緩存,共享緩存存于 /var/db/dyld/。對于每一種架構(gòu),操作系統(tǒng)都有一個單獨(dú)的文件,文件中包含了絕大多數(shù)的動態(tài)庫,這些庫都已經(jīng)鏈接為一個文件,并且已經(jīng)處理好了它們之間的符號關(guān)系。當(dāng)加載一個 Mach-O 文件 (一個可執(zhí)行文件或者一個庫) 時,動態(tài)鏈接器首先會檢查 共享緩存 看看是否存在其中,如果存在,那么就直接從共享緩存中拿出來使用。每一個進(jìn)程都把這個共享緩存映射到了自己的地址空間中。這個方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動時間。