详解三大编译器:gcc、llvm 和 clang ;ANSI/ISO

您所在的位置:网站首页 clang调试器 详解三大编译器:gcc、llvm 和 clang ;ANSI/ISO

详解三大编译器:gcc、llvm 和 clang ;ANSI/ISO

2024-01-04 20:41| 来源: 网络整理| 查看: 265

编译器一般构成

传统的编译器通常分为三个部分,前端(frontEnd),优化器(Optimizer)和后端(backEnd). 在编译过程中,前端主要负责词法和语法分析,将源代码转化为抽象语法树;优化器则是在前端的基础上,对得到的中间代码进行优化,使代码更加高效;后端则是将已经优化的中间代码转化为针对各自平台的机器代码。

GCC

GCC(GNU Compiler Collection,GNU 编译器套装),是一套由 GNU 开发的编程语言编译器。GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言。GCC 快速演进,变得可处理 C++、Fortran、Pascal、Objective-C、Java 以及 Ada 等他语言。

LLVM

LLVM (Low Level Virtual Machine,底层虚拟机))提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。简而言之,可以作为多种编译器的后台来使用。

苹果公司一直使用 GCC 作为官方的编译器。GCC 作为一款开源的编译器,一直做得不错,但 Apple 对编译工具会提出更高的要求。原因主要有以下两点:

其一,是 Apple 对 Objective-C 语言(包括后来对 C 语言)新增很多特性,但 GCC 开发者并不买 Apple 的账——不给实现,因此索性后来两者分成两条分支分别开发,这也造成 Apple 的编译器版本远落后于 GCC 的官方版本。

其二,GCC 的代码耦合度太高,很难独立,而且越是后期的版本,代码质量越差,但 Apple 想做的很多功能(比如更好的 IDE 支持),需要模块化的方式来调用 GCC,但 GCC一直不给做。

编译器大神 Chris Lattner 横空出世

2000 年,本科毕业的 Chris Lattner 像中国多数大学生一样,按部就班地考了 GRE,最终前往 UIUC(伊利诺伊大学厄巴纳香槟分校),开始了艰苦读计算机硕士和博士的生涯。在这阶段,他不仅周游美国各大景点,更是翻烂了《Compilers: Principles, Techniques, and Tools》,成了 GPA 满分(4.0) 牛人,并不断地研究探索关于编译器的未知领域,发表了一篇又一篇的论文。他在硕士毕业论文里提出了一套完整的在编译时、链接时、运行时甚至是在闲置时优化程序的编译思想,直接奠定了 LLVM 的基础。LLVM 在他念博士时更加成熟,使用 GCC 作为前端来对用户程序进行语义分析产生 IF(Intermidiate Format),然后 LLVM 使用分析结果完成代码优化和生成。这项研究让他在 2005 年毕业时就成为了业界小有名气的编译器专家,他也因此早早地被 Apple 盯上,最终成为其编译器项目的骨干。

刚进入 Apple,Chris Lattner 就大展身手:首先在 OpenGL 小组做代码优化,把 LLVM 运行时的编译架在 OpenGL 栈上,这样 OpenGL 栈能够产出更高效率的图形代码。如果显卡足够高级,这些代码会直接扔入 GPU 执行。但对于一些不支持全部 OpenGL 特性的显卡(比如当时的 Intel GMA卡),LLVM 则能够把这些指令优化成高效的 CPU 指令,使程序依然能够正常运行。这个强大的 OpenGL 实现被用在了后来发布的 Mac OS X 10.5 上。同时,LLVM 的链接优化被直接加入到 Apple 的代码链接器上,而 LLVM-GCC 也被同步到使用 GCC4.0 代码。

LLVM2.0 - Clang

Apple 吸收 Chris Lattner 的目的要比改进 GCC 代码更具野心 -- Apple 打算从零开始写 C、C++、Objective-C 语言的前端 Clang,完全替代掉 GCC。

Clang 是 LLVM 的前端,可以用来编译 C,C++,ObjectiveC 等语言。Clang 则是以 LLVM 为后端的一款高效易用,并且与IDE 结合很好的编译前端。

Clang 只支持C,C++ 和 Objective-C 三种语言。2007 年开始开发,C 编译器最早完成,而由于 Objective-C 只是 C 语言的一个简单扩展,相对简单,很多情况下甚至可以等价地改写为 C 语言对 Objective-C 运行库的函数调用,因此在 2009 年时,已经完全可以用于生产环境。C++ 在后来也得到了支持。

GCC 和 Clang 对比 Clang 特性

速度快:通过编译 OS X 上几乎包含了所有 C 头文件的 carbon.h 的测试,包括预处理 (Preprocess),语法 (lex),解析 (parse),语义分析 (Semantic Analysis),抽象语法树生成 (Abstract Syntax Tree) 的时间,Clang 比 GCC 快2倍多。

内存占用小:Clang 内存占用是源码的 130%,Apple GCC 则超过 10 倍。

诊断信息可读性强:其中错误的语法不但有源码提示,还会在错误的调用和相关上下文的下方有~~~~~和^的提示,相比之下 GCC 的提示很天书。

兼容性好:Clang 从一开始就被设计为一个 API,允许它被源代码分析工具和 IDE 集成。GCC 被构建成一个单一的静态编译器,这使得它非常难以被作为 API 并集成到其他工具中。

Clang 有静态分析,GCC 没有。

Clang 使用 BSD 许可证,GCC 使用 GPL 许可证。

GCC 优势

支持 JAVA/ADA/FORTRAN

GCC 支持更多平台

GCC 更流行,广泛使用,支持完备

GCC 基于 C,不需要 C++ 编译器即可编译

GCC、LLVM 和 Clang 如何选择?

目前不推荐使用老的 GCC 4.2,因为苹果不会维持它了,而且 LLVM-GCC 看起来会更好。在项目中途改编译选项可是一个大变动,需要慎重。

对新的项目而言,LLVM-GCC 看起來应该是个安全的选择,苹果公司认为它够稳定够成熟,所以才把它当做 Xcode 4 的预设选项。而且,既然选项使用的是 GCC parser,向后兼容性应该没问题。

LLVM-GCC 是个安全的选项,但并不是指 Clang/LLVM 比较不安全,只是成熟度还沒那么高效了。

总结 - 再探 LLVM

回顾 GCC 的历史,虽然它取得了巨大的成功,但开发 GCC 的初衷是提供一款免费的开源编译器,仅此而已。可后来随着 GCC 支持了越来越多的语言,GCC 架构的问题也逐渐暴露出来。但 GCC 到底有什么问题呢?

