# Linux C

# GCC 编译链

GCC 编译链 (toolchain) 是以 GCC 为核心的一整套工具,用于把源码转化为可执行应用程序。主要包括以下三部分:

  • gcc-core : 即 GCC 编译器,用于完成预处理和编译的过程.
  • Binutils : 除 GCC 编译器外的一些列小工具。包括了 链接器 ld , 汇编器 as , 目标文件格式查看器 readelf 等.
  • glibc : 包含了主要的 C 语言标准库函数,C 语言中常常使用的打印函数 printfmalloc 函数就在 glibc 库中

# GCC 编译器

GCC (GUN Compiler Collection) 是由 GUN 开发的编程语言编译器. GCC 最初代表 GNU C Compiler , 当时只支持 C 语言。后来逐渐支持 C++, JAVA 等,GCC 就被重定义为 GNU Compiler Collection , 称为史上最优秀的编译器。其比一般编译器编译器平均效率高出 20%~30%.
虽然大部分教程都声明 Ubuntu 默认安装好了 GCC, 我实测出来的都是 没有安装 gcc 的,建议先输入以下命令安装 gcc

sudo apt update
sudo apt install gcc -y

安装完毕后,输入 gcc -v 查看版本信息
GCC
对于标注进行以下说明

  • Target:x86_64-linux-gnu 表示该 GCC 的目标平台为 x86_64 架构 (Intel、 AMD 的 CPU), 表示它编译生成的应用程序只适用于 x86 架构,不适用于 ARM 开发板平台.
  • gcc version 9.3.0 表明该 GCC 的版本为 9.3.0, 部分程序可能会对编译器版本有要求,hello world 程序很简单不需要考虑兼容性。一开始可以不用在乎这个,而编译指定版本的 uboot、 Linux 内核的时候可能会对 GCC 有版本要求

# Binutils 工具集

Binutils(bin utility), 是 GNU 二进制工具集,通常跟 GCC 编译器一起打包安装到系统,它的官方说明网站地址为:https://www.gnu.org/software/binutils/
在进行程序开发的时候通常不会直接调用这些工具,而是在使用 GCC 编译指令的时候由 GCC 编译器间接调用.
下面是其中一些常用的工具:

  • as: 汇编器,把汇编语言代码转换为机器码(目标文件)
  • ld: 链接器,把编译生成的多个目标文件组织成最终的可执行程序文件
  • readelf: 可用于查看目标文件或可执行程序文件的信息
  • nm: 可用于查看目标文件中出现的符号
  • objcopy: 可用于目标文件格式转换,如.bin 转换成.elf 、 .elf 转换成.bin 等
  • objdump: 可用于查看目标文件的信息,最主要的作用是反汇编
  • size: 可用于查看目标文件不同部分的尺寸和总尺寸,例如代码段大小、数据段大小、使用的静态内存、总大小等

系统默认的 Binutils 工具集 位于 /usr/bin 目录下。可以使用如下命令查看

ls /usr/bin/ | grep linux-gun-

Binutils

# glibc 库

glibc 库是 GNU 组织为 GNU 系统以及 Linux 系统编写的 C 语言标准库,因为大部分 C 语言都依赖函数库,该文件甚至会直接影响到系统的正常运行,例如文件操作的 read write open , 标准输出的 printf , 内存管理的 malloc

在 Ubuntu 系统下,libc.so.6 是 glibc 的库文件,可直接执行该库文件查看版本,在主机上执行如下命令:

/lib/x86_64-linux-gun/libc.so.6

lib
在学习 C 语言的时候很好奇标准库是如何实现的,但是奈何 Window 下的 C 库并不开源.
在 Linux 下是开源的,可以直接 glibc 的源代码,甚至可以加入开发社区贡献自己的代码.
glibc 的官网地址为: https://www.gnu.org/software/libc/ , 可在该网站中下载源代码来学习.

# GCC 编译过程

# 基本语法

GCC 基本命令如下:

gcc [-option] fileName

