前面已经把 可运行文件格式 ELF,链接器 ld
,汇编器 gas
简单介绍了一下,终于到了要讲 编译器的章节。实际上在 整个编译系统里面,编译器是最重要的,开发难度也是最高的。链接器,汇编器做的都是一些简单的工作。
我们通常 会用 gcc
编译器 来编译 C 程序。但是 gcc
也可以编译其他的语言,例如 Objective-C,Fortran,Ada,Go 跟 D 语言。
GCC 为了防止项目被个人或者组织控制,成立了 steering committee ,目前有 13 位成员,分别来自 Google,Red Hat ,IBM 等公司,可以把这 13 个人理解为 船长/指导员,决定 GCC 项目的发展方向,更多信息请浏览 GCC官网。
编译过程 其实有两步,分别是 预处理 跟 编译,如下:

预处理器 是 cpp
命令,预处理是 处理那些 以 #
开头的语法,都是一些宏定义,宏其实也算一种小的编程语言,他也有 define,if 条件 之类的。宏语法还可以把 一些外部文件 include
进来。
下面就来讲解,如何用源代码编译出来 cpp
命令,环境是 Ubuntu18 ,Linux 操作系统一般都自带 cpp
命令,不过自带的 cpp
没有符号表不太方便调试,所以为了调试学习,还是要自己编译一下。
点击这个 地址 下载 gcc
源码。我选择的版本是 7.5 ,目前 gcc
最新版本是 11,不过为了方便学习,没有用最新版本,老版本编译起来会顺畅一些,而且网上很多文章书籍基于的都不是最新版本,用老版本方便读者对照。

下载完压缩包之后,在 Document
目录下创建一个 gcc_project
文件夹,解压后 目录如下:

3,现在如果直接执行 configure
会以下错误,需要先编译安装 GMP 4.2+ , MPFR 2.4.2 跟 MPC 0.8.0+。

4,下载完 GMP 4.3.2
, MPFR 2.4.2
跟 MPC 0.8.1
后,目录如下,把他们都放在 gcc_project
文件夹里面方便管理:

执行以下命令编译:
# 编译 gmp
sudo apt-get install m4 -y
cd ~/Documents/gcc_project/gmp-4.3.2
./configure --prefix=/usr/local/gmp_4.3.2
make -j 16
sudo make install
# 编译 mpfr
cd ~/Documents/gcc_project/mpfr-2.4.2
# 由于 mpfr 用到了 gmp 的头文件,需要指定 gmp 的路径
./configure --prefix=/usr/local/mpfr_2.4.2 --with-gmp=/usr/local/gmp_4.3.2
make -j 16
sudo make install
# 编译 mpc
cd ~/Documents/gcc_project/mpc-0.8.1
./configure --prefix=/usr/local/mpc_0.8.1 --with-gmp=/usr/local/gmp_4.3.2 \
--with-mpfr=/usr/local/mpfr_2.4.2
make -j 16
sudo make install
上面的库编译好之后,目录如下:

编译好 这 3个外部库之后,要把他们加进去 库的 搜索路径,这样 gcc
编译的时候才能找到这些库。其实gcc
的 configure
脚本可以指定 --with-gmp=
的库位置,但是我试了一下,gcc
的 configure
跟 makefile
并没有衔接得特别好,configure
指定的路径没有传递给 makefile
,所以make 的时候有点问题,为了方便起见,还是需要加一下搜索路径。操作如下:

如上图所示,直接在 /etc/ld.so.conf.d
目录下新增3个文件。 gmp.conf
, mpc.conf
,mpfr.conf
,分别填进去各个库的安装路径即可。最后运行一下以下命令,重新加载配置。
sudo ldconfig
需要的外部库 编译好之后,现在可以运行 GCC 的 configure
检测脚本了,很多项目都会有一个 configure 的文件,这是一个shell脚本,只是对环境做一些检测,并不是真正开始编译。一些常见的错误,例如 缺少库,库版本不匹配等等,都会在这个阶段检测处理。
执行以下命令开始编译:
cd ~/Documents/gcc_project/gcc-7.5.0
./configure CFLAGS="-g -O0" --prefix=/usr/local/gcc_7.5.0 \
--with-gmp=/usr/local/gmp_4.3.2 \
--with-mpfr=/usr/local/mpfr_2.4.2 --with-mpc=/usr/local/mpc_0.8.1 \
-enable-checking=release -enable-languages=c,c++ -disable-multilib
make -j48
注意,不要运行 make install ,让可执行文件保留在源码目录,方便调试。
这里讲一个扩展知识点,在其他开源项目里, configure
不一定会生成 makefile
,没有标准说 configure
脚本一定要生成 makefile
文件,有些项目为了管理方便,本身就有 makefile
,configure
只是生成一个子 makefile
让 主 makefile
能包进去。
GCC7.5的编译过程,采用了 bootstrapping 的技术,这个过程看起来很像套娃,一个套一个。他是先用我机器自带的 gcc
编译器,编译 gcc7.5 的源码 生成 gcc7.5 编译器,再用 新的 gcc7.5 编译器 编译 gcc7.5 的源码,生成 编译器 gcc7.5-B,然后 用 gcc7.5-B 编译器再编译一次gcc7.5 的源码 生成 编译器 gcc7.5-C。
最后 比对 gcc7.5-B 跟 gcc7.5-C 的内容,如果相同,就说明编译成功,如果不同,说明生成的编译器有问题。详细内容可以参考这个文档 《How are GCC and g++ bootstrapped》
上面的 命令 运行完毕之后,就会在源码目录生成以下 cc1
可执行文件,虽然《深入理解计算机系统》说 cpp
是预处理器, cc1
是编译器。但是技术发展到现在,cc1 这个命令,他是预处理器,也是汇编器,如下:

cc1
可执行文件已经编译出来,现在就来试一下这个文件好不好使,请下载测试代码包,百度网盘,提取码:tsdn 。把代码包放在 ~/Documents/
目录。测试命令如下:
cd ~/Documents/c-test
/home/ubuntu/Documents/gcc_project/gcc-7.5.0/host-x86_64-pc-linux-gnu/gcc/cc1 -E -o main.o main.c

从上图可以看到 ,sum.h
的内容已经被包进去 main.i
文件里面了。然后,编译命令如下:
/home/ubuntu/Documents/gcc_project/gcc-7.5.0/host-x86_64-pc-linux-gnu/gcc/cc1 -o main.s main.i
编译命令也是没有问题的,可以正常把 C 程序翻译成汇编指令。
下面开始配置 clion 进行调试 cc1 这个编译器的源码。
1,添加一个 Makefile Target ,如下图,直接把 Makefile 文件的路径填进去即可。Target 留空就会编译默认target。

2,添加 一个 Makefile Application,如下图:

这一步有几个注意的地方,里面的路径之类的,要根据自己的环境做调整,然后是那个 Target,Clion 2021 有时候会抽风不显示,需要你点后面那个设置按钮,1随便加一个 target ,再返回来,他就会显示出来了。
3,cc1 程序的 main 入口在 gcc/main.c
,直接在这个位置入口打个断点。如下图:

现在断点调试已经成功了,就可以一步一步调试 编译器的源码。
至此,《编译系统-自底向上研究方法》一书已经从最底层的 ELF 讲到了 最上层的编译器cc1,本来还想讲一下编译器的 词法分析逻辑,不过我还没看源码。后续本书的更新会比较缓慢。在这里推荐一些学习资料,方便有兴趣的读者继续深入研究。
1,《深入分析GCC》- 王亚刚,这本书讲的是 GCC4.4 版本的数据结构以及一些内部逻辑,非常不错。
2,《编译系统透视》- 新设计团队,这本书采用的 GCC 是自己精简过的代码,没有说是哪个版本,也没提供代码下载,不过有大量的插图跟视频动画,推荐阅读。
3,《现代编译原理C语言描述》《编译原理》《编译器设计》这3本书都是理论书籍,推荐阅读。
4,《汇编语言基于x86》这本书是 x86 汇编的入门书籍,推荐阅读。
通过 clion 断点调试,再结合这些书籍的理论实战知识,相信读者应该会很快掌握编译原理的精髓。
最后再讲一个扩展知识,我们知道 不同平台有不同的可运行文件格式,Windows 的PE格式,Linux 的ELF格式。有一种比较常用的编译方式是跨平台编译,例如在 Linux 系统编译出 exe 文件,放到 windows 运行。这个是因为 gcc
编译系统认识各种平台格式,只要把数据指令按照不同平台的标准放置,就是跨平台编译。
跨平台编译在一些嵌入式开发经常用,因为嵌入式设备可能没有编译环境,他只有运行环境,而且嵌入式设备的性能比较差,编译一些代码可能要很久。所以需要在一个性能特别好的机器把 指令数据 按照嵌入式设备的运行文件格式 弄好,再上传到嵌入式设备,就能运行了。
相关阅读:
3,《深入分析GCC》- 王亚刚
4,《GCC Source Code: An Internal View》
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1,QQ:2338195090。