LLVM 的优点也正是 GCC 的缺点。传统编译器工作的时候前端负责解析源代码,检查语法错误,并将其翻译为抽象的语法树(Abstract Syntax Tree)。优化器对这一中间代码进行优化,试图使代码更高效。后端则负责将优化器优化后的中间代码转换为目标机器的代码,这一过程后端会最大化的利用目标机器的特殊指令,以提高代码的性能。事实上,不光静态语言如此,动态语言也符合上面这个模型,例如 Java。JVM 也利用上面这个模型,将 Java 代码翻译为Java bytecode。这一模型的好处是,当我们要支持多种语言时,只需要添加多个前端就可以了。当需要支持多种目标机器时,只需要添加多个后端就可以了。对于中间的优化器,我们可以使用通用的中间代码。这种三段式的结构还有一个好处,开发前端的人只需要知道如何将源代码转换为优化器能够理解的中间代码就可以了,他不需要知道优化器的工作原理,也不需要了解目标机器的知识。这大大降低了编译器的开发难度,使更多的开发人员可以参与进来。虽然这种三段式的编译器有很多优点,并且被写到了教科书上,但是在实际中这一结构却从来没有被完美实现过。做的比较好的应该属 Java 和 .NET 虚拟机。虚拟机可以将目标语言翻译为 bytecode,所以理论上讲我们可以将任何语言翻译为 bytecode,然后输入虚拟机中运行。但是这一动态语言的模型并不太适合 C 语言,所以硬将 C 语言翻译为 bytecode 并实现垃圾回收机制的效率是非常低的。GCC 也将三段式做的比较好,并且实现了很多前端,支持了很多语言。但是上述这些编译器的致命缺陷是,他们是一个完整的可执行文件,没有给其它语言的开发者提供代码重用的接口。即使 GCC 是开源的,但是源代码重用的难度也比较大。

LLVM 最初的定位是比较底层的虚拟机。它的出现正是为了解决编译器代码重用的问题,LLVM 一上来就站在比较高的角度,制定了 LLVM IR 这一中间代码表示语言。LLVM IR 充分考虑了各种应用场景,例如在 IDE 中调用 LLVM 进行实时的代码语法检查,对静态语言、动态语言的编译、优化等。从上面这个图中我们发现 LLVM 与 GCC 在三段式架构上并没有本质区别。LLVM 与其它编译器最大的差别是,它不仅仅是 Compiler Collection,也是Libraries Collection。举个例子,假如说我要写一个 X 语言的优化器,我自己实现了 PassX算法,用以处理 X 语言与其它语言差别最大的地方。而 LLVM 优化器提供的 PassA 和 PassB算法则提供了 X 语言与其它语言共性的优化算法。那么我可以选择 X 优化器在链接的时候把LLVM 提供的算法链接进来。LLVM 不仅仅是编译器,也是一个 SDK。Apple LLVM compiler 4.2 是一个真正的 LLVM 编译器,前端使用的是 Clang,基于最新的 LLVM 3.2 编译的。LLVM GCC 4.2 编译器的核心仍然是 LLVM,但是前端使用的是 GCC 4.2 编译器。从 LLVM的下载页面可以看出,LLVM 从 1.0 到 2.5 使用的都是 GCC 作为前端,直到 2.6 开始才提供了 Clang 前端。

如果你下载 LLVM 的代码,那么它就是一个 IR 到 ARM/机器码的编译器。比如 bin/opt 就是对 IR 的优化器,bin/llc 就是 IR->ASM 的翻译,bin/llvm-mc 就是汇编器。如果你再从http://llvm.org 下载 Clang,那么就有了 C->IR 的翻译以及完整的编译器 Driver。GDB 是 GNU 的调试器。只要编译器支持 DWARF 格式,就可以用 GDB 调试。

原文链接:https://zhuanlan.zhihu.com/p/357803433

ansi/iso c我们常常称为的标准C(standard c )随着C的发展,C89,C99,C11标准在不断的推出,他们都是标准C,C的特性也在丰富,这也侧面反映出一个语言的生命力这种标准是一个规范,不同平台的编译器都会去支持(vc ,gcc…)新的标准发布后,编译器一般是立即可以支持(制定这些标准的组织和编译器公司在发布前,就已经沟通好)推荐gcc @_@,特性支持快也就是因为各个平台都支持标准C,所以如果你的程序都是只用标准C的特性,包括头文件,那么你的程序在不同的平台下重新编译就能运行POSIX首先,这也是个标准,运用于Linux,Unix,OS X,FreeBSD…posix 包括了ansi标准,是后者的父集,除了ansi 定义的函数外,posix还定义了:

Network socketsCreating new processesMultithreaded programmingMemory-mapped IO,IPC

随着 ansi 标准的变化,posix 也在变化

官网,查看具体的特性Glibc —GUN C library基于标准C和posix拓展,在linux下,对函数和接口的实现,包口头文件和函数的实现代码linux平台下的编译C代码,链接的库,都是用glibc编译后生成的库glibc 本身还有自己的特性,不仅仅是ansi和posix定义的函数官网,下载代码,查看文档,win32是win下,给C程序的api,包括例如图形接口api,还有

Network socketsCreating new processesMultithreaded programmingMemory-mapped IO等等接口 api没有函数实现的源代码,win不开源api的名称与posix定义的api的名称不一样,win按自己的标准————————————————版权声明:本文为CSDN博主「独自等待2016」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/luoyuping2016/article/details/45588329

 

编译器与Clang编译过程

原文:https://www.jianshu.com/p/2da08634b53a

前言

编译的主要任务是将源代码文件作为输入,最终输出目标文件,这期间发生了什么?便是我们本篇文章要介绍的。在开始之前我们先了解一下编译器。

编译器

编译器(compiler)是一种计算机程序,它会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言(目标语言)。引自维基百科

传统编译器的架构,一般分三部分:

前端(Frontend):解析源代码,检查源代码是否有错误,并构建特定语言的抽象语法树(Abstract Syntax Tree缩写:AST)来表示输入的代码。也负责选择性的地将AST转换为新的表示形式以进行优化。 优化器(Optimizer):负责进行各种转换,以尝试改善代码的运行时间,例如消除冗余计算,并且通常或多或少地独立于编程语言和目标代码。 后端(Backend):也称代码生成器,将代码映射到目标架构的指令集上;其常见部分有:指令选择,寄存器分配,指定调度。   传统编译器的架构

这种架构的优势在于解耦合,实现一种编程语言,只需要实现它的前端,对于优化器与后端部分是可以复用的;支持新的目标架构,只需要实现它的后端即可;如果编译器不是这种架构,三部分未分开,那么实现N个编程语言,去支持M个目标架构,就需要实现N*M个编译器。

  编译器架构分析

这种传统编译器的架构有三个成功的案例:

Java和.Net虚拟机;它们都提供了对JIT编译器和运行时的支持,并且还定义了字节码的格式(bytecode),这意味着任何可以编译为字节码的语言,都可以复用优化器和JIT(动态编译)和运行时能力。 将输入源转换为C代码(或其他某种语言)并通过现有的C编译器编译 这种模式的最终成功实施是GCC,GCC支持许多前端和后端,并拥有活跃而广泛的贡献者社区。 GCC GCC的概述

Xcode5之前的版本中使用的是GCC编译器,由于GCC,历史悠久,体系结构相对复杂,功能模块化复用难度大且不受苹果公司的约束,很难满足苹果系统的发展需求。因此在Xcode5中抛弃了GCC,采用Clang/LLVM进行编译。

GCC:是GNU Compiler Collection的缩写,指GNU编译器套装。Linux系统的核心组成部分就有GNU工具链,GCC也是GNU工具链的重要组成部分,因此GCC也是作为Linux系统的标准编译器。GCC可处理的语言有C、C++、Objective-C、Java、Go等。

  GCC编译流程

