clang到底是什么?gcc和clang到底有什么区别?

news2024/10/7 17:24:25

最近发现自己对 GNU GCC 和 Clang 的区别不太清楚,影响到一些实现和学习,所以趁这两天有空好好研究了一下。

在这个研究过程中,我发现很多问题其实源自于语言(不是指编程语言,而是中文和英文翻译的失真)和概念理解不严谨。

如果你上网去查clang,有些人会告诉你这是一个前端(frontend),然后从书上摘抄一些编译器的介绍,然后列出了一堆表格进行对比,并没有对原理和机制进行详尽的解释和介绍。所以这时候会有更多的问题冒出来:

  • 为什么clang是一个前端?难道它不是完整的编译器吗?如果clang是完整的编译器的话,那么为什么叫前端呢?如果它不是完整的,那么后端是什么呢?
  • 编译器的定义到底是什么?感觉书上编译器的定义和实际的gcc有所不同。

这里说明一下:这里的gcc指的是你在 Ubuntu 等 Linux 发行版里可以直接使用的命令(来自于 GNU 软件组),如果是指项目则会写作“GNU GCC”。如果是指llvm-gcc,则不会简写成gcc

本文将会逐步解答这一系列的问题,在这个过程中,不光会让你搞明白clang到底是什么,也会让你对编译过程、编译器和gcc,以及 LLVM 更加了解。

gcc这种现代叫做“编译器”的程序是一个工具集合

需要先理解一件事,也算是上面绝大部分问题的答案或者误解源头:gcc这种现代“编译器”是一个工具集合,包含了预处理器、编译器,而且会自己调用汇编器、连接器或加载器等多种工具,而不是单单的一个编译器(话说这种术语和名词的冲突也是误导的重要原因之一)。

先说出答案是为了让读者带着答案去看解释,这样能理解的更好。

编译器到底是什么(或者说编译流程是什么)

上文提到,“编译器”是一个时常表达有冲突的术语:在很多言论、博客、教科书和专业书中,编译器被描述成“将源代码转换成可执行程序的程序”(比如gcc这个“编译器”就可以直接将源代码编程可执行程序)。这句话简洁、准确地描述了使用gcc命令会发生什么,但是这并不是编译器的定义。

我们来看看最经典的编译相关的著作《编译原理》(也就是龙书)里对于编译器的介绍,这也算是编译器最经典的含义:

简单来说,编译器就是一个程序能读取某种语言的一个程序(这个语言是语言),然后将其翻译转换成另一种语言的等价程序(这个语言被称为目标语言)。编译器最重要的一个规定就是报告在翻译过程中发现“源”程序的错误。
如果这个目标程序是机器语言,那么这个目标程序就是可执行程序。

那么回到《编译原理》中的定义,其实就是:我们写的源代码经过编译器转换之后得到了另一种语言的代码,而转换之后的代码如果是机器语言,那么这个目标代码就是可执行程序。但是如果多源代码文件或者有外连接的库,那么可能是共享对象(shared object)。

“code”在英语中是指一堆数字、字母、符号,中文翻译成“代码”或“码”
应用程序本质就是一堆机器语言拼成的二进制文件。

也就是说,编译器实际上是将一种语言转换成另一种语言的程序

不过按照近几十年的标准编译流程来说,编译器指的是从将.c等文件转换成.s文件的程序。为了方便解释,除非特地说明,下文中的“编译器”都是按这个定义。

在这个定义下,编译器内部的工作流程大致如下:

C 语言代码
C 前端
优化器
C++ 语言代码
C++ 前端
Objective-C 语言代码
Objective-C 前端
X86后端
X86汇编代码
ARM 后端
ARM 汇编代码

编译器可以生成指定平台的汇编代码。然后再由汇编器来将汇编代码转成机器语言,最后由连接器连接成可执行程序。

