【Linux】Makefile秘籍

> 🍃 本系列为Linux的内容,如果感兴趣,欢迎订阅🚩

> 🎊个人主页:【小编的个人主页

>小编将在这里分享学习Linux的心路历程✨和知识分享🔍

>如果本篇文章有问题,还请多多包涵!🙏
> 🎀 🎉欢迎大家点赞👍收藏⭐文章

> ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍


目录

🐼前言

🐼预处理、编译、汇编和链接都在干什么??

🐼为什么推荐使用Makefile???

🐼make原理

🐼make最佳实践


🐼前言

一个可执行程序的生成通常需要经过预处理、编译、汇编和链接四个主要阶段。在 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先这么写;

cpp 复制代码
code.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文件链接。因此我们可以这么写;

cpp 复制代码
code.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,引入了**$()**.

其中()中是变量名,可以是文件。因此,为了方便替换,我们可以这么写:

bash 复制代码
BIN=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)换成^**

即将目标文件,换成@;依赖关系换成^

cpp 复制代码
BIN=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 文件

cpp 复制代码
BIN=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,我们加入一些提示词,并使用@来过滤掉命令提示!

最佳实践

cpp 复制代码
BIN=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完成了。

感谢你耐心地阅读到这里,你的支持是我不断前行的最大动力。如果你觉得这篇文章对你有所启发,哪怕只是一点点,那就请不吝点赞👍,收藏⭐️,关注🚩吧!你的每一个点赞都是对我最大的鼓励,每一次收藏都是对我努力的认可,每一次关注都是对我持续创作的鞭策。希望我的文字能为你带来更多的价值,也希望我们能在这个充满知识与灵感的旅程中,共同成长,一起进步。如果本篇文章有错误,还请大佬多多指正,再次感谢你的陪伴,期待与你在未来的文章中再次相遇!⛅️🌈 ☀️

相关推荐
流烟默2 小时前
编写脚本在Linux下启动、停止SpringBoot工程
linux·spring boot·shell
IT 古月方源2 小时前
Linux 删除 /boot 后 恢复 (多种方法)
linux·运维·服务器
潇然四叶草2 小时前
rk3588 linux的rootfs.img挂载后通过chroot切换根目录安装应用提示空间不足
linux·rootfs·扩容·空间不足
Arbori_262153 小时前
linux 命令 mkdir
linux·运维·服务器
秃小弟3 小时前
在windows中编写的脚本,在linux上运行时报错linux $‘\r‘: command not found
linux·运维·服务器
左直拳3 小时前
C++程序从windows移植到linux后cmake脚本CMakeLists.txt的修改
linux·c++·windows·cmake·cmakelists·移植到linux
所以经济危机就是没有新技术拉动增长了5 小时前
Android 和 Linux 之间关联和区别
android·linux·运维
郭源潮15 小时前
《Linux:动态库动态链接与静态库静态链接》
linux·运维·服务器·c语言
we19a0sen5 小时前
在kali linux中配置hadoop伪分布式
linux·hadoop·分布式
dawei.wang5 小时前
vsftpd服务权限配置
linux