一、初识C语言的编译流程
一、Linux 下的 C语言开发
1、GCC、glibc、GNU C解释
1.1 GCC
全程 GNU Compiler Collection,是 GNU 项目的一部分,主要是一套编译工具集,支持多种编程语言,包括 C、C++等。GCC 最初作为 GNU 操作系统的官方编译器,用于编译 GNU/Linux 系统和应用程序。
1.2 glibc
全程 GNU C Library,是 GNU 项目的一部分,是 C 语言标准库的一个实现版本,为 C 语言提供了标准的 API,包括输出输出、字符串操作、内存管理等。glibc 是 Linux 系统上最常用的 C 标准库实现之一,实现了 C 标准库的所有标准库函数以及 POSIX(可以直操作系统接口)的扩展。
1.3 GNU C
通常指的是 GNU 项目的 C 语言变成标准,特别是在 GCC 中实现的 C 语言的扩展和特性。包括 ANSI C 的所有特性以及对 C99、C11等现代 C 标准的支持和一些 GNU 特有的扩展。
1.4 三者关系
glibc 是标准库,GCC 使用 glibc这个标准库进行编译 C 语言程序,GNU C 是 GCC中实现的一个 C 语言的版本。
总结:GCC 是编译器,把源码转换为可执行的代码;glibc 是运行库,提供运行所需的标准函数和操作系统服务接口;而 GNU C 则定义了 GCC 支持的 C 语言的标准和扩展。
2、POSIX
全称“可移植操作系统接口”(Portable Operating System Interface),是一组标准,用来确保各种不同的操作系统能够提供相同的应用编程接口(API)。目的是促进应用软件与多种操作系统之间的兼容性,通过 POSIX 标准,开发人员可以编写能够在各种系统上运行的程序,而无需针对不同操作系统做大量修改。
主要内容:
(1)系统调用和库:定义了操作系统应提供的核心服务,如文件系统操作、进程管理等。
(2)Shell 和工具:规定了标准命令接口和一系列基本工具,如 awk、echo 等。
(3)程序接口:包括语言、函数库等接口规范,使程序能够在任何遵循 POSIX的操作系统上运行。
3、C语言的编译过程
C语言的编译是一个多过程的,文件类型也从==.c -> .i -> .s -> .o -> 可执行文件==一步一步转换

