一、GCC、glibc和GNU C的关系
GCC 全程 GUN Compiler Collection,是 GNU 项目的一部分,主要是一套编译器工具集,支持多种编程语言,包括 C、C++、Object-C、等。GCC 最初作为 GUN 操作系统的官方编译器,用于编译 GNU/Linux 系统和应用程序。它是自由软件,遵循 GNU General Public Licensse(GUN GPL)发布。
GCC 的主要作用是将源代码编译成机器语言,生成可执行文件或库文件。它也提供了一些优化选项,可以在编译过程中优化代码,提供程序运行的效率。
glibc,全程 GNU C Library,是 GNU 项目的一部分,是 C 语言标准库的一个实现版本,为 C 语言提供了标准的 API,包括输入输出处理、字符串操作、内存管理等。glibc 是 Linux 系统上最常用的 C 标准库实现之一,它实现了 C 标准规定的所有标准库函数以及 POSIX(可移植操作系统接口)的扩展。
GCC 使用 glibc 作为其 C 语言程序的标准库。当 GCC 编译 C 语言程序时,程序中使用的标准库函数是通过 glibc 提供的。GNU C 是 GCC 中实现的 C 语言的一个版本,包含了对 C 语言标准的支持以及 GUN 特有的扩展,这些扩展可以在使用 GCC 编译程序时通过特定的编译选项启动。
总的来说,GCC 是编译器,负责将源代码转换成可执行代码;glibc 是运行时库,提供程序运行所需的标准库函数和操作系统服务的接口;而 GNU C 则定义了 GCC 支持的 C 语言的标准和扩展。这三者共同构成了 GUN/Linux 系统下开发和运行 C 语言程序的基础。
二、什么是POSIX标准
POSIX,全程为 “可移植操作系统接口”(Portable Operating System Interface),是一组标准,用来确保各种不同的操作系统能够提供相同的应用编程接口(API)。POSIX 标准的主要目的是促进应用软件与多种类型的操作系统之间的兼容性。通过遵循 POSIX 标准,开发人员可以编写能够在各种不同系统上运行的程序,而无需对程序进行大量修改。
POSIX 标准的主要内容包含:
- 系统调用和库:定义了操作系统应提供的核心服务,如文件系统操作、进程管理和线程控制。
- Shell 和工具:规定了标准命令行接口和一系列基本工具,如 awk、echo 等。
- 程序接口:包括语言、函数库等接口规范。是程序能够在任何遵循 POSIX 的操作系统上运行。
三、一个简易的C程序
首先,我们需要在 Linux 系统上安装 GCC 编译器来编译 C 程序。
sudo apt install -y gcc
安装完 GCC 后,我们可以在终端中输入命令查看 GCC 版本来确定 GCC 是否安装成功。
gcc -v
我们新建一个 main.c 文件,用来编写 C 程序。
#include <stdio.h>int main(void)
{printf("Hello world!\n");return 0;
}
编写完 C 程序之后,我们进入该目录,在终端中使用 GCC 编译程序。
gcc -o a.out main.c
然后,我们运行编译生成的可执行文件。
./a.out
四、一个稍微复杂点的C程序
我们新建一个 hello.h 头文件,它的内容如下:
#ifndef __HELLO_H__
#define __HELLO_H__#include <stdio.h>void say_hello(char *name);#endif // !__HELLO_H__
然后,我们在新建一个 hello.c 源文件,它的内容如下:
#include "hello.h"void say_hello(char *name)
{printf("Hello %s, nice to meet you!\n", name);
}
接着,我们在修改 main.c 源文件,它的内容如下:
#include "hello.h"int main(void)
{say_hello("Sakura");return 0;
}
此时,我们在终端中运行 GCC 编译程序时需要将链接的源码文件都编译上。
gcc -o a.out main.c hello.c
然后,我们运行编译生成的可执行文件。
./a.out
五、GCC的编译过程
【1】、预处理命令
在 C 语言编译工程中,预处理是其中的第一个阶段,它的主要目的是处理源代码文件的预处理指令,将它们转换成编译器可以识别的形式。预处理主要包含 宏替换、文件包含、条件编译、注释移除 等几种任务。预处理的输出通常是经过预处理后的源代码文件,它会被保存成一个临时文件,并作为编译器的输入。预处理器处理后的文件通常会以原始源文件大,以为它会展开宏和包含其它文件的内容。
gcc -E 源文件.c -o 预处理后的文件.i
-E
:Expand(展开)的缩写,该选项指定 gcc 执行预处理操作。-o
:output(输出)的缩写,该选项用于指定输出文件名。- 预处理后的文件通过以
.i
(intermediate,中间的缩写)作为后缀。
【2】、编译
编译阶段,编译器会将经过预处理的源代码文件转换成汇编文件。在这个阶段,编译器会将源代码翻译成机器能够理解的中间代码,包括 词法分析、语法分析、语义分析 和 优化 等过程。编译器会检查代码的语法和语义,生成对应的汇编代码。编译阶段是整个编译过程中最复杂和耗时的阶段之一,它对源代码进行了深入的分析和转换,确保了程序的正确性和性能。
gcc -S 预处理后的文件.i -o 汇编文件.s
-S
:Source(源代码)的缩写,该参数指定 GCC 将预处理后的源码编译成汇编语言。- 通常编译后的汇编文件以
.s
(Assembly Source,汇编源码)作为后缀。
【3】、汇编
汇编阶段是 C 语言编译过程中的重要阶段,它将编译器生成的中间代码或汇编代码转换成目标机器的机器语言代码,也就是目标代码。这个阶段由汇编器(Assembler)完成,其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务:符号解析、指令翻译、地址关联、重定向、代码优化。最终,汇编器会将翻译和处理后的目标代码输出到目标文件中,用于后续的链接和生成可执行程序或共享库文件。
gcc -c 汇编文件.s -o 机器码文件.o
【4】、链接
链接阶段,由链接器完成。链接器将各个目标文件以及可能用到的库文件进行链接,生成最终的可执行程序。在这个阶段,链接器会解析目标文件中的符号引用,将将它们与符号定义进行匹配,以解决符号的地址的关联问题。链接器还会处理全局变量的定义和声明,解决冲定位问题,最终生成可执行文件或共享库文件。
C 语言的链接共有三种方式:静态链接、动态链接 和 混合链接。三者的区别就在于链接器在链接过程中对程序中库函数调用的解析。
静态链接 将所有目标文件和所需的库在编译时一起打包进最终的可执行文件。库的代码被复制到最终可执行文件中,使得可执行文件变得自包含,不需要在运行时查找或加载外部库。
gcc -static 机器码文件.o -o 可执行文件
-static
:该参数指示编译器进行静态链接,而不是默认的动态链接。
动态链接 时,库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为它们共享系统级的库代码。与静态链接不同,库代码不会包含在可执行文件中。
gcc 机器码文件.o -o 可执行文件
我们也可以将自己编写的部分代码处理为动态库。
gcc -fPIC -shared -o lib共享库.so 机器码文件.o
-fPIC
:告诉编译器为 “位置无关代码”(Position Independent Code)生成输出。它允许共享库被加载到内存中的任何位置,而不影响其执行。这是因为位置无关代码使用相对地址而非绝对地址进行数据访问和函数调用,使得库在被不同程序加载时能够灵活地映射到不同的地址空间。-shared
:这个选项指示 GCC 生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用,节省了内存和磁盘空间。- 按照惯例,Linux 下的共享库名称以
lib
开头,扩展名为.so
(表示共享对象)。
我们要链接自己的共享库的语法格式如下:
gcc 机器码.o -L 共享库路径 -l 共享库 -o 可执行文件
如果我们执行上述代码报错时,可能是因为没有找到动态链接库文件,导致链接失败无法执行。Linux 的默认动态链接库文件夹是 /lib
和 /usr/lib
,而我们自己的共享库不在其中,所以我们在执行过程中指明额外的动态链接库文件夹。
LD_LIBRARY_PATH=共享库路径 ./共享库
混合链接 的某些库静态链接,而其它库动态链接。这种方式结合了静态链接和动态链接的优点。
ar crv 静态库.a 机器码文件.o
ar
:归档命令,用于处理静态库文件。crv
:ar 命令的选项,由三个字符组成,每个字符代表一个选项。c
:创建归档文件。如果指定的归档文件不存在,ar 会创建它。r
:替换归档文件中现有的文件或者向归档文件中添加新文件。v
:详细模式(verbose mode),在处理文件时显示详情信息。使用这个选项,ar 会列出它正在执行的操作,包括哪些文件被添加或替换。
- 按照惯例,Linux 下的静态库文件名以 lib 开头,并以
.a
作为文件扩展名。