Objective-C源文件编译过程

简介

Objective-C文件的编译过程主要包括clang前端的预处理、编译、后端优化中间表示、生成汇编指令、链接、生成机器码这几个步骤。我们可以借助clang -ccc-print-phases xxx.m命令查看某个OC源文件的编译的过程,如下:
输入命令

clang -ccc-print-phases main.m

命令行输出

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

可见编译过程先后是读取源文件、预处理、编译生成中间表示、生成汇编码、链接生成image、绑定架构生成对应机器码。本篇文章我们着重分析预处理、编译、生成汇编代码、链接这4个步骤。

预处理

通常,一个源程序可能被分割为多个模块,并存放于独立的文件中,把源程序“聚合”在一起的任务叫做预处理。预处理操作由预处理器独立完成。正如我们所知,预处理器通常把那些称为宏的缩写形式转换为源语言的语句。比如宏定义、条件编译、文件包含。
如下命令可以对.c、.m源文件进行预处理,其中参数-E就是对源文件进行预处理操作:

clang -E xxx.m

如果我们的.m文件中import(文件包含)了其他的文件或者其他的库,执行以上命令对OC源文件进行预处理可能会遇到如下错误:

main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~~~~
# 10 "main.m"
# 1 "./AppDelegate.h" 1
# 11 "./AppDelegate.h"

解决办法:
添加-isysroot参数来指定iPhoneSimulator.sdk的路径-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
预处理 -E

clang -x objective-c -E -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

当然,把-E换成 -rewrite-objc就可以把OC源文件转成C++文件,如下:
OC转C++ -rewrite-objc

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

除了使用Clang查看OC源文件的预处理过程,还可以使用xcrun命令,如下:
注意下面的命令中必须加上-E(-E代表对文件进行预处理)

xcrun -sdk iphoneos clang -arch armv7 -F Foundation -E -fobjc-arc -c main.m

如果要生成.o目标文件,只需要在最后指定目标文件的名称,如下:

xcrun -sdk iphoneos clang -arch armv7 -F Foundation -E -fobjc-arc -c main.m -o main.o

编译

词法分析

编译器中负责将程序分解为一个一个符号的部分,一般称为“词法分析器”(引用自《C Traps and Pitfalls》)。
词法分析器读入组成源程序的字符流,并且将他们组织成为有意义的词素(lexeme)序列。对于每个词素,词法分析器产生词法单元token(符号)作为输出(引用自《编译原理》)。
token指的是程序的一个基本组成单元—词法单元。token的作用相当于一个句子中的单词,从某种意义上来说,一个单词无论出现在哪个句子中,它代表的意思都是一样的,是一个表义的基本单元。与此类似,token就是程序中的一个基本信息单元。词法分析器将源文件的字符流转换为token的过程被称作词法分析(lexical anaysis)

对某一个源文件进行词法分析,可以使用下面这个命令

clang -fmodules -E -Xclang -dump-tokens main.m

当然,和预处理一样,如果源文件中有import其他文件,那么还需要使用-isysroot 参数来指定iPhoneSimulator.sdk的路径,如下:

clang -fmodules -E -Xclang -dump-tokens -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

本文本着示例简单、避免干扰的原则,将会对如下源码进行词法分析

//  main.m
//  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;
}
}

如下是main.m文件词法分析后的结果

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>

如上,int 'int' [StartOfLine] Loc=<main.m:12:1>就是一个token,identifier 'main' [LeadingSpace] Loc=<main.m:12:5>也是一个token。每个token后面的Loc代表这个token在源文件中的位置。例如Loc=<main.m:12:1>代表这个token位于main.m文件中的第12行第1个位置。注意:这里的位置是从1开始,而非0。
而上述token中的'int'、'main' 就是词素
ps:由上面词法分析后的结果和源文件对照可知,注释虽然没有真实的意义,但是注释占用的行依旧是有效的,在词法分析阶段并没有被忽略掉。这样的好处是,词法分析后的token的Loc就是真实的Location。

语法分析

将词法分析的token解析处理成抽象语法树AST(abstract syntax tree)的过程称作语法分析(semantic analysis)。即语法分析的输入是token,输出是AST。AST则更加直观的反映了代码的内部结构和逻辑。使用clang -fmodules -fsyntax-only -Xclang -ast-dump main.m可以对源文件进行语法分析,如下:

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>

