riscv gcc工具链是如何被编译的
- 概述
- 编译器编译原理
- 历史背景
- gcc工具链是如何工作的?
- 工具链中有哪些组件?
- 工具链的构建顺序
- riscv gcc编译器的目录结构
- riscv gcc编译器的构建
- 编译最小支持RVB和RVV的riscv gcc
概述
gcc工具链是一个复杂而又巧妙的工程,随着riscv上层软件的逐渐完善,工具链和底层系统软件的开发也显得尤为重要。深入理解gcc的原理,能够更好的对计算机体系结构有一个完整的了解。
但是由于gcc的源码过于复杂,其诞生的年代也十分久远,入手gcc也相当棘手。而riscv是一个新的体系架构,在该架构上去理解gcc的开发和编译过程,不会有许多的历史包袱,这也是后面文章中主要进行分析的架构。
编译器编译原理
历史背景
GNU C编译器的早期作者是GNU项目的创始人Richard Stallman。
GNU项目最早开始于1984年,其目的是为了创建一个完全自由免费的类Unix的操作系统。由于每一个类Unix的操作系统都需要一个C编译器作为支持,但是当时并没有免费使用的编译器。所以GNU项目不得不从头开始进行编译器的开发工作。在1987年,第一个GCC正式版本诞生,这是一个免费发布并且可移植的编译器,此后GCC成为开发免费软件最重要的工具之一。
后来随着编译语言的增多,包括Fortran,ada,Java与Objective-C也被支持。
gcc工具链是如何工作的?
gcc工具链并不是一个单独的程序,而是一系列程序的合集,这些工具以一种串联的方式进行排列。
其中就包括预处理,编译,汇编,链接等过程。这种特性的特点就是上一个步骤的输出结果总是下一个过程的输入,最后生成了特定架构所需的可执行的文件。按照这种方式组合,形成了"工具链",当为不同的架构生成机器代码时,称为交叉编译工具链。
工具链中有哪些组件?
下图展示了riscv gcc编译完成后的组件。当然,最新发挥作用的是编译器gcc本身,将C文件转换成汇编代码。
汇编代码则由汇编器进行链接,生成特定的机器代码。
下面通过一个表格简单的描述一下
工具 | 功能 |
---|---|
addr2line | 可以将指令的地址转换成文件名,函数名和源代码行数的工具 |
ar | 库管理器,创建静态库 |
as | 汇编器,主要处理汇编代码 |
objcopy | 将文件转换成另外一种格式,比如.bin转换成.elf,或者将.elf转换成.bin |
objdump | 反汇编 |
readelf | 显示elf相关的信息 |
size | 列出可执行文件的每一部分的尺寸,代码段,数据段等大小信息 |
通过上述一些列的工具,可以将C语言转换成可以执行的代码程序,但是现在还缺少在目标机器上运行程序时的C库,C库提供了一个标准的抽象层,可以执行基本的任务,包括内存分配、终端输出、文件访问等等。对于不同的系统,也有着不同的C库,比如针对Linux桌面环境,有glibc或者eglibc或者uClibc等等。对于嵌入式Linux,可以选择eglibc或者uClibc,对于没有任何操作系统或者RTOS来说,可以使用newlib,甚至可以不使用。还有一些小众的C库,针对特定的需要进行设计,比如针对ramdisk优化的klibc等等。
工具链的构建顺序
这些工具构建需要一定的顺序,这是一件有趣的事情。
- 编译出的编译器需要C库
- 编译出C库需要编译器
这就带来一个问题,A依赖B,B也依赖A。这就是典型的先有鸡,后有蛋的问题。
编译器首先会构建一个不需要C库就能构建出来的精简编译器,这部分我们称为引导程序、初始编译器或者核心编译器。
- 最后的编译器需要C库
- 编译出C库需要编译器
- 编译器需要C库的头文件和引导程序
现在的问题变成了编译C库需要的头文件和引导文件。由于C库提供了"C runtime",也被称为CRT,但是这部分的编译还是需要依赖编译器。
- 最后的编译器需要C库
- 编译出C库需要编译器
- 编译器需要C库的头文件和引导程序
- 编译C库的引导程序
这样问题可能就变得简单一些了,我们只需要构建一个简单的编译器,他不需要C库头文件但是需要启动文件,该编译器同时也是C库的引导程序。我们称为这个简易编译器为pass1。最后完整的编译器为pass2。下面则变得清晰了起来:
- 最后的编译器需要C库
- 编译出C库需要编译器
- 编译器需要C库的头文件和引导程序
- 编译C库的引导程序
- 我们需要一个简易的编译器
这样下来,我们就可以得到一个编译器所需要的C库了,然后完整的编译出最终的编译器。
上述只是一个基本的流程概述,实际上过程比这个更加的复杂。
下面来看riscv gcc编译后生成的文件夹
带有build前缀的都是编译器编译时的中间产物。可以看到build-gcc-newlib-stage1
和build-gcc-newlib-stage2
。
实际上gcc在编译过程中编译了三次:
- 编译额外的C编译器(stage1)
- 用stage1的编译器重新编译GCC编译器(stage2)
- 用stage2的编译器再次编译GCC编译器(stage3)
stage2和stage3是为了更好的检查GCC编译的准确性,同时,也可以采用不同的优化等级对最后生成的gcc工具链进行优化。
其实stage2和stage3产生的应该是一样的结果,可以采用make compare
来进行校验,由于这种严密的编译机制,可以使得最后的gcc生成的产物变得准确无误。
riscv gcc编译器的目录结构
在了解如何编译之前,首先看一下riscv gcc仓库有哪些东西。
代码语言:javascript复制https://github.com/riscv-collab/riscv-gnu-toolchain
- qemu
工具链仓库的qemu左右是为了测试使用,结合riscv gcc的dejagnu测试框架,测试功能
代码语言:javascript复制make report-linux SIM=qemu # Run with qemu
- riscv-binutils
该目录用于binutils生成,比如ar,ld等二进制工具,工具集合,也是非常重要的一个仓库。
- riscv-dejagnu
dejagnu测试框架是测试gcc和binutils重要的工具,是保证gcc和binutils功能正常的非常重要的测试框架。
- riscv-gcc
gcc主要的程序
- riscv-gdb
通过外设接口,可以通过gdb调试
- riscv-glibc
支持编译的程序在Linux运行的glibc库
- riscv-newlib
支持编译的程序在rtos或者baremetal上运行的的C库
riscv gcc编译器的构建
当前公认的riscv gcc主线在
代码语言:javascript复制https://github.com/riscv-collab/riscv-gnu-toolchain
该master分支是稳定的可以使用的riscv 版本。但是现在做riscv扩展指令集分析,这里选择
代码语言:javascript复制https://github.com/riscv-collab/riscv-gnu-toolchain/tree/basic-rvv
该分支实现了也就是riscv b扩展,v扩展的最小支持。该分支作为入手gcc分析是最好的选择。
下载代码
代码语言:javascript复制git clone https://github.com/riscv-collab/riscv-gnu-toolchain.git
cd riscv-gnu-toolchain
git checkout basic-rvv
git submodule init && git submodule update
在官方的readme.md中介绍了如何编译的流程。
大概介绍一下:
该编译器支持两种libc库,支持rtos和barematel的newlib库和支持Linux的glibc。
默认使用make
时,链接的是newlib库,使用make linux
时,链接的是glibc。
同时由于riscv有着非常多的arch组合,可以编译单独的arch,比如
代码语言:javascript复制./configure --prefix=/opt/riscv --with-arch=rv32gc --with-abi=ilp32d
那么只编译arch是rv32gc,abi为ilp32d的组合。
如果同时支持rv32和rv64的组合配置,可以选择--enable-multilib
。
./configure --prefix=/opt/riscv --enable-multilib
不同的组合有着不同的需求,如果只是针对不同的芯片编译出的编译器,那么只选择一个配置即可,若需要发布更多组合的arch支持编译器,可以选择multilib。
编译最小支持RVB和RVV的riscv gcc
可以选择下面的配置
代码语言:javascript复制./configure --prefix=$RISCV/learn/ --with-arch=rv64gcv_zba_zbb_zbc_zbs --with-abi=lp64d --with-cmodel=medany --with-multilib-generator="rv64gcv_zba_zbb_zbc_zbs-lp64d--"
make -j $(nproc)
这里的RVV可以直接采用gcv即可,而RVB则被拆分成各种子扩展。
代码语言:javascript复制Zba - address generation
Zbb - basic bit manipulation
Zbc - carryless multiplication
Zbs - single-bit instructions
最后生成的riscv gcc工具链可以做测试。
结合编译参数,开启O2优化。
代码语言:javascript复制'-march=rv64gcv_zba_zbb_zbc_zbs' ,'-mabi=lp64d'
可以生成带有RVB扩展的格式的代码。
不难看出,当写出下面的函数时
代码语言:javascript复制long sh1add_test(long a, long b)
{
return a (b << 1);
}
而开启rvb扩展时的反汇编代码:
代码语言:javascript复制long sh1add_test(long a, long b)
{
800000a2: 1141 addi sp,sp,-16
800000a4: e422 sd s0,8(sp)
800000a6: 0800 addi s0,sp,16
return a (b << 1);
}
800000a8: 6422 ld s0,8(sp)
800000aa: 20a5a533 sh1add a0,a1,a0
800000ae: 0141 addi sp,sp,16
800000b0: 8082 ret
不开启rvb扩展时的反汇编代码如下:
代码语言:javascript复制long sh1add_test(long a, long b)
{
800000a2: 1141 addi sp,sp,-16
800000a4: e422 sd s0,8(sp)
800000a6: 0800 addi s0,sp,16
return a (b << 1);
}
800000a8: 6422 ld s0,8(sp)
return a (b << 1);
800000aa: 0586 slli a1,a1,0x1
}
800000ac: 952e add a0,a0,a1
800000ae: 0141 addi sp,sp,16
800000b0: 8082 ret
由此可见开启rvb扩展优化后,代码的code size和性能均会提高。
那么这个优化在gcc中是如何实现的,后面文章中会慢慢的提及。