Skip to content

LV005-GCC简介

一、 GCC 简介

1. GNU?

我们先来看一下 GNU,官网是这么说的:

GNU 是一个 自由软件 操作系统—就是说,它尊重其使用者的自由。GNU 操作系统包括 GNU 软件包(专门由 GNU 工程发布的程序)和由第三方发布的自由软件。GNU 的开发使你能够使用电脑而无需安装可能会侵害你自由的软件。

GNU 是一个类 Unix 操作系统。它是由多个应用程序、系统库、开发工具乃至游戏构成的程序集合。GNU 的开发始于 1984 年 1 月,称为 GNU 工程。GNU 的许多程序在 GNU 工程下发布;我们称之为 GNU 软件包

  • 自由软件运动

自由软件运动致力于通过自由软件使计算机用户获得自由计算的权利。自由软件的用户可以自主控制自己的计算。非自由软件使用户受制于软件开发者。现在自由软件运动远远不仅是开发 GNU 系统。

  • 自由软件意味着使用者有运行、复制、发布、研究、修改和改进该软件的自由。

自由软件关乎自由,而非价格。要理解这个概念,你应该考虑 “free” 是 “言论自由(free speech)”中的“自由”;而不是 “免费啤酒(free beer)”中的“免费”。

更精确地说,自由软件赋予软件使用者 四项基本自由

(1)不论目的为何,有运行该软件的自由(自由之零)。

(2)有研究该软件如何工作以及按需改写该软件的自由(自由之一)。取得该软件源代码为达成此目的之前提。

(3)有重新发布拷贝的自由,这样你可以借此来敦亲睦邻(自由之二)。

(4)有向公众发布改进版软件的自由(自由之三),这样整个社群都可因此受惠。取得该软件源码为达成此目的之前提。

2. GCC 编译工具链

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

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

在很多场合下会直接用 GCC 编译器来指代整套 GCC 编译工具链。

3. GCC 编译器是什么?

GCC 全称为 GNU CC , GNU 项目中符合 ANSI C 标准的编译系统 ,可以编译如 C 、 C++ 、 Object C 、 Java 、 Fortran 、 Pascal 、 Modula-3 和 Ada 等多种语言。

它是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一般的编译器相比平均效率要高 20%~30% 。它是一个交叉平台编译器 ,适合在嵌入式领域进行开发编译。GCC 还能运行在不同的操作系统上,如 Linux 、 Solaris 、 Windows 等。

GCC 支持的文件后缀名解释如下:

.c C 原始程序
.C/.cc/.cxx C++原始程序
.h 预处理文件(头文件)
.i 已经过预处理的 C 原始程序
.ii 已经过预处理的 C++原始程序
.s/.S 汇编语言原始程序
.o 目标文件
.a/.so 编译后的库文件
.m Objective-C 源代码文件

4. 怎么查看版本?

在 Ubuntu 系统下系统默认已经安装好 GCC 编译器(若是没安装,也可以直接使用 apt-get 等相关命令安装),可以通过如下命令查看 Ubuntu 系统中 GCC 编译器的版本及安装路径:

  • gcc --version
shell
sumu@sumu-virtual-machine:~$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  • gcc -v
shell
sumu@sumu-virtual-machine:~$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.2' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-9QDOt0/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2)

这个命令的显示信息更详细:

(1)“Target: x86_64-linux-gnu”表示该 GCC 的目标平台为 x86_64 架构(Intel、AMD 的 CPU), 表示它编译生成的应用程序只适用于 x86 架构,不适用于 ARM 开发板平台。

(2)“gcc version 9.4.0”表明该 GCC 的版本为 9.4.0,部分程序可能会对编译器版本有要求, 像编译指定版本的 uboot、Linux 内核的时候可能会对 GCC 有版本要求。

5. 安装在哪?

可以使用 which gcc 查看:

shell
sumu@sumu-virtual-machine:~$ which gcc
/usr/bin/gcc

二、Binutils 工具集

1. 工具简介

Binutils(bin utility),是 GNU 二进制工具集,通常跟 GCC 编译器一起打包安装到系统,它的官方说明网站地址为:Binutils - GNU Project - Free Software Foundation

在进行程序开发的时候通常不会直接调用这些工具,而是在使用 GCC 编译指令的时候由 GCC 编译器间接调用。下面是其中一些常用的工具:

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

2. 这些工具都在哪?

系统默认的 Binutils 工具集位于/usr/bin 目录下,可使用如下命令查看系统中存在的 Binutils 工具集:

shell
sumu@sumu-virtual-machine:~$ ls /usr/bin/ | grep linux-gnu-
#...
x86_64-linux-gnu-addr2line
x86_64-linux-gnu-ar
x86_64-linux-gnu-as
#...
x86_64-linux-gnu-gcc
x86_64-linux-gnu-gcc-9
#...
x86_64-linux-gnu-ld
#...
x86_64-linux-gnu-objcopy
x86_64-linux-gnu-objdump
#...
x86_64-linux-gnu-readelf
x86_64-linux-gnu-size
x86_64-linux-gnu-strings
x86_64-linux-gnu-strip
#...

