源代码编译介绍

源代码编译介绍

1. C/C++代码编译

1.1. 编译过程

hello程序

#include <stdio.h>

int main(){
    printf("hello world!\n");
    return 0;
}

在Unix系统上,从源文件到目标文件只需要一条命令:

$ gcc hello.c -o hello #编译
$ ./hello #执行
hello world!

这个过程中,GCC编译器驱动程序读取程序源文件hello.c,并把它翻译成一个可执行的目标文件hello。这个翻译过程可分为四个阶段,如下图所示。执行这四个阶段的程序(预处理器、编译器、汇编器、连接器)一起构成了编译系统(compilation system)。

在这里插入图片描述

  • 预处理(Preprocessing)

预处理器(ccp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到另外一个C程序,通常是以.i作为文件扩展名。

  • 编译(Compilation)

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义,如下所示:

main:
    subq $8, %rsp
    movl $.LC0, %edi
    call puts
    movl $0, %eax
    addq $8, %rsp
    ret

定义中2~7行的每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。

  • 汇编(Assemble)

接下来,汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码。

可重定位
在源程序中地址是从0开始的,而程序真正在内存中运行时的地址肯定不是从0开始的,而且在编写源代码的时候也不能知道程序的绝对地址,所以重定位能够将源代码的代码、变量等定位为内存具体地址。

在这里插入图片描述

静态重定位与动态重定位
在这里插入图片描述

  • 链接(Linking)

hello程序调用了printf函数,printf函数是每个C编译器都会停工的C标准库中一个函数。printf函数的指令编码存在于一个名为printf.o的文件中,它是一个单独预编译好了的目标文件。这个printf.o的文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到了hello文件,它是一个可执行目标文件(也简称为可执行文件),可以被加载到内存中,由系统执行。

  • 静态链接:在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的可执行程序,以后不再拆开。
  • 装入时动态链接 : 将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的方式。
  • 运行时动态链接:对某些目标模块的链接,是在程序执行中需要该目标模块时才进行的 。其优点是便于修改和更新,便于实现对 目标模块的共享。

1.2. 总结

当你对编译过程的各个阶段有了新的理解,你就能更好地理解编译错误或连接错误产生的原因,并避免这些与编译相关的潜在问题。比如,你理解了预处理过程,你就能利用头文件保护符(用于保护头文件内容不被错误地多次包含的预编译器指令)防止一些编译错误的出现,同时你也能明白为什么会出现缺少头文件的报错。再比如,你了解了链接过程,你就明白为什么缺少链接库的报错。

2. 常用编译器

2.1. GCC

2.1.1. 什么是GCC

GCC的全称是GNU Compiler Collection,它是一个能够编译多种语言的编译器。最开始gcc是作为C语言的编译器(GNU C Compiler),现在除了c语言,还支持C++、Java、Go等语言。

2.1.2. GCC的特点
  • gcc是一个可移植的编译器,支持多种硬件平台。例如X86、ARM、MIPS等等。
  • gcc不仅是个本地编译器,它还能跨平台交叉编译。所谓的本地编译器,是指编译出来的程序只能够在本地环境进行运行。而gcc编译出来的程序能够在其他平台进行运行。例如嵌入式程序可在x86上编译,然后在arm上运行。
  • gcc是自由软件。任何人都可以使用或更改这个软件。
2.1.3. GCC的使用
  • 总览
常用命令对应编译器源文件链接库
gcc [option|filename ]…C编译器*.cC标准库
g++ [option|filename ]…C++编译器*.cc/*.cppC++标准库

gcc和g++的用法基本相同,下面介绍一下给gcc常用选项。

选项名作用
-Wall生成所有警告信息。
-o产生目标(.i、.s、.o、可执行文件等)。
-E预处理后即停止,不进行编译,不生成文件,预处理后的代码送往标准输出。
-S编译后即停止,不进行汇编,生成.s的汇编代码。
-c汇编后即停止,不进行链接,生成.o文件。
-O0/-O1/-O2/-O3/指定编译器优化级别,-O0 表示没有优化, -O1 为默认值,-O3 优化级别最高
-g在目标文件中嵌入调试信息,以便gdb之类的调试程序调试 。
-Idir将dir目录加入搜索头文件的目录路径。
-Ldir将dir目录加入搜索库的目录路径。
-llib连接lib库。

下面是一些gcc的使用示例:

gcc -E hello.c -o hello.i    对hello.c文件进行预处理,生成了hello.i 文件
gcc -S hello.i -o hello.s    对预处理文件进行编译,生成了汇编文件
gcc -c hello.s -o hello.o    对汇编文件进行汇编,生成了目标文件
gcc hello.o -o hello    对目标文件进行链接,生成可执行文件
gcc hello.c -o hello    直接编译链接成可执行目标文件
gcc -c hello.c 或 gcc -c hello.c -o hello.o    编译生成可重定位目标文件
gcc hello.c main.c -o main    编译多个文件生成可执行文件main
2.1.4. 头文件与库文件

头文件:在使用C语言和其他语言进行程序设计的时候,我们需要头文件来提供对常数的定义和对系统及库函数调用的声明。

头文件位置:

  • /usr/include及其子目录底下的include文件夹
  • /usr/local/include及其子目录底下的include文件夹

库文件:一些预先编译好的函数集合,那些函数都是按照可重用原则编写的。

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。静态库比较占用磁盘空间,而且程序不可以共享静态库。运行时也是比较占内存的,因为每个程序都包含了一份静态库。

  • 动态库(.so或.sa):程序在运行的时候才去链接共享库的代码,多个程序共享使用库的代码,这样就减少了程序的体积。

默认情况下, gcc在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库。

库文件位置:

  • /usr/lib
  • /usr/local/lib
  • /lib

头文件、库文件搜索原则:
从左到右搜索-I -L -l指定的目录,如果在这些目录中找不到,那么gcc会从由环境变量指定的目录进行查找。头文件的环境变量是C_INCLUDE_PATH,库的环境变量是LIBRARY_PATH。如果还是找不到,那么会从系统指定指定的目录进行搜索。

2.2. LLVM与Clang

2.2.1. 传统编译器架构

在这里插入图片描述

传统的编译器架构(比如GCC)主要分为前端、优化器、后端(理论上优化器也是后端的一部分)。三者的作用分别如下:

  • 前端:词法分析、语法分析、语义分析、生成中间代码。
  • 优化器:中间代码作为输入,优化中间代码(与架构无关的代码优化),使代码运行更快,体积更小。
  • 后端:生成机器码(根据不同架构x86、x64等生成不同架构的机器码)。
2.2.2. LLVM架构

在这里插入图片描述

由上图可知,LLVM架构下,不同的前端和后端使用统一的中间代码LLVM InterMediate Representation(LLVM IR)。

如果需要支持一门新的编程语言,只需要实现一个新的前端。如果需要支持一款新的硬件设备,只需要实现一个新的后端。优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改。

相比之下,GCC的前端后端没有实现分离,前端后端耦合在了一起,所以GCC为了支持一门新的编程语言,或者为了支持一个新的硬件设备,就变得特别困难。

2.2.3. 什么是LLVM

LLVM项目是一个模块化的、可重用的编译器和工具链集合。尽管它的名字-LLVM与传统虚拟机(low level virtual machine)名字相似,但“LLVM”这个名字本身不是一个缩略词,它就是这个项目的全称。所以不要再把LLVM叫做low level virtual machine。

在理解LLVM时,我们可以认为它包括了一个狭义的LLVM和一个广义的LLVM。

  • 广义的LLVM其实就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及很多的模块。
  • 狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库。
2.2.4. 什么是Clang

在这里插入图片描述

Clang是LLVM的项目的子项目,它是LLVM架构下的C/C++/Objective-C的编译器前端。Clang 对源程序进行预处理、词法分析、语法分析,并将分析结果转换为抽象语法树(Abstract Syntax Tree),最后使用 LLVM 作为编译器后端代码的生成器。

Clang 的开发目标是提供一个可以替代 GCC 的前端编译器。相比较于GCC,Clang具有如下优点:

  • 编译速度快:在某些平台上,Clang的编译速度明显快过GCC。Debug模式下,Clang编译OC的速度比GCC快3倍。
  • 占用内存少:Clang生成的抽象语法树(AST),所占用的内存是GCC的五分之一左右。
  • 模块化设计:Clang作为LLVM项目下的一个子项目,采用基于库的模块化设计,易于IDE的集成及其他用途的重用。
  • 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告。
  • 设计清晰简单,容易理解,易于扩展增强。
2.2.5. Clang与LLVM的关系

在这里插入图片描述

Clang作为LLVM的前端,负责词法分析、语法分析、语义分析,然后生成中间代码。接下来把中间代码转交给优化器,优化器会对中间代码进行与架构无关的代码优化,优化后的代码体积更小、运行速度更快。最终LLVM后端会把优化后的中间代码转化为机器码。流程如下:
在这里插入图片描述

虽然Clang是LLVM的前端,但是LLVM的前端不只是Clang。Clang只是为C、C++、Objective-C设计的LLVM的编译器前端。除此之外,还有为Swift设计的编译器前端Swift等。

3. 参考资料

  1. 深入理解计算机系统(第三版)
  2. 2021王道操作系统考研复习指导
  3. Linux编译工具:gcc入门
  4. LLVM简介
  5. LLVM(clang)介绍
这个编译器的源代码是我原先为了完成编译原理实验课作业而写的,所以只具有教学价值,现在发出来和大家共享 ;-)<br/><br/>和网上流传的版本不同,它从文法开始,一直做到了符号表的实现. 想实现自己的编译器的话,只需在把Initializtion.h中的文法修改为自己的即可.<br/><br/>工程结构:<br/>Initializtion.h 初始化文法,便于进一步进行分析,它为构造GRAMMAR类提供了信息.其中默认非终极符用<>括上,修改时需要注意.<br/>Grammar.cpp Grammar.h 定义了文法GRAMMAR类,它通过initializtion.h的信息建立文法的内部表示。<br/>LL1_Analyser.cpp LL1_Analyser.h 定义了LL1分析器,即LL1_Analyser类.<br/>LL1_Recognizer.cpp LL1_Recognizer.h 为LL1语法分析驱动器,可以通过文法,TOKEN序列和LL1分析表,判定语法是否正确,同时驱动动作.<br/>Rec_Parse.cpp Rec_Pares.h 实现了递归下降分析器Rec_Parse类, 递归下降的思想和LL1驱动器一样,不过是把压栈改成调用自己,而把弹栈改成返回.<br/>Scanner.cpp Scanner.h 实现了词法分析器,可以将程序变为TOKEN序列. 扫描的源程序文件路径也在这里被定义(默认为.//demo.txt)<br/>Action.cpp Action.h 实现了语义栈的操作,_Action类定义了动作符号所对应的动作.<br/>SymTable.cpp SymTable.h 实现了符号表的建立和输出.<br/><br/>希望大家能通过该程序对STL和编译原理有更深刻的理解,Have Fun and Good Luck!<br/><br/> -- David.Morre
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值