Objective-C文件的編譯過(guò)程主要包括clang前端的預(yù)處理、編譯、后端優(yōu)化中間表示褒翰、生成匯編指令、鏈接匀泊、生成機(jī)器碼這幾個(gè)步驟优训。我們可以借助
clang -ccc-print-phases xxx.m
命令查看某個(gè)OC源文件的編譯的過(guò)程,如下: 輸入命令
<pre class="prism-token token language-javascript">clang -ccc-print-phases main.m</pre>
命令行輸出
<pre class="prism-token token language-javascript">0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image</pre>
可見(jiàn)編譯過(guò)程先后是讀取源文件各聘、預(yù)處理揣非、編譯生成中間表示、生成匯編碼躲因、鏈接生成image早敬、綁定架構(gòu)生成對(duì)應(yīng)機(jī)器碼忌傻。本篇文章我們著重分析預(yù)處理、編譯搞监、生成匯編代碼水孩、鏈接這4個(gè)步驟。
預(yù)處理
通常琐驴,一個(gè)源程序可能被分割為多個(gè)模塊俘种,并存放于獨(dú)立的文件中,把源程序“聚合”在一起的任務(wù)叫做預(yù)處理绝淡。預(yù)處理操作由預(yù)處理器獨(dú)立完成宙刘。正如我們所知,預(yù)處理器通常把那些稱(chēng)為宏的縮寫(xiě)形式轉(zhuǎn)換為源語(yǔ)言的語(yǔ)句牢酵。比如宏定義悬包、條件編譯、文件包含茁帽。 如下命令可以對(duì).c玉罐、.m源文件進(jìn)行預(yù)處理,其中參數(shù)-E
就是對(duì)源文件進(jìn)行預(yù)處理操作:
<pre class="prism-token token language-javascript">clang -E xxx.m</pre>
如果我們的.m文件中import(文件包含)了其他的文件或者其他的庫(kù)潘拨,執(zhí)行以上命令對(duì)OC源文件進(jìn)行預(yù)處理可能會(huì)遇到如下錯(cuò)誤:
解決辦法: 添加-isysroot參數(shù)來(lái)指定iPhoneSimulator.sdk的路徑
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
預(yù)處理 -E
<pre class="prism-token token language-javascript">clang -x objective-c -E -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m</pre>
當(dāng)然吊输,把-E換成 -rewrite-objc就可以把OC源文件轉(zhuǎn)成C++文件,如下: OC轉(zhuǎn)C++ -rewrite-objc
<pre class="prism-token token language-javascript">clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m</pre>
除了使用Clang查看OC源文件的預(yù)處理過(guò)程铁追,還可以使用xcrun
命令季蚂,如下: 注意下面的命令中必須加上-E(-E代表對(duì)文件進(jìn)行預(yù)處理)
<pre class="prism-token token language-javascript">xcrun -sdk iphoneos clang -arch armv7 -F Foundation -E -fobjc-arc -c main.m</pre>
如果要生成.o目標(biāo)文件,只需要在最后指定目標(biāo)文件的名稱(chēng)琅束,如下:
<pre class="prism-token token language-javascript">xcrun -sdk iphoneos clang -arch armv7 -F Foundation -E -fobjc-arc -c main.m -o main.o</pre>
編譯
詞法分析
編譯器中負(fù)責(zé)將程序分解為一個(gè)一個(gè)符號(hào)的部分扭屁,一般稱(chēng)為“詞法分析器”(引用自《C Traps and Pitfalls》)。 詞法分析器讀入組成源程序的字符流涩禀,并且將他們組織成為有意義的詞素(lexeme)序列料滥。對(duì)于每個(gè)詞素,詞法分析器產(chǎn)生詞法單元token(符號(hào))
作為輸出(引用自《編譯原理》)艾船。 token指的是程序的一個(gè)基本組成單元—詞法單元葵腹。token的作用相當(dāng)于一個(gè)句子中的單詞,從某種意義上來(lái)說(shuō)屿岂,一個(gè)單詞無(wú)論出現(xiàn)在哪個(gè)句子中难裆,它代表的意思都是一樣的诗充,是一個(gè)表義的基本單元弦疮。與此類(lèi)似匀伏,token就是程序中的一個(gè)基本信息單元。詞法分析器將源文件的字符流轉(zhuǎn)換為token的過(guò)程被稱(chēng)作詞法分析(lexical anaysis)运授。
對(duì)某一個(gè)源文件進(jìn)行詞法分析烤惊,可以使用下面這個(gè)命令
<pre class="prism-token token language-javascript">clang -fmodules -E -Xclang -dump-tokens main.m</pre>
當(dāng)然乔煞,和預(yù)處理一樣,如果源文件中有import其他文件撕氧,那么還需要使用-isysroot 參數(shù)來(lái)指定iPhoneSimulator.sdk的路徑瘤缩,如下:
<pre class="prism-token token language-javascript">clang -fmodules -E -Xclang -dump-tokens -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m</pre>
本文本著示例簡(jiǎn)單、避免干擾的原則伦泥,將會(huì)對(duì)如下源碼進(jìn)行詞法分析
<pre class="prism-token token language-javascript">//
// main.m
// ArrayWithArray:
//
// Created by sw on 2018/1/5.
// Copyright ? 2018年 sw. All rights reserved.
//
include <stdio.h>
define name "ws"
int main(int argc, char * argv[]) {
@autoreleasepool {
printf("hello %s", name);
return 1;
}
}</pre>
如下是main.m文件詞法分析后的結(jié)果
<pre class="prism-token token language-javascript">annot_module_include '#include <stdio.h>
define name "ws"
int main(int argc, char * argv[]) {
@autoreleasepool {
printf("hello %s", name);
' Loc=<main.m:9:1>
int 'int' [StartOfLine] Loc=<main.m:12:1>
identifier 'main' [LeadingSpace] Loc=<main.m:12:5>
l_paren '(' Loc=<main.m:12:9>
int 'int' Loc=<main.m:12:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:12:14>
comma ',' Loc=<main.m:12:18>
char 'char' [LeadingSpace] Loc=<main.m:12:20>
star '*' [LeadingSpace] Loc=<main.m:12:25>
identifier 'argv' [LeadingSpace] Loc=<main.m:12:27>
l_square '[' Loc=<main.m:12:31>
r_square ']' Loc=<main.m:12:32>
r_paren ')' Loc=<main.m:12:33>
l_brace '{' [LeadingSpace] Loc=<main.m:12:35>
at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:13:5>
identifier 'autoreleasepool' Loc=<main.m:13:6>
l_brace '{' [LeadingSpace] Loc=<main.m:13:22>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:14:9>
l_paren '(' Loc=<main.m:14:15>
string_literal '"hello %s"' Loc=<main.m:14:16>
comma ',' Loc=<main.m:14:26>
string_literal '"ws"' [LeadingSpace] Loc=<main.m:14:28 <Spelling=main.m:10:14>>
r_paren ')' Loc=<main.m:14:32>
semi ';' Loc=<main.m:14:33>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:15:9>
numeric_constant '1' [LeadingSpace] Loc=<main.m:15:16>
semi ';' Loc=<main.m:15:17>
r_brace '}' [StartOfLine] [LeadingSpace] Loc=<main.m:16:5>
r_brace '}' [StartOfLine] Loc=<main.m:17:1>
eof '' Loc=<main.m:17:2></pre>
如上,int 'int' [StartOfLine] Loc=<main.m:12:1>就是一個(gè)token锦溪,identifier 'main' [LeadingSpace] Loc=<main.m:12:5>也是一個(gè)token不脯。每個(gè)token后面的Loc代表這個(gè)token在源文件中的位置。例如Loc=<main.m:12:1>代表這個(gè)token位于main.m文件中的第12行第1個(gè)位置刻诊。注意:這里的位置是從1開(kāi)始防楷,而非0。 而上述token中的'int'则涯、'main' 就是詞素复局。 ps:由上面詞法分析后的結(jié)果和源文件對(duì)照可知,注釋雖然沒(méi)有真實(shí)的意義粟判,但是注釋占用的行依舊是有效的亿昏,在詞法分析階段并沒(méi)有被忽略掉。這樣的好處是档礁,詞法分析后的token的Loc就是真實(shí)的Location角钩。
語(yǔ)法分析
將詞法分析的token解析處理成抽象語(yǔ)法樹(shù)AST(abstract syntax tree)的過(guò)程稱(chēng)作語(yǔ)法分析(semantic analysis)。即語(yǔ)法分析的輸入是token呻澜,輸出是AST递礼。AST則更加直觀(guān)的反映了代碼的內(nèi)部結(jié)構(gòu)和邏輯。使用clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
可以對(duì)源文件進(jìn)行語(yǔ)法分析羹幸,如下:
<pre class="prism-token token language-javascript">TranslationUnitDecl 0x7f8c10009ee8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7f8c1000a780 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
|-BuiltinType 0x7f8c1000a480 '__int128' |-TypedefDecl 0x7f8c1000a7e8 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128' |
-BuiltinType 0x7f8c1000a4a0 'unsigned __int128'
|-TypedefDecl 0x7f8c1000a880 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
|-PointerType 0x7f8c1000a840 'SEL *' |
-BuiltinType 0x7f8c1000a6e0 'SEL'
|-TypedefDecl 0x7f8c1000a958 <<invalid sloc>> <invalid sloc> implicit id 'id'
|-ObjCObjectPointerType 0x7f8c1000a900 'id' |
-ObjCObjectType 0x7f8c1000a8d0 'id'
|-TypedefDecl 0x7f8c1000aa38 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
|-ObjCObjectPointerType 0x7f8c1000a9e0 'Class' |
-ObjCObjectType 0x7f8c1000a9b0 'Class'
|-ObjCInterfaceDecl 0x7f8c1000aa88 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7f8c100275e8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
|-RecordType 0x7f8c10027400 'struct __NSConstantString_tag' |
-Record 0x7f8c1000ab50 '__NSConstantString_tag'
|-TypedefDecl 0x7f8c10027680 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char '
|-PointerType 0x7f8c10027640 'char *' |
-BuiltinType 0x7f8c10009f80 'char'
|-TypedefDecl 0x7f8c10027948 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
|-ConstantArrayType 0x7f8c100278f0 'struct __va_list_tag [1]' 1 |
-RecordType 0x7f8c10027770 'struct __va_list_tag'
|-Record 0x7f8c100276d0 '__va_list_tag' |-ImportDecl 0x7f8c100281c0 <main.m:9:1> col:1 implicit Darwin.C.stdio |-FunctionDecl 0x7f8c100f6a78 <line:12:1, line:17:1> line:12:5 main 'int (int, char **)' | |-ParmVarDecl 0x7f8c10028210 <col:10, col:14> col:14 argc 'int' | |-ParmVarDecl 0x7f8c10028320 <col:20, col:32> col:27 argv 'char **':'char **' |
-CompoundStmt 0x7f8c100f7188 <col:35, line:17:1>
|-ObjCAutoreleasePoolStmt 0x7f8c100f7178 <line:13:5, line:16:5> |
-CompoundStmt 0x7f8c100f7158 <line:13:22, line:16:5>
| |-CallExpr 0x7f8c100f70a0 <line:14:9, col:32> 'int'
| | |-ImplicitCastExpr 0x7f8c100f7088 <col:9> 'int ()(const char *, ...)' <FunctionToPointerDecay>
| | |-DeclRefExpr 0x7f8c100f6f58 <col:9> 'int (const char *, ...)' Function 0x7f8c100f6b88 'printf' 'int (const char *, ...)' | | |-ImplicitCastExpr 0x7f8c100f70f0 <col:16> 'const char *' <BitCast> | | |
-ImplicitCastExpr 0x7f8c100f70d8 <col:16> 'char *' <ArrayToPointerDecay>
| | |-StringLiteral 0x7f8c100f6fb8 <col:16> 'char [9]' lvalue "hello %s" | |
-ImplicitCastExpr 0x7f8c100f7108 <line:10:14> 'char *' <ArrayToPointerDecay>
| |-StringLiteral 0x7f8c100f7028 <col:14> 'char [3]' lvalue "ws" |
-ReturnStmt 0x7f8c100f7140 <line:15:9, col:16>
|-IntegerLiteral 0x7f8c100f7120 <col:16> 'int' 1
-<undeserialized declarations></pre>
有了抽象語(yǔ)法樹(shù)脊髓,clang就可以對(duì)這個(gè)樹(shù)進(jìn)行分析,找出代碼中的錯(cuò)誤栅受,很多編譯期的檢查都是針對(duì)于抽象語(yǔ)法樹(shù)的檢查将硝。比如類(lèi)型不匹配,未實(shí)現(xiàn)對(duì)應(yīng)的方法窘疮。
AST是開(kāi)發(fā)者編寫(xiě)clang插件主要交互的數(shù)據(jù)結(jié)構(gòu)袋哼,clang也提供很多API去讀取AST。詳情參考:Introduction to the Clang AST闸衫。
語(yǔ)義分析
使用語(yǔ)法分析產(chǎn)生的語(yǔ)法樹(shù)和符號(hào)表檢查源程序是否和語(yǔ)言定義的語(yǔ)義一致的過(guò)程被稱(chēng)為語(yǔ)義分析涛贯。這個(gè)定義聽(tīng)起來(lái)比較繞,后面會(huì)解釋蔚出。語(yǔ)義分析的過(guò)程同時(shí)也收集類(lèi)型信息弟翘,并把類(lèi)型信息存儲(chǔ)在語(yǔ)法樹(shù)或符號(hào)表中虫腋,以便隨后的中間代碼生成過(guò)程中使用。 語(yǔ)義分析一個(gè)重要的部分就是“類(lèi)型檢查”和“自動(dòng)類(lèi)型轉(zhuǎn)換”稀余。編譯器檢查每個(gè)運(yùn)算符是否有匹配的運(yùn)算分量悦冀。所謂運(yùn)算分量就是指被運(yùn)算符操作的量。拿C語(yǔ)言的語(yǔ)義分析舉例睛琳,比如a + b, 其中“+”就是運(yùn)算符盒蟆,a和b就是這個(gè)運(yùn)算符的分量。如果a和b都是整型或浮點(diǎn)型师骗,這說(shuō)明“+”運(yùn)算符具有匹配的運(yùn)算分量历等。如果a或b其中一個(gè)是字符串類(lèi)型,則說(shuō)明“+”運(yùn)算符不具備匹配的運(yùn)算分量辟癌。又比如寒屯,很多語(yǔ)言中要求數(shù)組的下標(biāo)是一個(gè)非負(fù)整數(shù),如果浮點(diǎn)數(shù)作為下標(biāo)黍少,編譯器就必須報(bào)告錯(cuò)誤寡夹。
生成中間代碼
在把源程序翻譯成目標(biāo)代碼的過(guò)程中,一個(gè)編譯器可能構(gòu)造出一個(gè)或多個(gè)中間表示(Intermediate Representation或IR)厂置。這些中間表示可以有多種形式菩掏。語(yǔ)法樹(shù)(AST)就是一種中間表示形式。--摘抄自《編譯原理》 我們已經(jīng)知道农渊,語(yǔ)法分析生成AST患蹂,語(yǔ)義分析會(huì)對(duì)根據(jù)AST和符號(hào)表對(duì)源程序進(jìn)行檢查。那么語(yǔ)法分析和語(yǔ)義分析都完成后砸紊,clang會(huì)遍歷AST生成一種明確的传于、低級(jí)的或類(lèi)機(jī)器語(yǔ)言的中間表示。LLVM IR是LLVM套件里面的中間表示(LLVM Intermediate Representation)醉顽,LLVM IR也是前端(clang)的輸出沼溜,后端的輸入。 LLVM IR有3種表示形式游添,分別是:
- text格式:便于閱讀的文本格式系草,類(lèi)似于匯編語(yǔ)言,拓展名.ll唆涝,
xcrun clang -S -emit-llvm main.c -o main.ll
- memory格式:內(nèi)存格式
- bitcode格式:二進(jìn)制格式找都,拓展名.bc, $ clang -c -emit-llvm main.m ps:以上三種形式的本質(zhì)是等價(jià)的廊酣,就好比水可以有氣體能耻、液體、固體3種形態(tài)。 我們使用
clang -S -emit-llvm main.m
命令來(lái)獲取text格式的文件晓猛,文件后綴名是.ll饿幅,使用文本編輯器即可打開(kāi),如下:
<pre class="prism-token token language-javascript">; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
@.str = private unnamed_addr constant [9 x i8] c"hello %s\00", align 1
@.str.1 = private unnamed_addr constant [3 x i8] c"ws\00", align 1
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32, i8) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i8* @objc_autoreleasePoolPush() #2
%7 = call i32 (i8, ...) @printf(i8 getelementptr inbounds ([9 x i8], [9 x i8]* @.str, i32 0, i32 0), i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i32 0, i32 0))
store i32 1, i32* %3, align 4
call void @objc_autoreleasePoolPop(i8* %6)
%8 = load i32, i32* %3, align 4
ret i32 %8
}
declare i8* @objc_autoreleasePoolPush()
declare i32 @printf(i8*, ...) #1
declare void @objc_autoreleasePoolPop(i8*)
attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nounwind }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 14]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple LLVM version 10.0.1 (clang-1001.0.46.4)"}</pre>
Clang還會(huì)收集源程序的信息戒职,并把信息存放在符號(hào)表(symbol table)中栗恩。符號(hào)表和LLVM IR會(huì)被傳遞給后端。
代碼生成
代碼生成(CodeGen)由代碼生成器完成洪燥。以源程序的中間表示(IR)作為輸入磕秤,并把它映射到目標(biāo)語(yǔ)言。如果目標(biāo)語(yǔ)言是機(jī)器代碼蚓曼,那么就必須為程序使用的每個(gè)變量選擇寄存器或內(nèi)存位置亲澡。然后中間指令被翻譯成為能夠完成相同任務(wù)的機(jī)器指令序列。代碼生成的一個(gè)至關(guān)重要的方面是合力分配寄存器以存放變量的值纫版。
LLVM IR
有些編譯器的結(jié)構(gòu)單純的分為前端和后端,比如GCC客情。而LLVM的結(jié)構(gòu)并不是單純的分為前端和后端其弊。LLVM編譯器集合是圍繞著一組精心設(shè)計(jì)的中間表示形式而創(chuàng)建的,這些中間表示形式使得我們可以把特定語(yǔ)言的前端和特定目標(biāo)機(jī)的后端相結(jié)合膀斋。使用這些集合梭伐,我們可以把不同的前端和某個(gè)目標(biāo)機(jī)的后端結(jié)合起來(lái),為不同的源語(yǔ)言建立該目標(biāo)機(jī)上的編譯器仰担。類(lèi)似的糊识,我們可以把一個(gè)前端和不同的目標(biāo)機(jī)后端結(jié)合,簡(jiǎn)歷針對(duì)不同目標(biāo)機(jī)的編譯器摔蓝。 這樣說(shuō)可能比較繞赂苗,本質(zhì)上是LLVM IR優(yōu)化器會(huì)做一些與代碼無(wú)關(guān)的優(yōu)化,所以如果LLVM將來(lái)需要支持一門(mén)新的編程語(yǔ)言贮尉,只需針對(duì)這個(gè)編程語(yǔ)言提供一個(gè)新的前端拌滋。如果將來(lái)LLVM需要支持一款新的機(jī)器架構(gòu),只需要針對(duì)這款機(jī)器架構(gòu)提供一個(gè)新的后端猜谚。而LLVM IR優(yōu)化器是通用的败砂。這樣一來(lái)LLVM就變得易擴(kuò)展。
生成匯編代碼
LLVM對(duì)IR進(jìn)行優(yōu)化后魏铅,會(huì)針對(duì)不同架構(gòu)生成不同的目標(biāo)代碼昌犹,最后以匯編代碼的格式輸出:
生成arm 64匯編:
<pre class="prism-token token language-javascript">xcrun clang -S main.c -o main.s</pre>
匯編器
匯編器以匯編代碼作為輸入,將匯編代碼轉(zhuǎn)換為機(jī)器代碼览芳,最后輸出目標(biāo)文件(object file)斜姥。
<pre class="prism-token token language-javascript">xcrun clang -fmodules -c main.c -o main.o</pre>
鏈接
鏈接器把編譯產(chǎn)生的.o文件和(dylib,a,tbd)文件,生成一個(gè)mach-o文件。
<pre class="prism-token token language-javascript">xcrun clang main.o -o main</pre>
我們就得到了一個(gè)mach o格式的可執(zhí)行文件
<pre class="prism-token token language-javascript"> ./main
hello debug</pre>
在用nm命令疾渴,查看可執(zhí)行文件的符號(hào)表:
<pre class="prism-token token language-javascript">$ nm -nm main</pre>