Linux 的 C 语言开发,一般选择 GCC 工具链进行编译,通过下面的例子来演示 GCC 如何使用:
main.c
#include "hello.h"
int main()
{
say_hello();
return 0;
}
/* ------------------ */
hello.h
#ifndef __HELLO_H__
#define __HELLO_H__
void say_hello();
#endif
/* ------------------ */
hello.c
#include "hello.h"
#include <stdio.h>
void say_hello()
{
printf("Hello world!\n");
}
采用如下命令将编译过程被浓缩成一步,编译得到可执行文件并执行:
gcc main.c hello.c -o main
# -o:output的缩写,表示输出,用于指定输出文件名
./main
编译过程被浓缩成一步命令。但实际上,c 语言的编译是有一个多步骤流程的。
3.1 预处理
在 C 语言编译过程中,预处理是第一个结段,主要目的就是处理源代码中的预处理指令,将他们转换成编译器可以识别的形式。主要包括宏替换、文件包含、条件编译、注释移除等任务。预处理的输出通常是经过预处理后的源代码文件,会被保存成一个临时文件,并作为编译器的输出。预处理器处理后的文件通常会比原始的源文件大,因为它会展开宏和包含其他文件的内容。
用以下命令进行对源文件的预处理:
gcc -E hello.c -o hello.i
gcc -E main.c -o main.i
# -E:Expand的缩写,该参数指定gcc执行预处理操作
# .i:intermedi的缩写,预处理后的源文件通常以.i作为后缀
3.2 编译
编译阶段,编译器会将经过预处理的原代码文件转换成汇编代码。这个阶段,编译器会将原代码翻译成机器能够理解的中间代码,包括词法分析、语法分析、语义分析和优化等过程。编译器会检查代码的语法和语义,生成对应的汇编代码。编译阶段是整个编译过程中最复杂和耗时的阶段之一,他会对原代码进行深入的分析和转换,确保了程序的正确和性能。
以下命令对上一步生成的预处理文件进行编译:
gcc -S hello.i -o hello.s
gcc -S main.i -o main.s
# -S:Source的缩写,该参数指定gcc将预处理后的源码编译为汇编语言
# .s:Assembly Source(汇编源码)的缩写,通常编译后的汇编文件以.s作为后缀
3.3 汇编
汇编阶段将编译器生成的中间代码或汇编代码转换成目标机器的机器语言代码,也就是目标代码。这个阶段由汇编器(Assembler)完成,其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务:符号解析、指令解析、地址关联、重定位、代码优化。最终,汇编器会将翻译和处理后的目标代码输出到目标文件中,用于后续的链接和生成可执行程序或共享库文件。
以下命令对汇编文件进行汇编:
gcc -c main.s -o main.o
gcc -c hello.s -o hello.o
# -c:Compile的缩写,该参数指定gcc将汇编代码翻译为机器码,但是不做链接。此外,该参数也可以用于将.c文件直接处理为机器码,同样不做链接
# .o:Object(目标文件)的缩写,通常汇编得到的机器码文件以.o为后缀
3.4 链接
此阶段由链接器完成。链接器会将哥哥目标文件以及可能用到的库文件进行链接,生成最终可执行程序。在这个阶段,链接器会解析目标文件中的符号引用,并将它们与符号定义进行匹配,以解决符号的地址关联问题。链接器还会处理全局变量的定义和声明,解决重定位问题,最终生成可执行文件或共享库文件。
在 say_hello () 函数中调用了 printf () 函数,这个函数是在 stdio.h 中声明,来源于 glibc 库,函数的实现在 glibc 的二进制组件中,通常是在共享库(libc.so)或静态库 (libc.a) 文件中。因此除了要连接 main.o、hello.o,还需要和 glib.c 库的文件连接。通常情况下 C 语言的链接方式由三种:静态链接、动态链接和混合链接。三者的区别就在于链接器在连接过程中对程序中库函数调用的解析。
- 静态连接
将所有目标文件和所需的库在编译时一并打包进最终的可执行文件。库中的代码被复制到最终的可执行文件中,使得可执行文件变得自暴寒,不需要在运行时查找或加载外部库。
gcc -static main.o hello.o -o main
# static:该参数指示编译器进行静态链接,而不是默认的动态链接。使用这个参数,GCC会尝试将所有用到的库函数直接链接到最终生成的可执行文件中,包括C标准库(libc)、数学库(libm)和其他任何通过代码引用的外部库。
- 动态链接(GCC默认方式)
库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为它们共享系统级的库代码。与静态链接不同,库代码不包含在可执行文件中。
方式一:
不添加static关键字,gcc默认进行动态链接
gcc main.o hello.o -o main
# 没有添加-static关键字,gcc默认执行动态链接,即glibc库文件没有包含到可执行文件中
方式二:
执行下面的指令将 hello. o 编译为动态链接库 libhello. so
gcc hello.o -fPIC -shared -o libhello.so
# -fPIC:这个选项告诉编译器为“位置无关代码(Position Independent Code)”生成输出。在创建共享库时使用这个选项是非常重要的,因为它允许共享库被加载到内存中的任何位置,而不影响其执行。这是因为位置无关代码使用相对地址而非绝对地址进行数据访问和函数调用,使得库在被不同程序加载时能够灵活地映射到不同的地址空间。
# -shared:这个选项指示GCC生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用,节省了内存和磁盘空间。
# -o libhello.so:这部分指定了输出文件的名称。-o选项后面跟着的是输出文件的名字,这里命名为libhello.so。按照惯例,Linux下的共享库名称以lib开头,扩展名为.so(表示共享对象)。
# hello.o:这是命令的输入文件,即之前编译生成的目标文件。在这个例子中,GCC会将hello.o中的代码和数据打包进最终的共享库libhello.so中。
上述命令的作用是:使用 GCC,采用位置无关代码的方式,从 hello. o 目标文件创建一个名为 libhello. so 的动态共享库文件
使用动态链接库编译新的可执行文件:
gcc main.o -L ./ -lhello -o main_d
# -L ./:指定了库文件搜索路径。-L选项告诉链接器在哪些目录下查找库文件,./表示当前目录。这意味着在链接过程中,链接器将会在当前目录下搜索指定的库文件。
# -lhello:指定了要链接的库。-l选项后面跟库的名称,这里是hello。根据约定,链接器会搜索名为libhello.so(动态库)或libhello.a(静态库)的文件来链接。链接器会根据-L选项指定的路径列表查找这个库。
当前目录下只有 libhello. so 而没有 libhello. a,因此,这条命令的最终效果是动态链接当前目录下的 libhello. so 库以及默认的 glibc 库,生成可执行文件 main_d。
- 混合连接
某些库静态链接,而其他库动态链接。这种方式结合了静态链接和动态链接的优点。
通过执行下面的指令可以将 hello. o 编译为静态链接库 libhello. a:
ar crv libhello.a hello.o
# ar:归档命令,用于处理静态库文件。
# crv:ar命令的选项,由三个字符组成,每个字符代表一个选项:
# c:创建归档文件。如果指定的归档文件不存在,ar会创建它。
# r:替换归档文件中现有的文件或者向归档文件中添加新文件。如果hello.o已经在libhello.a中,它会被新版本替换;如果不存在,则会被添加。
# v:详细模式(verbose mode),在处理文件时显示详细信息。使用这个选项,ar会列出它正在执行的操作,包括哪些文件被添加或替换。
# libhello.a:要创建或更新的静态库文件的名称。按照惯例,Linux下的静态库文件名以lib开头,并以.a作为文件扩展名。
# hello.o:输入文件,即要添加到静态库libhello.a中的目标文件。此处只有一个目标文件hello.o,但ar命令支持同时指定多个文件。
利用静态库文件生成可执行的 main 文件:
gcc main.o -L ./ -lhello -o main
# -L ./:表示额外的库文件位置为当前目录;
# -lhello:表示链接libhello.a文件。注意这里要去掉开头的lib前缀和结尾的.a后缀。
编译完成后的 main 文件同样可以执行,并且不依赖于静态库 libhello.a。
静态链接 libhello. a 生成的 main 文件要大一些,这是因为 hello 库的代码被复制到了可执行文件 main 中,和动态链接相比,执行速度略高,但是二进制代码的复用性差,略微增加了二进制文件的体积。
需要注意的是,虽然静态链接了 libhello.a 库,但是 main 文件在执行时依然需要动态链接 glibc 的库。因此,这种方式实质上并非静态链接,而是混合链接。glibc 的动态库和静态库分别位于/usr/lib/x 86-64_64-linux-gnu/目录下的 libc.so 和 libc.a 文件中。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)