Linux开发工具(二)
目录
1.库
1.1 静态链接:
1.1.1 编译阶段:
1.1.2 运行阶段:
1.2 动态链接:
1.2.1 编译阶段:
1.2.2 运行阶段:
2 make/Makefile
2.1 make/Makefile与单条编译指令的对比:
1. 自动化处理复杂依赖关系
2. 增量编译,节省时间
3. 统一管理编译规则,减少错误
4. 支持多目标操作,扩展功能
5. 适配大型项目,便于协作
总结:适用场景对比
2.2 Makefile语法
2.3 奇怪的符号?
2.3.1 $@,$^,$<介绍
2.3.1 %的模式中,为什么不使用$^,而使用$<呢?
1. 功能本质不同
2. 角色与作用阶段不同
3. 依赖关系的体现
在讲这个之前,咱们先来看一张图片:
那么这张图片是由code.c或者源文件经过编译形成的可执行文件。那么咱们ldd这个可执行文件,就可以看出这个可执行文件依赖的是什么库。可以看出,依赖的是libc.so.6这个库,(因为这个code.c文件中有c语言函数。所以自然的也得依赖c语言库)
1.库
那么咱们已经知道了库分为动态库已经静态库。那么有了库,自然也得有链接库的方式,那么就分为动态链接以及静态链接。
那么动态库以及静态库本质上都是存储在磁盘上的文件(属于二进制文件,包含编译后的机器指令和代码)
静态库和动态库的默认路径由操作系统约定,目的是让编译器(如 gcc
)、链接器(如 ld
)能快速找到并使用这些库文件。
以下就是库的存储位置:
库类型 | 文件后缀 | 核心默认存放路径 | 路径用途说明 |
---|---|---|---|
静态库 | .a (archive) | - /usr/lib (系统级静态库,32 位)- /usr/lib64 (系统级静态库,64 位)- /usr/local/lib (用户自定义 / 手动安装的静态库) | - /usr/lib :系统自带的基础静态库(如 libc.a ,C 标准库的静态版本)- /usr/local/lib :用户通过源码编译安装的库(如自定义静态库) |
动态库 | .so (shared object) | - /lib (系统核心动态库,32 位)- /lib64 (系统核心动态库,64 位)- /usr/lib /usr/lib64 (系统级动态库)- /usr/local/lib (用户自定义动态库) | - /lib :最关键的动态库(如 libc.so ,C 标准库的动态版本,系统启动必需)- 其他路径:应用依赖的非核心动态库(如 libssl.so 加密库) |
那么其实静态库:编译时将库代码完整复制到可执行文件中,程序运行时随可执行文件一起加载到内存(磁盘上的静态库文件本身不再被访问),运行时不依赖外部库,但文件体积大、库更新需重新编译。
动态库:编译时仅记录 “依赖关系”,程序运行时由动态链接器(如 Linux 的 ld-linux.so
)从磁盘路径中读取动态库文件,加载到内存并共享(多个程序可共用一份内存中的动态库代码),运行时才加载库文件,可执行文件体积小、库更新无需重新编译,但运行时必须存在对应的动态库。
那么下面咱们来讲一下静态链接以及动态链接:
无论是静态链接还是动态链接的可执行程序:
- 未运行时:仅以文件形式存放在磁盘(如
/home/user/test
),不占用内存; - 运行时:操作系统会将其从磁盘加载到内存,CPU 从内存中读取指令执行。
就类似于,你的桌面上的永劫无间这款游戏。在你没有打开他之前,他仅以文件的形式存储在磁盘中吧(你放到d盘的,他就是在d盘中)。那么你一旦打开这款游戏,这款游戏是不是就占用了你的运行内存呀。(占的还不小).所以说可执行程序的运行过程与这个差不多。因为他们本质也都是可执行程序。
1.1 静态链接:
那么静态链接:
1.1.1 编译阶段:
静态链接发生在编译阶段,链接器会将程序依赖的静态库(.a)中所有被引用的代码和数据,完整复制到磁盘上的可执行文件中。
具体过程
- 编译器将源代码(.c)编译为目标文件(.o);
- 链接器(如
ld
)找到程序依赖的静态库(如libmath.a
); - 把库中被程序调用的函数、数据等全部复制到可执行文件,最终生成一个独立的可执行程序。
1.1.2 运行阶段:
运行时:这个完整文件被加载到内存,CPU 直接执行内存中的指令,无需再链接任何库(因为所需代码已随程序一起进内存)。
注意是:先在磁盘上把库代码整合到可执行文件,再一起加载到内存。
示例:若程序依赖 libmath.a
中的 add
函数,链接后 add
的代码会被复制到可执行文件里,程序运行时无需再找 libmath.a
。
1.2 动态链接:
动态链接的核心是 “运行时在内存中完成最终链接”,具体分两步:
1.2.1 编译阶段:
第一步(编译链接阶段,磁盘):生成的可执行文件中不包含库代码,编译器只记录 “依赖哪个动态库(如 libc.so)、需要库中的哪些函数”;
1.2.2 运行阶段:
第二步(程序运行阶段,内存):
1.可执行程序先被加载到内存;
2.系统的 “动态链接器”(如 ld-linux.so)会找到磁盘上的动态库,将其加载到另一块内存区域;
3.最后在内存中 “建立关联”:把可执行程序中调用库函数的指令(例如你的你的源文件中写了printf函数,那么这个库函数的指令就会映射为指向内存中动态库的对应函数地址)指向内存中动态库的对应函数地址 —— 这一步才是动态链接的 “内存链接” 过程。
简单说:动态链接是 “可执行程序和动态库分别进内存,在内存中‘牵手’完成链接”。
示例:若程序依赖 libmath.so
中的 add
函数,可执行文件中仅记录 “需要从 libmath.so
中找 add
”;运行时系统加载 libmath.so
,程序通过内存地址调用 add
。
总结:
维度 | 静态库(.a) | 动态库(.so) |
---|---|---|
链接方式 | 编译时复制代码到可执行文件 | 编译时记录依赖,运行时加载库 |
可执行文件大小 | 较大(包含库代码)(注意这个地方是可执行文件的大小) | 较小(仅含依赖记录) |
运行依赖 | 不依赖外部库,可独立运行 | 依赖系统中存在对应的动态库 |
更新维护 | 库更新后需重新编译程序 | 库更新后(保持接口兼容),程序无需重新编译 |
内存占用 | 多个程序使用同一库时,各自复制一份,内存占用高 | 多个程序共享同一份库的内存加载,内存占用低 |
那么咱们通常的GCC默认就是采用的是动态链接的方式编译程序。我们通常用动态库以及动态丽链接才是最佳实践。
一般的话,静态库是需要安装的。
普通账号下:sudo yum install glibc-static -y
sudo yum install libstdc++-static -y
那么通过以上讲的这些,咱们应该知道,为什么不经过链接,你的可重定位二进制文件不可以执行了吧。链接之后,就可以执行了。
这其实都是依赖库的。
那么下面咱们来看自动化构建工具make/Makefile
2 make/Makefile
make是⼀条命令,makefile是⼀个⽂件,两个搭配使⽤,完成项⽬⾃动化构建。
那么这个make是一个指令。二Makefile是一个文件。Makefile带来的好处就是自动化编译。那么这个时候就有人问了,咱们不是有gcc test.c -o test 嘛,是,但是这个指令只能适用于单个源文件编译。等你进了公司。⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,makefile定义了⼀ 系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄ 于进⾏更复杂的功能操作。
2.1 make/Makefile与单条编译指令的对比:
在 Linux 环境下,make/Makefile
相比直接使用单条编译指令(如 gcc
命令),带来的核心优势是自动化、高效化和可维护性,尤其在多文件、多模块的项目中差距尤为明显。具体好处如下:
1. 自动化处理复杂依赖关系
单文件项目用单条指令(如 gcc main.c -o app
)足够,但多文件项目(如包含 a.c
、b.c
、main.c
且相互依赖)时:
- 若手动编译,需记住繁琐的顺序(先编译依赖文件,再链接),例如:
gcc -c a.c -o a.o gcc -c b.c -o b.o gcc -c main.c -o main.o gcc a.o b.o main.o -o app
- 用
Makefile
只需定义一次依赖关系(如app: a.o b.o main.o
),make
会自动分析依赖顺序,按正确步骤编译,无需人工干预。
2. 增量编译,节省时间
当项目文件较多时(如几十上百个源文件):
- 单条指令编译会重新编译所有文件,即使只有一个文件被修改(例如
make clean && gcc *.c -o app
),浪费大量时间。 make
会对比文件修改时间:只重新编译被修改过的源文件及其依赖的文件,未修改的文件直接复用之前的编译结果。例如,若只修改了a.c
,make
只会重新编译a.o
并重新链接,其他文件(b.o
、main.o
)不变,大幅提升编译效率。
3. 统一管理编译规则,减少错误
- 单条指令编译时,每次编译都需手动输入完整参数(如
-Wall
警告、-I
头文件路径、-L
库路径等),容易遗漏或输入错误。 Makefile
可将编译器(CC = gcc
)、编译选项(CFLAGS = -Wall -O2
)、链接选项(LDFLAGS = -lm
)等统一定义为变量,所有编译步骤共享这些配置,确保一致性。例如,需要添加新的编译参数时,只需修改CFLAGS
变量,无需逐个修改每条指令。
4. 支持多目标操作,扩展功能
Makefile
可定义多个目标(如编译、清理、安装、测试等),实现一站式管理:
make
:默认编译生成可执行文件。make clean
:一键删除所有编译产物(.o
目标文件、可执行文件),避免手动删除的遗漏。make install
:自动将编译好的程序复制到系统目录(如/usr/local/bin
),配合sudo
即可完成安装。make test
:执行预设的测试脚本,验证程序功能。这些操作若用单条指令实现,需手动输入复杂命令(如rm -f *.o app
),且难以统一维护。
5. 适配大型项目,便于协作
在团队开发或大型项目(如 Linux 内核、开源工具)中:
- 单条指令完全无法应对( thousands of files),必须依赖
Makefile
组织模块关系。 Makefile
可通过include
指令拆分多个子文件(如Makefile.common
、Makefile.module1
),多人协作时各自维护负责模块的规则,避免冲突。- 支持条件编译(如
ifeq ($(ARCH),x86)
),可根据不同架构、不同配置生成适配的编译规则,单条指令难以实现这种灵活性。
总结:适用场景对比
场景 | 单条编译指令 | make/Makefile |
---|---|---|
项目规模 | 单文件或极简单项目 | 多文件、复杂依赖、大型项目 |
效率 | 全量编译,耗时久 | 增量编译,只更改变动文件 |
可维护性 | 指令零散,易出错 | 规则集中管理,便于修改和扩展 |
功能扩展 | 仅能编译,无额外功能 | 支持清理、安装、测试等多目标 |
简单说,make/Makefile
是为了解决 “手动编译复杂项目时的低效和混乱” 而设计的工具,小项目可能体现不出优势,但项目规模越大,其自动化和高效化的价值就越明显。
2.2 Makefile语法
咱们的使用Makefile得先知道它的语法吧:
咱们先来看一个这个东西:
code:code.o 这个冒号左边是目标文件。冒号右边是依赖文件列表(可以是一个,也可以是多个)
下面,必须要有一个Tab键,然后写上依赖方法。那么这个是怎么执行的呢?就是目标文件依赖于依赖文件,然后根据依赖方法,与依赖文件执行依赖方法。但是呢?那依赖文件又依赖于谁呢?没错,依赖于下面的code.s 下面写上了。所以,聪明的你一定会发现,这其实是一个不断依赖的过程。
那么咱们执行make(默认会从上到下进行执行)。特殊的待会说。
不对,你会发现,这个顺序与咱们写的顺序刚好相反呀。哎,再细看一下,这不就是像咱们说的栈吗?对呀,先进的后出,后进的先出。
所以说,make指令,make解析当前目录下的Makefile,形成推导栈(依赖方法的合集),之后最后出栈会发现依赖方法与Makefile中的正好顺序相反。
那么make是如何⼯作的,在默认的⽅式下,也就是我们只输⼊make命令:(咱们以
myproc:myproc.o
gcc myproc.o -o myproc
myproc.o:myproc.s
gcc -c myproc.s -o myproc.o
myproc.s:myproc.i
gcc -S myproc.i -o myproc.s
myproc.i:myproc.c
gcc -E myproc.c -o myproc.i
为例子:
1. make会在当前⽬录下找名字叫“Makefile”或“makefile”的⽂件。
2. 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 个⽂件,并把这个⽂件作为最终的⽬标⽂件。
3. 如果 myproc ⽂件不存在,或是 ⽐ my myproc 所依赖的后⾯的 proc 这个⽂件新(可以⽤ myproc 这 myproc.o ⽂件的⽂件修改时间要 touch 测试),那么,他就会执⾏后⾯所定义的命令来⽣成 myproc 这个⽂件。
4. 如果 myproc 所依赖的 myproc.o ⽂件不存在,那么 make 会在当前⽂件中找⽬标为 myproc.o ⽂件的依赖性,如果找到则再根据那⼀个规则⽣成 myproc.o ⽂件。(这有点像⼀ 个堆栈的过程) 5. 当然,你的C⽂件和H⽂件是存在的啦,于是 myproc.o ⽂件声明 make 会⽣成 myproc.o ⽂件,然后再⽤ make 的终极任务,也就是执⾏⽂件 hello 了。
6. 这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个 ⽬标⽂件。
7. 在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并 报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。
8. make只管⽂件的依赖性,即,如果在我找了依赖关系之后,冒号后⾯的⽂件还是不在,那么对 不起,我就不⼯作啦。
ok,那么咱们再来看下一个:就是
.PHONY:这个的英文意思就是假的,伪的。
.PHONY:clean
clean:(这个一定要顶格写)
$(RM) $(OBJ) $(BIN)# $(RM): 替换,⽤变量内容替换它(这一行被忘了前面的Tab)
作用:声明一个符号(clean,可以随便符号名,但是一般都是clean)
那么声明的符号为伪目标,(也是目标文件)。那么既然是目标文件,可得有依赖方法吧。
细节一:依赖方法必须存在,但是依赖关系可以为空
细节二:依赖方法可以是任何的shell命令
细节三:clean目标指示利用make的自动推导能力(就是入栈后的出栈),让他执行了rm命令,在构建工程的视角看来就是清理项目。(本质就是删除不需要的临时文件)。
细节四:make命令,后面可以跟“目标名”,其实这个后面跟谁,就解析谁的依赖关系和依赖方法。并且,make默认只会推导一条完整的推导过程。
make的执行顺序:
make默认只会推导从上到下的第一个完整的推导链路。(例如:把清理放第一个,那么make会执行clean)
.PHONY:用来修饰目标文件(是一个伪目标)。本质:总是被执行的。所以说,若test.exe存在,那么make后,它会提示不能在make了,因为你的test.exe已经是最新的了。这个待会讲。
但是呢,你要是make clean,不管是多少次,都是可以执行的。
所以说,make clean是要经常执行的。所以要经常做清理工作。所以clean要有.PHONY。但是目标文件一般不需要.PHONY。因为你经常推导浪费时间并且推导成功一次后,没必要再进行下一次了。这样是可以加速编译效率的。
那么咱们来探讨一下,为什么.PHONY是可以总是被执行的。但是之前的gcc却无法被二次编译呢?
这个更改时间就是图片上的Modify 时间。
那么这个时候,就有同学问了,那么咱们如何做到不更改文件的内容,直接修改文件的修改时间呢?这个其实也好办。
touch+已存在的文件即可。
之后stat 已存在的文件
就会发现,modify时间更新到最新。
那么还有一个Change,这个时间是干什么的呢?这个是文件属性修改时间(即文件的权限)。Modify是文件内容的修改时间
更改内容,会影响文件的大小,包括Modify时间也是属性(所以更改内容,mod与change都会发生改变),所以看第一张图片,就会发现,二者的时间都发生了改变。
最后再来看Access,这个是什么呢?这个是文件被访问的时间(就是cat文件)
但是你实践就会发现,没有改变Access,为什么呢?因为你更新时间——修改文件属性——刷新磁盘保存起来——cat次数多了,访问文件的次数也变多了——增加访问磁盘的次数——导致磁盘的效率降低——导致Linux操作系统整体效率下降(所以说,访问文件,得到一定的次数之后,才会改变Access)。而这个次数又与操作系统内核有关。
咱们来讲一个语法细节:
指令前面加上@的意思是:禁止指令回显。比如:@echo "nihao"
那么不会打印echo "nihao" 只会打印nihao
而正常的不加@,这两个都会打印的。
2.3 奇怪的符号?
那么接下来,咱们来讲一些奇怪的符号
BIN=test.exe
SRC=test.c
$(BIN):$(SRC)
$():将括号里面的内容替换为BIN右边的东西。有点像那个宏替换。$(BIN)就是将这个替换为test.exe
$@为目标文件。$^为依赖文件。
例如gcc -o $@ $^
那么现在有一百个源文件src{1..100}.c ,现在如何将这一百个源文件写到Makefile中呢?
SRC=$(wildcard *.c)
wildcard函数就是获取当前目录下所有的源文件。那么把.c换成.o该怎么做呢?就是
OBJ=$(SRC:.c=.o)
那么咱们再来讲一个东西:
%.o:%.c(把所有的.c文件替换为.o文件。之后再把.o链接形成可执行程序)
gcc -c $<
$<:是把.c文件一个一个的展开为.o,对展开的依赖的.c文件,一个一个的交给gcc
2.3.1 $@,$^,$<介绍
在 Linux Makefile 中,$@、$^、$< 是 自动变量(预定义变量),@ 是 命令前缀,它们的核心含义和作用如下:
1. 自动变量(核心用于简化规则编写)
• $@:代表当前规则中的 目标文件(Target),即规则冒号 : 左边的文件名。
作用:避免重复书写目标文件名,尤其在目标名较长或需批量生成时更高效。
例:test: test.o 中,$@ 就等于 test。
• $^:代表当前规则中的 所有依赖文件(Prerequisites),即规则冒号 : 右边的所有文件名,且会自动去重。
作用:批量引用所有依赖,无需逐个罗列,适合多依赖场景。
例:test: test.o func.o 中,$^ 等于 test.o func.o。
• $<:代表当前规则中的 第一个依赖文件,即规则冒号 : 右边的第一个文件名。
作用:常用于“单个依赖生成单个目标”的场景(如编译 .c 为 .o),简化单依赖引用。
例:test.o: test.c 中,$< 等于 test.c。
2. 命令前缀 @
• @:加在规则的命令前,作用是 不显示该命令本身,只显示命令的执行结果。
作用:减少 Make 执行时的冗余输出,让终端只显示关键信息(如编译进度、错误提示)。
例:@echo "编译完成" 会直接输出“编译完成”,而不会先显示 echo "编译完成" 这条命令。
一句话总结
$@ 指目标、$^ 指所有依赖、$< 指第一个依赖,三者用于简化规则;@ 用于隐藏命令本身,让输出更简洁。
2.3.1 %的模式中,为什么不使用$^,而使用$<呢?
在 %.o: %.c 这类模式规则中,$< 依然代表当前规则匹配时的第一个依赖文件,但这里的“第一个依赖”会随模式匹配的文件动态变化,核心逻辑和作用如下:
1. 模式规则中 $< 的理解:动态匹配“单个关键依赖”
在 %.o: %.c 这个模式规则里,% 是通配符,会匹配所有符合“.c 文件名对应 .o 文件名”的文件(比如 a.c 对应 a.o,b.c 对应 b.o)。
此时 $< 会针对每个匹配的文件对,自动指向其对应的 .c 文件(即该次匹配的“第一个依赖”)。
• 当匹配 a.o: a.c 时,$< = a.c;
• 当匹配 b.o: b.c 时,$< = b.c。
这恰好满足“编译单个 .c 生成单个 .o”的核心需求——每个 .o 只需要对应的那个 .c 文件作为关键依赖,$< 能精准定位到这个唯一关键依赖。
2. 为什么这里不用 $^?:$^ 适配场景不匹配
$^ 确实代表“所有依赖”,但在 %.o: %.c 场景下,通常不存在“多个依赖”:
• 编译 .c 为 .o 的基础规则中,依赖只有一个(即对应的 .c 文件),此时 $^ 和 $< 结果完全一样(比如 a.o 规则中,两者都是 a.c),用谁都能运行;
• 但如果后续给规则加了新依赖(比如 %.o: %.c head.h),$^ 会变成 %.c head.h(两个依赖),而 $< 依然是 %.c(第一个依赖)。
编译 .c 时,编译器只需要把 .c 文件作为输入(gcc -c 源文件 -o 目标文件),head.h 是被 .c 文件内部 #include 引用的,不需要显式传给编译器。此时用 $^ 会多传一个 head.h,虽然不报错,但属于冗余操作;而 $< 能精准拿到“真正需要传给编译器的 .c 文件”,更符合逻辑。
一句话总结
模式规则 %.o: %.c 中,$< 是“动态绑定的单个关键依赖”(每次匹配的 .c 文件),而 $^ 在这里要么和 $< 重复,要么会引入冗余依赖,因此 $< 是更精准、更符合编译逻辑的选择。
那么咱们来看一段综合的代码:
这段代码中OBJ=$(SRC:.c=.o)把所有的源文件的.c替换为.o
那么与@$(cc) -c $<这个也是把.c替换为.o,那么这两者有什么区别吗?当然有!
1. 功能本质不同
-
OBJ = $(SRC:.c=.o)
:这是纯文本替换操作,属于 Makefile 的变量替换语法(将SRC
变量中所有.c
后缀的文件名替换为.o
后缀)。它只是在 “字符串层面” 生成目标文件的文件名列表,不执行任何编译命令。例如:若SRC = a.c b.c
,则OBJ
会被替换为a.o b.o
,仅此而已。 -
%.o: %.c
:这是 Makefile 的规则定义(模式规则),表示 “所有.o
文件的生成,都依赖对应的.c
文件,并通过gcc -c $< -o $@
命令编译生成”。这里实实在在地执行了编译动作(gcc -c
是编译源文件生成目标文件的命令)。
2. 角色与作用阶段不同
OBJ = $(SRC:.c=.o)
:是变量定义,作用是收集所有需要生成的目标文件名称,为后续的链接规则(或其他依赖规则)提供 “目标文件列表”。它是 “规划要生成哪些.o
文件”。%.o: %.c
:是编译规则,作用是定义 “如何生成每个.o
文件”(即执行编译命令)。它是 “执行编译动作,把.c
变成.o
”。
3. 依赖关系的体现
OBJ
变量本身不包含依赖逻辑,只是文件名的集合。- 模式规则
%.o: %.c
明确了 **“.o
文件依赖于对应的.c
文件”**,并且指定了编译命令 —— 当 Make 检测到某个.c
文件的修改时间比对应的.o
新时,就会执行这条规则里的gcc
命令重新编译。
简单来说,OBJ = $(SRC:.c=.o)
是 “列清单(规划要做什么)”,%.o: %.c
是 “按清单执行编译(实际做什么)”。两者配合才能完成 “从 .c
到 .o
再到可执行程序” 的完整构建流程。
ok,本篇到此结束..................