有了抽象语法树,clang就可以对这个树进行分析,找出代码中的错误,很多编译期的检查都是针对于抽象语法树的检查。比如类型不匹配,未实现对应的方法。

AST是开发者编写clang插件主要交互的数据结构,clang也提供很多API去读取AST。详情参考:Introduction to the Clang AST

语义分析

使用语法分析产生的语法树和符号表检查源程序是否和语言定义的语义一致的过程被称为语义分析。这个定义听起来比较绕,后面会解释。语义分析的过程同时也收集类型信息,并把类型信息存储在语法树或符号表中,以便随后的中间代码生成过程中使用。
语义分析一个重要的部分就是“类型检查”和“自动类型转换”。编译器检查每个运算符是否有匹配的运算分量。所谓运算分量就是指被运算符操作的量。拿C语言的语义分析举例,比如a + b, 其中“+”就是运算符,a和b就是这个运算符的分量。如果a和b都是整型或浮点型,这说明“+”运算符具有匹配的运算分量。如果a或b其中一个是字符串类型,则说明“+”运算符不具备匹配的运算分量。又比如,很多语言中要求数组的下标是一个非负整数,如果浮点数作为下标,编译器就必须报告错误。

生成中间代码

在把源程序翻译成目标代码的过程中,一个编译器可能构造出一个或多个中间表示(Intermediate Representation或IR)。这些中间表示可以有多种形式。语法树(AST)就是一种中间表示形式。--摘抄自《编译原理》
我们已经知道,语法分析生成AST,语义分析会对根据AST和符号表对源程序进行检查。那么语法分析和语义分析都完成后,clang会遍历AST生成一种明确的、低级的或类机器语言的中间表示。LLVM IR是LLVM套件里面的中间表示(LLVM Intermediate Representation),LLVM IR也是前端(clang)的输出,后端的输入。
LLVM IR有3种表示形式,分别是:

  • text格式:便于阅读的文本格式,类似于汇编语言,拓展名.ll, clang -S -emit-llvm main.m 或 xcrun clang -S -emit-llvm main.c -o main.ll
  • memory格式:内存格式
  • bitcode格式:二进制格式,拓展名.bc, $ clang -c -emit-llvm main.m
    ps:以上三种形式的本质是等价的,就好比水可以有气体、液体、固体3种形态。
    我们使用clang -S -emit-llvm main.m命令来获取text格式的文件,文件后缀名是.ll,使用文本编辑器即可打开,如下:
; 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)"}

Clang还会收集源程序的信息,并把信息存放在符号表(symbol table)中。符号表和LLVM IR会被传递给后端。

代码生成

代码生成(CodeGen)由代码生成器完成。以源程序的中间表示(IR)作为输入,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。然后中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合力分配寄存器以存放变量的值。

LLVM IR

有些编译器的结构单纯的分为前端和后端,比如GCC。而LLVM的结构并不是单纯的分为前端和后端。LLVM编译器集合是围绕着一组精心设计的中间表示形式而创建的,这些中间表示形式使得我们可以把特定语言的前端和特定目标机的后端相结合。使用这些集合,我们可以把不同的前端和某个目标机的后端结合起来,为不同的源语言建立该目标机上的编译器。类似的,我们可以把一个前端和不同的目标机后端结合,简历针对不同目标机的编译器。
这样说可能比较绕,本质上是LLVM IR优化器会做一些与代码无关的优化,所以如果LLVM将来需要支持一门新的编程语言,只需针对这个编程语言提供一个新的前端。如果将来LLVM需要支持一款新的机器架构,只需要针对这款机器架构提供一个新的后端。而LLVM IR优化器是通用的。这样一来LLVM就变得易扩展。

生成汇编代码

LLVM对IR进行优化后,会针对不同架构生成不同的目标代码,最后以汇编代码的格式输出:

生成arm 64汇编:

xcrun clang -S main.c -o main.s

汇编器

汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)。

xcrun clang -fmodules -c main.c -o main.o

链接

链接器把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件。

xcrun clang main.o -o main

我们就得到了一个mach o格式的可执行文件

$ file main
main: Mach-O 64-bit executable x86_64
$ ./main 
hello debug

在用nm命令,查看可执行文件的符号表:

$ nm -nm main

下一篇文章将探讨《利用Clang探究block的本质》,持续更新,敬请期待

参考文章

iOS学习之深入理解程序编译过程
使用clang命令将OC代码转成C++代码
relocatable-pch-files
深入浅出iOS编译
iOS编译过程的原理和应用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容