常用 option:

  • -o : 小写字母 o , 指定生成可执行文件的名字,不指定的化生成的可执行文件名为 a.out
  • -E : 只进行预处理,不进行编译,也不进行汇编。简单来说就是去除头文件,替代宏定义。生成 .i 文件
  • -S : 只编译,不汇编。生成的是 .s 汇编文件
  • -c : 编译并进行汇编,不进行连接。生成 .o 文件
  • -g : 生成可执行文件待调试信息,便于 gdb 调试
  • -Ox : 大写字母 O 加数字,设置程序优化等级,例如 -O1 , -O2 , -O3 , 数字越大的代码优化等级越高,编译出来的程序一般会越小,但是越小的程序约有可能执行异常. ( i++ 优化的你都不认识)

# 编译过程

一般来说我们使用 gcc 编译单个文件基本上像这样 gcc hello.c -o hello , 就可以获得到可执行文件 hello.out , 事实上编译是可以分步的。特别是编译多文件的时候,我们会把文件先编译为 .o 文件用于链接,最后才去生成可执行文件.

# 一般来说:
gcc hello.c -o hello
# 等价于以下步骤
# 预处理
gcc -E hello.c -o hello.i
# 编译
gcc -S hello.i -o hello.s
# 汇编
gcc -c hello.s -o hello.o
# 链接
gcc hello.o -o Hello

GCC 编译工具链在编译一个 C 源文件时需要经过以下 4 步:

  1. 预处理,在预处理过程中,对源代码文件中的文件包含 (include)、预编译语句 (如宏定义 define 等) 进行
    展开,生成.i 文件。可理解为把头文件的代码、宏之类的内容转换成更纯粹的 C 代码,不过生成的文件
    以.i 为后缀.
  2. 编译,把预处理后的.i 文件通过编译成为汇编语言,生成.s 文件,即把代码从 C 语言转换成汇编语言,
    这是 GCC 编译器完成的工作.
  3. 汇编,将汇编语言文件经过汇编,生成目标文件.o 文件,每一个源文件都对应一个目标文件。即把汇编
    语言的代码转换成机器码,这是 as 汇编器完成的工作.
  4. 链接,最后将每个源文件对应的.o 文件链接起来,就生成一个可执行程序文件,这是链接器 ld 完成的
    工作

# 交叉编译工具链

先来看两个概念:

  • 本地编译:编译器和目标程序都是相同架构的编译过程
  • 交叉编译:编译器和目标程程序运行在不同架构下的编译过程

疑惑吗?交叉编译存在的意义是什么?
我们来设想一下,我们在 i.MX6ull 上编译 一个庞大的工程,那么我们就需要一个编译器,还需还对应的工程文件的存储空间,调试空间... 更致命的是,C/C++ 这种项目,随随便便一编译都是好几 s, 这还是在我们性能超强的多核 CPU 上.
咱的 ARM 架构的处理器资源和性能都无法和 x86 平台相比。尤其是对于 MCU 连 OS 都不配拥有,和谈编译器。有了交叉编译,我们就可以在 其他平台上使劲霍霍 CPU 了.

其实 MCU 开发的过程就是交编译,无论是 IAR, CCS 还是 keil 都是在 x86 上开发后下载到板子里然后执行.
PS: 能进行架构 "交叉" 编译的编译器被称为交叉编译器 (Corss Compiler)

# 编译链安装

Linux 下:

sudo apt install gcc-arm-linux-gnueabihf -y

查看一下版本 和 相关编译链
arm
version

使用 readelf -a hello 查看 可执行文件信息
x86 架构
x86_64
ARM 架构
ARM

# 编译器选择

除了我们安装的版本之外,编译器还有很多版本。目前来说大部分 ARM 开发者使用的都是由 Linaro 组织提供的交叉编译器,包括前面 APT 安装的 ARM-GCC 工具链也是来源于 Linaero.
Linaro 是由 ARM 公司发起,与其他 ARM SOC 公司共同投资的非盈利组织.
ARM 交叉编译链可以在这里找到: 传送门
version os
platform

