美文网首页
Clang LLVM 简介

Clang LLVM 简介

作者: z4ywzrq | 来源:发表于2020-06-17 17:32 被阅读0次

    原文链接: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

    相关文章

      网友评论

          本文标题:Clang LLVM 简介

          本文链接:https://www.haomeiwen.com/subject/siwsxktx.html