
目录
1.2 GCC (GNU Compiler Collection)
7.4.3.1 临时设置 LD_LIBRARY_PATH(开发调试用)
1. GCC、glibc和GNU C的关系
首先我们先举个例子,先对他们又一个概念,例如建房子:
- GNU C 规则:就像是建筑规范和设计图纸的规范。它规定了你可以使用哪些特殊的、更坚固或更方便的材料(GNU C 扩展),以及图纸应该怎么画。
- GCC 编译器:就像是施工队和建筑设备。他们根据你提供的、符合规范的图纸(源代码),使用各种工具,把原材料(你的代码)加工成房子的骨架和结构(目标文件)。
- glibc 库:就像是房子的基础设施和公共系统。比如自来水系统、电网、道路。你的房子(编译好的程序)必须连接上这些公共设施,才能正常“生活”(运行),比如打开水龙头(调用 printf)就有水,打开开关(调用 malloc)就有电(内存)。
简单概括一下:
- GNU C:是一套规则和扩展。它定义了在 GNU 项目下,C 语言应该怎么写,并提供了超越标准 C 的额外功能。
- GCC:是一个编译器。它是一段程序,其核心功能是将按照 C 语言规则(包括 GNU C 规则)写的源代码编译成机器代码或汇编代码。
- glibc:是一个核心函数库。它提供了 C 语言标准中规定的函数(如 printf, malloc)以及 Linux 系统运行所必需的核心接口(如系统调用的封装)。
1.1 GNU C
GNU C 并不是一个独立的软件,而是指 GNU 项目对 C 语言的使用方式和扩展集合,这些扩展提供了对硬件和操作系统更精细的控制,能生成更高效的代码,或者在标准 C 语法难以表达的情况下提供解决方案,它完全支持 ISO C 标准(如 C89, C99, C11 等)。
1.2 GCC (GNU Compiler Collection)
GCC 原名是 GNU C Compiler,现在已发展成一个编译器集合,支持 C, C++, Objective-C, Fortran, Ada, Go 等多种语言。我们这里特指它的 C 语言编译器部分(gcc 命令),它是一个翻译官。它的工作流程包括:预处理、编译、汇编、链接。
1.3 glibc (GNU C Library)
glibc 是 GNU 项目发布的 C 标准库的实现,它是 Linux 系统上最核心的库之一,其提供的:
- C 标准库函数: 如字符串处理(string.h)、数学计算(math.h)、输入输出(stdio.h)等。
- 系统调用封装: 应用程序不能直接调用操作系统内核的函数(系统调用)。glibc 提供了包装函数,例如 open()、read()、write() 等,这些函数内部会使用特定的指令从用户态切换到内核态,触发真正的系统调用。
- 其他必要功能: 如线程实现(pthreads)、加载器(用于在程序启动时动态链接其他库)、域名解析(DNS)等。
总的来说,GCC是编译器,负责将源代码转换为可执行代码;glibe是运行时库,提供程序运行所需的标准函数和操作系统服务的接口;而 GNU C 则定义了 GCC 支持的 C 语言的标准和扩展。
2. 在Linux上安装gcc
这里可以参考之前的一篇文章,可以先只看安装部分:
Linux应用开发·如何在Ubuntu安装、调试、运行gcc/g++,以及如何进行多文件编译(完整过程详解)_ubuntu gcc-优快云博客
3. 在Linux上安装VScode
为了方便代码的查看,我们安装一个VScode,首先输入命名:
sudo snap install code

翻译一下:

至于为什么会出现这个呢?这就相当于我们在手机上下载东西,如果不是在手机自带的应用商城下载软件,手机就会提示该软件可能存在风险,我们想要下载就只能无视风险安装,这里因为是命令行,因此没有是与不是的选项,但是上面提示了,想要安装需要加入--classic:
sudo snap install code --classic
可以看到开始下载:

下载完成:

打开方式有两种,一种直接按照图示,找到VScode的图标:

也可以直接通过命令输入code进行打开:

如果感觉英文不方便使用,可以找到按下图操作进行汉化:

下载完,重启,就可以发现已经汉化完成:

4. VScode环境搭建
我们VScode下载好了,现在想要使用它,需要搭建好环境才能使用,我们这里需要使用C语言的开发环境因此需要去搭建它,传统方法,我们需要找到扩展商店,一个一个区寻找下载,这样比较麻烦:

但是VScode是一个非常智能化的工具,我么可以如下操作,首先找到home目录,创建一个新的文件夹:

返回VScode打开该文件夹:

勾选,然后点击信任即可:

打开后在黄框内右键创建一个.c文件,会弹出右下角所示,点击安装,VScode就会将你所需要的基础扩展给你安装完:

我们回到扩展商店可以看到:

5. 运行第一个程序:Hello world
上面我们环境搭建等准备工作都完成了,下面我们来看看如何编译运行C语言程序,先编译一段打印“Hello world”的程序:
#include <stdio.h>
int main()
{
printf("Hello world!!!");
return 0;
}
编写完 Ctrl+S 保存一下:

找到图示位置点击编译,如果第一次编译,会弹出图示,需要选择一下编译器,两个编译器是同一个,只是一个显示的名字长,一个名字短一些:

编译完,找到“终端”,可以看到打印出来:

如果感觉看着不方便,换一下行就可以:

6. 如何在VScode进行多文件编译
我们在正常的工程项目当中,肯定不可能只有一个主函数,那么我们要如何实现多文件编译呢?首先我们知道对于STM32,我们使用Keil5进行编程,只需要将头文件包含进来即可,那么我们这里对于VScode可以吗,我们先根据上面创建main.c的方式创建两个文件,分别是hello.c:
#include<stdio.h>
#include"hello.h"
void Hello_World(void)
{
printf("Hello World!!!");
}
hello.h的代码:
#ifndef __HELLO_H__
#define __HELLO_H__
void Hello_World(void);
#endif
来到主函数:
#include"hello.h"
int main()
{
Hello_World();
return 0;
}
运行一下看一下:

为什么出现这个报错呢?那是因为对于VScode编译运行,是只运行当前这个文件(也就是main.c),因此其会显示不知道Hello_World的声明:

那么我们要如何进行编译呢?首先我们要去修改一些参数,找到图示位置,箭头所指向的代码表示在编译运行的时候,编译当前文件:

将上图箭头所指示位置注释掉,换成:
"*.c",

意思是编译所有.c文件,保存退出,我们回到主函数在运行一下,可以发现能够正常编译:

7. gcc编译流程介绍
我们先回到终端,通过命令查看一下HelloWorld目录下的内容,其中main*就是我们刚刚生成的编译文件:

这里的Linux命令就不在一一介绍了,详细可以了解:
我们先删掉它,可以再VScode右键删除:

也可以在终端通过命令删除:

删除完成之后,我们在终端重新生成编译文件,输入如下代码,通过gcc编译器,将main.c和hello.c两个独立的C源文件组合成一个完整可执行程序main,其能自动解析 main.c 中对 hello.c 中函数的调用,处理头文件包含和函数声明:
//gcc - GNU C 编译器
//main.c - 第一个源文件
//hello.c - 第二个源文件
//-o main - 指定输出可执行文件名为 main
gcc main.c hello.c -o main
然后通过./main执行该文件:

我们这里看到的只是一步到位的执行,那么其之间的过程都是有什么呢?我们来了解一些。gcc的编译流程可分为四个阶段:预编译(预处理)→ 编译和优化 → 汇编 → 链接。
| 文件名后缀 | 说明 | gcc参数 |
| .c | 源文件 | 无 |
| .i | 预处理后的C文件 | -E |
| .s | 编译后得到的汇编语言的源文件 | -S |
| .o | 汇编后得到的二进制文件 | -c |
注意下面这部分,只是让你理解一下gcc的编译过程,了解即可。
7.1 预处理
该阶段编译器将*.c代码的头文件编译进来,例如我们头文件声明的是:include<stdio.h>那么就会将stdio.h编译进来,用户可以使用gcc的选项“-E”进行查看,该选项的作用是让gcc在预处理结束后停止编译过程。
该阶段主要进行三件事:展开头文件、宏替换、去掉注释行。
为了更为直观的看到上面进行的三件事,我们来到VScode对mian.c文件进行一个简单的修改,加上一个宏定义以及注释行:
#include"hello.h"
#include <stdio.h>
#define num 3
//这是一个注释
int main()
{
int a = 10;
if(a<num)
{
Hello_World();
}
else
{
printf("This is gcc !!!\n");
}
return 0;
}

