咱們用 Xcode 構建一個程序的過程當中,會把源文件 (.m
和 .h
) 文件轉換爲一個可執行文件。這個可執行文件中包含的字節碼會將被 CPU (iOS 設備中的 ARM 處理器或 Mac 上的 Intel 處理器) 執行。html
本文將介紹一下上面的過程當中編譯器都作了些什麼,同時深刻看看可執行文件內部是怎樣的。實際上裏面的東西要比咱們第一眼看到的多得多。xcode
這裏咱們把 Xcode 放一邊,將使用命令行工具 (command-line tools)。當咱們用 Xcode 構建一個程序時,Xcode 只是簡單的調用了一系列的工具而已。Florian 對工具調用是如何工做的作了更詳細的討論。本文咱們就直接調用這些工具,並看看它們都作了些什麼。緩存
真心但願本文能幫助你更好的理解 iOS 或 OS X 中的一個可執行文件 (也叫作 Mach-O executable) 是如何執行,以及怎樣組裝起來的。架構
先來看一些基礎性的東西:這裏會大量使用一個名爲 xcrun
的命令行工具。看起來可能會有點奇怪,不過它很是的出色。這個小工具用來調用別的一些工具。原先,咱們在終端執行以下命令:app
% clang -v
複製代碼
如今咱們用下面的命令代替:編輯器
% xcrun clang -v
複製代碼
在這裏 xcrun
作的是定位到 clang
,並執行它,附帶輸入 clang
後面的參數。函數
咱們爲何要這樣作呢?看起來沒有什麼意義。不過 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
的主頁內容能夠了解到詳細內容。不用安裝 Command Line Tools,就能使用命令行中的開發者工具。工具
回到終端 (Terminal),建立一個包含一個 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;
}
複製代碼
保存並返回到終端,而後運行以下命令:
% xcrun clang helloworld.c
% ./a.out
複製代碼
如今你可以在終端上看到熟悉的 Hello World!
。這裏咱們編譯並運行 C 程序,全程沒有使用 IDE。深呼吸一下,高興高興。
上面咱們到底作了些什麼呢?咱們將 helloworld.c
編譯爲一個名爲 a.out
的 Mach-O 二進制文件。注意,若是咱們沒有指定名字,那麼編譯器會默認的將其指定爲 a.out。
這個二進制文件是如何生成的呢?實際上有許多內容須要觀察和理解。咱們先看看編譯器吧。
時下 Xcode 中編譯器默認選擇使用 clang
(讀做 /klæŋ/)。關於編譯器,Chris 寫了更詳細的文章。
簡單的說,編譯器處理過程當中,將 helloworld.c
當作輸入文件,並生成一個可執行文件 a.out
。這個過程有多個步驟/階段。咱們須要作的就是正確的執行它們。
#include
的展開咱們來看一個關於這些步驟的簡單的例子。
編譯過程當中,編譯器首先要作的事情就是對文件作處理。預處理結束以後,若是咱們中止編譯過程,那麼咱們可讓編譯器顯示出預處理的一些內容:
% xcrun clang -E helloworld.c
複製代碼
喔喔。 上面的命令輸出的內容有 413 行。咱們用編輯器打開這些內容,看看到底發生了什麼:
% xcrun clang -E helloworld.c | open -f
複製代碼
在頂部能夠看到的許多行語句都是以 #
開頭 (讀做 hash
)。這些被稱爲 行標記 的語句告訴咱們後面跟着的內容來自哪裏。若是再回頭看看 helloworld.c
文件,會發現第一行是:
#include <stdio.h>
複製代碼
咱們都用過 #include
和 import
。它們所作的事情是告訴預處理器將文件 stdio.h
中的內容插入到 #include
語句所在的位置。這是一個遞歸的過程:stdio.h
可能會包含其它的文件。
因爲這樣的遞歸插入過程不少,因此咱們須要確保記住相關行號信息。爲了確保無誤,預處理器在發生變動的地方插入以 #
開頭的 行標記
。跟在 #
後面的數字是在源文件中的行號,而最後的數字是在新文件中的行號。回到剛纔打開的文件,緊跟着的是系統頭文件,或者是被看作爲封裝了 extern "C"
代碼塊的文件。
若是滾動到文件末尾,能夠看到咱們的 helloworld.c
代碼:
# 2 "helloworld.c" 2
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
複製代碼
在 Xcode 中,能夠經過這樣的方式查看任意文件的預處理結果:Product -> Perform Action -> Preprocess。注意,編輯器加載預處理後的文件須要花費一些時間 -- 接近 100,000 行代碼。
下一步:分析和代碼生成。咱們能夠用下面的命令讓 clang
輸出彙編代碼:
% xcrun clang -S -o - helloworld.c | open -f
複製代碼
咱們來看看輸出的結果。首先會看到有一些以點 .
開頭的行。這些就是彙編指令。其它的則是實際的 x86_64 彙編代碼。最後是一些標記 (label),與 C 語言中的相似。
咱們先看看前三行:
.section __TEXT,__text,regular,pure_instructions
.globl _main
.align 4, 0x90
複製代碼
這三行是彙編指令,不是彙編代碼。.section
指令指定接下來會執行哪個段。
第二行的 .globl
指令說明 _main
是一個外部符號。這就是咱們的 main()
函數。這個函數對於二進制文件外部來講是可見的,由於系統要調用它來運行可執行文件。
.align
指令指出了後面代碼的對齊方式。在咱們的代碼中,後面的代碼會按照 16(2^4) 字節對齊,若是須要的話,用 0x90
補齊。
接下來是 main 函數的頭部:
_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 標記工做機制同樣的一些標記。它們是某些特定部分的彙編代碼的符號連接。首先是 _main
函數真正開始的地址。這個符號會被 export。二進制文件會有這個位置的一個引用。
.cfi_startproc
指令一般用於函數的開始處。CFI 是調用幀信息 (Call Frame Information) 的縮寫。這個調用 幀
以鬆散的方式對應着一個函數。當開發者使用 debugger 和 step in 或 step out 時,其實是 stepping in/out 一個調用幀。在 C 代碼中,函數有本身的調用幀,固然,別的一些東西也會有相似的調用幀。.cfi_startproc
指令給了函數一個 .eh_frame
入口,這個入口包含了一些調用棧的信息(拋出異常時也是用其來展開調用幀堆棧的)。這個指令也會發送一些和具體平臺相關的指令給 CFI。它與後面的 .cfi_endproc
相匹配,以此標記出 main()
函數結束的地方。
接着是另一個 label ## BB#0:
。而後,終於,看到第一句彙編代碼:pushq %rbp
。從這裏開始事情開始變得有趣。在 OS X上,咱們會有 X86_64 的代碼,對於這種架構,有一個東西叫作 ABI ( 應用二進制接口 application binary interface),ABI 指定了函數調用是如何在彙編代碼層面上工做的。在函數調用期間,ABI 會讓 rbp
寄存器 (基礎指針寄存器 base pointer register) 被保護起來。當函數調用返回時,確保 rbp
寄存器的值跟以前同樣,這是屬於 main 函數的職責。pushq %rbp
將 rbp
的值 push 到棧中,以便咱們之後將其 pop 出來。
接下來是兩個 CFI 指令:.cfi_def_cfa_offset 16
和 .cfi_offset %rbp, -16
。這將會輸出一些關於生成調用堆棧展開和調試的信息。咱們改變了堆棧和基礎指針,而這兩個指令能夠告訴編譯器它們都在哪兒,或者更確切的,它們能夠確保以後調試器要使用這些信息時,能找到對應的東西。
接下來,movq %rsp, %rbp
將把局部變量放置到棧上。subq $32, %rsp
將棧指針移動 32 個字節,也就是函數會調用的位置。咱們先將老的棧指針存儲到 rbp
中,而後將此做爲咱們局部變量的基址,接着咱們更新堆棧指針到咱們將會使用的位置。
以後,咱們調用了 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
標記在後面的彙編代碼中是如何定義的。它就是 C 字符串"Hello World!\n"
。 edi
和 rsi
寄存器保存了函數的第一個和第二個參數。因爲咱們會調用別的函數,因此首先須要將它們的當前值保存起來。這就是爲何咱們使用剛剛存儲的 rbp
偏移32個字節的緣由。第一個 32 字節的值是 0,以後的 32 字節的值是 edi
寄存器的值 (存儲了 argc
)。而後是 64 字節 的值:rsi
寄存器的值 (存儲了 argv
)。咱們在後面並無使用這些值,可是編譯器在沒有通過優化處理的時候,它們仍是會被存下來。
如今咱們把第一個函數 printf()
的參數 rax
設置給第一個函數參數寄存器 edi
中。printf()
是一個可變參數的函數。ABI 調用約定指定,將會把使用來存儲參數的寄存器數量存儲在寄存器 al
中。在這裏是 0。最後 callq
調用了 printf()
函數。
movl $0, %ecx
movl %eax, -20(%rbp) ## 4-byte Spill
movl %ecx, %eax
複製代碼
上面的代碼將 ecx
寄存器設置爲 0,並把 eax
寄存器的值保存至棧中,而後將 ect
中的 0 拷貝至 eax
中。ABI 規定 eax
將用來保存一個函數的返回值,或者此處 main()
函數的返回值 0:
addq $32, %rsp
popq %rbp
ret
.cfi_endproc
複製代碼
函數執行完成後,將恢復堆棧指針 —— 利用上面的指令 subq $32, %rsp
把堆棧指針 rsp
上移 32 字節。最後,把以前存儲至 rbp
中的值從棧中彈出來,而後調用 ret
返回調用者, ret
會讀取出棧的返回地址。 .cfi_endproc
平衡了 .cfi_startproc
指令。
接下來是輸出字符串 "Hello World!\n"
:
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World!\n"
複製代碼
一樣,.section
指令指出下面將要進入的段。L_.str
標記運行在實際的代碼中獲取到字符串的一個指針。.asciz
指令告訴編譯器輸出一個以 ‘\0’ (null) 結尾的字符串。
__TEXT __cstring
開啓了一個新的段。這個段中包含了 C 字符串:
L_.str: ## @.str
.asciz "Hello World!\n"
複製代碼
上面兩行代碼建立了一個 null 結尾的字符串。注意 L_.str
是如何命名,以後會經過它來訪問字符串。
最後的 .subsections_via_symbols
指令是靜態連接編輯器使用的。
更過關於彙編指令的資料能夠在 蘋果的 OS X Assembler Reference 中看到。AMD 64 網站有關於 ABI for x86 的文檔。另外還有 Gentle Introduction to x86-64 Assembly。
重申一下,經過下面的選擇操做,咱們能夠用 Xcode 查看任意文件的彙編輸出結果:Product -> Perform Action -> Assemble.
彙編器將可讀的彙編代碼轉換爲機器代碼。它會建立一個目標對象文件,通常簡稱爲 對象文件。這些文件以 .o
結尾。若是用 Xcode 構建應用程序,能夠在工程的 derived data 目錄中,Objects-normal
文件夾下找到這些文件。
稍後咱們會對連接器作更詳細的介紹。這裏簡單介紹一下:連接器解決了目標文件和庫之間的連接。什麼意思呢?還記得下面的語句嗎:
callq _printf
複製代碼
printf()
是 libc 庫中的一個函數。不管怎樣,最後的可執行文件須要能須要知道 printf()
在內存中的具體位置:例如,_printf
的地址符號是什麼。連接器會讀取全部的目標文件 (此處只有一個) 和庫 (此處是 libc),並解決全部未知符號 (此處是 _printf
) 的問題。而後將它們編碼進最後的可執行文件中 (能夠在 libc 中找到符號 _printf
),接着連接器會輸出能夠運行的執行文件:a.out
。
就像咱們上面提到的同樣,這裏有些東西叫作 section。一個可執行文件包含多個段,也就是多個 section。可執行文件不一樣的部分將加載進不一樣的 section,而且每一個 section 會轉換進某個 segment 裏。這個概念對於全部的可執行文件都是成立的。
咱們來看看 a.out
二進制中的 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。
當運行一個可執行文件時,虛擬內存 (VM - virtual memory) 系統將 segment 映射到進程的地址空間上。映射徹底不一樣於咱們通常的認識,若是你對虛擬內存系統不熟悉,能夠簡單的想象虛擬內存系統將整個可執行文件加載進內存 -- 雖然在實際上不是這樣的。VM 使用了一些技巧來避免所有加載。
當虛擬內存系統進行映射時,segment 和 section 會以不一樣的參數和權限被映射。
上面的代碼中,__TEXT
segment 包含了被執行的代碼。它被以只讀和可執行的方式映射。進程被容許執行這些代碼,可是不能修改。這些代碼也不能對本身作出修改,所以這些被映射的頁歷來不會被改變。
__DATA
segment 以可讀寫和不可執行的方式映射。它包含了將會被更改的數據。
第一個 segment 是 __PAGEZERO
。它的大小爲 4GB。這 4GB 並非文件的真實大小,可是規定了進程地址空間的前 4GB 被映射爲 不可執行、不可寫和不可讀。這就是爲何當讀寫一個 NULL
指針或更小的值時會獲得一個 EXC_BAD_ACCESS
錯誤。這是操做系統在嘗試防止引發系統崩潰。
在 segment中,通常都會有多個 section。它們包含了可執行文件的不一樣部分。在 __TEXT
segment 中,__text
section 包含了編譯所獲得的機器碼。__stubs
和 __stub_helper
是給動態連接器 (dyld
) 使用的。經過這兩個 section,在動態連接代碼中,能夠容許延遲連接。__const
(在咱們的代碼中沒有) 是常量,不可變的,就像 __cstring
(包含了可執行文件中的字符串常量 -- 在源碼中被雙引號包含的字符串) 常量同樣。
__DATA
segment 中包含了可讀寫數據。在咱們的程序中只有 __nl_symbol_ptr
和 __la_symbol_ptr
,它們分別是 non-lazy 和 lazy 符號指針。延遲符號指針用於可執行文件中調用未定義的函數,例如不包含在可執行文件中的函數,它們將會延遲加載。而針對非延遲符號指針,當可執行文件被加載同時,也會被加載。
在 _DATA
segment 中的其它常見 section 包括 __const
,在這裏面會包含一些須要重定向的常量數據。例如 char * const p = "foo";
-- p
指針指向的數據是可變的。__bss
section 沒有被初始化的靜態變量,例如 static int a;
-- ANSI C 標準規定靜態變量必須設置爲 0。而且在運行時靜態變量的值是能夠修改的。__common
section 包含未初始化的外部全局變量,跟 static
變量相似。例如在函數外面定義的 int a;
。最後,__dyld
是一個 section 佔位符,被用於動態連接器。
蘋果的 OS X Assembler Reference 文檔有更多關於 section 類型的介紹。
下面,咱們用 otool(1)
來觀察一個 section 中的內容:
% 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
對其設置了一個縮寫 -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
複製代碼
上面的內容是同樣的,只不過以反彙編形式顯示出來。你應該感受很熟悉,這就是咱們在前面編譯時候的代碼。惟一的不一樣就是,在這裏咱們沒有任何的彙編指令在裏面。這是純粹的二進制執行文件。
一樣的方法,咱們能夠查看別的 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
複製代碼
從側面來說,__DATA
和 __TEXT
segment對性能會有所影響。若是你有一個很大的二進制文件,你可能得去看看蘋果的文檔:關於代碼大小性能指南。將數據移至 __TEXT
是個不錯的選擇,由於這些頁歷來不會被改變。
使用連接符號 -sectcreate
咱們能夠給可執行文件以 section 的方式添加任意的數據。這就是如何將一個 Info.plist 文件添加到一個獨立的可執行文件中的方法。Info.plist 文件中的數據須要放入到 __TEXT
segment 裏面的一個 __info_plist
section 中。能夠將 -sectcreate segname sectname file
傳遞給連接器(經過將下面的內容傳遞給 clang):
-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist
複製代碼
一樣,-sectalign
規定了對其方式。若是你添加的是一個全新的 segment,那麼須要經過 -segprot
來規定 segment 的保護方式 (讀/寫/可執行)。這些全部內容在連接器的幫助文檔中都有,例如 ld(1)
。
咱們能夠利用定義在 /usr/include/mach-o/getsect.h
中的函數 getsectdata()
獲得 section,例如 getsectdata()
能夠獲得指向 section 數據的一個指針,並返回相關 section 的長度。
在 OS X 和 iOS 中可執行文件的格式爲 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
複製代碼
關於 Mach-O 文件格式 蘋果有詳細的介紹。
咱們可使用 otool(1)
來觀察可執行文件的頭部 -- 規定了這個文件是什麼,以及文件是如何被加載的。經過 -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
規定了這個可執行文件可以運行在哪些目標架構上。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
...
複製代碼
加載命令規定了文件的邏輯結構和文件在虛擬內存中的佈局。otool
打印出的大多數信息都是源自這裏的加載命令。看一下 Load command 1
部分,能夠找到 initprot r-x
,它規定了以前提到的保護方式:只讀和可執行。
對於每個 segment,以及segment 中的每一個 section,加載命令規定了它們在內存中結束的位置,以及保護模式等。例如,下面是 __TEXT __text
section 的輸出內容:
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 處結束。它在文件中的偏移量爲 3888。若是看一下以前 xcrun otool -v -t a.out
輸出的反彙編代碼,能夠發現代碼實際位置在 0x100000f30。
咱們一樣看看在可執行文件中,動態連接庫是如何使用的:
% 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
複製代碼
上面就是咱們可執行文件將要找到 _printf
符號的地方。
咱們來看看有三個文件的複雜例子:
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 對輸入每一個文件生成對應的目標文件:
% xcrun clang -c Foo.m
% xcrun clang -c helloworld.m
複製代碼
咱們歷來不編譯頭文件。頭文件的做用就是在被編譯的實現文件中對代碼作簡單的共享。Foo.m
和 helloworld.m
都是經過 #import
語句將 Foo.h
文件中的內容添加到實現文件中的。
最終獲得了兩個目標文件:
% file helloworld.o Foo.o
helloworld.o: Mach-O 64-bit object x86_64
Foo.o: Mach-O 64-bit object x86_64
複製代碼
爲了生成一個可執行文件,咱們須要將這兩個目標文件和 Foundation framework 連接起來:
xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
複製代碼
如今能夠運行咱們的程序了:
% ./a.out
2013-11-03 18:03:03.386 a.out[8302:303] Daniel Eggert
複製代碼
咱們這個簡單的程序是將兩個目標文件合併到一塊兒的。Foo.o
目標文件包含了 Foo
類的實現,而 helloworld.o
目標文件包含了 main()
函數,以及調用/使用 Foo
類。
另外,這兩個目標對象都使用了 Foundation framework。helloworld.o
目標文件使用了它的 autorelease pool,並間接的使用了 libobjc.dylib
中的 Objective-C 運行時。它須要運行時函數來進行消息的調用。Foo.o
目標文件也有相似的原理。
全部的這些東西都被形象的稱之爲符號。咱們能夠把符號當作是一些在運行時將會變成指針的東西。雖然實際上並非這樣的。
每一個函數、全局變量和類等都是經過符號的形式來定義和使用的。當咱們將目標文件連接爲一個可執行文件時,連接器 (ld(1)
) 在目標文件盒動態庫之間對符號作了解析處理。
可執行文件和目標文件有一個符號表,這個符號表規定了它們的符號。若是咱們用 nm(1)
工具觀察一下 helloworld.0
目標文件,能夠看到以下內容:
% 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
複製代碼
上面就是那個目標文件的全部符號。_OBJC_CLASS_$_Foo
是 Foo
Objective-C 類的符號。該符號是 undefined, external 。External 的意思是指對於這個目標文件該類並非私有的,相反,non-external
的符號則表示對於目標文件是私有的。咱們的 helloworld.o
目標文件引用了類 Foo
,不過這並無實現它。所以符號表中將其標示爲 undefined。
接下來是 _main
符號,它是表示 main()
函數,一樣爲 external,這是由於該函數須要被調用,因此應該爲可見的。因爲在 helloworld.o
文件中實現了 這個 main 函數。這個函數地址位於 0處,而且須要轉入到 __TEXT,__text
section。接着是 4 個 Objective-C 運行時函數。它們一樣是 undefined的,須要連接器進行符號解析。
若是咱們轉而觀察 Foo.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
已經定義了,而且對於 Foo.o
是一個外部符號 -- ·Foo.o· 包含了這個類的實現。
Foo.o
一樣有 undefined 的符號。首先是使用了符號 NSFullUserName()
,NSLog()
和 NSObject
。
當咱們將這兩個目標文件和 Foundation framework (是一個動態庫) 進行連接處理時,連接器會嘗試解析全部的 undefined 符號。它能夠解析 _OBJC_CLASS_$_Foo
。另外,它將使用 Foundation framework。
當連接器經過動態庫 (此處是 Foundation framework) 解析成功一個符號時,它會在最終的連接圖中記錄這個符號是經過動態庫進行解析的。連接器會記錄輸出文件是依賴於哪一個動態連接庫,並連同其路徑一塊兒進行記錄。在咱們的例子中,_NSFullUserName
,_NSLog
,_OBJC_CLASS_$_NSObject
,_objc_autoreleasePoolPop
等符號都是遵循這個過程。
咱們能夠看一下最終可執行文件 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 運行時符號依舊是 undefined,不過如今的符號表中已經多瞭如何解析它們的信息,例如在哪一個動態庫中能夠找到對應的符號。
可執行文件一樣知道去哪裏找到所需庫:
% 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)
複製代碼
在運行時,動態連接器 dyld(1)
能夠解析這些 undefined 符號,dyld
將會肯定好 _NSFullUserName
等符號,並指向它們在 Foundation 中的實現等。
咱們能夠針對 Foundation 運行 nm(1)
,並檢查這些符號的定義狀況:
% xcrun nm -nm `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName
0000000000007f3e (__TEXT,__text) external _NSFullUserName
複製代碼
有一些環境變量對於 dyld
的輸出信息很是有用。首先,若是設置了 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 個動態庫。這是因爲 Foundation 依賴於另一些動態庫。運行下面的命令:
% xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
複製代碼
能夠看到 Foundation 使用了 15 個動態庫。
當你構建一個真正的程序時,將會連接各類各樣的庫。它們又會依賴其餘一些 framework 和 動態庫。須要加載的動態庫會很是多。而對於相互依賴的符號就更多了。可能將會有上千個符號須要解析處理,這將花費很長的時間:通常是好幾秒鐘。
爲了縮短這個處理過程所花費時間,在 OS X 和 iOS 上的動態連接器使用了共享緩存,共享緩存存於 /var/db/dyld/
。對於每一種架構,操做系統都有一個單獨的文件,文件中包含了絕大多數的動態庫,這些庫都已經連接爲一個文件,而且已經處理好了它們之間的符號關係。當加載一個 Mach-O 文件 (一個可執行文件或者一個庫) 時,動態連接器首先會檢查 共享緩存 看看是否存在其中,若是存在,那麼就直接從共享緩存中拿出來使用。每個進程都把這個共享緩存映射到了本身的地址空間中。這個方法大大優化了 OS X 和 iOS 上程序的啓動時間。