此外有几点需要补充一下:

  1. 这里的各种语言代码是经过预处理过的;
  2. 某语言的前端一般指词汇解析器(Lexer)和语法分析程序(Parser),前端会将源代码一步一步(从高级到低级)转换成优化器需要的中间表达(IR),这是多个分析器实现的的。
  3. 一般在优化器前面会单独列出一个“AST(Abstract Syntax Trees)”,这是一个层级很高的中间表达,基本上就是源代码的重组。
  4. 优化器(Optimizer)有时也被称为中端(Middle end),优化器不仅提升性能,而且作为中端可以让前后端分离的更好,增加了交叉编译的可能性。

从源代码到高级中间表达,再从中级到低级的流程大致如下:

从源代码到高级中间表达,再从中级到低级的流程

这里有篇文章做了更详细的介绍:《Intermediate Representation》

从源代码转换成可执行程序的过程

从源代码转换成可执行程序的完整过程,也就是我们平时所说的“编译过程”。在过去的几十年里,这个标准流程大致如下(圆角矩形表示代码,矩形表示各种处理器):

源代码
预处理器
调整之后的代码
编译器
汇编语言代码
汇编器
可调整的机器语言代码
连接器或加载器
可执行程序

可以看到从源代码到可执行程序要经过预处理器(preprocessor)、编译器(compiler)、汇编器(assembler)和连接器(linker)或加载器(loader),而编译器只是负责将源代码转换成对应的汇编代码的功能。

过程展示:gcc和配套的cpp、as、ld处理转换程序

上文提到的的几种处理转换程序除了编译器和汇编器,其他三个估计都很很少听到。下面就用最经典的 C 语言和gcc来介绍这个过程,gcc包含的预处理器为cpp,还会调用汇编器as、连接器ld

关于三者的介绍,以及编译过程中,每一步时如何进行的详细过程,可以看看我的另一篇文章《使用gcc展示完整的编译过程》,这篇文章也通过实际操作,介绍了一些gcc操作方法。这篇文章非常建议读到这里就看一看,不然可能只理解字面的内容,文章内容原本是打算放在这里的,但是会让字数到 2 万字,这样阅读时间太长了

gcc内部工作流程

gcc内部工作流程如下,这里忽略了预处理过程:

C 语言代码
C 前端
AST 代码
优化器
LLVM IR 码
C++ 语言代码
C++ 前端
Objective-C 语言代码
Objective-C 前端
X86 后端
X86 汇编代码
ARM 后端
ARM 汇编代码

Clang内部的工作流程

随着时代的发展和进步,老式的编译流程以及不够用了:

  1. 性能优化需要的人力、物力过大(现在的汇编语言要比以前的复杂太多了,经典的 PDP-11 手册里关于指令的只有 30 页不到,但是现在 Intel X86 指令手册就 2500 页);
  2. 针对每种机器的开发消耗多(比如说在 ARM 和 X86 编译同一个程序);
  3. 编译器的“插件”不够(有时候需要新的优化或者处理)。

看到这里你便明白,这里的前端是指整个 C 语言家族的编译流程的前端,而不是一个编译器的前端。所以 clang 是一个完整的编译器,可以将.c转换成.s文件,但是可以调用汇编器和编译器生成最终的可执行文件。

clang 作为编译器可以将你写的 C 家族的语言转换成 LLVM IR(一种低级语言),然后转换输出一个.s文件,然后调用 LLVM 项目中的汇编器(或者其他汇编器)将其汇编成一个.o对象文件(也就是前文中提到的“汇编阶段”),最后调用连接器进行连接,输出一个可执行程序。

也就是之前描述的编译器内部流程变成如下流程,同样这里的省略了预处理过程:

C 语言代码
C 前端
AST
优化器
LLVM IR 码
C++ 语言代码
C++ 前端
Objective-C 语言代码
Objective-C 前端
后端llc
X86 汇编代码
ARM 汇编代码

clang后面使用的的汇编器和连接器,既可以使用 LLVM 集成,也可以使用 GNU 的,比如连接器可以使用 LLVM 集成的的lld,也可以使用 GNU 的ldgold,以及 MSVC的link.exe。不过默认情况下是使用 LLVM 集成的。

如果你好奇更详细 Clang 工作流程,和每一步的操作,比如说什么选项对应的是编译过程的某一步,可以看看这篇文档《An Overview of Clang》,我就不单独写博客了。