使用GCC命令gcc -ccc-print-phases main.m查看编译OC的步骤:

*deMacBook-Pro:Mach-O *$ gcc -ccc-print-phases main.m +- 0: input, "main.m", objective-c +- 1: preprocessor, {0}, objective-c-cpp-output +- 2: compiler, {1}, ir +- 3: backend, {2}, assembler +- 4: assembler, {3}, object +- 5: linker, {4}, image 6: bind-arch, "x86_64", {5}, image GCC的架构   GCC架构

前端读取源文件将其转化为AST,由于每种语言生成的AST是有差异的,所以需要需要转换为通用的与语言无关的统一形式GENERIC。

中端将GENERIC,利用gimplifier技术,简化GENERIC的复杂结构,将其转换为一种中间表示形式称为:GIMPLE,再转换为另一种SSA(static single assignment)的表示形式也是用于优化的,GCC对SSA树执行20多种不同的优化。经过SSA优化后,该树将转换回GIMPLE形式,用来生成一个RTL树,RTL寄存器转换语言,全称(register-transfer language);RTL是基于硬件的表示形式,与抽象的目标架构相对应,处理寄存器分配、指令调度等。RTL优化过程以RTL形式对树进行优化。

后端使用RTL表示形式生成目标架构的汇编代码。如:x86后端。

LLVM LLVM的概述

LLVM项目是模块化和可重用的编译器及工具链技术的集合。名称LLVM是Low Level Virtual Machine的缩写,尽管名称如此,但是LLVM与传统虚拟机关系不大,它是LLVM项目的全名。

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name "LLVM" itself is not an acronym; it is the full name of the project. 引自LLVM官网

LLVM有许多的子项目,比如Clang,LLDB,MLIR等。

LLVM的历史 LLVM起源于2000年Vikram Adve与Chris Lattner的研究,目的:为所有的静态语言(C/C++)和动态语言(运行时改变其结构的语言,如:OC/JavaScript )创造出动态编译技术,最初的目的只是对虚拟语法书的优化。 苹果公司2005雇佣Chris Lattner与他的团队为苹果电脑开发应用程序系统,LLVM为现今macOS与iOS开发工具的一部分。 因LLVM对产业的贡献,2012年获得了ACM软件系统奖。获得该奖项的有Unix、Java、TCP/IP、DNS、Mach 2019年10月开始,LLVM项目的代码托管正式迁移到了GitHub。 LLVM的架构

LLVM最重要的设计是中间表示Intermediate Representation(IR),它是在编译器中表示代码的一种形式。优化器使用LLVM IR作中间的转换与分析处理。LLVM IR本身就是具有良好语义定义的一流语言。

在基于LLVM的编译器中,Frontend负责对输入的代码进行解析,校验和分析错误,然后将解析后的代码转换为LLVM IR(通常情况,是将构建的抽象语法树AST转换为LLVM IR,但不总是这样的)。可以选择通过一系列分析和优化过程来传递LLVM IR,以改进代码,然后将其发送到代码生成器(Backend)中,生成原始的机器码。

  LLVM流程

LLVM IR不仅是完整的代码表示,而且也是优化器optimizer的唯一接口。这意味着写一个LLVM的前端只需要知道LLVM IR即可,这是LLVM的一个新颖的特性,也是LLVM成功地被广泛应用的一个主要原因。反观GCC编译器,写一个前端需要知道生成的GCC树的数据结构以及使用GIMPLE去写GCC的前端,GCC后端需要知道RTL是如何工作的。

LLVM IR是前端输出,后端的输入:

  LLVM架构

LLVM广义是指LLVM整个架构,狭义指整个编译器的终端或者说是优化器,但是当前的llvm已经实现了调用相应平台的汇编器能力。

Clang

Clang是C、C++、Objective C语言的编译器的前端。Clang编译Objective-C代码时速度为GCC的3倍。详见维基百科。

Clang编译过程

下面是一个基于简单的OC工程,不依赖Xcode,而是使用终端编译的例子。

编译前工程源代码主要分为main.m和Person.m类,代码如下:

///main.m #import #import "Person.h" #define SomeDefine @"你好,世界" int main(int argc, const char * argv[]) { @autoreleasepool { // 注释 NSLog(@"Hello, World!"); #pragma mark 我是注释 NSLog(@"%@",SomeDefine); /// MARK: 我也是注释 Person *instance = [[Person alloc]init]; [instance share]; } return 0; } ///Person.m #import "Person.h" @implementation Person - (void)share { NSLog(@"持之以恒"); } @end

首先我们运行clang -ccc-print-phases main.m查看整体的编译过程:

*deMacBook-Pro:Mach-O *$ clang -ccc-print-phases main.m +- 0: input, "main.m", objective-c +- 1: preprocessor, {0}, objective-c-cpp-output +- 2: compiler, {1}, ir +- 3: backend, {2}, assembler +- 4: assembler, {3}, object +- 5: linker, {4}, image 6: bind-arch, "x86_64", {5}, image

接下来,基于这个例子,我们使用终端逐步编译,生成我们的可执行文件,并最终控制台打印我们的信息。

预处理

基于输入,通过预处理器执行一系列的文本转换与文本处理。预处理器是在真正的编译开始之前由编译器调用的独立程序。

终端命令:

# 编译阶段选择参数: -E 运行预处理这一步 clang -E main.m # 预处理结果输出到main.mi文件中 clang -E main.m -o main.mi

输出结果:

# 193 "/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3 # 9 "main.m" 2 # 1 "./Person.h" 1 # 10 "./Person.h" #pragma clang assume_nonnull begin @interface Person : NSObject - (void)share; @end #pragma clang assume_nonnull end # 10 "main.m" 2 int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); NSLog(@"%@",@"你好,世界"); Person *instance = [[Person alloc]init]; [instance share]; } return 0; }

最终C输出.i文件,C++输出.ii文件,Objective-C输出.mi文件,Objective-C ++输出.mii文件。

预处理的任务:

将输入文件读到内存,并断行;

替换注释为单个空格;

Tokenization将输入转换为一系列预处理Tokens;

处理#import、#include将所引的库,以递归的方式,插入到#import或#include所在的位置;

替换宏定义;

条件编译,根据条件包括或排除程序代码的某些部分;

插入行标记;

在预处理的输出中,源文件名和行号信息会以# linenum filename flags形式传递,这被称为行标记,代表着接下来的内容开始于源文件filename的第linenum行,而flags则会有0或者多个,有1、2、3、4;如果有多个flags时,彼此使用分号隔开。详见此处。

每个标识的表示内容如下:

1表示一个新文件的开始 2表示返回文件(包含另一个文件后) 3表示以下文本来自系统头文件,因此应禁止某些警告 4表示应将以下文本视为包装在隐式extern "C" 块中。

比如# 10 "main.m" 2,表示导入Person.h文件后回到main.m文件的第10行。

词法分析

词法分析属于预处理部分,词法分析的整个过程,主要是按照:标识符、 数字、字符串文字、 标点符号,将我们的代码分割成许多字符串序列,其中每个元素我们称之为Token,整个过程称为Tokenization。

终端输入:

# -fmodules: Enable the 'modules' language feature # -fsyntax-only, Run the preprocessor, parser and type checking stages #-Xclang : Pass to the clang compiler # -dump-tokens: Run preprocessor, dump internal rep of tokens clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

-fmodules:启用“模块”语言功能。关于Modules特性,详见此处,大意为使用import代替include,编译速度快。

-fsyntax-only:运行预处理器,解析器和类型检查阶段。

-Xclang :传递参数到clang的编译器。

dump-tokens:运行预处理器,转储Token的内部表示。

更多关于Clang参数的描述,请前往此处。

输出结果:

.... int 'int' [StartOfLine] Loc= identifier 'main' [LeadingSpace] Loc= l_paren '(' Loc= int 'int' Loc= identifier 'argc' [LeadingSpace] Loc= comma ',' Loc= const 'const' [LeadingSpace] Loc= char 'char' [LeadingSpace] Loc= star '*' [LeadingSpace] Loc= identifier 'argv' [LeadingSpace] Loc= l_square '[' Loc= r_square ']' Loc= r_paren ')' Loc= l_brace '{' [LeadingSpace] Loc= at '@' [StartOfLine] [LeadingSpace] Loc= identifier 'autoreleasepool' Loc= l_brace '{' [LeadingSpace] Loc= identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc= l_paren '(' Loc= at '@' Loc= string_literal '"Hello, World!"' Loc= r_paren ')' Loc= semi ';' Loc= identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc= l_paren '(' Loc= at '@' Loc= string_literal '"%@"' Loc= comma ',' Loc= at '@' Loc= string_literal '"你好,世界"' Loc= r_paren ')' Loc= semi ';' Loc= identifier 'Person' [StartOfLine] [LeadingSpace] Loc= star '*' [LeadingSpace] Loc= identifier 'instance' Loc= equal '=' [LeadingSpace] Loc= l_square '[' [LeadingSpace] Loc= l_square '[' Loc= identifier 'Person' Loc= identifier 'alloc' [LeadingSpace] Loc= r_square ']' Loc= identifier 'init' Loc= r_square ']' Loc= semi ';' Loc= l_square '[' [StartOfLine] [LeadingSpace] Loc= identifier 'instance' Loc= identifier 'share' [LeadingSpace] Loc= r_square ']' Loc= semi ';' Loc= r_brace '}' [StartOfLine] [LeadingSpace] Loc= return 'return' [StartOfLine] [LeadingSpace] Loc= numeric_constant '0' [LeadingSpace] Loc= semi ';' Loc= r_brace '}' [StartOfLine] Loc= eof '' Loc=

词法分析中Token包含信息(详请见此处):

Sourece Location:表示Token开始的位置,比如:Loc=;

Token Kind:表示Token的类型,比如:identifier、numeric_constant、string_literal;

Flags:词法分析器和处理器跟踪每个Token的基础,目前有四个Flag分别是:

StartOfLine:表示这是每行开始的第一个Token;

LeadingSpace:当通过宏扩展Token时,在Token之前有一个空格字符。该标志的定义是依据预处理器的字符串化要求而进行的非常严格地定义。

DisableExpand:该标志在预处理器内部使用,用来表示identifier令牌禁用宏扩展。

NeedsCleaning:如果令牌的原始拼写包含三字符组或转义的换行符,则设置此标志。

语法分析(Parsing)与语义分析

此阶段对输入文件进行语法分析,将预处理器生成的Tokens转换为语法分析树;一旦生成语法分析树后,将会进行语义分析,执行类型检查和代码格式检查。这个阶段负责生成大多数编译器警告以及语法分析过程的错误。最终输出AST(抽象语法树)。

Parser的意义与作用

所谓 parser,一般是指把某种格式的文本(字符串)转换成某种数据结构的过程。最常见的 parser,是把程序文本转换成编译器内部的一种叫做“抽象语法树”(AST)的数据结构。摘自对 Parser 的误解-王垠

AST的示意图(来源):

  AST示意图

终端输入:

# -fmodules: Enable the 'modules' language feature # -fsyntax-only, Run the preprocessor, parser and type checking stages #-Xclang : Pass to the clang compiler # -ast-dump: Build ASTs and then debug dump them clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

输出结果:

TranslationUnitDecl 0x7f80ea01c408 |-TypedefDecl 0x7f80ea01cca0 implicit __int128_t '__int128' | `-BuiltinType 0x7f80ea01c9a0 '__int128' #... # cutting out internal declarations of clang #... |-ImportDecl 0x7f80ea27d9d8 col:1 implicit Foundation |-ImportDecl 0x7f80ea27da18 col:1 implicit Foundation |-ObjCInterfaceDecl 0x7f80ea294ff8 line:12:12 Person | |-super ObjCInterface 0x7f80ea27db18 'NSObject' | `-ObjCMethodDecl 0x7f80ea2951f0 col:1 - share 'void' `-FunctionDecl 0x7f80ea295620 line:11:5 main 'int (int, const char **)' |-ParmVarDecl 0x7f80ea2953b0 col:14 argc 'int' |-ParmVarDecl 0x7f80ea2954d0 col:33 argv 'const char **':'const char **' `-CompoundStmt 0x7f80ea29e5b8 |-ObjCAutoreleasePoolStmt 0x7f80ea29e570 | `-CompoundStmt 0x7f80ea29e540 | |-CallExpr 0x7f80ea2a26f0 'void' | | |-ImplicitCastExpr 0x7f80ea2a26d8 'void (*)(id, ...)' | | | `-DeclRefExpr 0x7f80ea2a25e0 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)' | | `-ImplicitCastExpr 0x7f80ea2a2718 'id':'id' | | `-ObjCStringLiteral 0x7f80ea2a2660 'NSString *' | | `-StringLiteral 0x7f80ea2a2638 'char [14]' lvalue "Hello, World!" | |-CallExpr 0x7f80ea298298 'void' | | |-ImplicitCastExpr 0x7f80ea298280 'void (*)(id, ...)' | | | `-DeclRefExpr 0x7f80ea2a2730 'void (id, ...)' Function 0x7f80ea295760 'NSLog' 'void (id, ...)' | | |-ImplicitCastExpr 0x7f80ea2982c8 'id':'id' | | | `-ObjCStringLiteral 0x7f80ea2a27a8 'NSString *' | | | `-StringLiteral 0x7f80ea2a2788 'char [3]' lvalue "%@" | | `-ObjCStringLiteral 0x7f80ea298260 'NSString *' | | `-StringLiteral 0x7f80ea298238 'char [16]' lvalue "\344\275\240\345\245\275\357\274\214\344\270\226\347\225\214" | |-DeclStmt 0x7f80ea29e4a8 | | `-VarDecl 0x7f80ea298320 col:17 used instance 'Person *' cinit | | |-ObjCMessageExpr 0x7f80ea2988d0 'Person *' selector=init | | | `-ObjCMessageExpr 0x7f80ea298658 'Person *' selector=alloc class='Person' | | `-FullComment 0x7f80ea2a3900 | | `-ParagraphComment 0x7f80ea2a38d0 | | `-TextComment 0x7f80ea2a38a0 Text=" MARK: 我也是注释" | `-ObjCMessageExpr 0x7f80ea29e510 'void' selector=share | `-ImplicitCastExpr 0x7f80ea29e4f8 'Person *' | `-DeclRefExpr 0x7f80ea29e4c0 'Person *' lvalue Var 0x7f80ea298320 'instance' 'Person *' `-ReturnStmt 0x7f80ea29e5a8 `-IntegerLiteral 0x7f80ea29e588 'int' 0

