這篇文章主要了介紹以下兩點(diǎn):
- 從源代碼到可執(zhí)行文件傲醉,編譯器都做了什么讶踪?
- Mach-O 可執(zhí)行文件里面是什么?
注:這篇文章的討論和示例不使用 Xcode缠犀,只使用命令行。
準(zhǔn)備工作:Xcode 工具鏈
xcrun 是 Xcode 基本的命令行工具聪舒,使用 xcrun 可以調(diào)用其他工具夭坪。
比如查看 clang 的版本,我們可以執(zhí)行下面的命令:
$ xcrun clang -v
而不是:
$ clang -v
如果要使用某個工具过椎,直接執(zhí)行那個工具的命令就行了室梅,為什么要使用 xcrun
呢?
因?yàn)槿绻愕碾娔X上安裝有多個不同版本的 Xcode,借助 xcrun
和 xcode-select
你可以:
- 選擇指定 Xcode 版本下的工具
- 選擇指定 Xcode 版本下的 SDK
如果你的電腦上只安裝了一個 Xcode亡鼠,就沒必要使用 xcrun
了赏殃。
一、不使用 IDE 來實(shí)現(xiàn)一個 Hello World
使用 clang 編譯一個簡單的 Hello World 小程序间涵,然后就可以直接執(zhí)行最后生成的 a.out
文件了仁热。
編寫 helloworld.c:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
然后使用 clang 將該文件編譯成一個 Mach-O 二進(jìn)制文件 a.out
,并執(zhí)行這個 a.out
文件:
$ xcrun clang helloworld.c
$ ./a.out
最終可以看到終端上輸出了 Hello World!
勾哩。
這個 a.out
是怎么生成的呢抗蠢?
二、編譯器是如何工作的
在上面的例子中思劳,我們所選用的編譯器是 clang迅矛,編譯器在將 helloworld.c
編譯成一個可執(zhí)行文件時,需要經(jīng)過好幾步潜叛。
編譯器處理的幾個步驟:
- Preprocessing
- Tokenization
- Macro expansion
-
#include
expansion
- Parsing and Semantic Analysis
- Translates preprocessor tokens into a parse tree
- Applies semantic analysis to the parse tree
- Outputs an Abstract Syntax Tree (AST)
- Code Generation and Optimization
- Translates an AST into low-level intermediate code (LLVM IR)
- Responsible for optimizing the generated code
- target-specific code generation
- Outputs assembly
- Assembler
- Translates assembly code into a target object file
- Linker
- Merges multiple object files into an executable (or a dynamic library)
1. 預(yù)處理
這個過程主要是對源代碼進(jìn)行標(biāo)記拆分秽褒、宏展開、#include
展開等等威兜。
使用下面的命令可以看到 helloworld.c
預(yù)處理后的結(jié)果:
$ xcrun clang -E helloworld.c
我們也可以將輸出的結(jié)果在文本編輯器中打開:
$ xcrun clang -E helloworld.c | open -f
最后得到的預(yù)處理結(jié)果大概有 542 行:
...
# 52 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/secure/_stdio.h" 3 4
extern int __snprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, ...);
extern int __vsprintf_chk (char * restrict, int, size_t,
const char * restrict, va_list);
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, va_list);
# 412 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h" 2 3 4
# 2 "helloworld.c" 2
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
與處理結(jié)果中那些 #
開頭的語句表示行標(biāo)記(linemarker)销斟,告訴我們后面接下來的內(nèi)容來自哪個文件的哪一行。
helloworld.c
中的 #include <stdio.h>
告訴預(yù)處理器要在那個地方插入 stdio.h
的內(nèi)容椒舵。這是一個遞歸的過程蚂踊,如果 stdio.h
中也引入了其他的 .h
文件,在預(yù)處理時同樣也會把這些語句替換成源文件中的內(nèi)容笔宿。
Tips: 在 Xcode 中打開菜單
Product -> Perform Action -> Preprocess
悴势,可以查看當(dāng)前打開文件的預(yù)處理結(jié)果。
2. 編譯
這個過程主要是對預(yù)處理后的代碼進(jìn)行語法分析措伐、語義分析特纤,并生成語法樹(AST),然后再翻譯成中間代碼侥加,并優(yōu)化代碼捧存,最后再針對不同平臺生成對應(yīng)的代碼,并轉(zhuǎn)成匯編代碼担败。
我們可以使用下面的命令生成匯編代碼:
$ xcrun clang -S -o - helloworld.c | open -f
生成的匯編代碼如下:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
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
xorl %ecx, %ecx
movl %eax, -20(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World!\n"
.subsections_via_symbols
以 .
開頭的是匯編器的指令昔穴。
.section
指令表示的是接下來的 section 是什么內(nèi)容。
.globl
指令表示 _main
是一個外部符號提前,也就是要暴露給其他模塊使用的符號吗货。
.p2align
指令表示的是字節(jié)對齊的規(guī)則是什么。
.cfi_startproc
表示一個函數(shù)的開始狈网,相應(yīng)地宙搬,.cfi_endproc
表示一個函數(shù)的結(jié)束笨腥。cfi
是 Call Frame Information 的縮寫。
.cfi_def_cfa_offset 16
和 .cfi_offset %rbp, -16
也是 cfi
指令勇垛,用來輸出一些函數(shù)堆棧展開信息和調(diào)試信息的脖母。
L_.str
標(biāo)簽可以讓我們在代碼中通過指針訪問到一個字符串常量。
.asciz
命令告訴匯編器輸出一個字面量字符串闲孤。
最后的 .subsections_via_symbols
是留給靜態(tài)鏈接編輯器使用的谆级。
Tips: 類似地,在 Xcode 中打開菜單
Product -> Perform Action -> Assemble
讼积,可以查看當(dāng)前打開文件的匯編代碼肥照。
3. 匯編
匯編的過程就是將匯編代碼翻譯成機(jī)器代碼,生成目標(biāo)文件勤众。
當(dāng)你用 Xcode 構(gòu)建你的 iOS App 時舆绎,你可以在你的項(xiàng)目的 Derived Data 目錄下找到一個 Objects-normal
文件夾,里面就是 .m
文件編譯后生成的目標(biāo)文件决摧。
4. 鏈接
鏈接器負(fù)責(zé)將各個目標(biāo)文件和庫合并成一個完整的可執(zhí)行文件亿蒸。在這個過程中凑兰,鏈接器需要解析各個目標(biāo)文件和庫之間的符號引用掌桩。
helloworld.c
中調(diào)用了 printf()
函數(shù),這個函數(shù)定義在 libc
庫中姑食,但是最終的可執(zhí)行文件需要知道 printf()
在內(nèi)存中的什么地方波岛,也就是 _printf
符號的地址。
鏈接器在鏈接時就會把所有的目標(biāo)文件(在我們這個例子中就是 helloworld.o
)和庫(libc
)作為輸入文件音半,然后解析它們之間符號引用(_printf
符號)则拷,最終生成一個可以運(yùn)行的可執(zhí)行文件。
二曹鸠、可執(zhí)行文件
一個可執(zhí)行文件中包含多個不同的 segment煌茬,,一個 segment 又包含一個或多個 section彻桃。
我們可以使用 size
工具查看目標(biāo)文件中的各個 section:
xcrun size -x -l -m a.out
下面是 helloworld.c
的目標(biāo)文件的各個 segment 和 section 的內(nèi)容:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x34 (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f84 offset 3972)
Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)
Section __cstring: 0xe (addr 0x100000fa6 offset 4006)
Section __unwind_info: 0x48 (addr 0x100000fb4 offset 4020)
total 0xaa
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
當(dāng)我們運(yùn)行可執(zhí)行文件時坛善,系統(tǒng)會把各個 segment 映射到進(jìn)程的地址空間中,在映射時邻眷,各個 segment 和 section 被分配不同的屬性眠屎,也就是權(quán)限。
我們來看看各個 segment 和 section 的具體含義:
-
__PAGEZERO
:從上面的信息中可以看出肆饶,這塊區(qū)域占 4 個 G 的大小改衩,不可讀不可寫,不可執(zhí)行驯镊, -
__TEXT
:代碼區(qū)葫督,具有只讀竭鞍、可執(zhí)行的權(quán)限-
__text
:編譯后生成的機(jī)器碼 -
__stubs
:用于動態(tài)鏈接 -
__stub_helper
:用于動態(tài)鏈接 -
__cstring
:字面量字符串,也就是寫在代碼里的字符串 -
__unwind_info
: -
__const
:常量
-
-
__DATA
:數(shù)據(jù)區(qū)候衍,可讀可寫笼蛛,但是不可執(zhí)行-
__nl_symbol_ptr
:non-lazy symbol pointers,局部符號蛉鹿,也就是定義在該文件內(nèi)的符號 -
__la_symbol_ptr
:lazy symbol pointers滨砍,外部符號,也就是定義在該文件外的符號 -
__const
:需要重定位的常量 -
__bss
:未初始化的靜態(tài)變量 -
__common
:未初始化的外部全局變量 -
__dyld
:給動態(tài)鏈接器使用的
-
__LINKEDIT
1. Section Content
我們可以使用 otool 查看目標(biāo)文件中指定 section 的內(nèi)容:
xcrun otool -s __TEXT __text a.out
得到的結(jié)果如下:
a.out:
Contents of (__TEXT,__text) section
0000000100000f50 55 48 89 e5 48 83 ec 20 48 8d 05 47 00 00 00 c7
0000000100000f60 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7
0000000100000f70 b0 00 e8 0d 00 00 00 31 c9 89 45 ec 89 c8 48 83
0000000100000f80 c4 20 5d c3
上面的機(jī)器代碼幾乎沒辦法看懂妖异,不過我們可以使用 otool 來查看反匯編后的代碼:
xcrun otool -v -t a.out
得到的結(jié)果如下:
a.out:
(__TEXT,__text) section
_main:
0000000100000f50 pushq %rbp
0000000100000f51 movq %rsp, %rbp
0000000100000f54 subq $0x20, %rsp
0000000100000f58 leaq 0x47(%rip), %rax
0000000100000f5f movl $0x0, -0x4(%rbp)
0000000100000f66 movl %edi, -0x8(%rbp)
0000000100000f69 movq %rsi, -0x10(%rbp)
0000000100000f6d movq %rax, %rdi
0000000100000f70 movb $0x0, %al
0000000100000f72 callq 0x100000f84
0000000100000f77 xorl %ecx, %ecx
0000000100000f79 movl %eax, -0x14(%rbp)
0000000100000f7c movl %ecx, %eax
0000000100000f7e addq $0x20, %rsp
0000000100000f82 popq %rbp
0000000100000f83 retq
2. Mach-O
Mach-O 是 Mach object file 格式的縮寫惋戏,Mach-O 是一種可執(zhí)行文件,Mac OS 上的可執(zhí)行文件都是 Mach-O 格式的他膳。
使用下面的命令可以查看一下 a.out
的文件格式:
$ file a.out
a.out: Mach-O 64-bit executable x86_64
我們可以使用 otool 查看可執(zhí)行文件的 Mach-O header:
$ otool -v -h a.out
得到的結(jié)果如下:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 15 1200 NOUNDEFS DYLDLINK TWOLEVEL PIE
ncmds 和 sizeofcmds 表示的是加載命令(load commands)响逢,可以通過 -l
參數(shù)查看詳細(xì)信息:
otool -v -l a.out | open -f
a.out:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
...
找到 Load command 1
部分的 initprot
字段,其值為 r-x
棕孙,表示 read-only 和 executable舔亭。
load command 指定了每一個 segment 和每個 section 的內(nèi)存地址以及權(quán)限保護(hù)。
下面是 __TEXT __text
section 的信息:
...
Section
sectname __text
segname __TEXT
addr 0x0000000100000f50
size 0x0000000000000034
offset 3920
align 2^4 (16)
reloff 0
nreloc 0
type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0
reserved2 0
...
這段代碼的 addr
值是 0x0000000100000f50
蟀俊,跟上面用 xcrun otool -v -t a.out
查看的 _main
的入口地址是一樣的钦铺。
三、一個更復(fù)雜的例子
我們現(xiàn)在有三個文件肢预,F(xiàn)oo.h矛洞、Foo.m 和 helloworld.m,如下烫映。
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;
}
}
1. 編譯
分別編譯 Foo.m 和 helloworld.m 這兩個文件:
$ xcrun clang -c Foo.m
$ xcrun clang -c helloworld.m
問題:為什么我們不需要編譯
.h
文件沼本?
因?yàn)轭^文件存在的目的,就是為了讓我們能通過import
和include
實(shí)現(xiàn)在多個不同的文件中共享一些代碼(比如函數(shù)聲明锭沟、變量聲明和類聲明等)抽兆,這樣我們就不用在每個用到相同聲明的地方寫重復(fù)代碼了。
得到兩個目標(biāo)文件:
$ file Foo.o helloworld.o
Foo.o: Mach-O 64-bit object x86_64
helloworld.o: Mach-O 64-bit object x86_64
為了能夠得到一個可執(zhí)行文件族淮,我們需要將這兩個目標(biāo)文件以及 Foundation 框架鏈接起來:
xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
我們會得到一個最終的可執(zhí)行文件 a.out
辫红,然后我們在執(zhí)行這個文件,可以看到打印的結(jié)果:
$ ./a.out
2019-02-02 17:27:18.207 a.out[4181:265495] ShannonChen
2. 符號解析和鏈接
Foo.o
和 helloworld.o
都用到了 Foundation 框架瞧筛,helloworld.o
中用到了 autorelease pool厉熟,而且 Foo.o
和 helloworld.o
都在 libobjc.dylib
的幫助下間接使用了 Objective-C runtime,因?yàn)?Objective-C 方法調(diào)用時發(fā)送消息需要用到 runtime较幌。
什么是符號揍瑟?
每一個我們定義的或者用到的函數(shù)、全局變量和類都是符號乍炉。
在鏈接時绢片,鏈接器會解析各個目標(biāo)文件以及庫之間的符號滤馍,每個目標(biāo)文件都有一個符號表來說明它的符號。
我們可以使用工具 nm
來查看目標(biāo)文件 helloworld.o
的所有符號:
$ xcrun nm -nm helloworld.o
(undefined) external _OBJC_CLASS_$_Foo
(undefined) external _objc_autoreleasePoolPop
(undefined) external _objc_autoreleasePoolPush
(undefined) external _objc_msgSend
0000000000000000 (__TEXT,__text) external _main
_OBJC_CLASS_$_Foo
符號就是我們定義的 Objective-C 類 Foo
底循,我可以看到巢株,這個符號的解析狀態(tài)是 undefined(因?yàn)?helloworld.o
中引用了 Foo
類,但是沒有定義這個類)熙涤,屬性是 external(表示這個 Foo
類不是私有的)阁苞。
_main
符號對應(yīng)的就是我們的 main()
函數(shù),它的屬性也是 external祠挫,因?yàn)樗侨肟诤瘮?shù)那槽,需要暴露出來被系統(tǒng)調(diào)用(值得注意的是,它的地址是 0)等舔。
然后骚灸,我們再看看目標(biāo)文件 Foo.o
中的所有符號:
xcrun nm -nm Foo.o
(undefined) external _NSFullUserName
(undefined) external _NSLog
(undefined) external _OBJC_CLASS_$_NSObject
(undefined) external _OBJC_METACLASS_$_NSObject
(undefined) external ___CFConstantStringClassReference
(undefined) external __objc_empty_cache
0000000000000000 (__TEXT,__text) non-external -[Foo run]
0000000000000060 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000a8 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000c8 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000110 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000138 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
在這里,_OBJC_CLASS_$_Foo
符號不再是 undefined 的了慌植,因?yàn)?Foo.o 中定義了 Foo
這個類甚牲。
當(dāng)這兩個目標(biāo)文件和 Foundation 庫鏈接時,鏈接器就會根據(jù)上面的這些符號表解析目標(biāo)文件中的符號蝶柿,解析成功后就能知道這個符號的地址了丈钙。
最后,我們再看看最終生成的可執(zhí)行文件的符號表信息:
xcrun nm -nm a.out
(undefined) external _NSFullUserName (from Foundation)
(undefined) external _NSLog (from Foundation)
(undefined) external _OBJC_CLASS_$_NSObject (from libobjc)
(undefined) external _OBJC_METACLASS_$_NSObject (from libobjc)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external __objc_empty_cache (from libobjc)
(undefined) external _objc_autoreleasePoolPop (from libobjc)
(undefined) external _objc_autoreleasePoolPush (from libobjc)
(undefined) external _objc_msgSend (from libobjc)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e90 (__TEXT,__text) external _main
0000000100000f10 (__TEXT,__text) non-external -[Foo run]
0000000100001138 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001160 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
我們可以看到只锭,跟 Foundation 和 Objective-C runtime 相關(guān)的符號依然是 undefined 狀態(tài)(這些需要在加載程序進(jìn)行動態(tài)鏈接時來解析)著恩,但是這個符號表中已經(jīng)有了如何解析這些符號的信息院尔,也就是從哪里可以找到這些符號蜻展。
比如,符號 _NSLog
后面有一個 from Foundation
的說明邀摆,這樣在動態(tài)鏈接時就知道是去 Foundation 庫找 _NSLog
這個符號的定義了纵顾。
而且,可執(zhí)行文件知道去哪里找到這些需要參與鏈接的動態(tài)庫:
$ xcrun otool -L a.out
a.out:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1555.10.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.200.5)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1555.10.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
這些 undefined symbols 會在運(yùn)行時被動態(tài)鏈接器 dyld
解析栋盹,當(dāng)我們運(yùn)行這個可執(zhí)行文件時施逾, dyld
可以保證 _NSFullUserName
這些符號能夠指向它們在 Foundation
以及其他動態(tài)庫中的實(shí)現(xiàn)。
3. dyld 的共享緩存
有些應(yīng)用程序可能會用到大量的 framework 和動態(tài)庫例获,這樣在鏈接時就會有成千上萬的符號需要解析汉额,從而影響鏈接速度。
為了縮短這個流程榨汤,在 macOS 和 iOS 上會針對每個架構(gòu)蠕搜,預(yù)先將所有的動態(tài)庫鏈接成一個庫,緩存到 /var/db/dyld/
目錄下收壕。當(dāng)一個 Mach-O 文件被加載到內(nèi)存中時妓灌,動態(tài)鏈接器首先去緩存目錄中檢查是否有緩存轨蛤,如果有就直接使用緩存好的動態(tài)庫。通過這種方式虫埂,大大提高了 macOS 和 iOS 上的應(yīng)用啟動速度祥山。