Clang LLVM 简介

原文链接:http://www.yupeng.fun/2020/01/11/clang-llvm/


本文将简单介绍 Clang LLVM 相关的知识,然后介绍一下代码是如何一步步的编译运行的,以及可以利用 clang 能做些什么。

简介

编译器就是语言翻译器,把高级语言翻译成计算机能够执行的机器语言。

语言翻译主要工作流程:
源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (linker) → 可执行程序 (executables)

LLVM (Low Level Virtual Machine) 是一个开源的编译器架构。Clang 是 LLVM 的一个编译器前端,它目前支持 C, C++, Objective-C 等编程语言。

Clang 对源程序进行预处理、词法分析、语法分析,并将分析结果转换为 Abstract Syntax Tree ( 抽象语法树 ) ,最后使用 LLVM 作为编译器后端代码的生成器。

Clang 的开发目标是提供一个可以替代 GCC 的前端编译器。Apple 对 Objective-C 新增很多特性,想做的很多功能,比如更好的IDE支持,GCC 不能很好的支持,于是,苹果开发了 Clang 与 LLVM 来完全取代GCC。Clang作为编译器前端,LLVM作为编译器后端。

与 GCC 相比,Clang 是一个重新设计的编译器前端,具有一系列优点,例如模块化,代码简单易懂,占用内存小以及容易扩展和重用等。由于 Clang 在设计上的优异性,使得 Clang 非常适合用于设计源代码级别的分析和转化工具。Clang 也已经被应用到一些重要的开发领域,如 Static Analysis 是一个基于 Clang 的静态代码分析工具。


用 Clang 编译 OC 程序

当用 Xcode 创建了项目,然后点击 run 运行的时候,可以在 Xcode 中看到编译的信息:

看一下编译 main.m 文件的信息,相当于执行了一长串的命令,其中命令的参数就是你在 Build Settings 里面设置的一些选项,拼接成了这一串命令,主要的就是调用 Clang 编译的命令:

clang -x objective-c -fobjc-arc ... main.m -o main.o


clang 命令:

在命令行中 clang 相当于一个黑盒的驱动,里面封装了编译管线、前段命令、LLVM 命令、Toolchain 命令等。


clang 编译的过程

下面通过编译 main.m 文件,来看一下编译的过程。main.m 中是很简单的打印代码:

#import <Foundation/Foundation.h>
int main() {
    @autoreleasepool {
        NSLog(@"Hello world!");
    }
    return 0;
}

命令行输入命令:
clang -fobjc-arc -framework Foundation main.m -o main

-fobjc-arc 表示编译器需要支持 ARC 特性
-framework Foundation 表示引用Foundation框架

上面的命令会生成可执行文件 main,然后命令行输入执行文件 main:
./main
看到运行结果:
Hello world!

实质上,上述编译过程是分为四个阶段进行的,即预处理(Preprocess)、编译(Compilation)、汇编 (Assembly)和连接(Linking)。


1、预处理(Preprocess)

这个步骤会进行,import 头文件的处理,宏定义的展开,#开头的预处理指令等,的处理。
预处理的命令:
clang -E main.m 或者 clang -E main.m -o test.i

查看文件可看到头部,十几万行的预处理


-fmodules 参数可以把那些库打包导入,import Foundation,这样每次编译都不用展开那么多东西
clang -E -fmodules main.m


2、词法分析(Lexical Analysis)

词法分析,将预处理过的代码转化成一个个的 Token,对应的命令为:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

处理后的结果为:


可看到代码,拆分成了一个个的 Token


3、语法分析(Semantic Analysis)

语法分析,在 clang 中由 Parser 和 Sema 两个模块配合完成,来验证语法是否正确。
提示代码哪里写错了,少了冒号、括号等一些提示。
根据当前语言的语法,生成语义节点,并将所有节点组合成抽象语法树(AST: Abstract Syntax Tree),对应的命令为:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

处理后,生成的语法树:


FunctionDecl,表示函数名 main
CompoundStmt,表示大括号 {}
ObjCAutoreleasePoolStmt,表示 @autoreleasepool

静态分析:
编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还做了其它更高级的一些分析,以确保程序没有错误。

还有一种类型检查:动态分析
动态的在运行时做检查,静态的在编译时做检查。编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。


4、IR 代码生成 (CodeGen)

clang 完成代码的标记,解析和分析后,接着就会生成 LLVM 代码。将上一步的语法树从顶至下遍历,翻译成 LLVM IR。
LLVM IR 是编译前端的输出,也是 LLVM 后端的输入,是前后端桥接语言。