Clang的AST是从TranslationUnitDecl节点开始进行递归遍历的;AST中许多重要的Node,继承自Type、Decl、DeclContext、Stmt。

Type :表示类型,比如BuiltinType Decl :表示一个声明declaration或者一个定义definition,比如:变量,函数,结构体,typedef; DeclContext :用来声明表示上下文的特定decl类型的基类; Stmt :表示一条陈述statement; Expr:在Clang的语法树中也表示一条陈述statements; 代码优化和生成

这个阶段主要任务是将AST转换为底层中间的代码LLVM IR,并且最终生成机器码;期间负责生成目标架构的代码以及优化生成的代码。最终输出.s文件(汇编文件)。

LLVM IR有三种格式:

文本格式:.ll文件 内存中用以优化自身时,执行检查和修改的数据结构(编译过程中载入内存的形式) 磁盘二进制(BitCode)格式:.bc文件

LLVM提供了.ll与.bc相互转换的工具:

llvm-as:可将.ll转为.bc llvm-dis:可将.bc转为.ll

终端输入:

# -S : Run LLVM generation and optimization stages and target-specific code generation,producing an assembly file # -fobjc-arc : Synthesize retain and release calls for Objective-C pointers # -emit-llvm : Use the LLVM representation for assembler and object files # -o : Write output to # 汇编表示成.ll文件 -fobjc-arc 可忽略,不作代码优化 clang -S -fobjc-arc -emit-llvm main.m -o main.ll # 目标文件表示成 .bc 文件 # -c : Only run preprocess, compile, and assemble steps clang -emit-llvm -c main.m -o main.bc #.ll与.bc的相互转换 llvm-as main.ll -o main.bc llvm-dis main.bc -o main.ll

此处使用了参数-emit-llvm,来查看LLVM IR。

输出结果:

# 此处只贴main函数部分 define i32 @main(i32 %0, i8** %1) #1 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i8**, align 8 %6 = alloca %0*, align 8 store i32 0, i32* %3, align 4 store i32 %0, i32* %4, align 4 store i8** %1, i8*** %5, align 8 %7 = call i8* @llvm.objc.autoreleasePoolPush() #2 notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)) notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %1*)) %8 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_", align 8 %9 = bitcast %struct._class_t* %8 to i8* %10 = call i8* @objc_alloc_init(i8* %9) %11 = bitcast i8* %10 to %0* store %0* %11, %0** %6, align 8 %12 = load %0*, %0** %6, align 8 %13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9 %14 = bitcast %0* %12 to i8* call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %14, i8* %13) %15 = bitcast %0** %6 to i8** call void @llvm.objc.storeStrong(i8** %15, i8* null) #2 call void @llvm.objc.autoreleasePoolPop(i8* %7) ret i32 0 } 代码优化

Clang代码优化参数有-O0、 -O1、 -O2、 -O3、 -Ofast、-Os、 -Oz 、-Og、 -O、-O4

-O0:表示没有优化;编译速度最快并生成最可调试的代码 -O1:优化程度介于-O0~-O2之间。 -O2:适度的优化水平,可实现最优化 -O3:与-O2相似,不同之处在于它优化的时间比较长,可能会生成更大的代码 -O4:当前等效于-O3 -Ofast:启用-O3中的所有优化并且可能启用一些激进优化 -Os:与-O2一样,具有额外的优化功能以减少代码大小 -Oz:类似于-Os,进一步减小了代码大小 -Og:类似-O1 -O:相当于-O2

终端输入:

clang -S -O2 -fobjc-arc -emit-llvm main.m -o main.ll

输出结果:

#LLVM IR文件头信息 ; ModuleID = 'main.m' source_filename = "main.m" target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.15.0" #结构体的定义 %0 = type opaque %struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 } %struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* } %struct._objc_cache = type opaque %struct._class_ro_t = type { i32, i32, i32, i8*, i8*, %struct.__method_list_t*, %struct._objc_protocol_list*, %struct._ivar_list_t*, i8*, %struct._prop_list_t* } %struct.__method_list_t = type { i32, i32, [0 x %struct._objc_method] } %struct._objc_method = type { i8*, i8*, i8* } %struct._objc_protocol_list = type { i64, [0 x %struct._protocol_t*] } %struct._protocol_t = type { i8*, i8*, %struct._objc_protocol_list*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct.__method_list_t*, %struct._prop_list_t*, i32, i32, i8**, i8*, %struct._prop_list_t* } %struct._ivar_list_t = type { i32, i32, [0 x %struct._ivar_t] } %struct._ivar_t = type { i64*, i8*, i8*, i32, i32 } %struct._prop_list_t = type { i32, i32, [0 x %struct._prop_t] } %struct._prop_t = type { i8*, i8* } # 全局变量、私有/外部/内部常量的定义或声明 @__CFConstantStringClassReference = external global [0 x i32] @.str = private unnamed_addr constant [14 x i8] c"Hello, World!\00", section "__TEXT,__cstring,cstring_literals", align 1 # --全局结构体定义与初始化 @_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i64 13 }, section "__DATA,__cfstring", align 8 #0 @.str.1 = private unnamed_addr constant [3 x i8] c"%@\00", section "__TEXT,__cstring,cstring_literals", align 1 @_unnamed_cfstring_.2 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8 #0 @.str.3 = private unnamed_addr constant [6 x i16] [i16 20320, i16 22909, i16 -244, i16 19990, i16 30028, i16 0], section "__TEXT,__ustring", align 2 @_unnamed_cfstring_.4 = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([6 x i16]* @.str.3 to i8*), i64 5 }, section "__DATA,__cfstring", align 8 #0 @"OBJC_CLASS_$_Person" = external global %struct._class_t @"OBJC_CLASSLIST_REFERENCES_$_" = internal global %struct._class_t* @"OBJC_CLASS_$_Person", section "__DATA,__objc_classrefs,regular,no_dead_strip", align 8 @OBJC_METH_VAR_NAME_ = private unnamed_addr constant [6 x i8] c"share\00", section "__TEXT,__objc_methname,cstring_literals", align 1 @OBJC_SELECTOR_REFERENCES_ = internal externally_initialized global i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i64 0, i64 0), section "__DATA,__objc_selrefs,literal_pointers,no_dead_strip", align 8 @llvm.compiler.used = appending global [3 x i8*] [i8* bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8*), i8* getelementptr inbounds ([6 x i8], [6 x i8]* @OBJC_METH_VAR_NAME_, i32 0, i32 0), i8* bitcast (i8** @OBJC_SELECTOR_REFERENCES_ to i8*)], section "llvm.metadata" # main函数的入口:`dso_local`:main函数解析为统一链接单元的符号,而非外部替换的符号 ; Function Attrs: ssp uwtable define dso_local i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 { %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #2 notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*)), !clang.arc.no_objc_arc_exceptions !8 notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.2 to i8*), %0* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.4 to %0*)), !clang.arc.no_objc_arc_exceptions !8 %4 = load i8*, i8** bitcast (%struct._class_t** @"OBJC_CLASSLIST_REFERENCES_$_" to i8**), align 8 %5 = tail call i8* @objc_alloc_init(i8* %4), !clang.arc.no_objc_arc_exceptions !8 %6 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8 tail call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %5, i8* %6), !clang.arc.no_objc_arc_exceptions !8 tail call void @llvm.objc.release(i8* %5) #2, !clang.imprecise_release !8 tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #2 ret i32 0 } #函数声明 ; Function Attrs: nounwind declare i8* @llvm.objc.autoreleasePoolPush() #2 declare void @NSLog(i8*, ...) local_unnamed_addr #3 declare i8* @objc_alloc_init(i8*) local_unnamed_addr ; Function Attrs: nonlazybind declare i8* @objc_msgSend(i8*, i8*, ...) local_unnamed_addr #4 ; Function Attrs: nounwind declare void @llvm.objc.release(i8*) #2 ; Function Attrs: nounwind declare void @llvm.objc.autoreleasePoolPop(i8*) #2 #属性组 attributes #0 = { "objc_arc_inert" } attributes #1 = { ssp uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #2 = { nounwind } attributes #3 = { "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" } attributes #4 = { nonlazybind } #该`module`的元数据 ##命名元数据 !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 1, !"Objective-C Garbage Collection", i8 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 = !{!"clang version 12.0.0"} !8 = !{} 浅析 LLVM IR

