Mach-O 可執行文件

咱們用 Xcode 構建一個程序的過程當中,會把源文件 (.m.h) 文件轉換爲一個可執行文件。這個可執行文件中包含的字節碼會將被 CPU (iOS 設備中的 ARM 處理器或 Mac 上的 Intel 處理器) 執行。html

本文將介紹一下上面的過程當中編譯器都作了些什麼,同時深刻看看可執行文件內部是怎樣的。實際上裏面的東西要比咱們第一眼看到的多得多。xcode

這裏咱們把 Xcode 放一邊,將使用命令行工具 (command-line tools)。當咱們用 Xcode 構建一個程序時,Xcode 只是簡單的調用了一系列的工具而已。Florian 對工具調用是如何工做的作了更詳細的討論。本文咱們就直接調用這些工具,並看看它們都作了些什麼。緩存

真心但願本文能幫助你更好的理解 iOS 或 OS X 中的一個可執行文件 (也叫作 Mach-O executable) 是如何執行,以及怎樣組裝起來的。架構

xcrun

先來看一些基礎性的東西:這裏會大量使用一個名爲 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-selectxcrun 能夠選擇使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具。在許多其它平臺中,這是不可能作到的。查閱 xcrunxcode-select 的主頁內容能夠了解到詳細內容。不用安裝 Command Line Tools,就能使用命令行中的開發者工具。工具

不使用 IDE 的 Hello World

回到終端 (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。

這個二進制文件是如何生成的呢?實際上有許多內容須要觀察和理解。咱們先看看編譯器吧。

Hello World 和編譯器

時下 Xcode 中編譯器默認選擇使用 clang(讀做 /klæŋ/)。關於編譯器,Chris 寫了更詳細的文章。

簡單的說,編譯器處理過程當中,將 helloworld.c 當作輸入文件,並生成一個可執行文件 a.out。這個過程有多個步驟/階段。咱們須要作的就是正確的執行它們。

預處理
  • 符號化 (Tokenization)
  • 宏定義的展開
  • #include 的展開
語法和語義分析
  • 將符號化後的內容轉化爲一棵解析樹 (parse tree)
  • 解析樹作語義分析
  • 輸出一棵抽象語法樹(Abstract Syntax Tree* (AST))
生成代碼和優化
  • 將 AST 轉換爲更低級的中間碼 (LLVM IR)
  • 對生成的中間碼作優化
  • 生成特定目標代碼
  • 輸出彙編代碼
彙編器
  • 將彙編代碼轉換爲目標對象文件。
連接器
  • 將多個目標對象文件合併爲一個可執行文件 (或者一個動態庫)

咱們來看一個關於這些步驟的簡單的例子。

預處理

編譯過程當中,編譯器首先要作的事情就是對文件作處理。預處理結束以後,若是咱們中止編譯過程,那麼咱們可讓編譯器顯示出預處理的一些內容:

% xcrun clang -E helloworld.c
複製代碼

喔喔。 上面的命令輸出的內容有 413 行。咱們用編輯器打開這些內容,看看到底發生了什麼:

% xcrun clang -E helloworld.c | open -f
複製代碼

在頂部能夠看到的許多行語句都是以 # 開頭 (讀做 hash)。這些被稱爲 行標記 的語句告訴咱們後面跟着的內容來自哪裏。若是再回頭看看 helloworld.c 文件,會發現第一行是:

#include <stdio.h>
複製代碼

咱們都用過 #includeimport。它們所作的事情是告訴預處理器將文件 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 instep 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 %rbprbp 的值 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"edirsi 寄存器保存了函數的第一個和第二個參數。因爲咱們會調用別的函數,因此首先須要將它們的當前值保存起來。這就是爲何咱們使用剛剛存儲的 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,而且每一個 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-lazylazy 符號指針。延遲符號指針用於可執行文件中調用未定義的函數,例如不包含在可執行文件中的函數,它們將會延遲加載。而針對非延遲符號指針,當可執行文件被加載同時,也會被加載。

_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 類型的介紹。

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 的長度。

Mach-O

在 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
複製代碼

cputypecpusubtype 規定了這個可執行文件可以運行在哪些目標架構上。ncmdssizeofcmds 是加載命令,能夠經過 -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.mhelloworld.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_$_FooFoo Objective-C 類的符號。該符號是 undefined, externalExternal 的意思是指對於這個目標文件該類並非私有的,相反,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 個動態庫。

dyld 的共享緩存

當你構建一個真正的程序時,將會連接各類各樣的庫。它們又會依賴其餘一些 framework 和 動態庫。須要加載的動態庫會很是多。而對於相互依賴的符號就更多了。可能將會有上千個符號須要解析處理,這將花費很長的時間:通常是好幾秒鐘。

爲了縮短這個處理過程所花費時間,在 OS X 和 iOS 上的動態連接器使用了共享緩存,共享緩存存於 /var/db/dyld/。對於每一種架構,操做系統都有一個單獨的文件,文件中包含了絕大多數的動態庫,這些庫都已經連接爲一個文件,而且已經處理好了它們之間的符號關係。當加載一個 Mach-O 文件 (一個可執行文件或者一個庫) 時,動態連接器首先會檢查 共享緩存 看看是否存在其中,若是存在,那麼就直接從共享緩存中拿出來使用。每個進程都把這個共享緩存映射到了本身的地址空間中。這個方法大大優化了 OS X 和 iOS 上程序的啓動時間。


原文: Mach-O Executables

譯文 objc.io 第6期 Mach-O 可執行文件

相關文章
相關標籤/搜索