《编译系统-自底向上研究方法》用clion调试编译器gcc源码 - 弦外之音

/ 0评 / 0

前面已经把 可运行文件格式 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.2MPC 0.8.0+

4,下载完 GMP 4.3.2 , MPFR 2.4.2MPC 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 编译的时候才能找到这些库。其实gccconfigure 脚本可以指定 --with-gmp= 的库位置,但是我试了一下,gccconfiguremakefile 并没有衔接得特别好,configure指定的路径没有传递给 makefile ,所以make 的时候有点问题,为了方便起见,还是需要加一下搜索路径。操作如下:

如上图所示,直接在 /etc/ld.so.conf.d 目录下新增3个文件。 gmp.conf , mpc.confmpfr.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 文件,有些项目为了管理方便,本身就有 makefileconfigure 只是生成一个子 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 编译系统认识各种平台格式,只要把数据指令按照不同平台的标准放置,就是跨平台编译。

跨平台编译在一些嵌入式开发经常用,因为嵌入式设备可能没有编译环境,他只有运行环境,而且嵌入式设备的性能比较差,编译一些代码可能要很久。所以需要在一个性能特别好的机器把 指令数据 按照嵌入式设备的运行文件格式 弄好,再上传到嵌入式设备,就能运行了。


相关阅读:

1,《记录编译安装 GCC 11.2.0》

2,《Linux指定动态库路径》

3,《深入分析GCC》- 王亚刚

7,《gcc-conceptual-structure》

4,《GCC Source Code: An Internal View》


由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1,QQ:2338195090。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注