Module:LLVM程序是由Module组成的,每个Module是输入程序的翻译单元。每个Module都是由functions、global variables和symbol table entries组成。Module会通过LLVM链接器组合到一起,链接器会合并函数以及全局变量的定义,解决前置声明以及合并符号表。

Target Datalayout:Module需要以字符串的形式指定特定于目标架构的数据布局方式,该字符串指定如何在内存中布局数据。如:target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"。

元数据:LLVM IR允许元数据被附加到能够传递代码额外信息给优化器和代码生成器的程序指令上。所有元数据在语法上均由!标识。元数据的两个原语:元数据字符串和元数据节点。

元数据字符串:用""引起来的字符串,以!作为前缀。如:!"clang version 12.0.0" 元数据节点:用{}括起来,使用,隔开多个元素,以!作为前缀。如:!{i32 7, !"PIC Level", i32 2}

命名元数据:是元数据节点的集合

; Some unnamed metadata nodes, which are referenced by the named metadata. !0 = !{!"zero"} !1 = !{!"one"} !2 = !{!"two"} ; A named metadata. !name = !{!0, !1, !2}

Linkage Types:所有全局变量和函数都具有链接类型,如上述IR中的:

external:module外部可用 private:module内部可用 appending:仅应用于数组类型的全局变量的指针。当两个使用了appending的全局变量链接到一起的时候,这两个全局的数组会被拼接到一起。 internal:与private相似,但该值在目标文件中显示为本地符号。与C语言中static关键字的概念相对应。

属性组:属性组是IR中的对象(函数、全局变量)引用的属性组合。它们对于保持.ll文件的可读性很重要,因为许多函数将使用相同的属性集。如上述IR中的#0~#4。

函数属性:被用来传递一个函数附加信息。函数属性被认为是函数的一部分,而不是函数类型,所以不同的函数属性可以有相同的函数类型。上述IR中用到最多的函数属性:

nounwind:表示函数不会抛出异常 nonlazybind:阻止函数中某些符号的延迟绑定。

参数属性:函数的返回类型以及每个参数都有与之关联的参数属性集合。被用来传递一个函数的返回值与参数的附加信息。参数属性是函数的一部分,而不是函数类型,所以有不同参数属性的函数可以有相同的参数类型。上述IR中用到最多的参数属性:

nocapture:表示函数调用不会捕获参数的指针,这个属性对于返回值是无效的,仅适用于参数。 readnone:应用于参数表示函数不会取消对此参数指针的引用。

标识符:

@为全局标识符。以其开头标识函数,全局变量; %为本地标识符。以其开头标识寄存器名称,类型; 标识符的不同的格式: 命名值,表示为以上述标识符为前缀的字符串,如:%struct._ivar_t、@.str 未命名值,表示为以上述标识为前缀的无符号的数值,如:%0、%1

结构体的定义:

#语法 %T1 = type { } ; Identified normal struct type %T2 = type ; Identified packed struct type #表示结构体的对齐方式为1字节 #示例 {i32, i32} %mytype = type { %mytype*, i32 }

数组的定义:

#语法 [ x ] #语义 `elements`是个`integer`的值;`elementtype`是任意有大小的类型 #示例 [40 x i32] Array of 40 32-bit integer values

全局变量的定义:

#语法 @ = [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [ExternallyInitialized] [] [, section "name"] [, comdat [($name)]] [, align ] (, !name !N)* #示例 @G = external global i32 #just declare @G = external global i32 8 #InitializerConstant global constant:表示该变量的内容将永远不会被修改。 unnamed_addr:表示该变量的地址并不重要,仅指示内容。 local_unnamed_addr:表示变量的地址在module内并不重要。

Runtime Preemption Specifiers:运行时抢占说明符。全局变量,函数和别名可以具有一个可选的运行时抢占说明符。如果未明确指定抢占说明符,则假定该符号为dso_preemptable。

dso_preemptable:表示函数或者变量在运行时会被外部的链接单元替换 dso_local:表示函数或变量将解析为同一链接单元中的符号。即使定义不在此编译单元内,也将生成直接访问

call:代表一个简单的函数调用;

#语法 = [tail | musttail | notail ] call [fast-math flags] [cconv] [ret attrs] [addrspace()] | () [fn attrs] [ operand bundles ]

可选的tail和musttail标记优化器应执行尾部调用优化

notail标记用于防止执行尾部调用优化

ret:该指令表示函数返回

#语法 ret ; Return a value from a non-void function ret void ; Return from void function #示例 ret i32 5 ; Return an integer value of 5 ret void ; Return from a void function ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2

bitcast...to:bitcast将 value 的类型转换为类型 ty2 而不改变它的任何位bits

#语法 = bitcast to ; yields ty2

其他:i32:代表32-bit的整数,i8:代表8-bit的整数;

代码生成

生成目标架构的汇编代码。

终端输入:

#生成目标架构的汇编代码 clang -S -fobjc-arc main.m -o main.s

输出结果:

.section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 15 sdk_version 10, 15, 6 .globl _main ## -- Begin function main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## %bb.0: pushq %rbp #将%rbp的内容压栈,保存栈帧到%rsp中 .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp # 将栈指针传送至%rbp中,设置当前栈帧 .cfi_def_cfa_register %rbp subq $32, %rsp # 栈指针 - 32 (申请32个字节的空间) movl $0, -4(%rbp)# 将 0 传送至存储器中,存储器位置为: M[-4 + %rbp] movl %edi, -8(%rbp) # 将%edi的内容 传送至存储器中,存储器位置为: M[-8 + %rbp] movq %rsi, -16(%rbp)# 将%rsi的内容 传送至存储器中,存储器位置为: M[-16 + %rbp] callq _objc_autoreleasePoolPush #调用_objc_autoreleasePoolPush leaq L__unnamed_cfstring_(%rip), %rcx #将`L__unnamed_cfstring_(%rip)`的有效地址写入`%rcx`中 movq %rcx, %rdi # 将%rcx的内容 传送至寄存器%rdi movq %rax, -32(%rbp) ## 8-byte Spill # 将%rax的内容 传送至存储器中,存储器位置为: M[-32 + %rbp] movb $0, %al # 将立即数0 传送至寄存器的低八位的单字节寄存器`%al`中 callq _NSLog #调用 _NSLog leaq L__unnamed_cfstring_.2(%rip), %rcx leaq L__unnamed_cfstring_.4(%rip), %rdx movq %rcx, %rdi # 将%rcx的内容 传送至寄存器%rdi movq %rdx, %rsi #将%rdx的内容 传送至寄存器%rsi movb $0, %al # 将立即数0 传送至寄存器的低八位的单字节寄存器`%al`中 callq _NSLog #调用 _NSLog movq _OBJC_CLASSLIST_REFERENCES_$_(%rip), %rcx movq %rcx, %rdi callq _objc_alloc_init movq %rax, -24(%rbp) movq -24(%rbp), %rax movq _OBJC_SELECTOR_REFERENCES_(%rip), %rsi movq %rax, %rdi callq *_objc_msgSend@GOTPCREL(%rip) xorl %r8d, %r8d # 使用异或对寄存器`%r8d`清0 movl %r8d, %esi leaq -24(%rbp), %rax movq %rax, %rdi callq _objc_storeStrong movq -32(%rbp), %rdi ## 8-byte Reload callq _objc_autoreleasePoolPop xorl %eax, %eax # 使用异或对寄存器`%eax`清0 addq $32, %rsp popq %rbp #将%rbp的内容弹出栈 retq .cfi_endproc ## -- End function .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "Hello, World!" .section __DATA,__cfstring .p2align 3 ## @_unnamed_cfstring_ L__unnamed_cfstring_: .quad ___CFConstantStringClassReference .long 1992 ## 0x7c8 .space 4 .quad L_.str .quad 13 ## 0xd .section __TEXT,__cstring,cstring_literals L_.str.1: ## @.str.1 .asciz "%@" .section __DATA,__cfstring .p2align 3 ## @_unnamed_cfstring_.2 L__unnamed_cfstring_.2: .quad ___CFConstantStringClassReference .long 1992 ## 0x7c8 .space 4 .quad L_.str.1 .quad 2 ## 0x2 .section __TEXT,__ustring .p2align 1 ## @.str.3 l_.str.3: .short 20320 ## 0x4f60 .short 22909 ## 0x597d .short 65292 ## 0xff0c .short 19990 ## 0x4e16 .short 30028 ## 0x754c .short 0 ## 0x0 .section __DATA,__cfstring .p2align 3 ## @_unnamed_cfstring_.4 L__unnamed_cfstring_.4: .quad ___CFConstantStringClassReference .long 2000 ## 0x7d0 .space 4 .quad l_.str.3 .quad 5 ## 0x5 .section __DATA,__objc_classrefs,regular,no_dead_strip .p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_" _OBJC_CLASSLIST_REFERENCES_$_: .quad _OBJC_CLASS_$_Person .section __TEXT,__objc_methname,cstring_literals L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_ .asciz "share" .section __DATA,__objc_selrefs,literal_pointers,no_dead_strip .p2align 3 ## @OBJC_SELECTOR_REFERENCES_ _OBJC_SELECTOR_REFERENCES_: .quad L_OBJC_METH_VAR_NAME_ .section __DATA,__objc_imageinfo,regular,no_dead_strip L_OBJC_IMAGE_INFO: .long 0 .long 64 .subsections_via_symbols 汇编指令

所有以.开头的行,都是指导编译器与链接器的命令。

.section指定汇编器将生成的汇编代码,写入对应的区section。

语法:

.section segname , sectname [[[ , type ] , attribute ] , sizeof_stub ]

示例:

#`regular`类型 表示该区存放程序指令或初始化数据 #`pure_instructions`属性 表示此区仅包含机器指令 .section __TEXT,__text,regular,pure_instructions #`cstring_literals`类型 表示该区存放以null结尾的c字符串 .section __TEXT,__cstring,cstring_literals

.global symbol_name标记符号为外部符号;

.align对齐指令,指定汇编代码的对齐方式

语法:

.align align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]] .p2align align_expression [ , 1byte_fill_expression [,max_bytes_to_fill]] .p2alignw align_expression [ , 2byte_fill_expression [,max_bytes_to_fill]] .p2alignl align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]] .align32 align_expression [ , 4byte_fill_expression [,max_bytes_to_fill]]

示例:

# 以16(2^4)字节的方式对齐,不足的使用0x90补齐 .p2align 4, 0x90

CFA

在栈上分配的内存区域,称为“调用帧”。调用帧由栈上的地址标识。我们将此地址称为CFA(Canonical Frame Address)。通常,将CFA定义为前一帧调用者上的栈指针的值(可能与当前帧的值不同)。

An area of memory that is allocated on a stack called a “call frame.” The call frame is identified by an address on the stack. We refer to this address as the Canonical Frame Address or CFA. Typically, the CFA is defined to be the value of the stack pointer at the call site in the previous frame (which may be different from its value on entry to the current frame).引自DWARF规范-6.4

.cfi_def_cfa_offset OFFSET:cfi_def_cfa_offset指令用来修改计算CFA的规则。注意:OFFSET是绝对偏移量,它会被加到帧指针寄存%ebp或者%rbp上,重新计算CFA的地址。

.cfi_def_cfa REGISTER, OFFSET:cfi_def_cfa这个指令从寄存器中获取地址并且加上这个OFFSET 。

.cfi_def_cfa_register REGISTER:cfi_def_cfa_register这个指令让%ebp或%rbp被设置为新值且偏移量保持不变。

上述设置只是为了用来辅助调试的,比如打断点,获取调用堆栈信息。

CFI

调用帧信息,英文全称:Call Frame Information。

cfi_startproc,表示函数或过程开始。 .cfi_endproc,表示函数或过程结束。

更多细节请查看苹果官网

汇编器

这个阶段主要任务是运行目标架构的汇编程序(汇编器),将编译器的输出转换为目标架构的目标(object)文件,即:.o文件。

终端输入:

# -c : Run all of the above, plus the assembler, generating a target ".o" object file. # -o : write to file clang -c main.m -o main.o clang -c Person.m -o person.o

输出结果:

#使用命令查看生成文件 #file main.o person.o #输出 main.o: Mach-O 64-bit object x86_64 person.o: Mach-O 64-bit object x86_64

通过汇编器将可读的汇编代码,转换为目标架构的目标文件,最终输出.o文件,也称机器码。

链接器

这个阶段会运行目标架构的链接器,将多个object文件合并成一个可执行文件或动态库。最终的输出a.out、.dylib或.so。

在上述OC代码示例中,Main函数中引用了Person类,因此若要生成可执行的文件,需要将main.o与person.o进行链接