虽然编译器的命名没有严格的规则,但他们的名字中一般包含我们最为关心的内容,可以根据名字来选择要使用的编译器:
arch [-os] [-(gun)eabi(hf)] -gcc
其中各段文字意义如下表所示.
GCC 命名格式:

字段含义
arch目标芯片
gnuC 标准库类型
eabi应用二进制接口
hf浮点模式
arm-linux-gnueabihf-gcc 编译器为例来分析一下.
  • arm: 芯片架构为 ARM
  • linux: 目标操作系统为 Linux
  • gnu: 使用 GNU 的 C 语言标准库即 glibc
  • eabi: 使用嵌入式应用二进制接口 (eabi)
  • hf: 编译器浮点模式为硬浮点 hard-float.
    相对于 arm-linux-gnueabi-gcc 查遍仅仅在于是否使携带 hf , 像 M3 内核的处理器就不能使用 hf , 其本身不带 fpu

# 相关字段详细说明

# 目标芯片架构

目标芯片架构就是指交叉编译生成的程序运行的平台,如 ARM、MIPS, 其中 ARM 交叉编译又分为 ARMv7、ARMv8 以及 aarch64 架构.
i.MX6ull 的内核为 Cortex-A7, 使用的是 ARMv7 架构.

# 大小端

指目标芯片的大小端模式,i.MX6ull 使用的是小端模式,若是大端模式 (big edian), 编译器名字中会带 beeb 字段进行标注

# 目标操作系统

目标操作系统表示编译后程序运行的系统,主要由 Linux 或 bare-metal (裸机) 两种, arm-linux-guneabigcc 表示它的进程目标运行环境为 linux, 程序可以使用 linux 下的 C 标准库或 Linux 内核提供的 API, 例如 fork 等进程函数。而 arm-eabi-gccarm-none-eabi-gcc 表示它们的目标程序运行在无操作系统的环境中.
严格来说,编译 Linux 应用程序时应该使用带 linux 的编译器,而编译 uboot 、裸机程序时,应该使用 bare-metal 类型的裸机编译器.

# C 标准库

C 标准库一般 gnu、uglibc 等,分别表示 GNU 的 glibc 和 uclibc 库. C 库取决于系统,不过二者是相互兼容的,基本不需要特别注意。除了裸机开发之外不带 c 库,其他编译器 c 库都是 glibc 的库.

# 应用二进制接口

应用二进制接口 (Application Binary Interface), 描述了应用程序和操作系统之间或其他应用程序的低级接口.
在编译器中主要有 abieabi 两种类型, abi 通常应用在 x86 架构上,而 eabi 表示 emabi abi , 即嵌入式架构,如 ARM、MIPS 等

# 浮点模式

部分 ARM 处理器带浮点运算单元,代码需要进行浮点运算时若交给 fpu 处理,可以加快运算速度。编译器针对浮点运算的不同处理情况提供了以下几种模式:

  • hard: 硬件浮点类型 (hard-float), 采用 fpu 参与浮点运算.
  • soft: 软浮点类型 (soft-float), 即使有 fpu 运算单元也不使用,使用软件模拟
  • softfp: 允许使用浮点指令,但是保持与软浮点 ABI 的兼容性

PS: ABI 即 application binary interface , 即编译器将 c 代码编译成汇编代码时使用的一种规则

# 不同编译器对程序的影响

这里主要测试一下软浮点编译的动态编译和静态编译
先来理解一下两个概念:

  • 静态链接:在编译阶段就会把所有用到的库打包到自己的可执行程序中,其优点是具有较好的兼容性,不依赖外部环境,但是生成的程序比较大.
  • 动态链接:在应用程序运行时,链接器去加载外部的共享库,并完成共享库和动态编译程序之间的链接。不同的程序可以共用代码库,节省内存空间.

大道五十,天衍四十九,人遁其一!