三、C 库

1. C 标准库

C 语言标准库是由 C 语言标准(如 ANSI C、C99、C11 等)定义的一组函数和宏的集合。这些函数和宏被组织在多个头文件中,开发者可以通过包含这些头文件来使用它们高效地完成常见编程任务,如输入输出、字符串操作、数学计算、内存管理等。标准库的设计目标是提供高效、可移植的工具,帮助开发者编写跨平台的 C 程序。我们写的第一个 helloworld 中就用到了 C 标准库的 stdio.h 这个头文件,我们还会经常见到这些:

  • <stdio.h>:提供输入输出功能,如 printfscanffopen 等。
  • <stdlib.h>:提供内存管理、随机数生成、程序终止等功能,如 mallocfreeexit 等。
  • <string.h>:提供字符串处理功能,如 strcpystrlenstrcmp 等。
  • <math.h>:提供数学函数,如 sincossqrt 等。
  • <time.h>:提供时间和日期处理功能,如 timeclock 等。

2. 为什么需要标准库

(1)不同的操作系统和硬件架构,其底层操作方式完全不同。在 Windows 上打印文字 可能需要调用 WriteFile API。在 Linux 上打印文字,可能需要调用 write 系统调用。在单片机上打印文字,可能需要操作串口寄存器。

(2)避免重复造轮子(提升开发效率)。有些功能极其通用且复杂,如果每个程序员都要自己写一遍,将是巨大的浪费。例如 内存管理,实现一个高效、无碎片的 malloc(内存分配)算法非常困难。还有 字符串处理,复制字符串、查找子串,逻辑繁琐。C 标准库提供了这些经过几十年优化、测试的高质量代码,开发者可以直接调用,专注于业务逻辑。

(3)定义了 C 程序的运行环境。C 标准库不仅仅是一堆函数,它还定义了程序运行的基础规则,例如 程序入口,规定了程序从 main() 函数开始。还有 错误处理,提供了 errno 机制来报告错误。

总的来说,C 语言语法就像乐高积木的拼接规则,C 语言标准库就像乐高套装里的基础零件包(轮子、窗户、门等),我们自己的代码逻辑就是使用这些规则和基础零件包来搭建我们想要的城堡。如果我们没有那个基础零件包(标准库),就得自己用原始塑料去切一个轮子、磨一块玻璃做窗户。有了标准库,我们可以直接拿来用,专注于搭建城堡。

3. 有哪些标准库的实现

3.1 windows 标准库实现

Windows 标准库实现是和微软的官方编译器 Visual Studio 绑定在一起的,该标准库曾被称为 C/C++ Run-time Library (CRT)

从 Windows95 开始,微软以 MSVCRT+版本号.DLL 的命名实行来发布,到了 1997 年,将其简化为了 MSVCRT.DLL

从 Visual Studio 2015 之后,Windows 中 C/C++标准库被称为了 Universal C Runtime Library (Universal CRT,简称 UCRT),即 UCRTBASE.DLL,此后 Windows 标准库开始同 Win10 一起发布。

参考:C/C++ 语言和标准库参考 | Microsoft Learn

3.2 linux 标准库实现

可以看一下 Comparison of C/POSIX standard library implementations for Linux 这里,会发现这里对比了四种标准库 musl、uClibc、dietlibc 和 glibc,网上查资料的话还会有 Newlib,这里简单做一个梳理:

名称 全称 典型应用场景 核心特点 大小/体积
glibc GNU C Library 桌面发行版、服务器、高性能计算
(Ubuntu, CentOS 默认)
功能最全,兼容性最好 。支持所有 POSIX 标准、NPTL 线程、国际化。代码庞大,启动稍慢。 最大
~2MB - 3MB
musl musl libc 嵌入式 Linux、容器镜像、轻量级发行版
(Alpine Linux, OpenWrt)
轻量、快速、静态链接友好 。代码简洁,MIT 协议,内存占用低,数学库精度高。
~600KB - 800KB
uClibc uClibc (Microcontroller C Library) 旧式嵌入式设备、路由器
(早期 OpenWrt)
专为嵌入式设计 。配置灵活,可裁剪。原版已停止维护,主流分支为 uClibc-ng 较小
~300KB - 500KB
dietlibc diet libc 极度资源受限设备、单片机环境 极度精简 。专注于生成最小的静态二进制文件。兼容性较差,移植需修改代码。 极小
几十 KB
Newlib Newlib 裸机开发 、RTOS (实时操作系统)
(STM32, ARM Cortex-M)
非 Linux 专用 。专为无 OS 环境设计,需开发者自行实现底层硬件接口。 极小
几十 KB

我们写程序,需要用到很多 c 语言的库函数。所有的库函数加起来,就是对应的 C 语言(标准)函数库。

目前在普通 GNU/Linux 系统中所用的 C 语言标准库,叫做 glibc。其功能很全,它包含了很多桌面和服务器才需要的功能,占用大量 Flash 和 RAM。它的函数很多,代码太多,编译出来的函数库的大小也很大。

