> 🍃 本系列为Linux的内容,如果感兴趣,欢迎订阅🚩
> 🎊个人主页:【小编的个人主页】
>小编将在这里分享学习Linux的心路历程✨和知识分享🔍
>如果本篇文章有问题,还请多多包涵!🙏
> 🎀 🎉欢迎大家点赞👍收藏⭐文章
> ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍
目录
🐼前言
一个可执行程序的生成通常需要经过预处理、编译、汇编和链接四个主要阶段。在 Linux 中,对于简单的单文件项目,我们常常可以直接使用 gcc code.c -o code 一步到位地生成可执行程序,然而,如果我们反复执行这条命令,依旧会编译,这就极大的浪费了时间和空间。对于大型项目,情况就复杂得多。大型项目通常包含多个
.c
文件、头文件以及其他资源文件。在这种情况下,手动管理这些文件的编译和链接过程不仅繁琐,而且容易出错。为了简化大型项目的构建过程,Linux 引入了make 工具。
make
是一个强大的自动化构建工具,它可以根据项目中的文件依赖关系,自动执行预处理、编译、汇编和链接等步骤,生成最终的可执行程序。但要使用 make,我们需要在项目目录下编写一个 Makefile 文件,它定义了项目的构建规则。因此,本篇博客将分享一个比较完善的 Makefile 编写模板,帮助大家更好地理解和使用 Makefile 来构建大型项目。
🐼预处理、编译、汇编和链接都在干什么??
预处理:进行宏替换,去注释,条件编译替换,头文件展开
如果我们当前目录已有code.c这个文件,code.c内容是:
cpp#include<stdio.h> #define N 100 int main() { printf("hello Linux,%d\n",N); #ifdef DEBUG_MODE printf("Debug mode is enabled.\n"); #else printf("Debug mode is disabled.\n"); #endif #ifndef DEBUG_MODE printf("This code is only compiled if DEBUG_MODE is not defined.\n"); #endif // 条件编译:根据变量值选择性编译 #if x > 5 printf("x is greater than 5.\n"); #elif x == 5 printf("x is equal to 5.\n"); #else printf("x is less than 5.\n"); #endif return 0; }
我们执行gcc -E code.c -o code.i 形成预处理后的.i文件(其中**-o后是目标文件**)
在我们目录下生成了,code.i文件,code.i其内容是
头文件展开:
预处理后确实发生进行宏替换,去注释,条件编译替换,头文件展开
头文件展开是将头文件的内容,拷贝到我们code.c中
条件编译和注释,本质是对代码的裁剪。
因此,在预处理之后,我们可以不需要在使用头文件了。
编译:形成汇编语言
我们使用gcc -S code.i -o code.s 形成编译后的汇编文件code.s,这条指令的意思是从预处理文件开始,到编译完成就结束
code.s其内容是:
我们发现,确实是汇编语言。
我们再观察当前目录所有文件:
下面,我们使用gcc -c code.s -o code.o形成可重定位目标二进制文件。(也就是计算器认识的语言,二进制代码)。code.o
我们发现是一堆乱码。
我们尝试运行一下。
现在有个问题,为什么我们明明都已经形成了机器读懂的二进制文件,为什么还是运行不了。原因是我们所写的二进制文件,只是我们写的,并没有链接一些库,导致编译器不认识一些库函数(printf)等,只有和库库文件链接,才形成真的可执行程序。
因此,最后一步,我们进行和库链接。执行
使用命令:gcc code.o -o code.exe
程序被执行出来了!!!!!
🐼为什么推荐使用Makefile???
那我们既然能在命令行手动执行命令输出我们想到的程序,那我们为什么还要专门自已写一个Makefile,再使用make命令运行我们的可制成程序???
对于文件个数不多,我们还能处理,并且我们只要给计算机下达帮我们编译的命令,就能帮我们编译。那如果文件个数很多,比如有1000个.c文件,10个头文件等等,我们在通过命令行的方式手动编译,显得不现实,并且,我们已经有了可执行程序,我们再使用gcc code.o -o code.exe, 编译器依旧会执行。
这点很致命,浪费了时间,效率。并且,我们也不好清理。因此,我们推荐使用Makefile根据项目中的文件依赖关系,自动执行预处理、编译、汇编和链接等步骤,生成最终的可执行程序并且完成清理工作。
🐼make原理
make是Linux中一个内置命令
makefile是我们需要自已手动新建的文件
如果当前目录已经有了makefile文件,我们可以直接使用make命令
下面我们编写一个简单的makefile(我们直接通过.c生成可执行程序)
code.exe:code.c gcc code.c -o code.exe .PHONY:clean clean: rm -rf code.exe
其中code.exe,我们叫做目标文件 ,code.c是code.exe的依赖关系。打个比方,想要踢足球,就必须要有足球。
gcc code.c -o code.exe我们叫做依赖方法。其前面必须又空一个Tab键
关键字.PHONY后面修饰的目标是一个伪目标,而伪目标总是被执行。
那有个问题,什么叫做总是被执行????
而为什么第一个就不修饰成伪目标???
总是被执行,指的是,只要我们通过命令(make clean)执行,他都会被执行。而不是伪目标的,只会默认形成一个目标,就是从上向下的第一个目标,并且只会被执行一次,这里是make形成可执行程序,只会形成一次。
可是它是怎么知道make形成的可执行程序只执行一次,而.PHONY修饰的文件,总是被执行???
我们知道,Linux一切皆文件,文件=文件内容+文件属性。
而系统知道该文件最近修改时间,因此可以对比.src和.exe文件修改时间。来决定是否需要重新编译。只要.exe文件比.src更新,就不修改。反之
而.PHONY无视修改时间,总是执行。
因此我们可以理解,下面这个现象:
我们再make,发现不让我make了。这就解释了,make只会从Makefile从上到下扫描第一个目标文件,并且只会执行一次。系统通过比对.src的修改时间,如果该文件没被修改之前,make无法执行,也就只会编译一次。而我们不希望,文件忘记清理。
最佳实践,可执行程序不要被修饰成.PHONY,而clean,我们希望修饰成.PHONY,总是被执行
这样让我们的编译效率尽可能高一点。
🐼make最佳实践
下面我们编写几个Makefile,最后,引入我们通用的Makefile版本
既然是从Makefile从上往下依次找依赖关系code.exe依赖code.o依赖code.s依赖code.i依赖code.c。并且形成可执行程序要四步那Makefile先这么写;
cppcode.exe:code.o gcc code.o -o code.exe code.o:code.s gcc -c code.s -o code.o code.s:code.i gcc -S code.i -o code.s code.i:code.c gcc -E code.c -o code.i .PHONY:clean clean: rm -rf code.exe code.i code.o code.s
使用命令make:
形成一系列文件
调用make clean完成清理
其实:我们的习惯都是,先直接让单个程序形成单个的obj文件然后停下,然后再将所有obj文件链接。因此我们可以这么写;
cppcode.exe:code.o gcc code.o -o code.exe code.o:code.c gcc -c code.c .PHONY:clean clean: rm -rf code.exe code.o
但是这样写还是不够普遍性,对于文件名不同,改起来麻烦。所以,在Makefile,引入了**$()**.
其中()中是变量名,可以是文件。因此,为了方便替换,我们可以这么写:
bashBIN=code.exe SRC=code.c OBJ=code.o CC=gcc LFLAGS=-o CFLAGS=-c RM = rm -rf $(BIN):$(OBJ) $(CC) $(LFLAGS) $(BIN) $(OBJ) $(OBJ):$(SRC) $(CC) $(CFLAGS) $(SRC) $(LFLAGS) $(OBJ) .PHONY:clean clean: $(RM) $(BIN) $(OBJ)
这样我们通过替换,可以直接修改文件名,以及依赖方法等。
并且,为了方便,我们可以直接将依赖方法中的**(BIN)换成@ ,(OBJ)换成^**
即将目标文件,换成@;依赖关系换成^
cppBIN=code.exe SRC=code.c OBJ=code.o CC=gcc LFLAGS=-o CFLAGS=-c RM = rm -rf $(BIN):$(OBJ) $(CC) $(LFLAGS) $@ $^ $(OBJ):$(SRC) $(CC) $(CFLAGS) $(SRC) .PHONY:clean clean: $(RM) $(BIN) $(OBJ)
但是我们这里只针对一个文件,当有多个文件时,我们希望可以处理所有的.c->.o文件 && .o->.exe文件,可以这么写:
对于所有的源文件
SRC=$(wildcard *.c)使用函数
wildcard
来查找当前目录下所有的 .c 文件,并将这些文件的列表赋值给变量SRC
。OBJ=$(SRC: .c=.o)将
SRC
变量中的每个.c
文件扩展名替换为.o
,生成对象文件列表,赋值给变量OBJ
。
%.o:%.c
:定义了一个通用规则,目标是任意 .o 文件,依赖是对应的 .c 文件 。规则的命令是$(CC) $(CFLAGS) $<
,这意味着使用gcc
编译每个 .c 文件生成对应的 .o 文件。
cppBIN=code.exe SRC=$(wildcard: *.c) OBJ=$(SRC:.c=.o) CC=gcc LFLAGS=-o CFLAGS=-c RM =rm -rf $(BIN):$(OBJ) $(CC) $(LFLAGS) $@ $^ %.o:%.c $(CC) $(CFLAGS) $< .PHONY:clean clean: @$(RM) $(BIN) $(OBJ) .PHONY:print print: @echo $(SRC) @echo "--------------------------" @echo $(OBJ)
最后,为了完善Makefile,我们加入一些提示词,并使用@来过滤掉命令提示!
最佳实践
cppBIN=code.exe SRC=$(wildcard *.c) OBJ=$(SRC:.c=.o) CC=gcc LFLAGS=-o CFLAGS=-c RM =rm -rf $(BIN):$(OBJ) @$(CC) $(LFLAGS) $@ $^ @echo "编译$^成为$@" %.o:%.c @$(CC) $(CFLAGS) $< @echo "链接$<成为$@" .PHONY:clean clean: @$(RM) $(BIN) $(OBJ) @echo "清理完成" .PHONY:print print: @echo $(SRC) @echo "--------------------------" @echo $(OBJ)
为了测试:我们使用touch file{1..100}.c当前目录下创建100个空文件。
调用make命令:
最终编译101个.o文件形成code.exe可执行文件。
最后我们使用**./code.exe**,执行:
我们通过make指令执行自动化编译过程,Makefile允许定义一组规则来指定如何编译和链接程序。这样,我们一个比较完整的Makefile完成了。
感谢你耐心地阅读到这里,你的支持是我不断前行的最大动力。如果你觉得这篇文章对你有所启发,哪怕只是一点点,那就请不吝点赞👍,收藏⭐️,关注🚩吧!你的每一个点赞都是对我最大的鼓励,每一次收藏都是对我努力的认可,每一次关注都是对我持续创作的鞭策。希望我的文字能为你带来更多的价值,也希望我们能在这个充满知识与灵感的旅程中,共同成长,一起进步。如果本篇文章有错误,还请大佬多多指正,再次感谢你的陪伴,期待与你在未来的文章中再次相遇!⛅️🌈 ☀️