Linux操作系统从入门到实战(八)详细讲解编译器gcc/g++编译步骤与动静态库链接
前言
- 在上一篇博客中,我们探讨了 Vim 编译器的使用方法。
- 而在本篇博客中,我们将开启对 gcc/g++ 编译器的讲解之旅。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Linux知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12879535.html?spm=1001.2014.3001.5482
一、gcc/g++ 背景知识
为什么有gcc/g++?
- GCC(GNU Compiler Collection)是GNU项目开发的编译器集合,最初是为C语言设计的(gcc),后来扩展支持了更多语言。
- G++是GCC中专门用于编译C++代码的前端工具。
之所以需要两个工具,是因为:
- 语言差异:C++比C多了很多特性(类、模板、异常处理等),需要特殊处理
- 链接需求:C++程序默认需要链接C++标准库(如
libstdc++
),而C程序不需要 - 编译选项:g++会自动启用一些C++必需的编译选项
- 简单来说:编译C代码用gcc,编译C++代码用g++。
二、gcc编译过程四步走
第一步:预处理
预处理就像做饭前的备菜环节,主要帮你处理代码里的"杂事":
- 宏替换:比如你定义了
#define PI 3.14
,预处理会把代码里所有的PI
换成3.14 - 文件包含:遇到
#include <stdio.h>
,就会把stdio.h文件里的内容"复制粘贴"到你的代码里 - 去注释:代码里//或/* */的注释会被删掉,因为电脑不需要看这些说明
- 条件编译:比如
#if 0 ... #endif
中间的代码会被暂时去掉
操作方法:
- 用
gcc -E hello.c -o hello.i
命令,就能得到预处理后的文件hello.i
。 - 这里的
-E
就像告诉gcc:“做完预处理就停下,别往下走了”,-o
是指定输出文件的名字。
第二步:编译
- 预处理完的代码还是C语言,电脑还是看不懂。
- 编译阶段就像把中文翻译成"电脑能懂的半成品语言"(汇编语言)。
这一步会先检查你的代码写得对不对:
- 有没有少写分号?
- 变量是不是没定义就用了?
- 函数调用的参数对不对?
如果有错误,会报错让你修改;没错误的话,就生成汇编代码。
操作方法:
用gcc -S hello.i -o hello.s
命令,生成.s
后缀的汇编文件。
-S
的意思是"只编译到汇编语言,别继续往下做"。- 汇编代码里会有很多
mov
、add
之类的指令,这是更接近电脑操作的语言。
第三步:汇编
- 汇编语言电脑还是不能直接执行,得变成二进制的机器码(0和1)才行。
- 这一步就像把"半成品语言"翻译成"电脑的母语"。
操作方法:
用gcc -c hello.s -o hello.o
命令,生成.o
后缀的目标文件。
-c
的作用是"把汇编代码转成二进制目标文件",.o
文件里都是0和1组成的机器码,但现在还不能直接运行。
第四步:链接
- 我们的代码可能用到了别人写的功能(比如
printf
函数),这些功能放在其他的库文件里。 - 链接阶段就像组装机器,把我们的目标文件和需要的库文件拼在一起,形成一个能直接运行的程序。
操作方法:
用gcc hello.o -o hello
命令,生成可执行文件(Windows里是.exe
,Linux里没有后缀)。
现在我们输入./hello
(Linux),就能看到程序运行的结果了!
总结一下四步流程
hello.c
(源代码)→预处理→hello.i
(预处理后的代码)hello.i
→编译→hello.s
(汇编代码)hello.s
→汇编→hello.o
(二进制目标文件)hello.o
→链接→hello
(可执行文件)
三、动态链接与静态链接
1. 为什么需要链接?
- 想象我们搭积木,每个源文件(.c)都是一个单独的积木块,编译后变成目标文件(.o)。
- 这些积木块(.c)各自有不同的功能,但单独一块没法用——比如
main.c
里调用了add.c
里的加法函数,可main.o
里并没有这个函数的具体实现。
链接的作用就是:把这些零散的积木块(.o文件)拼在一起,让它们能相互配合工作,最后形成一个完整的"模型"(可执行程序)。
2. 静态链接
静态链接就像搭积木时用胶水把所有积木块粘死——一旦粘好,每个零件都成了整体的一部分,再也分不开。
2.1 静态链接的过程:
- 每个
.c
文件单独编译成.o
目标文件(比如add.c
→add.o
,main.c
→main.o
) - 静态链接器把所有
.o
文件"合并打包",生成一个可执行程序 - 这个可执行程序里包含了所有需要的代码(自己写的+调用的库函数,比如
printf
)
2.2 静态链接的优缺点:
优点:
- 运行快:因为所有代码都在一个文件里,程序启动时不用临时找零件
- 独立运行:拿到这个可执行程序,随便复制到其他电脑,只要系统兼容就能直接跑
缺点:
- 太占空间:如果多个程序都用了
printf
函数,每个程序里都会有一份printf
的代码。就像每个房子都单独装一个一模一样的水龙头,其实完全可以共用 - 更新麻烦:如果
printf
函数有bug需要修复,所有用静态链接的程序都得重新编译打包,不能只更新printf
这一个零件
静态链接示例
1. 创建源文件
// add.c - 加法函数实现
int add(int a, int b) {
return a + b;
}
// main.c - 主程序,调用add函数
#include <stdio.h>
int add(int a, int b); // 函数声明
int main() {
int result = add(3, 4);
printf("3 + 4 = %d\n", result);
return 0;
}
2. 编译与静态链接步骤
# 1. 编译源文件为目标文件
gcc -c add.c -o add.o
gcc -c main.c -o main.o
# 2. 创建静态库(可选)
ar rcs libadd.a add.o
# 3. 静态链接(方式一:直接链接目标文件)
gcc main.o add.o -o static_program1
# 3. 静态链接(方式二:链接静态库)
gcc main.o -L. -ladd -o static_program2
3. 验证静态链接结果
# 查看依赖关系(无动态库依赖)
ldd static_program2
# 输出示例:
# not a dynamic executable
3. 动态链接
动态链接是为了解决静态链接的"浪费"问题而发明的。
- 它像用可拆卸的螺丝组装积木——程序本身不包含所有零件,而是在运行时才去"借"需要的零件。
3.1 动态链接的过程:
- 源文件还是编译成
.o
目标文件 - 链接时不把库函数的代码打包进程序,只记录"我需要某某函数,在某某库文件里"
- 程序运行时,系统会找到对应的"共享库"(比如
libc.so.6
),临时把需要的函数代码"链接"进来使用
3.2 动态链接的优缺点:
优点:
- 节省空间:多个程序可以共用一个共享库。比如10个程序都用
printf
,只需要一份libc.so.6
文件,所有程序共享它 - 更新方便:如果
printf
函数修复了bug,只需要更新libc.so.6
这一个文件,所有用动态链接的程序下次运行时自动用上新功能
缺点:
- 依赖库文件:如果电脑里没有对应的共享库(比如
libc.so.6
丢了),程序会报错"找不到某某库" - 启动稍慢:运行时需要临时找库文件并链接,比静态链接多了一点点准备时间(一般用户感觉不到)
动态链接示例
1. 创建源文件(与静态示例相同)
// add.c - 加法函数实现
int add(int a, int b) {
return a + b;
}
// main.c - 主程序,调用add函数
#include <stdio.h>
int add(int a, int b); // 函数声明
int main() {
int result = add(3, 4);
printf("3 + 4 = %d\n", result);
return 0;
}
2. 编译与动态链接步骤
# 1. 编译源文件为位置无关代码(PIC)
gcc -fPIC -c add.c -o add.o
# 2. 创建动态库
gcc -shared -o libadd.so add.o
# 3. 动态链接
gcc main.c -L. -ladd -o dynamic_program
# 4. 运行前需设置动态库搜索路径(临时方法)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
3. 验证动态链接结果
# 查看依赖关系
ldd dynamic_program
# 输出示例:
# linux-vdso.so.1 (0x00007ffd9b7fb000)
# libadd.so => ./libadd.so (0x00007f8c3f9fc000)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8c3f81a000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f8c3f9fe000)
4. 什么是"库"?
- 前面总提到"库文件",其实库就是一堆现成函数的集合,像一个工具包。
比如:
- 你写
printf("Hello")
时,自己的代码里只写了调用命令,并没有实现"怎么在屏幕上显示文字"的具体代码 - 这些具体代码早就被专家写好,放在了
libc.so.6
这个库文件里(Linux系统下) - 链接的作用就是告诉程序:“你要的
printf
在libc.so.6
里,到时候去找它”
5. 如何查看程序依赖的动态库?
Linux系统里有个超好用的命令ldd
,可以查看一个程序依赖哪些动态库。比如:
ldd hello
运行后会显示类似这样的内容:
linux-vdso.so.1 => (0x00007fffeb1ab000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff776af5000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff776ec3000)
这说明hello
程序运行时需要libc.so.6
等库文件,如果这些文件丢失,程序就跑不起来。
6. 静态与动态的核心区别
对比项 | 静态链接 | 动态链接 |
---|---|---|
代码存放 | 所有代码打包进可执行程序 | 程序只存"地址",运行时找库 |
文件大小 | 较大 | 较小 |
共享性 | 不共享,每个程序一份 | 共享,多个程序用同一个库 |
更新方式 | 必须重新编译程序 | 只更新库文件即可 |
独立性 | 复制到任何地方都能运行 | 依赖系统里的库文件 |
四、 静态库和动态库
1. 静态库与动态库
- 前面我们讲了静态链接和动态链接的原理,其实它们背后都依赖"库文件"。
- 就像工具箱分两种:一种是一次性打包带走的,另一种是大家共用的。
- 下面我们具体说说这两种库的区别。
2. 静态库和动态库长啥样?
库文件就像提前做好的"功能模块包",里面装着各种现成的函数(比如打印、计算等)。但根据链接方式不同,库分两种:
2.1 静态库
- 特点:编译链接时,会把库里面的所有代码都复制到你的程序里,相当于"买工具回家"
- 后缀名:
- Linux系统里叫
.a
(比如libmath.a
,a可以理解为"archive归档") - Windows系统里叫
.lib
(注意:Windows的.lib可能是静态库或动态库的导入库,这里只说静态库的情况)
- Linux系统里叫
- 后果:生成的可执行文件比较大,但一旦生成,就再也不需要原来的库文件了
2.2 动态库
- 特点:编译链接时,只记录库的位置,不复制代码,程序运行时才去"借"功能,相当于"用的时候去工具间拿"
- 后缀名:
- Linux系统里叫
.so
(比如libc.so.6
,so是"shared object共享对象"的缩写) - Windows系统里叫
.dll
(比如kernel32.dll
,dll是"dynamic link library动态链接库"的缩写)
- Linux系统里叫
- 后果:生成的可执行文件小,但运行时必须有对应的动态库在场,否则会报错
3. 为什么动态库更受欢迎?
- 假设网吧有100台电脑,都要装同款游戏。
- 如果用"静态库思路",每台电脑都得把游戏需要的所有库文件(比如图形处理、声音处理的库)复制一份,100台电脑就有100份相同的库,
太浪费硬盘空间了。
- 但用"动态库思路",网吧服务器上只存一份动态库(.so或.dll),100台电脑运行游戏时都去服务器"借用"这份库,不用重复存储。这
- 样不仅省空间,以后游戏库更新了,只需要改服务器上的那一份,所有电脑都能用上新功能——这就是动态库的优势!
我们平时用的软件、手机APP,大多像网吧的游戏一样,依赖动态库运行。
4. gcc默认是动态链接
当你用gcc hello.o -o hello
生成可执行文件时,gcc悄悄做了件事:默认使用动态库链接。也就是说,你的hello
程序里并没有包含printf
这些函数的具体代码,而是依赖系统里的libc.so.6
动态库。
怎么验证这一点?
用file
命令查看可执行文件的属性:
file hello
如果看到类似"dynamically linked"(动态链接)的字样,就说明这是动态链接的程序。
如果想强制用静态链接,需要加-static
参数:
gcc -static hello.o -o hello_static
这时生成的hello_static
会包含所有需要的代码(包括libc
静态库),文件体积会大很多,但复制到其他Linux系统时,不用管对方有没有libc.so.6
都能运行。
静态库没安装?这样补上
很多云服务器或精简系统里,默认没装C/C++静态库(因为大家常用动态库)。如果编译静态链接程序时报错"找不到静态库",可以手动安装:
CentOS系统:
yum install glibc-static libstdc++-static -y
这条命令会安装C语言静态库(glibc-static
)和C++静态库(libstdc++-static
)。
扩展
1. 条件编译有什么用?
简单说,就是让同一套代码能适应不同场景。比如:
- 写一个程序,想在Windows和Linux上都能跑,用
#ifdef _WIN32
…#else
…#endif
区分不同系统的代码 - 调试时想打印详细信息,发布时去掉这些信息,用
#ifdef DEBUG
控制
2. 为什么非要把代码变成汇编?
汇编语言是"人和电脑的中间翻译官":
- 高级语言(C、Python)好写但电脑看不懂
- 机器码(0和1)电脑看得懂但人没法写
- 汇编语言介于两者之间,既接近机器码,又比机器码好理解,是编译器翻译的必经步骤
3. 什么是"编译器自举"?
简单说就是"自己编译自己":
- 最早的编译器可能是用机器码或汇编写的
- 后来人们用C语言写了C编译器,再用这个编译器编译自己的源代码,得到新的编译器
- 就像用自己做的面包机烤面包,最后面包机可以做出能烤出自己的面包——听起来绕,但这是编译器发展的重要方式
五、 gcc其他常用选项(了解即可)
第一类:控制编译步骤的"进度开关"
还记得gcc编译的四步流程吗?(预处理→编译→汇编→链接)这些选项能让编译过程在指定步骤停下,方便我们查看中间结果。
1. -E:只做预处理,不往下走
- 作用:只执行预处理(处理
#include
、#define
等),做完就停,不生成可执行文件 - 特点:默认不生成文件,需要用
-o
指定输出文件,否则结果会直接打印在屏幕上 - 例子:
gcc -E hello.c -o hello.i
把hello.c
预处理后存成hello.i
(对应编译第一步)
2. -S:编译到汇编语言就刹车
- 作用:从源代码一直处理到生成汇编语言,不进行后续的汇编和链接
- 适用场景:想看看C代码对应的汇编指令长啥样时用
- 例子:
gcc -S hello.c -o hello.s
直接把hello.c
编译成汇编文件hello.s
(对应编译第二步)
3. -c:编译到目标代码就停
- 作用:走完预处理、编译、汇编三步,生成二进制的目标文件(.o),不进行链接
- 特点:生成的.o文件不能直接运行,但可以用来后续链接成可执行程序
- 例子:
gcc -c hello.c -o hello.o
生成目标文件hello.o
(对应编译第三步)
第二类:控制输出的"文件管理开关"
这类选项主要用来指定输出文件的名字或格式,最常用的就是-o
。
-o:给输出文件起名字
- 作用:指定生成文件的名称,避免gcc自动起默认名(比如默认生成a.out)
- 用法:
-o
后面直接跟你想取的文件名 - 例子:
gcc hello.c -o myprogram
把编译结果存成myprogram
(而不是默认的a.out);
gcc hello.o -o run
把目标文件链接成名为run
的可执行程序
第三类:控制链接方式的"库开关"
这类选项决定程序是用静态库还是动态库链接,和我们之前讲的静态/动态链接直接相关。
1. -static:强制用静态链接
- 作用:不管系统里有没有动态库,都强制使用静态库链接,把所有代码打包进可执行文件
- 效果:生成的文件体积大,但可以独立运行(不用依赖系统里的动态库)
- 例子:
gcc hello.c -static -o hello_static
生成静态链接的hello_static
,复制到其他Linux系统不用怕缺库
2. -shared:尽量用动态库
- 作用:告诉gcc优先使用动态库链接,生成的文件体积小(默认其实就是动态链接)
- 注意:生成的程序运行时需要系统里有对应的动态库,否则会报错
- 例子:
gcc hello.c -shared -o hello_shared
(实际效果和默认编译差不多,主要用于制作动态库时)
第四类:调试和优化的"功能开关"
这类选项能帮我们调试程序或让程序运行得更快。
1. -g:给程序加"调试标记"
- 作用:生成调试信息(就像给程序加了"定位器"),让GDB等调试工具能找到代码对应的位置
- 适用场景:写代码时难免出错,加了
-g
就能用调试器一步步看程序运行过程 - 例子:
gcc hello.c -g -o hello_debug
生成带调试信息的程序,之后可以用gdb hello_debug
进行调试
2. 优化选项:-O0 到 -O3(字母O,不是数字0)
- 作用:控制编译器对代码的优化程度,就像照片美颜的不同级别
-O0
:不优化(默认),编译快,适合调试(代码和你写的几乎一致)-O1
:基础优化,让程序跑快一点,编译时间增加不多-O2
:中级优化,比-O1
优化得更细致,适合发布程序-O3
:最高级优化,会对代码做深度调整(比如循环优化、函数内联),编译慢但程序可能更快
- 例子:
gcc hello.c -O3 -o hello_fast
生成优化到最高级的程序,运行速度可能比默认编译的快
第五类:控制警告信息的"提示开关"
这类选项决定编译器是否提醒你代码里的潜在问题,就像老师批改作业时的评语。
1. -w:关闭所有警告
- 作用:不管代码里有多少不规范的地方,编译器都不提示警告
- 不推荐:新手最好别用,警告往往是帮你发现错误的好机会
2. -Wall:显示所有警告
- 作用:开启所有常见警告(Wall可以理解为"All Warnings"),比如变量定义了不用、函数没返回值等
- 推荐用法:写代码时加上
-Wall
,让编译器当你的"纠错小助手" - 例子:
gcc hello.c -Wall -o hello
如果代码里有int a;
但没用到a,编译器会提示warning: unused variable ‘a’
常用选项速查表
选项 | 作用一句话总结 | 适用场景 |
---|---|---|
-E |
只做预处理,输出.i文件 | 查看宏替换、头文件包含结果 |
-S |
生成汇编代码.s文件 | 学习C代码和汇编的对应关系 |
-c |
生成目标文件.o | 多文件编译时单独处理每个文件 |
-o |
指定输出文件名 | 不想用默认的a.out时 |
-static |
静态链接,程序独立运行 | 需要移植程序到其他系统时 |
-g |
生成调试信息 | 需要用GDB调试程序时 |
-O3 |
最高级优化 | 发布程序时让运行更快 |
-Wall |
显示所有警告 | 写代码时检查潜在问题 |
以上就是这篇博客的全部内容,下一篇我们将继续探索Linux的更多精彩内容。
我的个人主页
欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Linux知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12879535.html?spm=1001.2014.3001.5482
非常感谢您的阅读,喜欢的话记得三连哦 |