我們用 Xcode 構(gòu)建一個(gè)程序的過程中猫态,會(huì)把源文件 (.m
和 .h
) 文件轉(zhuǎn)換為一個(gè)可執(zhí)行文件。這個(gè)可執(zhí)行文件中包含的字節(jié)碼會(huì)將被 CPU (iOS 設(shè)備中的 ARM 處理器或 Mac 上的 Intel 處理器) 執(zhí)行。
本文將介紹一下上面的過程中編譯器都做了些什么寓盗,同時(shí)深入看看可執(zhí)行文件內(nèi)部是怎樣的傀蚌。實(shí)際上里面的東西要比我們第一眼看到的多得多。
這里我們把 Xcode 放一邊箩艺,將使用命令行工具 (command-line tools)。當(dāng)我們用 Xcode 構(gòu)建一個(gè)程序時(shí)静汤,Xcode 只是簡(jiǎn)單的調(diào)用了一系列的工具而已虫给。Florian 對(duì)工具調(diào)用是如何工作的做了更詳細(xì)的討論杰捂。本文我們就直接調(diào)用這些工具挨队,并看看它們都做了些什么盛垦。
真心希望本文能幫助你更好的理解 iOS 或 OS X 中的一個(gè)可執(zhí)行文件 (也叫做 Mach-O executable) 是如何執(zhí)行颊埃,以及怎樣組裝起來的。
xcrun
先來看一些基礎(chǔ)性的東西:這里會(huì)大量使用一個(gè)名為 xcrun
的命令行工具罗标。看起來可能會(huì)有點(diǎn)奇怪宙拉,不過它非常的出色。這個(gè)小工具用來調(diào)用別的一些工具澳化。原先缎谷,我們?cè)诮K端執(zhí)行如下命令:
% clang -v
現(xiàn)在我們用下面的命令代替:
% xcrun clang -v
在這里 xcrun
做的是定位到 clang
,并執(zhí)行它希痴,附帶輸入 clang
后面的參數(shù)。
我們?yōu)槭裁匆@樣做呢嫩实?看起來沒有什么意義。不過 xcode
允許我們: (1) 使用多個(gè)版本的 Xcode晃洒,以及使用某個(gè)特定 Xcode 版本中的工具缘挑。(2) 針對(duì)某個(gè)特定的 SDK (software development kit) 使用不同的工具。如果你有 Xcode 4.5 和 Xcode 5惶翻,通過 xcode-select
和 xcrun
可以選擇使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具颅筋。在許多其它平臺(tái)中,這是不可能做到的先口。查閱 xcrun
和 xcode-select
的主頁(yè)內(nèi)容可以了解到詳細(xì)內(nèi)容碉京。不用安裝 Command Line Tools,就能使用命令行中的開發(fā)者工具凡蜻。
不使用 IDE 的 Hello World
回到終端 (Terminal)设凹,創(chuàng)建一個(gè)包含一個(gè) C 文件的文件夾:
% mkdir ~/Desktop/objcio-command-line
% cd !$
% touch helloworld.c
接著使用你喜歡的文本編輯器來編輯這個(gè)文件 -- 例如 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
編譯為一個(gè)名為 a.out
的 Mach-O 二進(jìn)制文件。注意缩擂,如果我們沒有指定名字,那么編譯器會(huì)默認(rèn)的將其指定為 a.out博脑。
這個(gè)二進(jìn)制文件是如何生成的呢?實(shí)際上有許多內(nèi)容需要觀察和理解疗杉。我們先看看編譯器吧烟具。
Hello World 和編譯器
時(shí)下 Xcode 中編譯器默認(rèn)選擇使用 clang
(讀作 /kl??/)嗡午。關(guān)于編譯器,Chris 寫了更詳細(xì)的文章应媚。
簡(jiǎn)單的說,編譯器處理過程中丢胚,將 helloworld.c
當(dāng)做輸入文件,并生成一個(gè)可執(zhí)行文件 a.out
峡蟋。這個(gè)過程有多個(gè)步驟/階段。我們需要做的就是正確的執(zhí)行它們蓬戚。
預(yù)處理
- 符號(hào)化 (Tokenization)
- 宏定義的展開
-
#include
的展開
語(yǔ)法和語(yǔ)義分析
- 將符號(hào)化后的內(nèi)容轉(zhuǎn)化為一棵解析樹 (parse tree)
- 解析樹做語(yǔ)義分析
- 輸出一棵抽象語(yǔ)法樹(Abstract Syntax Tree* (AST))
生成代碼和優(yōu)化
- 將 AST 轉(zhuǎn)換為更低級(jí)的中間碼 (LLVM IR)
- 對(duì)生成的中間碼做優(yōu)化
- 生成特定目標(biāo)代碼
- 輸出匯編代碼
匯編器
- 將匯編代碼轉(zhuǎn)換為目標(biāo)對(duì)象文件子漩。
鏈接器
- 將多個(gè)目標(biāo)對(duì)象文件合并為一個(gè)可執(zhí)行文件 (或者一個(gè)動(dòng)態(tài)庫(kù))
所以總的流程以流程圖來表示的話如下圖所示
我們來看一個(gè)關(guān)于這些步驟的簡(jiǎn)單的例子。
預(yù)處理
編譯過程中,編譯器首先要做的事情就是對(duì)文件做處理重父。預(yù)處理結(jié)束之后,如果我們停止編譯過程丹允,那么我們可以讓編譯器顯示出預(yù)處理的一些內(nèi)容:
% xcrun clang -E helloworld.c
喔喔。 上面的命令輸出的內(nèi)容有 413 行批狐。我們用編輯器打開這些內(nèi)容,看看到底發(fā)生了什么:
% xcrun clang -E helloworld.c | open -f
在頂部可以看到的許多行語(yǔ)句都是以 #
開頭 (讀作 hash
)食零。這些被稱為 行標(biāo)記 的語(yǔ)句告訴我們后面跟著的內(nèi)容來自哪里迁霎。如果再回頭看看 helloworld.c
文件频伤,會(huì)發(fā)現(xiàn)第一行是:
#include <stdio.h>
我們都用過 #include
和 import
憋肖。它們所做的事情是告訴預(yù)處理器將文件 stdio.h
中的內(nèi)容插入到 #include
語(yǔ)句所在的位置膊升。這是一個(gè)遞歸的過程:stdio.h
可能會(huì)包含其它的文件。
由于這樣的遞歸插入過程很多债查,所以我們需要確保記住相關(guān)行號(hào)信息。為了確保無誤,預(yù)處理器在發(fā)生變更的地方插入以 #
開頭的 行標(biāo)記
缸榄。跟在 #
后面的數(shù)字是在源文件中的行號(hào)甚带,而最后的數(shù)字是在新文件中的行號(hào)〖淹罚回到剛才打開的文件鹰贵,緊跟著的是系統(tǒng)頭文件,或者是被看做為封裝了 extern "C"
代碼塊的文件畜晰。
如果滾動(dòng)到文件末尾砾莱,可以看到我們的 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)一些時(shí)間 -- 接近 100,000 行代碼主慰。
編譯
下一步:分析和代碼生成鲤妥。我們可以用下面的命令讓 clang
輸出匯編代碼:
% xcrun clang -S -o - helloworld.c | open -f
我們來看看輸出的結(jié)果。首先會(huì)看到有一些以點(diǎn) .
開頭的行刁憋。這些就是匯編指令。其它的則是實(shí)際的 x86_64 匯編代碼惫皱。最后是一些標(biāo)記 (label)扫皱,與 C 語(yǔ)言中的類似。
我們先看看前三行:
.section __TEXT,__text,regular,pure_instructions
.globl _main
.align 4, 0x90
這三行是匯編指令拣宏,不是匯編代碼罩引。.section
指令指定接下來會(huì)執(zhí)行哪一個(gè)段领炫。
第二行的 .globl
指令說明 _main
是一個(gè)外部符號(hào)砚哗。這就是我們的 main()
函數(shù)涯竟。這個(gè)函數(shù)對(duì)于二進(jìn)制文件外部來說是可見的,因?yàn)橄到y(tǒng)要調(diào)用它來運(yùn)行可執(zhí)行文件。
.align
指令指出了后面代碼的對(duì)齊方式。在我們的代碼中,后面的代碼會(huì)按照 16(2^4) 字節(jié)對(duì)齊,如果需要的話倘感,用 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)記违诗。它們是某些特定部分的匯編代碼的符號(hào)鏈接趁怔。首先是 _main
函數(shù)真正開始的地址丁稀。這個(gè)符號(hào)會(huì)被 export。二進(jìn)制文件會(huì)有這個(gè)位置的一個(gè)引用。
.cfi_startproc
指令通常用于函數(shù)的開始處洒扎。CFI 是調(diào)用幀信息 (Call Frame Information) 的縮寫。這個(gè)調(diào)用 幀
以松散的方式對(duì)應(yīng)著一個(gè)函數(shù)颤绕。當(dāng)開發(fā)者使用 debugger 和 step in 或 step out 時(shí),實(shí)際上是 stepping in/out 一個(gè)調(diào)用幀。在 C 代碼中忧吟,函數(shù)有自己的調(diào)用幀厕倍,當(dāng)然,別的一些東西也會(huì)有類似的調(diào)用幀。.cfi_startproc
指令給了函數(shù)一個(gè) .eh_frame
入口,這個(gè)入口包含了一些調(diào)用棧的信息(拋出異常時(shí)也是用其來展開調(diào)用幀堆棧的)翁都。這個(gè)指令也會(huì)發(fā)送一些和具體平臺(tái)相關(guān)的指令給 CFI碍论。它與后面的 .cfi_endproc
相匹配,以此標(biāo)記出 main()
函數(shù)結(jié)束的地方柄慰。
接著是另外一個(gè) label ## BB#0:
鳍悠。然后,終于坐搔,看到第一句匯編代碼:pushq %rbp
藏研。從這里開始事情開始變得有趣。在 OS X上概行,我們會(huì)有 X86_64 的代碼蠢挡,對(duì)于這種架構(gòu),有一個(gè)東西叫做 ABI ( 應(yīng)用二進(jìn)制接口 application binary interface)凳忙,ABI 指定了函數(shù)調(diào)用是如何在匯編代碼層面上工作的业踏。在函數(shù)調(diào)用期間,ABI 會(huì)讓 rbp
寄存器 (基礎(chǔ)指針寄存器 base pointer register) 被保護(hù)起來消略。當(dāng)函數(shù)調(diào)用返回時(shí)堡称,確保 rbp
寄存器的值跟之前一樣,這是屬于 main 函數(shù)的職責(zé)艺演。pushq %rbp
將 rbp
的值 push 到棧中却紧,以便我們以后將其 pop 出來。
接下來是兩個(gè) CFI 指令:.cfi_def_cfa_offset 16
和 .cfi_offset %rbp, -16
胎撤。這將會(huì)輸出一些關(guān)于生成調(diào)用堆棧展開和調(diào)試的信息晓殊。我們改變了堆棧和基礎(chǔ)指針,而這兩個(gè)指令可以告訴編譯器它們都在哪兒伤提,或者更確切的巫俺,它們可以確保之后調(diào)試器要使用這些信息時(shí),能找到對(duì)應(yīng)的東西肿男。
接下來介汹,movq %rsp, %rbp
將把局部變量放置到棧上。subq $32, %rsp
將棧指針移動(dòng) 32 個(gè)字節(jié)舶沛,也就是函數(shù)會(huì)調(diào)用的位置嘹承。我們先將老的棧指針存儲(chǔ)到 rbp
中,然后將此作為我們局部變量的基址如庭,接著我們更新堆棧指針到我們將會(huì)使用的位置叹卷。
之后,我們調(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
會(huì)將 L_.str
的指針加載到 rax
寄存器中骤竹。留意 L_.str
標(biāo)記在后面的匯編代碼中是如何定義的帝牡。它就是 C 字符串"Hello World!\n"
。 edi
和 rsi
寄存器保存了函數(shù)的第一個(gè)和第二個(gè)參數(shù)蒙揣。由于我們會(huì)調(diào)用別的函數(shù)靶溜,所以首先需要將它們的當(dāng)前值保存起來。這就是為什么我們使用剛剛存儲(chǔ)的 rbp
偏移32個(gè)字節(jié)的原因鸣奔。第一個(gè) 32 字節(jié)的值是 0墨技,之后的 32 字節(jié)的值是 edi
寄存器的值 (存儲(chǔ)了 argc
)。然后是 64 字節(jié) 的值:rsi
寄存器的值 (存儲(chǔ)了 argv
)挎狸。我們?cè)诤竺娌]有使用這些值扣汪,但是編譯器在沒有經(jīng)過優(yōu)化處理的時(shí)候,它們還是會(huì)被存下來锨匆。
現(xiàn)在我們把第一個(gè)函數(shù) printf()
的參數(shù) rax
設(shè)置給第一個(gè)函數(shù)參數(shù)寄存器 edi
中崭别。printf()
是一個(gè)可變參數(shù)的函數(shù)。ABI 調(diào)用約定指定恐锣,將會(huì)把使用來存儲(chǔ)參數(shù)的寄存器數(shù)量存儲(chǔ)在寄存器 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
將用來保存一個(gè)函數(shù)的返回值赫段,或者此處 main()
函數(shù)的返回值 0:
addq $32, %rsp
popq %rbp
ret
.cfi_endproc
函數(shù)執(zhí)行完成后,將恢復(fù)堆棧指針 —— 利用上面的指令 subq $32, %rsp
把堆棧指針 rsp
上移 32 字節(jié)矢赁。最后糯笙,把之前存儲(chǔ)至 rbp
中的值從棧中彈出來,然后調(diào)用 ret
返回調(diào)用者撩银, ret
會(huì)讀取出棧的返回地址给涕。 .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í)際的代碼中獲取到字符串的一個(gè)指針。.asciz
指令告訴編譯器輸出一個(gè)以 ‘\0’ (null) 結(jié)尾的字符串抄邀。
__TEXT __cstring
開啟了一個(gè)新的段首启。這個(gè)段中包含了 C 字符串:
L_.str: ## @.str
.asciz "Hello World!\n"
上面兩行代碼創(chuàng)建了一個(gè) null 結(jié)尾的字符串。注意 L_.str
是如何命名撤摸,之后會(huì)通過它來訪問字符串。
最后的 .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ī)器代碼。它會(huì)創(chuàng)建一個(gè)目標(biāo)對(duì)象文件楔绞,一般簡(jiǎn)稱為 對(duì)象文件结闸。這些文件以 .o
結(jié)尾。如果用 Xcode 構(gòu)建應(yīng)用程序酒朵,可以在工程的 derived data 目錄中桦锄,Objects-normal
文件夾下找到這些文件。
鏈接器
稍后我們會(huì)對(duì)鏈接器做更詳細(xì)的介紹蔫耽。這里簡(jiǎn)單介紹一下:鏈接器解決了目標(biāo)文件和庫(kù)之間的鏈接结耀。什么意思呢?還記得下面的語(yǔ)句嗎:
callq _printf
printf()
是 libc 庫(kù)中的一個(gè)函數(shù)匙铡。無論怎樣图甜,最后的可執(zhí)行文件需要能需要知道 printf()
在內(nèi)存中的具體位置:例如,_printf
的地址符號(hào)是什么鳖眼。鏈接器會(huì)讀取所有的目標(biāo)文件 (此處只有一個(gè)) 和庫(kù) (此處是 libc)黑毅,并解決所有未知符號(hào) (此處是 _printf
) 的問題。然后將它們編碼進(jìn)最后的可執(zhí)行文件中 (可以在 libc 中找到符號(hào) _printf
)钦讳,接著鏈接器會(huì)輸出可以運(yùn)行的執(zhí)行文件:a.out
矿瘦。
Section
就像我們上面提到的一樣,這里有些東西叫做 section蜂厅。一個(gè)可執(zhí)行文件包含多個(gè)段匪凡,也就是多個(gè) section【蛟常可執(zhí)行文件不同的部分將加載進(jìn)不同的 section病游,并且每個(gè) section 會(huì)轉(zhuǎn)換進(jìn)某個(gè) segment 里。這個(gè)概念對(duì)于所有的可執(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 個(gè) segment改橘。有些 segment 中有多個(gè) section滋尉。
當(dāng)運(yùn)行一個(gè)可執(zhí)行文件時(shí),虛擬內(nèi)存 (VM - virtual memory) 系統(tǒng)將 segment 映射到進(jìn)程的地址空間上飞主。映射完全不同于我們一般的認(rèn)識(shí)狮惜,如果你對(duì)虛擬內(nèi)存系統(tǒng)不熟悉高诺,可以簡(jiǎn)單的想象虛擬內(nèi)存系統(tǒng)將整個(gè)可執(zhí)行文件加載進(jìn)內(nèi)存 -- 雖然在實(shí)際上不是這樣的。VM 使用了一些技巧來避免全部加載碾篡。
當(dāng)虛擬內(nèi)存系統(tǒng)進(jìn)行映射時(shí)虱而,segment 和 section 會(huì)以不同的參數(shù)和權(quán)限被映射。
上面的代碼中开泽,__TEXT
segment 包含了被執(zhí)行的代碼牡拇。它被以只讀和可執(zhí)行的方式映射。進(jìn)程被允許執(zhí)行這些代碼穆律,但是不能修改惠呼。這些代碼也不能對(duì)自己做出修改,因此這些被映射的頁(yè)從來不會(huì)被改變峦耘。
__DATA
segment 以可讀寫和不可執(zhí)行的方式映射剔蹋。它包含了將會(huì)被更改的數(shù)據(jù)。
第一個(gè) segment 是 __PAGEZERO
贡歧。它的大小為 4GB滩租。這 4GB 并不是文件的真實(shí)大小,但是規(guī)定了進(jìn)程地址空間的前 4GB 被映射為 不可執(zhí)行利朵、不可寫和不可讀律想。這就是為什么當(dāng)讀寫一個(gè) NULL
指針或更小的值時(shí)會(huì)得到一個(gè) EXC_BAD_ACCESS
錯(cuò)誤。這是操作系統(tǒng)在嘗試防止引起系統(tǒng)崩潰绍弟。
在 segment中技即,一般都會(huì)有多個(gè) section。它們包含了可執(zhí)行文件的不同部分樟遣。在 __TEXT
segment 中而叼,__text
section 包含了編譯所得到的機(jī)器碼。__stubs
和 __stub_helper
是給動(dòng)態(tài)鏈接器 (dyld
) 使用的豹悬。通過這兩個(gè) section葵陵,在動(dòng)態(tài)鏈接代碼中,可以允許延遲鏈接瞻佛。__const
(在我們的代碼中沒有) 是常量脱篙,不可變的,就像 __cstring
(包含了可執(zhí)行文件中的字符串常量 -- 在源碼中被雙引號(hào)包含的字符串) 常量一樣伤柄。
__DATA
segment 中包含了可讀寫數(shù)據(jù)绊困。在我們的程序中只有 __nl_symbol_ptr
和 __la_symbol_ptr
,它們分別是 non-lazy 和 lazy 符號(hào)指針适刀。延遲符號(hào)指針用于可執(zhí)行文件中調(diào)用未定義的函數(shù)秤朗,例如不包含在可執(zhí)行文件中的函數(shù),它們將會(huì)延遲加載笔喉。而針對(duì)非延遲符號(hào)指針取视,當(dāng)可執(zhí)行文件被加載同時(shí)硝皂,也會(huì)被加載。
在 _DATA
segment 中的其它常見 section 包括 __const
贫途,在這里面會(huì)包含一些需要重定向的常量數(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)行時(shí)靜態(tài)變量的值是可以修改的秧倾。__common
section 包含未初始化的外部全局變量怨酝,跟 static
變量類似。例如在函數(shù)外面定義的 int a;
那先。最后农猬,__dyld
是一個(gè) section 占位符,被用于動(dòng)態(tài)鏈接器售淡。
蘋果的 OS X Assembler Reference 文檔有更多關(guān)于 section 類型的介紹斤葱。
Section 中的內(nèi)容
下面,我們用 otool(1)
來觀察一個(gè) 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
對(duì)其設(shè)置了一個(gè)縮寫 -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)該感覺很熟悉,這就是我們?cè)谇懊婢幾g時(shí)候的代碼贮泞。唯一的不同就是楞慈,在這里我們沒有任何的匯編指令在里面。這是純粹的二進(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
性能上需要注意的事項(xiàng)
從側(cè)面來講,__DATA
和 __TEXT
segment對(duì)性能會(huì)有所影響令蛉。如果你有一個(gè)很大的二進(jìn)制文件聚霜,你可能得去看看蘋果的文檔:關(guān)于代碼大小性能指南。將數(shù)據(jù)移至 __TEXT
是個(gè)不錯(cuò)的選擇言询,因?yàn)檫@些頁(yè)從來不會(huì)被改變俯萎。
任意的片段
使用鏈接符號(hào) -sectcreate
我們可以給可執(zhí)行文件以 section 的方式添加任意的數(shù)據(jù)。這就是如何將一個(gè) Info.plist 文件添加到一個(gè)獨(dú)立的可執(zhí)行文件中的方法运杭。Info.plist 文件中的數(shù)據(jù)需要放入到 __TEXT
segment 里面的一個(gè) __info_plist
section 中夫啊。可以將 -sectcreate segname sectname file
傳遞給鏈接器(通過將下面的內(nèi)容傳遞給 clang):
-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist
同樣辆憔,-sectalign
規(guī)定了對(duì)其方式撇眯。如果你添加的是一個(gè)全新的 segment报嵌,那么需要通過 -segprot
來規(guī)定 segment 的保護(hù)方式 (讀/寫/可執(zhí)行)。這些所有內(nèi)容在鏈接器的幫助文檔中都有熊榛,例如 ld(1)
锚国。
我們可以利用定義在 /usr/include/mach-o/getsect.h
中的函數(shù) getsectdata()
得到 section,例如 getsectdata()
可以得到指向 section 數(shù)據(jù)的一個(gè)指針玄坦,并返回相關(guān) section 的長(zhǎng)度血筑。
Mach-O
在 OS X 和 iOS 中可執(zhí)行文件的格式為 Mach-O:
% file a.out
a.out: Mach-O 64-bit executable x86_64
對(duì)于 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ī)定了這個(gè)文件是什么煎楣,以及文件是如何被加載的豺总。通過 -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ī)定了這個(gè)可執(zhí)行文件能夠運(yùn)行在哪些目標(biāo)架構(gòu)上。ncmds
和 sizeofcmds
是加載命令择懂,可以通過 -l
來查看這兩個(gè)加載命令:
% 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í)行慷丽。
對(duì)于每一個(gè) segment蹦哼,以及segment 中的每個(gè) section,加載命令規(guī)定了它們?cè)趦?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í)行文件中佛南,動(dòng)態(tài)鏈接庫(kù)是如何使用的:
% 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
符號(hào)的地方丈牢。
一個(gè)更復(fù)雜的例子
我們來看看有三個(gè)文件的復(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;
}
}
編譯多個(gè)文件
在上面的示例中喷市,有多個(gè)源文件骡尽。所以我們需要讓 clang 對(duì)輸入每個(gè)文件生成對(duì)應(yīng)的目標(biāo)文件:
% xcrun clang -c Foo.m
% xcrun clang -c helloworld.m
我們從來不編譯頭文件。頭文件的作用就是在被編譯的實(shí)現(xiàn)文件中對(duì)代碼做簡(jiǎn)單的共享竹祷。Foo.m
和 helloworld.m
都是通過 #import
語(yǔ)句將 Foo.h
文件中的內(nèi)容添加到實(shí)現(xiàn)文件中的谈跛。
最終得到了兩個(gè)目標(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
為了生成一個(gè)可執(zhí)行文件,我們需要將這兩個(gè)目標(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
符號(hào)表和鏈接
我們這個(gè)簡(jiǎn)單的程序是將兩個(gè)目標(biāo)文件合并到一起的塑陵。Foo.o
目標(biāo)文件包含了 Foo
類的實(shí)現(xiàn)感憾,而 helloworld.o
目標(biāo)文件包含了 main()
函數(shù),以及調(diào)用/使用 Foo
類令花。
另外阻桅,這兩個(gè)目標(biāo)對(duì)象都使用了 Foundation framework凉倚。helloworld.o
目標(biāo)文件使用了它的 autorelease pool,并間接的使用了 libobjc.dylib
中的 Objective-C 運(yùn)行時(shí)嫂沉。它需要運(yùn)行時(shí)函數(shù)來進(jìn)行消息的調(diào)用稽寒。Foo.o
目標(biāo)文件也有類似的原理。
所有的這些東西都被形象的稱之為符號(hào)趟章。我們可以把符號(hào)看成是一些在運(yùn)行時(shí)將會(huì)變成指針的東西杏糙。雖然實(shí)際上并不是這樣的。
每個(gè)函數(shù)尤揣、全局變量和類等都是通過符號(hào)的形式來定義和使用的搔啊。當(dāng)我們將目標(biāo)文件鏈接為一個(gè)可執(zhí)行文件時(shí),鏈接器 (ld(1)
) 在目標(biāo)文件和動(dòng)態(tài)庫(kù)之間對(duì)符號(hào)做了解析處理北戏。
可執(zhí)行文件和目標(biāo)文件有一個(gè)符號(hào)表,這個(gè)符號(hào)表規(guī)定了它們的符號(hào)漫蛔。如果我們用 nm(1)
工具觀察一下 helloworld.o
目標(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
上面就是那個(gè)目標(biāo)文件的所有符號(hào)。_OBJC_CLASS_$_Foo
是 Foo
Objective-C 類的符號(hào)莽龟。該符號(hào)是 undefined, external 蠕嫁。External 的意思是指對(duì)于這個(gè)目標(biāo)文件該類并不是私有的,相反毯盈,non-external
的符號(hào)則表示對(duì)于目標(biāo)文件是私有的剃毒。我們的 helloworld.o
目標(biāo)文件引用了類 Foo
,不過這并沒有實(shí)現(xiàn)它搂赋。因此符號(hào)表中將其標(biāo)示為 undefined赘阀。
接下來是 _main
符號(hào),它是表示 main()
函數(shù)脑奠,同樣為 external基公,這是因?yàn)樵摵瘮?shù)需要被調(diào)用,所以應(yīng)該為可見的宋欺。由于在 helloworld.o
文件中實(shí)現(xiàn)了 這個(gè) main 函數(shù)轰豆。這個(gè)函數(shù)地址位于 0處,并且需要轉(zhuǎn)入到__TEXT,__text
section齿诞。接著是 4 個(gè) Objective-C 運(yùn)行時(shí)函數(shù)酸休。它們同樣是 undefined的,需要鏈接器進(jìn)行符號(hào)解析祷杈。
如果我們轉(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)定義了,并且對(duì)于 Foo.o
是一個(gè)外部符號(hào) -- ·Foo.o· 包含了這個(gè)類的實(shí)現(xiàn)吠式。
Foo.o
同樣有 undefined 的符號(hào)陡厘。首先是使用了符號(hào) NSFullUserName()
抽米,NSLog()
和 NSObject
。
當(dāng)我們將這兩個(gè)目標(biāo)文件和 Foundation framework (是一個(gè)動(dòng)態(tài)庫(kù)) 進(jìn)行鏈接處理時(shí)糙置,鏈接器會(huì)嘗試解析所有的 undefined 符號(hào)云茸。它可以解析 _OBJC_CLASS_$_Foo
。另外谤饭,它將使用 Foundation framework标捺。
當(dāng)鏈接器通過動(dòng)態(tài)庫(kù) (此處是 Foundation framework) 解析成功一個(gè)符號(hào)時(shí),它會(huì)在最終的鏈接圖中記錄這個(gè)符號(hào)是通過動(dòng)態(tài)庫(kù)進(jìn)行解析的揉抵。鏈接器會(huì)記錄輸出文件是依賴于哪個(gè)動(dòng)態(tài)鏈接庫(kù)亡容,并連同其路徑一起進(jìn)行記錄。在我們的例子中冤今,_NSFullUserName
闺兢,_NSLog
,_OBJC_CLASS_$_NSObject
戏罢,_objc_autoreleasePoolPop
等符號(hào)都是遵循這個(gè)過程屋谭。
我們可以看一下最終可執(zhí)行文件 a.out
的符號(hào)表,并注意觀察鏈接器是如何解析所有符號(hào)的:
% 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)行時(shí)符號(hào)依舊是 undefined龟糕,不過現(xiàn)在的符號(hào)表中已經(jīng)多了如何解析它們的信息桐磁,例如在哪個(gè)動(dòng)態(tài)庫(kù)中可以找到對(duì)應(yīng)的符號(hào)。
可執(zhí)行文件同樣知道去哪里找到所需庫(kù):
% 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)行時(shí)讲岁,動(dòng)態(tài)鏈接器 dyld(1)
可以解析這些 undefined 符號(hào)我擂,dyld
將會(huì)確定好 _NSFullUserName
等符號(hào),并指向它們?cè)?Foundation 中的實(shí)現(xiàn)等缓艳。
我們可以針對(duì) Foundation 運(yùn)行 nm(1)
校摩,并檢查這些符號(hào)的定義情況:
% xcrun nm -nm `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName
0000000000007f3e (__TEXT,__text) external _NSFullUserName
動(dòng)態(tài)鏈接編輯器
有一些環(huán)境變量對(duì)于 dyld
的輸出信息非常有用。首先郎任,如果設(shè)置了 DYLD_PRINT_LIBRARIES
秧耗,那么 dyld
將會(huì)打印出什么庫(kù)被加載了:
% (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
[...]
上面將會(huì)顯示出在加載 Foundation 時(shí),同時(shí)會(huì)加載的 70 個(gè)動(dòng)態(tài)庫(kù)舶治。這是由于 Foundation 依賴于另外一些動(dòng)態(tài)庫(kù)分井。運(yùn)行下面的命令:
% xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
可以看到 Foundation 使用了 15 個(gè)動(dòng)態(tài)庫(kù)。
dyld 的共享緩存
當(dāng)你構(gòu)建一個(gè)真正的程序時(shí)霉猛,將會(huì)鏈接各種各樣的庫(kù)尺锚。它們又會(huì)依賴其他一些 framework 和 動(dòng)態(tài)庫(kù)。需要加載的動(dòng)態(tài)庫(kù)會(huì)非常多惜浅。而對(duì)于相互依賴的符號(hào)就更多了瘫辩。可能將會(huì)有上千個(gè)符號(hào)需要解析處理,這將花費(fèi)很長(zhǎng)的時(shí)間:一般是好幾秒鐘伐厌。
為了縮短這個(gè)處理過程所花費(fèi)時(shí)間承绸,在 OS X 和 iOS 上的動(dòng)態(tài)鏈接器使用了共享緩存,共享緩存存于 /var/db/dyld/
挣轨。對(duì)于每一種架構(gòu)军熏,操作系統(tǒng)都有一個(gè)單獨(dú)的文件,文件中包含了絕大多數(shù)的動(dòng)態(tài)庫(kù)卷扮,這些庫(kù)都已經(jīng)鏈接為一個(gè)文件荡澎,并且已經(jīng)處理好了它們之間的符號(hào)關(guān)系。當(dāng)加載一個(gè) Mach-O 文件 (一個(gè)可執(zhí)行文件或者一個(gè)庫(kù)) 時(shí)晤锹,動(dòng)態(tài)鏈接器首先會(huì)檢查 共享緩存 看看是否存在其中摩幔,如果存在,那么就直接從共享緩存中拿出來使用鞭铆。每一個(gè)進(jìn)程都把這個(gè)共享緩存映射到了自己的地址空間中或衡。這個(gè)方法大大優(yōu)化了 OS X 和 iOS 上程序的啟動(dòng)時(shí)間。
譯文 objc.io 第6期 Mach-O 可執(zhí)行文件
精細(xì)校對(duì) @BeyondVincent