title: clang
categories: IT
一、编译器
为什么需要编译?
计算机 CPU 只能读懂机器码(machine code,也就是由一堆 0 和 1 组成的编码),但程序员现在编写的代码并不是机器码,而是高级编程语言(Objective-C、Swift、Java、...),最终也可以被计算机所执行,这就需要编译了。在编译的过程中,编译器的作用便是把我们的高级编程语言通过一系列的操作转化成可被计算机执行的机器语言。
编译器是如何设计的?
经典的三段式设计(three phase design):前端(Frontend)->优化器(Optimizer)->后端(Backend)
- 前端负责分析源代码,可以检查语法级错误,并构建针对该语言的抽象语法树(AST)
- 抽象语法树可以进一步转换为优化,最终转为新的表示方式,然后再交给让优化器和后端处理
- 最终由后端生成可执行的机器码
为什么要使用三段式设计?优势在哪?
首先解决了一个很大的问题:假如有 N 种语言(C、OC、C++、Swift...)的前端,同时也有 M 个架构(模拟器、arm64、x86...)的 Target,是否就需要 N × M 个编译器?
三段式架构的价值就体现出来了,通过共享优化器的中转,很好的解决了这个问题。
假如你需要增加一种语言,只需要增加一种前端;假如你需要增加一种处理器架构,也只需要增加一种后端,而其他的地方都不需要改动。
编译源文件有哪些主要步骤?
先列举一些整个编译过程的主要步骤,后面再详细介绍每个步骤都做了哪些事情。
主要编译步骤如下:
源代码(source code)
↓
预处理器(preprocessor)
↓
编译器(compiler)
↓
汇编程序(assembler)
↓
目标代码(object code)
↓
链接器(Linker)
↓
可执行文件(executables)
二、Xcode 编译器发展简史
Xcode 3 以前:GCC;
Xcode 3:增加 LLVM,GCC(前端) + LLVM(后端);
Xcode 4.2:出现 Clang - LLVM 3.0 成为默认编译器;
Xcode 4.6: LLVM 升级到 4.2 版本;
Xcode 5: GCC 被废弃,新的编译器是 LLVM 5.0
为什么苹果的 Xcode 会使用 Clang+LLVM 取代 GCC?
GCC 是第三方开源的,不属于苹果维护也不能完全掌控其开发进程,Apple 为 Objective-C 增加许多新特性,但 GCC 开发者对这些支持却不友好;Apple 需要做模块化,GCC 开发者却拖着迟迟不实现。
随着 Apple 对其 IDE(也就是 Xcode)性能的要求越来越高,最终还是从零开发了一个 Clang 前端加 LLVM 后端的编译器,这个编译器的作者是大名鼎鼎的 Swift 之父 Chris Lattner。
Clang 比 GCC 优秀在哪些方面?
- 传说新的 Clang 编译器编译 Objective-C 代码速度比 GCC 快 3 倍
- 提供了友好的代码提示
三、Clang 的简介
Clang: a C language family frontend for LLVM。
LLVM 的 C 语言家族(C、C++、OC)前端。
上面是官网对于 Clang的一句话介绍,其实 Clang 就是上文所提到的编译器前端。
用途:输出代码对应的抽象语法树(Abstract Syntax Tree, AST),并将代码编译成 LLVM Bitcode。接着在后端(back-end)使用 LLVM 编译成平台相关的机器语言。
四、Clang 的编译过程
4.1 预处理
预处理顾名思义是预先处理。预处理的内容如下:
import 头文件替换
在面向对象编程的思维下,写代码会经常用到其他类的属性或方法等,只需要导入头文件就可以用了,如:
#import <Foundation/Foundation.h> // 这里将会在预处理时把 Foundation.h 文件的内容拷贝过来并替换
如果 A.h 文件引用 B.h,并且 B.h 也引用了 A.h,就会导致了循环引入。
解决办法是在头文件中使用
@class A;
代替#import "A.h"
。这个意思是声明 A 是一个类,这样就可以使用 A 做类名,如果需要使用 A 的方法属性等,可以在 .m 实现文件中通过
#import A.h
的方式使用,这种方法不但可以解决互相引入的问题还可以优化编译速度。macro 宏展开
无参宏。如:
#define COUNT 3
带参宏。如:
#define SUM(a, b) a + b
在宏定义的作用域内,输入了 COUNT、SUM(),在预处理过程中都会被替换。
处理其他的预编译指令
条件编译语句也是在预处理阶段完成,并且条件编译只允许编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。如以下代码就只会保留一个 return 语句:
#if DEBUG return YES;
#else
return NO;
#endif
简单来说,“#”
这个符号是编译器预处理的标志。
预处理指令 | 说明 |
---|---|
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译以下代码 |
#ifdef | 如果宏已经定义,则编译以下代码 |
#ifndef | 如果宏没有定义,则编译以下代码 |
#elif | 如果前面的 #if 的条件不为真,当前条件为真,则编译以下代码 |
#endif | 结束一个 #if……#else 条件编译块 |
4.2 Lexical Analysis - 词法分析(输出 token 流)
词法分析其实是编译器开始工作真正意义上的第一个步骤,其所做的工作主要为将输入的代码转换为一系列符合特定语言的词法单元,这些词法单元类型包括了关键字、操作符、变量等等。
举个例子:
Objective-C 语言包含了关键字 if、else、new 等,那么在词法分析步骤时,遇到 i与f 或 n与e与w 组合在一起的时候,需要将这几个字母组合为关键字 if 或 new 等词法单元。
词法分析,只需要将源代码以字符文本的形式转化成 Token 流的形式,不涉及校验语义,不需要递归,是线性的。
什么是 token 流?
就是有“类型”,有“值”的一些小单元。
举个例子:
一个运算表达式:(28 + 78) * 2
只需要解析出 ( 是一个开括号,28 是数字整形,+ 是一个运算符号即可。
编译指令: $clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
里面的每一行都可以说是一个 token 流。一个表达式也会被逐个的解析。
4.3 Semantic Analysis - 语法分析(输出(AST)抽象语法树)
编译指令:$clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 语法分析的最终产物是输出抽象语法树
- 语法分析在 Clang 中由 Parser 和 Sema 两个模块配合完成
- 校验语法是否正确
- 根据当前语言的语法,生成语意节点,并将所有节点组合成抽象语法树(AST)
- 这一步跟源码等价,可以反写出源码
- Static Analysis 静态分析
- 通过语法树进行代码静态分析,找出非语法性错误
- 模拟代码执行路径,分析出 control-flow graph(CFG) 【MRC时代会分析出引用计数的错误】
- 预置了常用 Checker(检查器)
4.4 CodeGen - IR(Intermediate Representation)中间代码生成
CodeGen 负责将语法树从顶至下遍历,翻译成 LLVM IR。
LLVM IR 是 Frontend 的输出,LLVM Backend 的输入,前后端的桥接语言 (Swift也是转成这个)
与 Objective-C Runtime 桥接
- Class/Meta Class/Protocol/Category 内存结构生成,并存放在指定 section 中(如 Class:_DATA, _objc_classrefs)
- Method/lvar/Property 内存结构生成
- 组成 method_list/ivar_list/property_list 并填入 Class
- Non-Fragile ABI:为每个 Ivar 合成 OBJC_IVAR_$_ 偏移值常量
- 存取 Ivar 的语句(ivar = 123; int a = ivar;)转写成 base + OBJC_IVAR$_ 的形式
- 将语法树中的 ObjcMessageExpr 翻译成相应版本的 objc_msgSend,对 super 关键字的调用翻译成 objc_msgSendSuper
- 根据修饰符 strong/weak/copy/atomic 合成 @property 自动实现的 setter/getter
- 处理 @synthesize
- 生成 block_layout 的数据结构
- 变量的 capture(__block/__weak)
- 生成 _block_invoke 函数
- ARC:分析对象引用关系,将 objc_storeStrong/objc_storeWeak 等 ARC 代码插入
- 将 ObjCAutoreleasePoolStmt 转译成 objc_autoreleasePoolPush/Pop
- 实现自动调用 [super dealloc]
- 为每个拥有 ivar 的 Class 合成 .cxx_destructor 方法来自动释放类的成员变量,代替 MRC 时代的“self.xxx = nil”
4.5 Optimize - 优化 IR
递归优化成尾递归
4.6 LLVM Bitcode - 生成字节码
4.7 Assemble - 生成 Target 相关汇编
Assemble - 生成Target相关Object(Mach-O)
4.8 Link 生成 Executable
五、问题
clang 编译错误: fatal error: 'UIKit/UIKit.h' file not found
fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~~~~
1 error generated.
解决 1:
$ clang -rewrite-objc xx.m
替换成:
$ clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xx.m
这个命令很繁琐,可以采用 alias 来起一个别名来代替这个命令。
- 终端键入 $ vim ~/.bash_profile
- 编辑状态键入 $ alias rewriteoc='clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'
- 键入完毕后, esc 退出编辑状态, 再键入 :wq 退出 vim
- 键入命令 source ~/.bash_profile
解决 2:
# 模拟器
$ xcrun -sdk iphonesimulator clang -rewrite-objc main.m
# 真机
$ xcrun -sdk iphoneos clang -rewrite-objc main.m
# 带有版本的真机、模拟器
$ xcrun -sdk iphonesimulator9.3 clang -rewrite-objc main.m
查看设备上都装哪些 SDK。
$ xcodebuild -showsdks
...
iOS SDKs:
iOS 12.1 -sdk iphoneos12.1
iOS Simulator SDKs:
Simulator - iOS 12.1 -sdk iphonesimulator12.1
macOS SDKs:
macOS 10.14 -sdk macosx10.14
tvOS SDKs:
tvOS 12.1 -sdk appletvos12.1
tvOS Simulator SDKs:
Simulator - tvOS 12.1 -sdk appletvsimulator12.1
watchOS SDKs:
watchOS 5.1 -sdk watchos5.1
watchOS Simulator SDKs:
Simulator - watchOS 5.1 -sdk watchsimulator5.1
指定 framework
$ xcrun -sdk iphonesimulator clang -rewrite-objc -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/UIKit.framework MyObject.m
$ xcrun -sdk iphonesos clang -arch arm64 -framework Foundation main.m -o main.cpp
六、Clang Attributes
Clang Attributes 是 Clang 提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如 Static Analyzer、Name Mangling、Code Generation 等过程,一般以 __attribute__(xxx) 的形式出现在代码中;为方便使用,一些常用属性也被 Cocoa 定义成宏,比如在系统头文件中经常出现的 NS_CLASS_AVAILABLE_IOS(9_0) 就是 __attribute__(availability(...)) 这个属性的简单写法。
objc_subclassing_restricted
使用这个属性可以定义一个 Final Class,不允许被继承。
__attribute__((objc_subclassing_restricted)) @interface Eunuch : NSObject
@end
@interface Child : Eunuch // <--- Compile Error
@endobjc_requires_super
标志子类继承这个方法时需要调用 super,否则给出编译警告
@interface Father : NSObject - (void)hailHydra __attribute__((objc_requires_super));
@end
@implementation Father
- (void)hailHydra {
NSLog(@"hail hydra!");
}
@end
@interface Son : Father
@end
@implementation Son
- (void)hailHydra {
} // <--- Warning missing [super hailHydra]
@endobjc_boxable
Objective-C 中的 @(...) 语法糖可以将基本数据类型 box 成 NSNumber 对象,假如想 box 一个 struct 类型或是 union 类型成 NSValue 对象,可以使用这个属性:
typedef struct __attribute__((objc_boxable)) { CGFloat x, y, width, height;
} XXRect;这样一来,XXRect 就具备被 box 的能力:
CGRect rect1 = {1, 2, 3, 4}; NSValue * value1 = @(rect1); // <--- Compile Error
XXRect rect2 = {1, 2, 3, 4};
NSValue * value2 = @(rect2); // √constructor / destructor
顾名思义,构造器和析构器,加上这两个属性的函数会在分别在可执行文件(或 shared library)load 和 unload 时被调用,可以理解为在 main() 函数调用前和 return 后执行:
__attribute__((constructor)) static void beforeMain(void) {
NSLog(@"beforeMain");
}
__attribute__((destructor))
static void afterMain(void) {
NSLog(@"afterMain");
}
int main(int argc, const char * argv[]) {
NSLog(@"main");
return 0;
}
// Console:
// "beforeMain" -> "main" -> "afterMain"constructor 和 +load 都是在 main 函数执行前调用,但 +load 比 constructor 更加早一丢丢,因为 dyld(动态链接器,程序的最初起点)在加载 image(可以理解成 Mach-O 文件)时会先通知 objc runtime 去加载其中所有的类,每加载一个类时,它的 +load 随之调用,全部加载完成后,dyld 才会调用这个 image 中所有的 constructor 方法。
所以 constructor 是一个干坏事的绝佳时机:
- 所有 Class 都已经加载完成
- main 函数还未执行
- 无需像 +load 还得挂载在一个 Class 中
FDStackView 的 FDStackViewPatchEntry 方法便是使用的这个时机来实现偷天换日的伎俩。
PS:若有多个 constructor 且想控制优先级的话,可以写成
__attribute__((constructor(101)))
,里面的数字越小优先级越高,1 ~ 100 为系统保留。enable_if
这个属性只能用在 C 函数上,可以用来实现参数的静态检查:
static void printValidAge(int age) __attribute__((enable_if(age > 0 && age < 120, "你丫火星人?"))) {
printf("%d", age);
}它表示调用这个函数时必须满足 age > 0 && age < 120 才被允许,于是乎:
printValidAge(26); // √ printValidAge(150); // <--- Compile Error
printValidAge(-1); // <--- Compile Errorcleanup
声明到一个变量上,当这个变量作用域结束时,调用指定的一个函数,Reactive Cocoa 用这个特性实现了神奇的 @onExit。
overloadable
用于 C 函数,可以定义若干个函数名相同,但参数不同的方法,调用时编译器会自动根据参数选择函数原型:
__attribute__((overloadable)) void logAnything(id obj) { NSLog(@"%@", obj);
}
__attribute__((overloadable)) void logAnything(int number) {
NSLog(@"%@", @(number));
}
__attribute__((overloadable)) void logAnything(CGRect rect) {
NSLog(@"%@", NSStringFromCGRect(rect));
}
// Tests
logAnything(@[@"1", @"2"]);
logAnything(233);
logAnything(CGRectMake(1, 2, 3, 4));objc_runtime_name
用于 @interface 或 @protocol,将类或协议的名字在编译时指定成另一个:
__attribute__((objc_runtime_name("SarkGay"))) @interface Sark : NSObject
@end
NSLog(@"%@", NSStringFromClass([Sark class])); // "SarkGay"所有直接使用这个类名的地方都会被替换(唯一要注意的是这时用反射就不对了),最简单粗暴的用处就是去做个类名混淆:
__attribute__((objc_runtime_name("40ea43d7629d01e4b8d6289a132482d0dd5df4fa"))) @interface SecretClass : NSObject
@end还能用数字开头,怕不怕 - -,假如写个脚本把每个类前加个随机生成的 objc_runtime_name,岂不是最最精简版的代码混淆就完成了。
它是我所了解的唯一一个对 objc 运行时类结构有影响的 attribute,通过编码类名可以在编译时注入一些信息,被带到运行时之后,再反解出来,这就相当于开设了一条秘密通道,打通了写码时和运行时。脑洞一下,假如把这个 attribute 定义成宏,以 annotation 的形式完成某些功能,比如:
// @singleton 包裹了 __attribute__((objc_runtime_name(...))) // 将类名改名成 "SINGLETON_Sark_sharedInstance"
@singleton(Sark, sharedInstance)
@interface Sark : NSObject
+ (instancetype)sharedInstance;
@end在运行时用 __attribute__((constructor)) 获取入口时机,用 runtime 找到这个类,反解出 “sharedInstance” 这个 selector 信息,动态将 +alloc,-init 等方法替换,返回 +sharedInstance 单例。
unavailable
表明类不能被调用,编辑器会给出红色警告。
+ (instancetype)new __attribute__((unavailable("Singleton2类只能初始化一次")));
七、内容来源
Developer_Yancy - iOS底层探索(一) - 从零开始认识Clang与LLVM
Developer_Yancy - iOS底层探索(二) - 从零开始认识Clang与LLVM
http://clang.llvm.org/
http://www.aosabook.org/en/llvm.html
Clang
LLVM
C预处理器
The Compiler
Clang Attributes 黑魔法小记