这种编译方式对于适配不同平台来说非常方便。当出现一个新的平台,只要将指令与 LLVM IR 对应即可,完全不用开发者去写一个全新的优化器和代码生成器去将源代码转换成汇编代码,省时省力。

为什么clang是一个前端?难道它不是完整的编译器吗?如果clang是完整的编译器的话,那么为什么叫前端呢?如果它不是完整的,那么后端是什么呢?

Clang 是一个完整的编译器,也是一个前端。不过是将源代码转换成可执行程序流程的前端,而不是编译器的前端。如果说是编译器的前端,那是预处理器、词义分析器(Lexer)和语法分析器(Parser)等部分构成的。

clang对应的后端指的是 LLVM 内含的,或者 GNU 等软件组的连接器、编译器等工具,这些工具负责将汇编代码汇编、连接成最后的可执行文件。

编译器的定义到底是什么?感觉书上编译器的定义和实际的gcc有所不同

关于编译器的定义前文有详细的解释,现在一般情况下“编译器”指的是从将.c等文件转换成.s文件的程序。

实际上编译器,比如gcc包含了一些工具(比如预处理器),也会去调用其他的工具(汇编器和连接器),所以与定义有所不同。

LLVM 项目是干什么项目?

前文提到,很多编译器是需要多个中间表达(IR)的,这些中间表达可能是词汇分析器生成的,也可能是语义分析器生成的,就很不统一,这就导致更新指令和优化性能随着数量的大幅提升成为了一件很困难的事情。

LLVM 全名“Low-Level Virtual Machine”,是一架构和中间表达的实现。而 LLVM 项目最初是一套围绕着 LLVM 代码的工具,C 语言和对应的 LLVM 代码如下(源自Chris Lattner 的《Architecture for a Next-Generation GCC》):
请添加图片描述

LLVM 代码有三种用途:

  1. 编译器的中间表达;
  2. 存放在硬盘里的位码(bitcode);
  3. 人类可读的汇编语言表达

这三种用途实际上都是等价的,要么能共用,要么有工具可以很轻松的转换,这点就让 LLVM 兼容新的机器、优化性能、开发新的语言,甚至是反汇编都是很容易的。

整个项目最核心内容其实就是 LLVM IR。LLVM IR 旨在成为某种“通用IR”,希望足够低级,可以将高级代码干净地映射到 LLVM IR(类似于处理器使用的指令是“通用IR”,允许将许多种不同的语言映射到这些汇编语言)。这给使用 LLVM IR 的编译器带来了性能很不错提升。

关于 LLVM 设计更详细的介绍还是请看文档:《LLVM Language Reference Manual》。

关于 LLVM 带来的性能提升可以看 Intel 的这篇文章:《Intel® C/C++ Compilers Complete Adoption of LLVM》

reinders-2021-LLVM-benchmarks-01

gcc和clang有什么区别?

LLVM 早期有一个名为llvm-gcc的项目,它和 GNU GCC 的最大区别就在于:llvm-gcc在编译器最后使用的是 LLVM 作为最低一级的中间表达,而不是 GNU GCC 使用的的 RTL 作为最低一级的中间表达,所以llvm-gcc编译器的最后一部分是处理 LLVM IR,而不是处理 RTL(Register Transfer Language)。

其他方面,llvm-gccgcc一样将会输出一个汇编文件,工作原理也一样。不过可以通过使用-emit-llvm选项来让llvm-gcc输出 LLVM 字节码。

后来 LLVM 创始人 Chris Lattner 在苹果的时候就开创了一个中间表达全部使用 LLVM 作为中间表达的 C 语言家族的编译器,也就是 Clang。

虽然clang淘汰了llvm-gcc,虽然现在还是有llvm-gcc,但是使用率和性能都不如clang。也正是因为 LLVM IR,Clang进行反汇编也很方便。

