GCC
是在linux下使用的编译器,Clang
是在mac
上使用的编译器,两者的命令几乎一样.
对于苹果iOS
,使用的编译器是LLVM
,相比于xcode 5
版本前使用的GCC
编译速度提高了3倍。
LLVM
是编译器工具链技术的一个集合,其中的lld
项目,就是内置链接器。LLVM
架构前段为Clang
后端为LLVM
整体的编译器架构就是 LLVM
架构;Clang
大致可以对应到编译器的前端,主要处理一些和具体机器无关的针对语言的分析操作;编译器的优化器和后端部分就是之前提到的 LLVM
后端,即狭义的 LLVM
。
本文运行环境为mac
编译指令
gcc/clang -g -O2 -o test test.c -I... -L... -l...
-g
输出文件中的调试信息
-O
大写的O,对输出文件做指令优化,2表示对文件进行第二个级别的编译优化,更彻底,如果调试的话,则不需要-O
-o
输出文件
-I
指定头文件位置,使用第三方库或者自己创建,
-L
指定lib库位置
-l
意为link,指定使用哪个库,不一定所有都用
编译过程
vim main.c
创建一个main
函数文件,c程序
必须以main函数为入口点
# include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
预处理
gcc -E main.c -o main.i
-E
表示生成预处理文件
具体内容:
- 宏定义指令
#define xxxx
- 条件编译
#if #else #endif...
- 头文件包含指令
#include
将头文件和源代码拷贝到一起
- 特殊符号处理 例如对方法参数中的空格处理
常见问题:
不能在头文件中定义全局变量,因为在头文件中定义全局变量将会使所有包含该头文件的文件存在该段代码,也就是说这些文件将定义一个相同的全局变量,这样将不可避免的造成冲突
首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件。即绑定头文件和宏判定等操作,将头文件和源代码拷贝到一起,以便编译器进行编译
生成汇编代码
gcc -S main.i -o main.s
-S
让编译程序在生成汇编语言输出
具体内容:
编译环节是指对源代码进行语法分析,并优化产生对应的汇编代码的过程,通过词法分析
和语法分析
,将其翻译成等价的中间代码或汇编代码。局部优化、控制流分析和循环优化、数据流的分析和全局优化。
进行词法分析、语法分析和语义分析等,生成*.s的汇编文件
词法分析
编译器中负责将程序分解为一个一个符号的部分,一般称为“词法分析器
”(引用自《C Traps and Pitfalls》)。
词法分析器读入组成源程序的字符流,并且将他们组织成为有意义的词素(lexeme)
序列。对于每个词素,词法分析器产生词法单元token(符号)
作为输出(引用自《编译原理》)。
token
指的是程序的一个基本组成单元—词法单元。token
的作用相当于一个句子中的单词,从某种意义上来说,一个单词无论出现在哪个句子中,它代表的意思都是一样的,是一个表义的基本单元。与此类似,token
就是程序中的一个基本信息单元。词法分析器将源文件的字符流转换为token
的过程被称作词法分析(lexical anaysis)
。
对某一个源文件进行词法分析
,可以使用下面这个命令
clang -fmodules -E -Xclang -dump-tokens main.m
annot_module_include '# include <s' Loc=<main.m:1:1>
int 'int' [StartOfLine] Loc=<main.m:3:1>
identifier 'main' [LeadingSpace] Loc=<main.m:3:5>
l_paren '(' Loc=<main.m:3:9>
r_paren ')' Loc=<main.m:3:10>
l_brace '{' [StartOfLine] Loc=<main.m:4:1>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:5:5>
l_paren '(' Loc=<main.m:5:11>
string_literal '"hello world\n"' Loc=<main.m:5:12>
r_paren ')' Loc=<main.m:5:27>
semi ';' Loc=<main.m:5:28>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:6:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:6:12>
semi ';' Loc=<main.m:6:13>
r_brace '}' [StartOfLine] Loc=<main.m:7:1>
eof '' Loc=<main.m:7:2>
int 'int' [StartOfLine] Loc=<main.m:3:1>
就是一个token
,每个token
后面的Loc
代表这个token
在源文件中的位置。例如Loc=<main.m:4:1>
代表这个token位于main.m文件中的第4行第1个
位置。
注意:这里的位置是从1
开始,而非0
。
ps:注释
虽然没有真实的意义,但是注释占用的行
依旧是有效的,在词法分析阶段并没有被忽略掉。这样的好处是,词法分析后的token
的Loc
就是真实的Location
。
当然,和预处理一样,如果源文件中有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
语法分析
将词法分析的token
解析处理成抽象语法树AST(abstract syntax tree)
的过程称作语法分析(semantic analysis)
。即语法分析的输入是token
,输出是AST
。AST
则更加直观的反映了代码的内部结构和逻辑。使用clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
可以对源文件进行语法分析。
TranslationUnitDecl 0x7f85fb828ce8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7f85fb829260 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7f85fb828f80 '__int128'
|-TypedefDecl 0x7f85fb8292d0 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7f85fb828fa0 'unsigned __int128'
|-TypedefDecl 0x7f85fb829370 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7f85fb829330 'SEL *'
| `-BuiltinType 0x7f85fb8291c0 'SEL'
|-TypedefDecl 0x7f85fb829458 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7f85fb829400 'id'
| `-ObjCObjectType 0x7f85fb8293d0 'id'
|-TypedefDecl 0x7f85fb829538 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7f85fb8294e0 'Class'
| `-ObjCObjectType 0x7f85fb8294b0 'Class'
|-ObjCInterfaceDecl 0x7f85fb829590 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7f85fb8298f8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7f85fb829700 'struct __NSConstantString_tag'
| `-Record 0x7f85fb829660 '__NSConstantString_tag'
|-TypedefDecl 0x7f85fb829990 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7f85fb829950 'char *'
| `-BuiltinType 0x7f85fb828d80 'char'
|-TypedefDecl 0x7f85fb861888 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f85fb861830 'struct __va_list_tag [1]' 1
| `-RecordType 0x7f85fb8616a0 'struct __va_list_tag'
| `-Record 0x7f85fb861600 '__va_list_tag'
|-ImportDecl 0x7f85fb862100 <main.m:1:1> col:1 implicit Darwin.C.stdio
|-FunctionDecl 0x7f85fb862190 <line:3:1, line:7:1> line:3:5 main 'int ()'
| `-CompoundStmt 0x7f85fb9f77e8 <line:4:1, line:7:1>
| |-CallExpr 0x7f85fb9f7750 <line:5:5, col:27> 'int'
| | |-ImplicitCastExpr 0x7f85fb9f7738 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7f85fb9f7678 <col:5> 'int (const char *, ...)' Function 0x7f85fb862290 'printf' 'int (const char *, ...)'
| | `-ImplicitCastExpr 0x7f85fb9f7798 <col:12> 'const char *' <BitCast>
| | `-ImplicitCastExpr 0x7f85fb9f7780 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7f85fb9f76d8 <col:12> 'char [13]' lvalue "hello world\n"
| `-ReturnStmt 0x7f85fb9f77d0 <line:6:5, col:12>
| `-IntegerLiteral 0x7f85fb9f77b0 <col:12> 'int' 0
`-<undeserialized declarations>
有了抽象语法树,clang
就可以对这个树进行分析,找出代码中的错误,很多编译期的检查都是针对于抽象语法树
的检查。比如类型不匹配
,未实现对应的方法
。
链接:https://www.jianshu.com/p/94c2a7a311d4
语义分析
使用语法分析
产生的语法树(AST)
和符号表
检查源程序是否和语言定义的语义一致的过程被称为语义分析
。这个定义听起来比较绕,后面会解释。
语义分析的过程同时也收集类型
信息,并把类型信息存储在语法树(AST)
和符号表
中,以便随后的中间代码生成过程中使用。
语义分析一个重要的部分就是“类型检查”
和“自动类型转换”
。编译器检查每个运算符是否有匹配的运算分量。所谓运算分量
就是指被运算符
操作的量
。拿C语言的语义分析举例,比如a + b
, 其中“+”
就是运算符,a和b
就是这个运算符的分量。如果a和b都是整型或浮点型,这说明“+”
运算符具有匹配的运算分量
。如果a或b
其中一个是字符串类型
,则说明“+”
运算符不具备匹配的运算分量
。又比如,很多语言中要求数组的下标是一个非负整数
,如果浮点数
作为下标,编译器就必须报告错误。
生成中间代码
在把源程序
翻译成目标代码
的过程中,一个编译器可能构造出一个或多个
中间表示(Intermediate Representation或IR)
。这些中间表示可以有多种形式。语法树(AST)
就是一种中间表示形式。–摘抄自《编译原理》
我们已经知道,语法分析
生成AST
,语义分析
会对根据AST和符号表
对源程序进行检查
。那么语法分析
和语义分析
都完成后,clang
会遍历AST
生成一种明确的、低级的或类机器语言
的中间表示IR
。LLVM IR
是LLVM
套件里面的中间表示(LLVM Intermediate Representation)
,LLVM IR
也是前端(clang)
的输出,后端的输入。
LLVM IR
有3种表示形式,分别是:
text格式
:便于阅读的文本格式,类似于汇编语言,拓展名.ll
, 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.13.0"
@.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00", align 1
; Function Attrs: noinline optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
%2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
ret i32 0
}
declare i32 @printf(i8*, ...) #1
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" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6}
!llvm.ident = !{!7}
!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"Apple LLVM version 10.0.0 (clang-1000.11.45.5)"}
Clang
还会收集源程序的信息,并把信息存放在符号表(symbol table
)中。符号表
和LLVM IR
会被传递给后端。
代码生成
代码生成(CodeGen)
由代码生成器
完成。以源程序的中间表示(IR
)作为输入,并把它映射到目标语言。如果目标语言是机器代码
,那么就必须为程序使用的每个变量
选择寄存器或内存位置
。然后中间指令(IR)
被翻译成为能够完成相同任务的机器指令序列(二进制文件)
。代码生成的一个至关重要的方面是合力分配寄存器
以存放变量的值
。
LLVM IR
有些编译器的结构单纯的分为前端
和后端
,比如GCC
。而LLVM
的结构并不是单纯的分为前端和后端
。LLVM编译器集合
是围绕着一组精心设计的中间表示形式
而创建的,这些中间表示形式
使得我们可以把特定语言
的前端
和特定目标机
的后端
相结合。使用这些集合
,我们可以把不同的前端
和某个目标机的后端
结合起来,为不同的源语言
建立该目标机上的编译器。类似的,我们可以把一个前端
和不同的目标机后端
结合,针对不同目标机的编译器。
这样说可能比较绕,本质上是LLVM IR优化器
会做一些与代码无关的优化,所以如果LLVM
将来需要支持一门新的编程语言
,只需针对这个编程语言提供一个新的前端
。如果将来LLVM需要支持一款新的机器架构
,只需要针对这款机器架构提供一个新的后端
。而LLVM IR优化器
是通用的。这样一来LLVM就变得易扩展
。
个人理解大概是这个意思:
编译过程从左至右
前端(C/C++) - LLVM - 后端(新架构)
前端(新语言) - LLVM - 后端(x86_64)
汇编
汇编器以汇编代码
作为输入,将汇编代码
转换为机器代码
,最后输出目标文件(object file)。
gcc -c main.s -o main.o
-c
表示编译、汇编指定的源文件(也就是编译源文件),但是不进行链接
将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件.o
链接
链接器把编译产生的.o文件
和(dylib,a,tbd
)文件,生成一个mach-o(可执行文件)
文件。
xcrun clang main.o -o main
使用nm
命令,查看可执行文件的符号表
nm -nm main
lib库的链接
vi add.c
创建一个add方法文件
#ifndef __MY_LIBRARY__
#define __MY_LIBRARY__
int add(int a, int b){
return (a+b);
}
#endif
然后编译,并使用libtool
,生成一个静态库包含add.o
gensees-iMac:study gensee$ vi add.c
gensees-iMac:study gensee$ clang -g -c add.c
gensees-iMac:study gensee$ ls add.o
add.o
gensees-iMac:study gensee$ ./add.o
gensees-iMac:study gensee$ libtool -static -o libmylib.a add.o
生成了一个lib文件
Ex
: 这里使用ar命令可以查看静态库的组成
gensees-iMac:study gensee$ ar -t /Users/gensee/Desktop/study/libmylib.a
__.SYMDEF SORTED
add.o
ar
是linux
命令,更多查看 https://www.runoob.com/linux/linux-comm-ar.html
创建一个main
函数文件 main.c
,并编译为.o
#include <stdio.h>
#include "add.h"
int main()
{
int sum = add(1,5);
printf("%d\n",sum);
return 0;
}
clang -g -c main.c
这里同样我们也需要创建一个add.h
头文件以便引用
使用vi add.h
#ifndef __MY_LIBRARY__
#define __MY_LIBRARY__
int add(int a, int b);
#endif
然后我们进行链接
clang -o main main.o -L . -lmylib
gensees-iMac:study gensee$ clang -o main main.o -L . -lmylib
gensees-iMac:study gensee$ ./main
6
gensees-iMac:study gensee$
可以看到最后执行了add
方法结果为6
clang 编译指令
前提是编译好mylib
clang -g -o main mian.c -I . -L . -lmylib
-I
. 表示当前目录头文件
-L
. 表示当前目录lib
gensees-iMac:study gensee$ clang -g -o main main.c -I . -L . -lmylib
gensees-iMac:study gensee$ ./main
6
gensees-iMac:study gensee$
链接方式
链接系统的库,三方库,分为动态链接
和静态链接
静态链接
和动态链接
两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时。
静态链接:将源文件中用到的库函数与汇编生成的目标文件.o合并生成可执行文。每一个可执行程序对所有需要的目标文件有一份副本,浪费空间,而且更新需要重新编译形成可执行程序。
动态链接:解决了静态链接浪费空间,更新困难的问题,只需要加载一次,存在内存中,每个程序都可以使用。
指令查看网址:http://c.biancheng.net/view/2375.html
将OC反编译为C
将.m
编译为cpp
clang -rewrite-objc main.m -o test.cpp
指定架构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o testMain.cpp
文本部分内容摘自 https://www.jianshu.com/p/94c2a7a311d4