与 OC Runtime 桥接:

  • Class、Meta Class、Protocol、Category 内存结构生成,并存放在指定 section 中(如 Class:_DATA, _objc_classrefs)

  • Method、Ivar、Property 内存结构生成

  • 组成 method_list、ivar_list、property_list 并填入 Class

  • Non-Fragile ABI:为每个 Ivar 合成 OBJC_IVAR_$_ 偏移值常量

  • 存取 Ivar 的语句(_ivar = 123; int a = _ivar;)
    转写成 base + OBJC_IVAR_$_的形式

  • 将语法树中的 ObjCMessageExpr 翻译成相应版本的 obj_msgSend,对 super 关键字的调用翻译成 objc_msgSendSuper

  • 根据修饰符 strong、weak、copy、atomic 合成 @property 自动实现的 setter、getter

  • 处理 @synthesize

  • 生成 block_layout 的数据结构

  • 变量的 capture(__block __weak)

  • 生成 _block_invoke 函数

  • ARC 分析对象引用关系,将 obj_storeStrong/objc_storeWeak 等 ARC 代码插入

  • 将 ObjcCAutoreleasePoolStmt 转译成 objc_autoreleasePoolPush/Pop

  • 实现自动调用 [super dealloc]

  • 为每个拥有 ivar 的 Class 合成 .cxx_destructor 方法来自动释放类的成员变量,代替 MRC 时代的 self.xxx = nil

可以使用下面的命令,查看生成 IR 代码:
clang -S -fobjc-arc -emit-llvm main.m -o main.ll
-S 编译到汇编层面
-fobjc-arc 开启ARC
-emit-llvm 生成中间的LLVM语言

执行命令后可得到文件 main.ll:

define i32 @main() #0 {
 %1 = alloca i32, align 4
 store i32 0, i32* %1, align 4
 %2 = call i8* @llvm.objc.autoreleasePoolPush() #1
 notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
 call void @llvm.objc.autoreleasePoolPop(i8* %2)
 ret i32 0
}

这里 LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别 -O1,-O3,-Os :
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll

下面是优化后的中间LLVM代码

define i32 @main() local_unnamed_addr #0 {
  %1 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)), !clang.arc.no_objc_arc_exceptions !9
  tail call void @llvm.objc.autoreleasePoolPop(i8* %1) #1
  ret i32 0
}

生成字节码 LLVM Bitcode
clang -emit-llvm -c main.m -o main.bc
将上面生成的 IR 代码生成二进制码格式


5、生成 Target 相关汇编(Assemble)

生成对应机器相关的汇编语言:
clang -S -fobjc-arc main.m -o main.s

生成 Target 相关 Mach-O 文件
clang -fmodules -c main.m -o main.o

Mach-O(Mach object)文件,是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式,是macOS/iOS上程序以及库的标准格式。


6、生成可执行文件

链接生成可执行文件 main。
将程序的目标文件与所需的所有附加的目标文件(静态连接库和动态连接库)连接起来,最终生成可执行文件。
clang main.o -o main
执行就可以看到程序运行
./main


7、编译过程小结
  • 预处理
    • 符号化 (Tokenization)
    • 宏定义的展开
    • #include 的展开
  • 语法和语义分析
    • 将符号化后的内容转化为一棵解析树 (parse tree)
    • 解析树做语义分析
    • 输出一棵抽象语法树(Abstract Syntax Tree* (AST))
  • 生成代码和优化
    • 将 AST 转换为更低级的中间码 (LLVM IR)
    • 对生成的中间码做优化
    • 生成特定目标代码
    • 输出汇编代码
  • 汇编器
    • 将汇编代码转换为目标对象文件。
  • 链接器
    • 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)


clang 的应用

上面介绍了,用 clang 编译的过程,从预处理到词法分析、语法分析,再到生成汇编语言。
那么我们能用 clang 做些什么?

libclang
libclang 是一个 C 的类库,提供了简洁的 API,可以对 C 和 clang 做桥接,并可以用它对所有的源码做分析处理,如获取 Tokens、遍历语法树、代码补全、获取诊断信息等。
libclang api 稳定,不受 clang 源码更新影响,但是只能访问上层语法树,不能获取全部信息。
推荐使用 ClangKit 它是基于 clang 提供的功能,用 Objective-C 进行封装的一个库。

clang 还提供了一个直接使用 LibTooling 的 C++ 类库。它能够发挥 clang 的强大功能,可以对源码做任意类型的分析,甚至重写程序,对语法树有完全的控制权。如果你想要给 clang 添加一些自定义的分析、创建自己的重构器 (refactorer)、或者需要基于现有代码做出大量修改,甚至想要基于工程生成相关图形或者文档,那么可以使用 LibTooling。


将 OC 代码转换为 C/C++

当需要查看 OC 代码底层实现时,可以利用 clang 将 OC 代码转换为 C/C++ 代码。
下面命令可以进行转换:

clang -rewrite-objc main.m

//-fobjc-arc 表示ARC环境。-fobjc-runtime 表示当前运行时环境。
clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.15 main.m

执行后,可在当前文件夹下生成 main.cpp 文件。

当 .m 文件包含系统头文件时,会报错找不到头文件,可以指定SDK解决:

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

或者

xcrun -sdk iphonesimulator13.2 clang -rewrite-objc main.m
//其中的 sdk iphonesimulator13.2 可以通过命令 xcodebuild -showsdks 来查看

或者使用下面命令,指定了 SDK 和架构,转换后的代码会少一些:

xcrun -sdk iphoneos clang -arch arm64 -w -rewrite-objc main.m


References

https://llvm.org/
http://clang.llvm.org/docs/index.html
https://objccn.io/issue-6-2/
https://objccn.io/issue-6-3/
https://www.cnblogs.com/wfwenchao/p/5543595.html
https://www.ibm.com/developerworks/cn/opensource/os-cn-clang/index.html

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