嵌入式系统中,也需要 C 语言写代码实现特定功能,也需要用到 C 语言函数库,但是由于嵌入式系统中,一般资源比较有限,所以不适合直接使用(太占用资源的)gLibc。所以有人就又(没有参考 glibc,而是从头开始,)重新实现了一个用于嵌入式系统中的,代码量不是很大的,资源占用相对较少的,C 语言函数库,叫做 uClibc,它是一个独立开源项目并不是GNU项目。并且,uClibc 不支持 MMU(内存管理单元)。uClibc 比 glibc 占用的资源小,虽然 uClibc 和 glibc 在已有的接口上是兼容的,而且采用 uClibc 编译应用程序比采用 glibc 编译应用程序要更方便,但是 uClibc 并没有包括 glibc 中的所有接口实现,因此有些应用可能在 uClibc 中不能编译。

Tips:glibc和uclibc是否兼容?这个问题需要从两个维度来看:源代码级兼容二进制级兼容。源代码级兼容性上大部分兼容,但不完全兼容。二进制级兼容性上不兼容。我们不能拿针对 glibc 编译好的动态库或可执行文件,直接放到 uClibc 的系统上去运行,反之亦然。

后来,glibc 的开发者,又推出个 Embedded glibc,简称 eglibc,其主要目的也是将 glibc 用于嵌入式领域,glibc 在源码和二进制上兼容。EGLIBC 的目标很明确:在保持 glibc 兼容性的前提下,让它更适合嵌入式。但是后来 glibc 开始采纳 EGLIBC 的许多改进,例如 glibc 开始引入更好的配置选项和裁剪机制,改进了交叉编译支持。所以 eglibc 后来慢慢的就退出了。

这里有一个 eglibc 的 github 仓库:eglibc/README at master · Xilinx/eglibc · GitHub

4. glibc 库简介

glibc 库是 GNU 组织为 GNU 系统以及 Linux 系统编写的 C 语言标准库,因为绝大部分 C 程序都依赖该函数库,该文件甚至会直接影响到系统的正常运行,例如常用的文件操作函数 read、write、open,打印函数 printf、动态内存申请函数 malloc 等。

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

shell
sumu@sumu-virtual-machine:~$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.16) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 9.4.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

表示本系统中使用的 glibc 是 2.31 版本,是由 GCC 9.4.0 版本的编译器编译出来的。学习 C 语言的时候,可能特别好奇 printf、malloc 之类的函数是如何实现的, 但是在 Windows 下的 C 库是不开源的,无法查看,而在 Linux 下, 则可以直接研究 glibc 的源代码,甚至加入开发社区贡献自己的代码,glibc 的官网地址为: The GNU C Library - GNU Project - Free Software Foundation,可在该网站中下载源代码来学习。

二、编译器

了解了那么多 GCC 的相关基础知识,那么什么是编译器呢?

1. 什么是编译器

编译器,作为计算机科学与软件工程的核心工具,将人类可读的高级语言源代码转换成机器可执行的低级指令。这一复杂的过程涉及多个阶段,包括词法分析、语法分析、语义分析、优化和代码生成等。

2. 编译器由哪些组成?

一个编译器主要的组成部分如下所示:

分析器 分析器将源语言程序代码转换为汇编语言。因为要从一种格式转换为另一种格式(C 到汇编),所以分析器需要知道目标机器的汇编语言。
汇编器 汇编器将汇编语言代码转换为 CPU 可以执行字节码。
链接器 链接器将汇编器生成的单独的目标文件组合成可执行的应用程序。链接器需要知道这种目标格式以便工作。
标准 C 库 核心的 C 函数都有一个主要的 C 库来提供。如果在应用程序中用到了 C 库中的函数,这个库就会通过链接器和源代码连接来生成最终的可执行程序。

3. 编译器要做哪些事?

编译器主要由前端和后端两大部分组成。前端负责处理源代码,包括词法分析、语法分析和语义分析,生成中间表示(IR)。后端则根据中间表示生成目标代码,包括优化和代码生成等步骤。

4. 编译器的核心技术

(1)词法分析与正则表达式:编译器使用正则表达式或有限自动机进行词法分析,识别源代码中的不同成分。

(2)语法分析与解析算法:编译器通常采用递归下降解析或基于上下文的无关文法(CFG)进行语法分析,构建 AST(抽象语法树)。

抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。

抽象语法树在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。

(3)语义分析与类型系统:编译器通过检查类型、作用域、符号表等来确保源代码的语义正确性。

(4)中间表示与优化技术:编译器使用 SSA(静态单一赋值)形式、控制流图等中间表示,并采用各种优化技术如常量折叠、死代码消除、循环展开等来提高代码质量。

(5)目标代码生成与机器模型:编译器根据目标机器的指令集和特性生成高效的机器代码。

参考资料:

编译器:原理与技术的奥秘-阿里云开发者社区 (aliyun.com)

C 语言标准库概述

(7 封私信) 程序员应如何理解标准库 - 知乎

C 函数库 ——libc, glibc, eglibc, uClibc, newlib_newlib glibc-CSDN 博客