riscv gcc工具链是如何被编译的

2022-01-10 08:10:09 浏览数 (1)

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-stage1build-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

代码语言:javascript复制
./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中是如何实现的,后面文章中会慢慢的提及。

0 人点赞