Clang前端源码分析

发布时间 2023-05-03 04:55:24作者: 吴建明wujianming

Clang前端源码分析

C语言编译器之二,Clang

Clang编译器是由APPLE公司的编译器大牛ChrisLattner主导下编写的,其目标是替换大名鼎鼎的GCC编译器;

2.1、Clang和GCC编译器架构

从源代码到可执行程序一般经过预处理、编译、链接过程,而编译是编译器的工作,编译分为三个阶段,分别为前端、优化器、后端。

 i.编译前端:将源代码转化成中间代码。其详细过程包括:词法分析、语法分析、生成中间代码;

ii.优化器:对编译器生成的中间代码进行一些优化,最终提供给编译后端;

iii.编译后端:根据不同的 cpu 架构,将中间代码汇编,产生汇编代码,最后解析汇编指令,生成目标代码,也就是机器码;

编译器的这种前端、优化器、后端的架构的优点是:

 a.当为新的语言开发编译器时,只需要针对新的语言开发前端,产生标准通用的中间代码,这样优化器与后端可以不用修改;

b.当为新的架构开发编译器时,只需要针对新的架构开发后端,而无需修改前端和优化器。

         所以,这种架构对编译器的开发维护工作就简单许多,同时提升执行效率。

2.2、Clang起源

         GCC编译器从20世纪90年代就已经开发出来了,特别作为 Linux的当家编译器,GCC开源使得其广为流传,包括Apple在内,最开始也是使用GCC编译器,因为当时的Apple并没有自己的编译器,因此早期的XCODE使用GCC编译器编译Object-C,由于Object-C不断地引入新的特性,Apple公司不停与GCC组织协商改进GCC,可能当时的Apple还未有什么名气,GCC不太配合修改,这让Apple公司很是恼火。

         转机来了,2005年刚研究生毕业的ChrisLattner被Apple招致麾下,Chris Lattner可是精通编译器理论,其博士阶段的LLVM-GCC架构的编译器在其进入Apple公司后就被完善使用,Apple公司甚至将其当作替代GCC编译器的不二之选。但是,GNU组织出台了《GCC 运行环境豁免条款》从根本上限制了LLVM-GCC 的开发,这让Apple公司下定决心,从新开始编写C编译器,由Chris Lattner牵头,这就是Clang编译器的诞生,基于LLVM架构,为LLVM Compiler 1.0,这个版本不支持C++。

         这就是早期XCODE同时支持GCC、LLVM-GCC、LLVMCompiler的原因:

 

                       

     从XCODE4开始,也就是 MacOS X 10.6版本系统上,Apple 宣布停止更新GCC编译器,这样GCC停留在GCC4.2版本,并建议大家使用LLVM Compiler 2.0(LLVM-Clang),该版本完全支持C++/ Objective-C++,并提供libc++库来支持新的C++ standard(C++0x标准),而GCC/LLVM-GCC支持的是GCC标准库libstdc++;

          从XCODE4.2开始,就默认使用LLVM-Clang,彻底抛弃了GCC;而LLVM-GCC毕竟也是亲儿子,改为一个GCC的插件DragonEgg。

        由于Clang设计之初就考虑到模块化设计,因此,清晰简单,出错提示更好,易于扩展,容易与IDE集成;而GCC由于早期设计只支持C语言,后面不断扩展C++/ Java/Ada/Fortran/Go等,虽然支持更多平台;更流行,广泛使用,支持完备,但是其代码接口耦合性强,更新维护和性能等较差。

         由于LLVM-Clang的优秀设计,AndroidNDK从r11开始建议大家切换到clang。并且把GCC标记为deprecated,将GCC版本锁定在GCC 4.9不再更新;Android NDK从r13起,默认使用Clang进行编译。但是暂时也没有把GCC删掉,Google考虑 libc++(LLVM-Clang的c++标准库)还不够稳定; Android NDK 在 r17 中宣称不再支持 GCC 并在后续的 r18 中删掉 GCC。现在GCC主战场只剩Linux跟部分Windows应用软件开发。

         现在最新LLVM版本号已经到了12.0.1版本,官方地址如下:

LLVM编译器基础架构:http://llvm.org/

Clanghttp://clang.llvm.org/

DragonEgg – LLVM-GCC:http://dragonegg.llvm.org/

  二进制安装文件在github仓库中,地址为:

 https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1

        可见Clang也在x86/arm/powerpc架构下,及Linux系统下已经广泛支持,Clang与GCC进入激烈竞争的局面,对于吃瓜群众来说,应该是好事!

     Clang起源具体细节可以参考“Mac OS X 背后的故事(作者王越)”,该文详细介绍了Apple公司从创立之初到现今富可敌国的公司,中间的起起伏伏,堪比一部电视连续剧。

clang 源码导读: clang driver 参数解析

本文会对 clang driver 的 参数解析 流程进行分享

为了控制 clang 的运行,clang 必须支持不同的参数对各种行为进行控制,所以,clang driver 启动后的第一个主要任务就是 参数解析

正式分享前,我们先按照惯例分享本文涉及的主要 类图 和 流程图,方便对 参数解析 的主要流程进行理解

