🏡简介

今天学习linux,第一次遇到了Makefile,写下这篇文章记录一些学到的知识。
Makefile是一种常用的构建工具,用于自动化编译、链接和部署程序。它最初是为Unix系统开发的,但现在已经广泛用于各种操作系统和编程语言中。Makefile的核心思想是基于依赖关系自动化构建,它会根据文件的修改时间和依赖关系来确定哪些文件需要重新编译和链接。
这也正是makefile的主要功能。
一个特别大的项目,一般来说会有很多的源文件,被分门别类的放在不同的目录中,有时候也会在一个目录里存放了多个程序的源代码。这时,如何对这些代码的编译就成了个问题。Makefle就是为这个问题而生的,它定义了一套规则,决定了哪些文件要先编译,哪些文件后编译,哪些文件要重新编译
不过在了解makefile之前,还得先补习一点关于程序编译的知识点。

🏫linux与gcc/g++

在Linux系统中,gcc/g++是一款非常常用的编译器。它可以将C/C++的源代码编译、汇编、链接,生成可执行文件或库文件。

在编译过程中,一个代码文件需要经过预处理、编译、汇编、连接等步骤才能转化为可执行的程序。

  1. 预处理:主要进行宏替换、文件包含、条件编译、去注释等操作。预处理指令以#号开头。

  2. 编译:在这个阶段中,gcc/g++ 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,然后将代码翻译成汇编语言

  3. 汇编:汇编阶段是将编译阶段生成的“.s”文件转成目标文件

  4. 链接:在成功编译之后,就进入了链接阶段,将目标文件链接成可执行文件或库文件。
    可能有人很早就有疑惑:在我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么到底printf是在哪里实现的?
    系统把这些函数实现都被做到名为 libc.so.6 的库文件中,在没有特别指定时,gcc会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。

函数库一般分为静态库和动态库两种。

  1. 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也
    就不再需要库文件了。其后缀名一般为“.a”
  2. 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时 链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”,如前面所述的 libc.so.6 就是动态
    库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件。

二者可以看作是网吧的电脑(动)和自己家的电脑(静),想玩电脑时,可以选择玩家里的或者去网吧上网,一旦网吧停业,就会有大批家里没有电脑的人无法上网,但自己买一台电脑价格也是高昂的。
总的来说,动态库虽然有效的节约了资源(不用自己买电脑),但一旦缺失,几乎各个程序都会无法运行(都上不了网)。而静态库虽然可以让程序独立的运行,但体积大比较消耗资源(单独买电脑价格高昂)是他的弊病。

这么说了一大串可能对初次接触的同学有些困难,可以结合图片来看一下。

可以看到从我们写出来的源文件到变成一个可执行的exe文件,大概经过了什么步骤。

在linux中,我们使用这样的指令:

1
gcc/g++的使用格式为:gcc [选项] 要编译的文件 [选项] [目标文件]

预处理:使用选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程。例如,要将hello.c文件预处理成hello.i文件,可以使用如下命令:

1
gcc –E hello.c –o hello.i

编译:使用选项“-S”,该选项只进行编译而不进行汇编,生成汇编代码。例如,要将hello.i文件编译成hello.s文件,可以使用如下命令:

1
gcc –S hello.i –o hello.s

汇编:使用选项“-c”,将汇编代码转化为“.o”的二进制目标代码。例如,要将hello.s文件汇编成hello.o文件,可以使用如下命令:

1
gcc –c hello.s –o hello.o

连接:将目标文件链接成可执行文件或库文件。例如,要将hello.o文件链接成hello可执行文件,可以使用如下命令:

1
gcc hello.o –o hello

以上就是一些关于gcc/g++的基础知识点了。接下来进入正题。

🏢linux项目自动化构建工具-make/Makefile

正如上面所铺垫的知识点,MakeFile是一个GNU推出的编译开发工具,能为编译过程提供服务。将c、.cpp、.h文件编译成最后能够执行的.exe文件就是makefile的职责。
你可以手动的一步一步的编译,也可以用MakeFile来辅助你编译,用了MakeFile除了能提升效率,还能避免人为操作导致错误。
这一节有两个重要的概念要理清楚:

  1. make是一个命令。
  2. makefile是一个当前目录下的文件。

makefile存在如下的三种基本语法:
变量
变量可以用于存储各种信息,如编译器选项、源文件列表、目标文件名等。在Makefile中,变量以$符号开头,可以使用=或:=来进行赋值。=将在每次使用该变量时进行展开,而:=则只在赋值时进行展开。

1
2
3
CC = gcc
CFLAGS = -Wall -g
OBJS = main.o utils.o

在上面的例子中,CC表示编译器名称,CFLAGS表示编译器选项,OBJS表示目标文件列表。

规则
规则是Makefile的核心,它描述了一个目标文件、其依赖关系以及如何生成目标文件的命令。规则的格式如下:

1
2
target: dependencies
command

注意,这里有一点很重要,command前必须是tab个空格!也就是说缩进不当可能导致报错Makefile:2: missing separator. Stop.这种情况只需要在command前按下tab即可(此时command必须在此行首位)。
其中,target表示目标文件名,dependencies表示该目标文件所依赖的其他文件,command表示生成目标文件的具体命令。例如:

1
2
main.o: main.c utils.h
$(CC) $(CFLAGS) -c main.c

在上面的例子中,main.o是目标文件名,main.c和utils.h是其依赖关系,$ … main.c是生成目标文件的具体命令。

伪目标
伪目标是指不对应任何文件的目标,其主要作用是在Makefile中定义一些常用的操作,如清除临时文件、生成文档等。伪目标的名称前面需要加上.PHONY关键字,以告诉Makefile不要将其当做文件名来处理。例如:

1
clean: rm -f mycode

在上面的例子中,clean是伪目标的名称,rm -rf *.o是清除临时文件的具体命令。
来看一个具体的例子:
比如现在有一个hello.c的源文件,我们需要通过make命令将其转换为可执行的文件。
首先创建一个新的文件makefile或者Makefile都行。

1
touch makefile

对makefile进行编辑,输入如下内容:

1
2
mycode:hello.c
gcc hello.c -o mycode

此时直接输入make指令就可以自动的启用makefile文件了。

1
make

这里的工作原理就是:

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“mycode”这个文件,
    并把这个文件作为最终的目标文件。简单的说,就是查找是否存在依赖关系,也就是有没有hello.c这个文件,有的话就执行指令,没有的话还得去找hello.c作为target的文件。
    这样说可能有些晦涩,让我们将上述例子再扩充一下:
1
2
3
4
5
6
7
8
9
10
mycode:hello.o                                                                                                                                                                                              
gcc hello.o -o mycode
hello.o: hello.s
gcc -c hello.s -o hello.o

hello.s: hello.i
gcc -S hello.i -o hello.s

hello.i: hello.c
gcc -E hello.c -o hello.i

这里可以看出,mycode并没有与之对应的hello.o文件,那么此时make就会去寻找target为hello.o的target的文件与其依赖关系,若是没有hello.o的就会去找hello.s的。
执行makefile命令后会显示如下指令顺序:

1
2
3
4
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o mycode

简单的说:如果hello文件不存在,或是hello所依赖的后面的hello.o文件的文件修改时间要比hello这个文件新,那么,他就会执行后面所定义的命令来生成hello这个文件。
如果hello所依赖的hello.o文件不存在,那么make会在当前文件中找目标为hello.o文件的依赖性,如果
找到则再根据那一个规则生成hello.o文件。当然,c和h文件肯定是存在的,如果iso三种文件都没有,就会从c开始执行指令。这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不会管他。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么makefile就会放弃工作