回到终端这边输入命令:
gcc -E hello.c -o hello.i
gcc -E main.c -o main.i
可以看到多出两个.i文件,这就是预处理完的文件:

我们通过VScode查看一下,可以发现这里注释被去掉了,并且将宏定义给替换掉了,这里因为引入了#include<stdio.h>这是一个库函数,展开后特别长,这里之截了一部分,可以自行查看一下:

7.2 编译
在编译阶段,gcc首先要检查代码的规范性,以及收否有语法错误等,以确定代码是否能正常工作,无误后,gcc把代码翻译成汇编语言,用户可以使用“-S”选项进行查看,该选项只是进行编译而不进行汇编,最终生成一个汇编代码。
将刚刚生成的预处理文件(也就是.i文件)进行编译,在终端输入如下命令:
gcc -S main.i -o main.s
gcc -S hello.i -o hello.s

来到VScode查看一下,这是一些汇编语言,读不懂没关系,不用进行特别深入,这是比较底层的东西,直到即可:

7.3 汇编
该阶段将编译器生成的中间代码或汇编代码转换成目标机器的机器语言代码,也就是目标代码。这个阶段由汇编器(Assembler)完成,其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务:符号解析、指令翻译、地址关联、重定位、代码优化。最终,汇编器会将翻译和处理后的目标代码输出到目标文件中,用于后续的链接和生成可执行程序或共享库文件。
简单点来说就是将人能看懂的代码,变成机器能看懂的代码。执行如下命令将刚刚生成的汇编文件进行汇编:
gcc -c hello.s -o hello.o
gcc -c main.s -o main.o

我们来到VScode,发现不支持查看:

所以还是回到终端,通过vi编辑器查看:

对于vi编辑器的使用:
可以看到一对乱码:

想要退出,点击键盘Esc,输入":q":

7.4 链接
编译成功后,就进入了链接阶段。通常,C语言的链接共用三种:静态链接、动态链接和混合链接,三者的区别就在于链接器在链接过程中对程序库函数调用的解析。
静态库在编译链接时,把库文件的代码全部加入可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,其后缀名一般为".a”。
动态库在编译链接时并没有把库文件的代码加入可执行文件中,而是在程序执行时链接文件加载库,这样可以节省系统的开销,动态库的后缀名一般为“.so”。 gcc在编译时默认使用动态库。
7.4.1 静态链接
将所有目标文件和所需的库在编译时一并打包进最终的可执行文件。库的代码被复制到最终的可执行文件中,使得可执行文件变得自包含,不需要在运行时查找或加载外部库。
如何进行静态链接呢?我们先来到VScode当中,找到main现将其删掉:

来到终端输入命令:
gcc -static main.o hello.o -o main
-static:该参数指示编译器进行静态链接,而不是默认的动态链接。使用这个参数,GCC 会尝试将所有用到的库函数直接链接到最终生成的可执行文件中,包括 C 标准库(libe)、数学库(libm)和其他任何通过代码引用的外部库。
我们可以使用ll命令看一下其大小,可以和下面使用动态链接的做个对比:

特点:
- 编译时直接将库代码复制到可执行文件中
- 生成独立的可执行文件,不依赖外部库文件
- 文件体积较大,但部署简单
7.4.2 动态链接
库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为它们共享系统级的库代码。与静态链接不同,库代码不包含在可执行文件中。
因为编译器默认的动态链接,因此我们不需要其他操作,直接链接即可:
gcc main.o hello.o -o main1
其中main*是静态链接生成的文件,main1是动态链接,可以看出其大小有很明显的区别:

7.4.3 拓展:打包动态库
在大型项目开发的时候,我们可能需要多个小组进行开发,但是这样肯定会有部分功能重复,如果每个小组都对这个功能进行开发,会造成资源的浪费,举个例子,一家公司要开发三个图形处理产品:
- 图片编辑器 - 功能完整的编辑软件
- 批量处理工具 - 命令行批量处理图片
- 网页图片服务 - 在线图片处理服务
那么开发的时候可能就遇到:
project/
├── photo_editor/
│ ├── main.c
│ ├── image_utils.c # 图像处理函数
│ └── image_utils.h
├── batch_tool/
│ ├── main.c
│ ├── image_utils.c # 同样的图像处理函数
│ └── image_utils.h
└── web_service/
├── main.c
├── image_utils.c # 同样的图像处理函数
└── image_utils.h
这样会造成:同样的代码复制了3份,发现bug需要修改3个地方,每个程序都很大(包含所有图像处理代码),更新功能需要重新编译所有程序。
如何解决呢?我们可以将相同的部分进行打包,这样每个小组开发的时候只需要调用库即可:
project/
├── lib/
│ ├── libimage.so # 动态库
│ └── image_utils.h # 头文件
├── photo_editor/
│ └── main.c # 只包含业务逻辑
├── batch_tool/
│ └── main.c
└── web_service/
└── main.c
那么如何打包呢?我们来到终端,输入如下命令将hello.o编译为动态链接库libhello.so:
gcc -fPIC -shared -o libhello.so hello.o
- -fPIC:这个选项告诉编译器为“位置无关代码(Position Independent Code)”生成输出。在创建共享库时使用这个选项是非常重要的,因为它允许共享库被加载到内存中的任何位置,而不影响其执行。这是因为位置无关代码使用相对地址而非绝对地址进行数据访问和函数调用,使得库在被不同程序加载时能够灵活地映射到不同的地址空间。
- -shared:这个选项指示GCC生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用,节省了内存和磁盘空间。
- -o libhello.so:这部分指定了输出文件的名称。-0选项后面跟着的是输出文件的名字,这里命名为libhello.so。按照惯例, Linux下的共享库名称以lib开头,扩展名为.so(表示共享对象)。
- hello.o:这是命令的输入文件,即之前编译生成的目标文件。在这个例子中,GCC 会将 hello.o 中的代码和数据打包进最终的共享库 libhello.so 中。
可以看到生成好的库:

此时我们直接使用的话,发现是无法使用的:

这句报错的意思时main2在执行过程中,没有找到动态链接库文件libhello.so文件,链接失败无法执行。 Linux的默认动态链接库文件夹是/lib和/usr/lib,而我们的libhello.so不在其中,所以我们需要在执行的时候指明额外的动态链接库文件夹。
7.4.3.1 临时设置 LD_LIBRARY_PATH(开发调试用)
我们若想要使用,需要告诉他路径,如果不知道自己路径可以通过pwd进行查看:
LD_LIBRARY_PATH=/home/dky/HelloWorld ./main2

或者:

7.4.3.2 编译时指定库路径(推荐用于开发)
使用 -Wl,-rpath 在可执行文件中嵌入库搜索路径:
gcc main.c -L. -lhello -Wl,-rpath=. -o main2

- gcc main.c:编译 main.c 源文件
- -L.:告诉链接器在当前目录 (.) 中查找库文件
- -lhello:链接名为 libhello.so 的库(自动添加 lib 前缀和 .so 后缀)
- -Wl,-rpath=.:设置运行时库搜索路径,-Wl, 表示将后面的参数传递给链接器,-rpath=. 告诉运行时加载器在当前目录查找动态库
- -o main2:指定输出文件名为 main2
7.4.3.3 永久配置方案
我们上面也提到了Linux的默认动态链接库文件夹是/lib和/usr/lib,而我们的libhello.so不在其中,所以我们需要在执行的时候指明额外的动态链接库文件夹。那么我们直接将libhello.so复制到系统库目录就可以了:
# 复制库到系统目录
sudo cp libhello.so /usr/local/lib/
# 更新库缓存
sudo ldconfig
# 现在可以直接运行
./main2
不过这个有一点不好的是,因为是更改系统文件,若是不小心将取了和系统文件相同的名字,将系统文件给覆盖了,就不好了。
7.4.4 混合链接
混合链接,部分库静态链接,部分动态链接,这样我们可以将系统文件使用动态链接,自研使用静态链接,防止更改系统文件,首先先创建hello.o的静态链接库:
ar crv libhello.a hello.o

此时路径下既有动态库 libhello.so 和静态库 libhello.a 若是我们此时运行会发现,还是报错:

那是因为如果文件当中即存在动态链接库,又存在静态链接库,系统会默认使用动态链接,因此我们需要将动态链接库删除掉:

注意删除完动态链接库后,需要重新链接一下,因为刚才链接的是动态库,删除后,重新链接到静态库当中。


Linux下VSCode与GCC开发全攻略

1万+

被折叠的 条评论
为什么被折叠?