Info[1] 是保存了预定义的各种 Option 信息的结构体。比如 -v 参数的帮助信息是 Show commands to run and use verbose output

 Option[2] 是持有 Info 和 OptTable,提供了一些封装好的方法,比如通过 OptionClass getKind() 方法暴露 Info 的类型

 Arg[3] 持有了 Option 和其它命令行参数信息,比如 -arch armv64 的 arm64 会被保存到 Arg

 OptTable[4] 提供解析参数,并懒加载创建 Option 的相关方法

 InputArgList[5] 持有了输入的原始参数和解析后的参数列表

 DriverOptTable[6] 记录了 clang driver 相关的 Info 信息,是 OptTable 的子类

 一、DriverOptTable

DriverOptTable 记录了 clang driver 相关的 Info 信息,是 OptTable 的子类

DriverOptions 模块提供了函数 const llvm::opt::OptTable &clang::driver::getDriverOptTable() 可以获取 clang driver 支持的所有参数信息

 DriverOptTable 初始化时依赖的 InfoTable 参数是通过 clang/Driver/Options.inc 生成的

通过下图,我们可以看到 InfoTable 的长度是 2776

 小知识:当我们编译 llvm 项目时,会由 TableGen 工具将 Options.td 文件生成 Options.inc

原始的文本信息如下:

因为 DriverOptTable 继承自 OptTable,所以,这里会触发 OptTable 的初始化方法
imageimage
OptTable 的初始化时,会记录一些关键的 ID,用于后续使用,比如 TheInputOptionID

 同时,会通过 PrefixChars 和 PrefixesUnion 记录合法的参数前缀,用于后续的快速参数合法性判断,比如 -v 参数的前缀是 -

 

 二、Driver::ParseArgStrings

Driver::ParseArgStrings 方法的作用是将字符串数组解析为 ArgList,并做相关的校验

具体流程如下:

调用 Driver::getOpts 获取 clang driver 支持的所有参数 Info

 调用 ParseArgs 解析命令行参数

 对解析到的命令行参数进行判断,检测到 不支持 或者 未知 的参数时,会抛出异常

 如何区分 不支持 或者 不认识 的参数

clang driver 不支持 的参数,都可以通过 Options.td 文件查到

 以 -pass-exit-codes 为例,gcc 支持该参数,但是 clang 不支持 此参数

 imageimage

 不认识 的参数就是类似于 -test 这种,开发者随意拼写的参数

 三、ParseArgs

OptTable::ParseArgs 方法负责将字符串数组解析为 ArgList

具体流程如下:

先初始化 InputArgList 的实例,并存储原始的入参信息
通过 while 对原始参数字符串进行遍历,并通过 OptTable::ParseOneArg 方法将所有的原始参数字符串解析为 Arg 的实例
最后 Args 会持有所有的解析后的参数

通过添加调试代码,我们可以感受一下以下命令行对应的原始参数和解析后的 Arg 实例分别是什么样子

clang -arch arm64 /var/folders/4j/jqzrrjzn0nvgm4pyxrqddxnmm530jm/C/main.m -target arm64-apple-ios11.1

 四、ParseOneArg

OptTable::ParseOneArg 方法负责解析单个参数

具体流程如下:

先移除参数的前缀,并通过 std::lower_bound 查找第一个前缀匹配的 Info

 比如,-arch 会变成 arch

根据 Info 初始化 Option 持有参数信息

 通过 Option::accept 方法校验参数是否正常

 参数正常时直接返回

 如果没有找到合适的参数,再判断参数是否以 / 开头,如果开始,会把参数当做源码文件进行处理

 其它情况下,会当做参数当做 未知参数 进行下一步处理

 std::lower_bound 会依赖下面两个方法查找第一个前缀匹配的参数

 Info.Name 和 Name 的查找逻辑比较复杂,需要深入研究的同学,可以逐步调试帮助理解

 imageimage

 Option::accept 方法会依次进行以下处理

 比如,-fembed-bitcode-marker 就是 -fembed-bitcode=marker 参数的别名,两个参数的意义完全相同

 

 先转发到 Option::acceptInternal 方法进行参数校验

 判断解析到的参数是否属于别名

如果别名,会进行特殊处理
Option::acceptInternal 方法会根据 Option 的类型进行处理并生成 Arg 实例。

因为 -arch 的类型是 SeparateClass ,所以,会将下一个原始参数字符串arm64)当做 value 进行处理

 

类型

 

示例

Separate

-arch arm64

Flag

-v

Joined

-fembed-bitcode=marker

 

 总结

本文通过分析 DriverOptTable 的生成机制并分析Driver::ParseArgStrings 内部流程,对 clang driver 的参数解析流程做了简单的分析



参考资料

[1]Info: https://www.llvm.org/doxygen/structllvm_1_1opt_1_1OptTable_1_1Info.html#details

[2]Option: https://www.llvm.org/doxygen/classllvm_1_1opt_1_1Option.html

[3]Arg: https://www.llvm.org/doxygen/classllvm_1_1opt_1_1Arg.html

[4]OptTable: https://www.llvm.org/doxygen/classllvm_1_1opt_1_1OptTable.html#details

[5]InputArgList: https://www.llvm.org/doxygen/classllvm_1_1opt_1_1InputArgList.html

[6]DriverOptTable: https://clang.llvm.org/doxygen/DriverOptions_8cpp_source.html

 

参考文献链接

https://mp.weixin.qq.com/s/PjNoc7LV3sXtcw-8Utg4dg

https://mp.weixin.qq.com/s/GoEk5LJ5TkcSCKLIg4sPhQ