终端输入:

# no stage selection option # If no stage selection option is specified, all stages above are run, and the # linker is run to combine the results into an executable or shared library. clang main.o person.o -o main

输出结果:

"_NSLog", referenced from: _main in main.o -[Person share] in person.o "_OBJC_CLASS_$_NSObject", referenced from: _OBJC_CLASS_$_Person in person.o "_OBJC_METACLASS_$_NSObject", referenced from: _OBJC_METACLASS_$_Person in person.o "___CFConstantStringClassReference", referenced from: CFString in main.o CFString in main.o CFString in main.o CFString in person.o "__objc_empty_cache", referenced from: _OBJC_METACLASS_$_Person in person.o _OBJC_CLASS_$_Person in person.o "_objc_alloc_init", referenced from: _main in main.o "_objc_autoreleasePoolPop", referenced from: _main in main.o "_objc_autoreleasePoolPush", referenced from: _main in main.o "_objc_msgSend", referenced from: _main in main.o ld: symbol(s) not found for architecture x86_64 clang-12: error: linker command failed with exit code 1 (

链接器未找到上述的符号,原因是我们代码引入了Foundation库,在生成可执行文件时,未进行链接。

在解决这个问题之前先介绍一下工具xcrun,使用xcrun可以从命令行定位和调用开发者工具

#--show-sdk-path : show selected SDK install path xcrun --show-sdk-path # 输出`MacOSX.sdk`的路径 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk

基于此路径链接我们的Foundation库:

# -Wl, Pass the comma separated arguments in to the linker #传参给链接器 # `xcrun --show-sdk-path` 等同 $(xcrun --show-sdk-path) 视为命令替换 clang main.o person.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation -o main

最终输出如下图:

  输出的可执行文件

执行这个可执行文件:

#执行 ./main #输出 2021-05-08 17:40:45.134 main[30561:1257231] Hello, World! 2021-05-08 17:40:45.135 main[30561:1257231] 你好,世界 2021-05-08 17:40:45.135 main[30561:1257231] 持之以恒

main文件查看:

file main #输出 main: Mach-O 64-bit executable x86_64 符号对比

符号表查看工具nm,允许我们查看Object文件的符号表内容。

使用nm终端工具,先观察一下mian.o和person.o

#输入 nm -nm main.o person.o #输出 (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_Person (undefined) external ___CFConstantStringClassReference (undefined) external _objc_alloc_init (undefined) external _objc_autoreleasePoolPop (undefined) external _objc_autoreleasePoolPush (undefined) external _objc_msgSend 0000000000000000 (__TEXT,__text) external _main 00000000000000e8 (__TEXT,__ustring) non-external l_.str.3 00000000000000f8 (__DATA,__objc_classrefs) non-external _OBJC_CLASSLIST_REFERENCES_$_ 0000000000000108 (__DATA,__objc_selrefs) non-external _OBJC_SELECTOR_REFERENCES_ (undefined) external _NSLog (undefined) external _OBJC_CLASS_$_NSObject (undefined) external _OBJC_METACLASS_$_NSObject (undefined) external ___CFConstantStringClassReference (undefined) external __objc_empty_cache 0000000000000000 (__TEXT,__text) non-external -[Person share] 0000000000000024 (__TEXT,__ustring) non-external l_.str 0000000000000058 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person 00000000000000a0 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person 00000000000000c0 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person 0000000000000108 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person 0000000000000130 (__DATA,__objc_data) external _OBJC_CLASS_$_Person

external表示该符号针对当前目标文件不是私有的,与non-external相反。undefined表示该符号未找到。

使用nm观察一下可执行文件main的符号表

#输入 nm -nm main #输出 (undefined) external _NSLog (from Foundation) (undefined) external _OBJC_CLASS_$_NSObject (from libobjc) (undefined) external _OBJC_METACLASS_$_NSObject (from libobjc) (undefined) external ___CFConstantStringClassReference (from CoreFoundation) (undefined) external __objc_empty_cache (from libobjc) (undefined) external _objc_alloc_init (from libobjc) (undefined) external _objc_autoreleasePoolPop (from libobjc) (undefined) external _objc_autoreleasePoolPush (from libobjc) (undefined) external _objc_msgSend (from libobjc) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100003e80 (__TEXT,__text) external _main #私有符号 0000000100003f00 (__TEXT,__text) non-external -[Person share] 0000000100008020 (__DATA,__objc_const) non-external __OBJC_METACLASS_RO_$_Person 0000000100008068 (__DATA,__objc_const) non-external __OBJC_$_INSTANCE_METHODS_Person 0000000100008088 (__DATA,__objc_const) non-external __OBJC_CLASS_RO_$_Person #非私有 00000001000080e0 (__DATA,__objc_data) external _OBJC_METACLASS_$_Person 0000000100008108 (__DATA,__objc_data) external _OBJC_CLASS_$_Person #私有符号 0000000100008130 (__DATA,__data) non-external __dyld_private

可以发现在经过链接器处理后,为每个符号增加了来源。当我们运行可执行文件时,会由动态链接器dyld通过这些来源对处于undifined的符号进行解析,比如_NSLog,来自Foundation,在运行时会在Foundation中找到指向它的函数地址,并最终调用执行。

系统符号

目标文件的显示工具otool,可以查看Mach-O文件特定Section和Segment的内容。

可执行文件是知道它需要链接那些库的

# -L :display the names and version numbers of the shared libraries that the object file uses otool -L main # 输出 main: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1677.104.0) /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1) /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1677.104.0) /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)

上述输出我们发现在链接器生成可执行文件时,我们通过-Wl传递给链接器的Foundation的路径与可执行文件最终链接的Foundation路径不一致。参数路径下的文件内容:

  image.png

 

.tbd文件

the .tbd files are new "text-based stub libraries", that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size. 引自stackoverflow

.tbd是个文本文件,提供的是SDK的更简洁版本,明显的降低Xcode的下载大小,具体内容:

  .tbd文件内容

.tbd文件包含了与文件本身相关的元数据,与架构相关的信息,还有Foundation库针对特定架构的symbols,以及该库所依赖的库。并指定了Foundation库的最终安装路径。

  Foundation

查看系统符号

#输入 nm -nm /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep '_NSLog' #输出NSlog的调用地址 000000000004ce6e (__TEXT,__text) external _NSLog 总结

OC代码编译时,首先会经过预处理,接着进行词法分析将文本字符串Token化, 再通过语法与语义分析检查代码的类型与格式,最终生成AST,并在代码优化与生成阶段,将AST转换为底层的中间代码LLVM IR,并最终生成目标架构的汇编代码,交给汇编器进行处理后,将可读的汇编代码转换为目标架构的机器码,即:.O文件,通过链接器,解决.O文件与库的链接问题,最终根据特定的机器架构生成可执行文件。

参考资料

http://www.aosabook.org/en/llvm.html

https://en.m.wikibooks.org/wiki/GNU_C_Compiler_Internals/GNU_C_Compiler_Architecture

http://www.yinwang.org/blog-cn/2015/09/19/parser

https://objccn.io/issue-6-3/

https://llvm.org/docs/LangRef.html

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3