下面是 Chris Lattner 简历中提到 Clang 诞生的部分(https://www.nondot.org/sabre/Resume.html#Apple):

Chris Lattner简历截图

这里字太小了,机翻一下:

机翻

总结一下,gccclang的区别在于:clang的各个中间层均为 LLVM IR,而gcc的各个中间层为 TRL 或其他一些事物。

这里需要注意一点,llvm-gcc和 LLVM 创始人 Chris Lattner 在《Architecture for a Next-Generation GCC》中描述的 LLVM 编译器不是一个东西。这个 LLVM 编译器与后来的 Clang 也不是一个东西。论文中对 LLVM 编译器的示意图如下:

请添加图片描述

区别在于中间加了一个连接层,整个编译器中进行了两次连接。不过很明显,根据 Intel 的数据来看, LLVM 编译器的性能和效果相比 GNU GCC 差不多。不过现在你还是可以在 GitHub 上下到它,目前最新版本为 16:https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.0

你可以选择和clang一起下载:
请添加图片描述

也可以单独下载:
请添加图片描述

写本博客的过程中,我自己对于gccclang编译器的使用和理解也有了更深刻的理解。不过由于本文太长了,所以难免出现披露,如果你在阅读过程中发现错误(错误、笔误、忘删掉的一些东西等等),还请评论告知我一下~

希望能帮到有需要的人~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/632575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端前端学习不断

卷吧卷吧...,这东西什么时候是个头啊……

智能指针(2)

智能指针(2) shared_ptr(共享型智能指针)基础知识特点引用计数器共享型智能指针结构理解 shared_ptr仿写删除器类计数器类shared_ptr类使用以及仿写代码的理解 循环引用_Weaks 初始化智能指针的方法 shared_ptr(共享型智能指针) 基础知识 在java中有一…

chatgpt赋能python:Python如何判断输入的字符——基础教程与实例

Python如何判断输入的字符——基础教程与实例 时至今日,互联网已经成为人们获取信息的重要途径,而搜索引擎优化(SEO)则是网站重要的推广手段之一。而Python作为一种高级编程语言,在实现SEO时也有很大的优势&#xff0…

chatgpt赋能python:Python如何进行升序和降序排列

Python如何进行升序和降序排列 Python是一种非常流行的编程语言,由于其在数据科学、机器学习和人工智能等领域的强大表现,越来越多的人开始学习和使用Python。在Python中,排序是一项非常常见的操作。在这篇文章中,我将向您介绍如…

stable diffusion webui 登录接口(login)api接口调用(使用C#)

唠嗑 本次将跟读者讲一下如何通过C#请求sd webui api【login】接口,如果读者觉得文章有用,请给【点个赞】吧,有问题可以评论区提问。 实战 1.配置api启用参数 启动webui时,需加上【–api】 、【–api-auth 账号:密码】 和【–…

chatgpt赋能python:Python字符类型判断:如何判断字符是字母或数字

Python字符类型判断:如何判断字符是字母或数字 在Python编程中,经常需要判断一个字符是字母还是数字。本文将介绍如何在Python中判断字符类型,并给出几个示例。 判断字符类型的方法 在Python中,可以使用以下方法来判断字符类型…

chatgpt赋能python:Python中如何删除变量中的字符

Python中如何删除变量中的字符 在Python编程中,我们有时需要清除变量中的字符。删除字符可以是去掉字符串中的某些字符,也可以是从列表或元组中删除某些元素。本文将介绍Python中如何删除变量中的字符。 删除字符串中的字符 Python使用字符串的切片操…

网络安全入门学习第十五课——PHP基础

文章目录 一、WEB技术1、什么是web2、B/S架构3、C/S架构 二、PHP概述1、PHP是什么2、PHP受欢迎的原因3、基于MVC模式的PHP框架4、常用编译工具5、PHP环境搭建6、开发工具 三、PHP基本语法格式1、标记2、输出语句3、注释4、标识符 四、数据与运算1、常量1.1、常量定义1.2、预定义…

前端vue实现页面加水印文字 单个页面所有页面加水印 水印颜色

前端vue实现页面加水印文字, 可以实现系统所有页面加水印,也可以单个页面加水印, 可更改水印颜色, 下载完整代码请访问uni-app插件市场地址: https://ext.dcloud.net.cn/plugin?id12889 效果图如下: #### 使用方法 使用方法 /* 给系统所有页面加水印*/ // 第一个参数:水印…

Shell脚本函数简介及运用

目录 一、函数的作用 二、定义函数 三、调用函数 1.在脚本中调用函数 2.在函数中调用函数 四、函数传参 五、函数的返回值 六、函数的递归 七、函数及其变量的作用范围 八、外部脚本调用函数 一、函数的作用 语句块定义成函数约等于别名,定义函数&#xf…

【云原生网关】Apache ShenYu 使用详解

目录 一、前言 二、Apache ShenYu 介绍 2.1 为什么叫ShenYu 2.2 ShenYu特点 2.3 ShenYu架构图 2.4 shenyu数据同步原理 2.4.1 Zookeeper数据同步原理 三、Apache ShenYu 安装部署 3.1 部署流程 3.1.1 创建 Docker Network 3.1.2 拉取Apache ShenYu Admin镜像 3.1.3…

C语言之函数栈帧的创建与销毁(2)

上一篇博客我们讲到了函数栈帧的创建与销毁(1)今天我们来讲解Add函数的函数栈帧相关知识 在开始本章博客之前,大家可以把上一篇博客的主要内容仔细复习一下 看图 第一个mov:把b的值放到eax里面去 第二个mov:把a的…

【python】【excel】用excel中指定单元格的内容去替换一个文本中指定的字符

1 使用背景 理正的.spw文件是文本格式,类似于该软件的前处理,相关参数字段可通过文本替换,快速修改参数。 后续用途可用在:用EXCEL整理数据,通过修改文本批量获取多个截面参数的spw文件 2 ExcelSheet-shift-textstr…

macOS中解决matplotlib中文乱码

现象 图表上中文变方框,日志中报错如下: findfont: Generic family sans-serif not found because none of the following families were found: 解决办法 下载字体 http://xiazaiziti.com/210356.html 查询字体保存路径 查看配置文件路径 import…

01- 数据类型(C语言)

一 变量和常量 1.1 标识符 1、在我们所写的“第一个C程序”中出现了很多的标识符,例如include、main、printf、return。标识符是⽤来标识变量、函数,或任何其他⽤户⾃定义项⽬的名称。 2、标识符的约束规范: 只能包含数字、字母和下划线不…

chatgpt赋能python:Python怎么删除列表

Python怎么删除列表 什么是Python列表? 在Python中,列表是一个可变的序列,它可以包含不同类型的数据。列表可以使用中括号 [] 来定义,每个元素之间用逗号分隔。列表中的元素可以通过其下标进行访问,下标从0开始。 P…

Skywalking基础使用

Skywalking基础使用 agent的使用Linux下Tomcat7和8中使用Spring Boot中使用RocketBot的使用 agent的使用 agent探针可以让我们不修改代码的情况下,对java应用上使用到的组件进行动态监控,获取运行数据发送到OAP上进行统计和存储。agent探针在java中是使…

C语言之数组初级(5-8)

目录 1. 一维数组的创建和初始化 2. 一维数组的使用 3. 一维数组在内存中的存储 4. 二维数组的创建和初始化 5. 二维数组的使用 6. 二维数组在内存中的存储 7. 数组越界 8. 数组作为函数参数 二维数组的画图讲解 例如现在我要找第二行第三列这个元素,下面是…

放大电路与频率特征(期末模电速成)

目录 1、放大电路基础 2、放大电路三种组态 3、放大电路分析(必考) 4、多级放大电路 5、差动放大电路 6、频率特征 1、放大电路基础 晶体管输出特性曲线中的线性区域是指 放大区 ,iC βiB NPN 型静态工作点过高,容易产生 饱…

Vue 中的表格操作

Vue 中的表格操作 在 Web 开发中,表格是非常常见的元素之一。在 Vue 中,我们可以使用一些组件和插件来实现表格的操作。在本文中,我们将介绍 Vue 中的表格操作的基本原理和用法,并给出一些实例代码来帮助读者更好地理解。 表格…