當我們在Xcode中構建一個程式的時候,其中有一部分就是把原始檔(.m和.h)檔案轉變成可執行檔案。這個可執行檔案包含了將會在CPU(iOS裝置上的arm處理器或者你mac上的Intel處理器)執行的位元組碼。
我們將會過一遍編譯器這個過程的做了些什麼,同時也看一下可執行檔案的內部到底是怎樣的。其實,裡面的東西比你看到的要多很多。
讓我們先把Xcode放一邊,踏入Commond-Lines的大陸。當我們在Xcode中構建一個App時,Xcode只是簡單的呼叫了一系列的工具而已。希望這將會讓你更好的明白一個可執行檔案(被稱之為Mach-O可執行檔案),是怎樣組裝起來的,並且是怎樣在iOS或者os x上執行的
XCrun
先從一些基礎性的東西開始:我們將會使用一個叫做Xcrun的命令列工具。他看起來很奇怪,但是的確相當出色。這個小工具是用來呼叫其他工具的。 原先的時候我們執行:
1 |
% clang -v |
現在在終端中,我們可以執行:
1 |
% xcrun clang -v |
Xcrun定位Clang,並且使用相關的引數來執行Clang。
為什麼我們要做這個事情?這看起來毫無重點,胡扯八道。但是Xcrun允許我們使用多個版本的Xcode,或者使用特定Xcode版本里面的工具,或者針對特點的SDK使用不同的工具。如果你恰好有Xcode4.5和xcode5、使用xcode-select和xcrun你可以選擇選擇使用來自Xcode4.5裡面的SDK的工具,或者來自Xcode5裡面的SDK的工具。在大多數其他平臺上,這將是一個不可能的事情。如果你看一下幫助手冊上xcode-select和xcrun的一些細節。你就能在不安裝命令列工具的情況下,使用在終端中使用開發者工具。
一個不使用IDE的Hello World
回到終端,建立一個包含一個c檔案的目錄:
1 2 3 4 5 |
% mkdir ~/Desktop/objcio-command-line % cd !$ % touch helloworld.c |
現在使用你喜歡的文字編輯器來編輯這個檔案,例如TextEdit.app:
1 |
% open -e helloworld.c |
錄入下面的程式碼:
1 2 3 4 5 6 |
#include <stdio.h> int main(int argc, char *argv[]) { printf("Hello World!\n"); return 0; } |
儲存,並且回到終端執行:
1 2 3 |
% xcrun clang helloworld.c % ./a.out |
現在你能夠在終端上看到熟悉的Hello World!。你編譯了一個C程式並且執行了它。所有都是在不使用IDE的情況下做的。深呼吸一下,高興高興。
我們在這裡做了些什麼?我們將hellowrold.c編譯成了叫a.out的Mach-o二進位制檔案。a.out是編譯器的預設名字,除非你指定一個別的。
Hello World和編譯器
現在可選擇的編譯器是Clang(讀作:/’kl /)。Chris寫了一些更多關於Clang細節的介紹,可以參考: about the compiler
概括一下就是,編譯器將會讀入處理hellowrold.c,輸出可執行檔案a.out。這個過程包含了非常多的步驟。我們所要做的就是正確的執行它們。
預處理:
- 序列化
- 巨集定義展開
- #include展開(引用檔案展開)
語法和語義分析:
- 使用預處理後的單詞構建詞法樹
- 執行語義分析生成語法樹
- 輸出AST (Abstract Syntax Tree)
程式碼生成和優化
- 將AST轉化成更低階的中間碼(LLVM IR)
- 優化生成程式碼
- 目的碼生成
- 輸出彙編程式碼
彙編程式
- 將彙編程式碼轉化成目標檔案
聯結器
- 將多個目標檔案合併成可執行檔案(或者一個動態庫) 我們來看一個關於這些步驟的簡單的例子。
預處理
編譯器將做的第一件事情是處理檔案。使用Clang展示一下這個過程:
1 |
% xcrun clang -E helloworld.c |
歐耶。輸出了413行內容。開啟個編輯器看看到底發生了什麼:
1 |
% xcrun clang -E helloworld.c | open -f |
在檔案頂部我們能看到很多以”#”開頭的行。這些被稱之為行標記語句的語句告訴我們它後面的內容來自哪裡。我們需要這個。如果我再看一下hellowrold.c,第一行是:
1 |
#include <stdio.h> |
我們都用過#include和#import。它們做的就是告訴於處理器在#include語句的地方插入stdio.h的內容。在剛剛的檔案裡就是插入了一個以#開頭的行標記。跟在#後面的數字是在原始檔中的行號。每一行最後的數字是在新檔案中的行號。回到剛才開啟的檔案,接下來是系統標頭檔案,或者一些被看成包裹著extern “C”的檔案。
如果你滾動到檔案末尾,你將會發現我們的helloworld.c的程式碼:
1 2 3 4 5 6 |
# 2 "helloworld.c" 2 int main(int argc, char *argv[]) { printf("Hello World!\n"); return 0; } |
在Xcode中,你可以通過使用Product->Perform Action-> Preprocess來檢視任何一個檔案的預處理輸出。一定要注意這將會花費一些時間來載入預處理輸出檔案(接近100,000行)。
編譯
下一個步驟:文字處理和程式碼生成。我們可以呼叫clang輸出彙編程式碼就像這樣:
1 |
% xcrun clang -S -o - helloworld.c | open -f |
看一看輸出。我們首先注意到的是一些以點開頭的行。這些是彙編指令。其他的是真正的x86_64彙編程式碼。最後是些標記,就像C中的那些標記一樣。
我們從前三行開始:
1 2 3 |
.section __TEXT,__text,regular,pure_instructions .globl _main .align 4, 0x90 |
這三行是彙編指令,不是彙編程式碼。”.section”指令指出了哪一個段接下來將會被執行。比用二進位制表示好看多了。
下一個,.global指令說明_main是一個外部符號。這就是我們的main()函式。它能夠從我們的二進位制檔案之外看到,因為系統要呼叫它來執行可執行檔案。
.align指令指出了下面程式碼的對齊方式。從我們的角度看,接下來的程式碼將會按照16位元對齊並且如果需要的時候用0x90補齊。
下面是main函式的頭部:
1 2 3 4 5 6 7 8 9 10 11 12 |
_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函式真正的開始地址。這個也是被丟擲的符號。二進位制檔案將會在這個地方產生一個引用。
.cfi_startproc指令一半會在函式開始的地方使用。CFI是Call Frame Information的縮寫。幀鬆散的與一個函式互動。當你使用偵錯程式,並且單步執行的時候,你實際上是在呼叫幀中跳轉。在C程式碼中,函式有自己的呼叫幀,除了函式之外的一些結構也會有呼叫站。.cfi_startproc指令給了函式一個.en_frame的入口,這個入口包含了堆疊展開資訊(表示異常如何展開呼叫幀堆疊)。這個指令也會傳送一些和具體平臺相關的指令給CFI。檔案後面的.cfi_endproc與.cfi_startproc相匹配,來表示結束main函式。
下一步,這裡有另外一個Label ## BB#0.然後,終於來了第一句彙編程式碼:pushq %rbp。從這裡開始事情開始變得有趣。在OS X上,我們將會有x84_64的程式碼。對於這種架構,有一個東西叫做ABI(application binary interface),ABI表示函式呼叫是怎樣在彙編程式碼層面上工作的。ABI指出在函式呼叫時,rbp暫存器必須被保護起來。這是main函式的責任,來確保返回時,rbp暫存器中有資料。pushq %rbp將它的資料推進堆疊,以便我們以後使用。
下面是,兩個CFI指令: .cfi_def_cfa_offset 16 和 .cfi_offset %rbp, -16. 這將會輸出一些資訊,這些資訊是關於生成呼叫堆疊展開資訊和除錯資訊的。我們改變了堆疊,並且這兩個指令告訴編譯器指標指向哪裡,或者它們說出了之後偵錯程式將會使用的資訊。
現在movq %rsp, %rbp將會把區域性變數載入進堆疊。subq $32,%rsp將堆疊指標移動32位元,也就是函式將會呼叫的位置。我們先在rbp中儲存了老的堆疊指標,然後將此作為我們區域性變數的基址,然後我們更新堆疊指標到我們將會使用的位置。
之後,我們呼叫了printf():
1 2 3 4 5 6 7 |
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位元是零,之後32個位元是edi的值(儲存了argc),然後是64bit的rsi暫存器的值。我們在後面不會使用這些資料。但是如果編譯器沒有使用優化的時候,它們還是會被存下來。
現在,我們將會把第一個函式(printf)的引數載入進暫存器edi。printf函式是一個可變引數的函式。ABI呼叫約定指定,將會把使用來儲存引數的暫存器數量儲存在暫存器al中。對我們來講是0。最後callq呼叫了printf函式。
1 2 3 |
movl $0, %ecx movl %eax, -20(%rbp) ## 4-byte Spill movl %ecx, %eax |
這將設定ecx寄存的值為0,並且把eax的值壓棧。然後從ecx複製0到eax。ABI指定eax將會儲存函式的返回值,我們man函式的返回值是0:
1 2 3 4 |
addq $32, %rsp popq %rbp ret .cfi_endproc |
函式執行完成後,將恢復堆疊指標,通過上移32bit在rsp中的堆疊指標。我們將會出棧我們早先儲存的rbp的值,然後呼叫ret來返回,ret將會讀取離開堆疊的地址。.cfi_endproc平衡了.cfi_startproc指令。
下一步是一個字一個字的輸出我們的字串:“hello world!\n”:
之後.section指令指出下面將要跳入的段。L_.str標記允許獲取一個字元轉的指標。.asciz指定告訴彙編器輸出一個0的字串結尾。
__TEXT __cstring開始了一個新的段。這個段包含了C字串:
1 2 3 |
.section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "Hello World!\n" |
這兩行建立了一個沒有結束符的字元創。注意L_.str是怎樣命名,和來獲取字串的。
最後.subseciton_via_symbols指令是靜態連結編輯器使用的。
更多關於彙編指令的資訊可以從蘋果的Apple’s assemebler reference獲取。AMD64網站有關於ABI for x86的文件。同時也有Gentle Introduction to x86-64 Assemble。 再一次,Xcode允許你檢視任何檔案的彙編程式碼通過 Product->Perform Action -> Assemble.
彙編編譯器:
彙編編譯器,只是簡單的將彙編程式碼轉換成機器碼。它建立了一個目標檔案。這些檔案以.o結尾。如果你使用Xcode構建一個app,你將會在Derived Data目錄下面的你的工程目錄中的objects-normal目錄下面發現這些檔案。
聯結器:
我們將會多談一點關於連結的東西。但是簡單的說,聯結器確定了目標檔案和庫之間的連結。這是什麼意思? 重新呼叫 callq _printf. printf是在libc庫中的一個函式。無論怎樣,最後的可執行檔案需要能知道printf()在記憶體中的什麼位置。例如符號_printf的地址。聯結器將會讀取所有的目標檔案,所有的庫和結束任何未定義的符號。然後將它們編碼進最後的可執行檔案,然後輸出最後的可執行檔案:a.out。
段
就像我們上面提到的一樣,這裡有些東西叫做段。一個可執行檔案包含多個段。可執行檔案不同的部分將會載入進不同的段。並且每個段將會轉化進一個“Segment”中。這對我們隨便寫的app如此,對我們用心寫的app也一樣。
我們來看看在a.out中的段。我們可以使用size:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
% 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檔案有四個段。其中一些有section。
當我們執行一個可執行檔案。虛擬記憶體系統會將segment對映到程式的地址空間中。對映完全不同於我們一般的認識,但是如果你對虛擬記憶體系統不熟悉,可以簡單的想象VM會將整個檔案載入進記憶體,雖然在實際上這不會發生。VM使用了一些技巧來避免全部載入。
當虛擬記憶體系統進行對映時,資料段和可執行段會以不同的引數和許可權被對映。
__TEXT段包含了可執行的程式碼。它們被以只讀和可執行的方式對映。程式被允許執行這些程式碼,但是不能修改。這些程式碼也不能改變它們自己,並且這些頁從來不會被汙染。
__DATA段以可讀寫和不可執行的方式對映。它包含了將會被更改的資料。
第一個段是__PAGEZERO。這個有4GB大小。這4GB並不是檔案的真實大小,但是說明了程式的前4GB地址空間將會被對映為,不能執行,不能讀,不能寫。這就是為什麼在去寫NULL指標或者一些低位的指標的時候,你會得到一個EXC_BAD_ACCESS錯誤。這是作業系統在嘗試防止你引起系統崩潰。
在每一個段內有一些片段。它們包含了可執行檔案的不同的部分。在_TEXT段,_text片段包含了編譯得到的機器碼。_stubs和_stub_helper是給動態連結器用的。著允許動態連結的程式碼延遲連結。_const是不可變的部分,就像_cstring包含了可執行檔案的字串一樣。
_DATA段包含了可讀寫資料。從我們的角度,我們只有_nl_sysmol_ptr 和__la_symble_ptr,它們是延遲連結的指標。延遲連結的指標被用來執行未定義的函式。例如,那些沒有包含在可執行檔案本身內部的函式。它們將會延遲載入。那些非延遲連結的指標將會在可執行檔案被夾在的時候確定。
其他在_DATA中共同的段是_const。她包含了那些需要重定位的不可變資料。一個例子是chat* const p = “foo”; p指標指向的資料不是靜態的。_bss片段包含了沒有被初始化的靜態變數例如static int a; ANSI C標準指出這些靜態變數將會被設定為零。但是在執行時可以被改變。_common片段包含了被動態連結器使用的佔位符片段。
蘋果的文件OSX Assembler Reference有更多關於片段定義的內容。
段內容:
我們能檢查每一個片段的內容,使用otool像這樣:
1 2 3 4 5 6 7 8 |
% 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
% 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 |
這裡有些內容反彙編的程式碼中的一樣,你應該感覺很熟悉,這就是我們在前面編譯時候的程式碼。唯一的不同就是,在這裡我們沒有任何的彙編指令在裡面。這是純粹的二進位制執行檔案。
同樣的方法,我們可以查案一下其他片段:
1 2 3 4 5 |
% xcrun otool -v -s __TEXT __cstring a.out a.out: Contents of (__TEXT,__cstring) section 0x0000000100000f8a Hello World!\n |
或者:
1 2 3 4 5 6 |
% 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段會影響效能。如果你有一個非常大的二進位制檔案,你可能回想檢視蘋果的程式碼大小優化指南。將資料移到__TEXT段是個不錯的選擇,因為這些頁從來不會變髒。
任意的片段
你可以以片段的方式向你的二進位制檔案新增任何的資料,通過-sectcreate連結引數。這就是你怎樣新增info.plist到一個獨立的二進位制檔案。Info.plist的資料需要被放在_TEXT段的_info_plist片段。你可以使用聯結器的命令-sectcreate segname sectname file來實現:
1 |
-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist |
同樣的,-sectalign也致命了對齊方式。如果你新增一個全新的段,通過-segprot來制定資料的保護方式。這些都是在聯結器中的幫助手冊中的。
你能夠到達在/usr/include/mach-o/getsect.h中定義的函式在二進位制檔案中的那些片段,通過使用getsectdata(),它將會返回片段資料的指標和大小。
Mch-o
在OS X和iOS中可執行檔案是Mach-o格式的:
1 2 3 |
% file a.out a.out: Mach-O 64-bit executable x86_64 |
對於GUI的程式來說也是這樣:
1 2 3 |
% file /Applications/Preview.app/Contents/MacOS/Preview /Applications/Preview.app/Contents/MacOS/Preview: Mach-O 64-bit executable x86_64 |
你可以從這裡找到關於mach-o檔案格式的詳細資料。
我們可以使用otool來看一看mach-o檔案的頭部。這說明了這個檔案是什麼,和怎樣被載入的。我們將會使用-h引數來列印頭部資訊。
1 2 3 4 5 |
% 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引數來檢視:
1 2 3 4 5 6 7 8 9 10 |
% 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 comand 1部分,我們看到了initprot r-x,這指明瞭我們上面提到的資料保護模式:只讀並且可執行。
對於每一個段和每一個段中的片段,載入命令說明了它們會在記憶體中的位置和它們的保護模式,例如,這是關於__TEXT __text片段的輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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。如果你看一下a.out的範彙編輸出。你能夠在0x100000f30處看到我們的程式碼。
我們同樣可以看一下在可執行檔案中,動態連結庫是怎樣使用的:
1 2 3 4 5 |
% 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符號連結將要用到的庫。
一個更復雜的例子
讓我們來看一個有三個檔案的複雜的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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來生成目標檔案:
1 2 3 |
% xcrun clang -c Foo.m % xcrun clang -c helloworld.m |
我們從來不編譯標頭檔案。標頭檔案的目的是在實現檔案中貢獻程式碼,並通過這種方式來唄編譯。通過#import語句Foo.m和helloworld.m中都被插入了foo.h的內容。 我們得到了兩個檔案:
1 2 3 |
% file helloworld.o Foo.o helloworld.o: Mach-O 64-bit object x86_64 Foo.o: Mach-O 64-bit object x86_64 |
為了生成可執行檔案,我們需要連結這兩個目標檔案和Foundation系統庫:
1 |
xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk- path`/System/Library/Frameworks/Foundation.framework/Foundation |
現在,我們可以執行我們的程式了。
符號表和連結
我們這個簡單的app是通過兩個目標檔案合併到一起得到的。Foo.o包含了Foo類的實現,同事helloworld.o包含了呼叫Foo類方法run的main函式。 進一步,兩個檔案都使用了Foundation庫。在helloworld.o中autorelease pool使用了這個庫,以簡潔的方式使用了libobjc.dylib中的Objctive-c執行時。它需要使用執行時的函式來傳送訊息呼叫。foo.o也是一樣的。
這些被形象的稱之為符號。我們可以把符號看成一些在執行時將會變成指標的東西。雖然實際上並不是這樣能夠。 每一個函式,全域性變數,類等等都是通過符號的方式來使用的。當我們為可執行檔案連線一個目標檔案,聯結器將會按需要決定目標檔案和動態庫之間的所有符號。 可執行檔案和目標檔案都有一個符號表來儲存這些符號。如果你使用nm工具來檢視一下helloworld.o你會發現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
% 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的符號連結。它還沒有被決定成Foo類的外部連結。外部表示它對不是私有的。與此相反non-external表明符號連結對於特定的檔案是私有的。 我們的helloworld.o檔案引用了Foo類,但是並沒有實現它。於是符號最後以未確定結尾。
下面,main函式同樣是外部連結,因為它需要能夠被外部看到並被呼叫。無論怎樣,main函式是在helloworld中實現的。並且放在了地址0,和放在__TEXT __text片段中。然後是四個objc執行時的函式。它們同樣是未定義的,需要聯結器來決定。
我們再來看看Foo.o檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
% 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的實現。 Foo.o也有未定義的符號。最前面的是它使用過的NSFullUserName(),NSLog()和NSObject。 當我們連線著兩個檔案還有Foundation庫的時候,將會確定這些在動態連結庫中的符號。臨界期記錄了輸出檔案以來特定的動態連結庫和它們的位置。這就是NSFullName()等將會發生的事情。
我們可以看一下最後的執行檔案a.out的符號表,就能夠發現聯結器是怎樣確定這些符號的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
% 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和Objctive-C執行時的一些符號依然是未確定的。但是符號表中,記錄了怎樣去確定它們。例如那些它們可以去查詢的動態連結庫。
可執行檔案一樣也知道去哪找這些庫:
1 2 3 4 5 6 7 |
% 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)確定。當我們執行程式的時候,dyld將會在Foundation中確定指向_NSFullUserName等的實現的指標,等等等等
我們可以再次使用nm來檢視你這些符號在Foundation中的情況,實際上,如下:
1 2 3 |
% 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將會輸出已經載入的東戴連結庫:
1 2 3 4 5 6 7 8 9 |
% (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的時候載入的動態連結庫。這是因為Foundation庫也依賴於其他很多動態連結庫, 你可以執行:
1 |
% xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation |
來檢視五十多個Foundation依賴的庫。
dyld的共享快取
當你構建一個真正的程式的時候,你將會連結各種各樣的庫。它們又會依賴其他的一些框架和動態連結庫。於是要載入的動態連結庫會非常多。同樣非獨立的符號也非常多。這裡就會有成千上萬的符號要確定。這個工作將會話費很多時間——幾秒鐘。 為了優化這個過程,OS X和iOS上動態連結器使用了一個共享快取,在/var/db/dyld/。對於每一種架構,作業系統有一個單獨的檔案包含了絕大多數的動態連結庫,這些庫已經互相連線並且符號都已經確定。當一個Mach-o檔案被載入的時候,動態連結器會首先檢查共享快取,如果存在相應的庫就是用。每一個程式都把這個共享快取對映到了自己的地址空間中。這個方法戲劇性的優化了OS X和iOS上程式的載入時間。