在解释 makefile
前,首先解释一下 .c
文件变成 .exe
文件要经过的四个步骤——预处理、编译、汇编和链接(参考来源):
windows 系统下最后生成的可执行文件为 .exe
,但 Linux 系统下为 .out
。此处的可执行文件仅针对一般 .c/.cpp
代码而言。
预处理分为四步:
#define
#
部分的代码。如:
“#if”、“#ifdef”、“#elif”、“#else”、“#endif”
;#include
,将被包含的头文件插入到该编译指令的位置。(这个过程是递归进行的,因为被包含的文件可能还包含了其他文件)“//”
和 “/* */”
。#pragma
编译指令,因为编译器需要使用它们。编译过程是整个程序构建的核心部分,编译成功,会将源代码由 文本形式转换成机器语言 ,编译过程就是把预处理完的文件进行一系列 词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件(.s
)。
词法分析: 使用一种叫做 lex
的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。
语法分析: 语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。用于语法分析也有一个现成的工具,叫做:yacc。
语义分析: 语法分析完成了对表达式语法层面的分析,但是它不了解这个语句是否真正有意义。有的语句在语法上是合法的,但是却是没有实际的意义,比如说两个指针的做乘法运算,这个时候就需要进行语义分析,但是编译器能分析的语义也只有静态语义。
中间代码生成: 初始代码是可以进行优化的,对于一些在编译期间就能确定的值,可以直接直接进行处理,比如说 2+6,在编译期间就可以确定他的值为8了,但是直接在语法上进行优化的话比较困难,这时优化器会先将语法树转成中间代码。中间代码一般与目标机器和运行环境无关。(不包含数据的尺寸、变量地址和寄存器的名字等)。中间代码在不同的编译器中有着不同的形式,比较常见的有三地址码和P-代码。
中间代码使得编译器可以分为前端和后端。编译器前端负责产生于机器无关的中间代码,编译器后端将中间代码换成机器代码。
目标代码生成与优化: 代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。
最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等。
汇编过程调用 汇编器 as
来完成,将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
使用命令 as hello.s -o hello.o
或者使用 gcc -c hello.s -o hello.o
来执行汇编,对应生成的文件是 .o
文件。
链接的主要内容就是将各个模块之间相互引用的部分正确的衔接起来。它的工作就是把一些指令对其他符号地址的引用加以修正。
链接过程主要包括了地址和空间分配、符号决议和重定向:
符号决议: 有时候也被叫做符号绑定、名称绑定、名称决议、或者地址绑定,其实就是指用符号来去标识一个地址。
比如说 int a = 6;这样一句代码,用a来标识一个块4个字节大小的空间,空间里边存放的内容就是4.
重定位: 重新计算各个目标的地址过程叫做重定位。
链接有两种模式:
静态链接: 程序运行前,将每个模块的源代码文件编译成目标文件(Linux:.o Windows:.obj)
,然后将 目标文件 和 库 一起链接形成最后的可执行文件。
库其实就是一组目标文件的包,就是一些最常用的代码变异成目标文件后打包存放。最常见的库就是运行时库,它是支持程序运行的基本函数的集合。
动态链接: 程序运行期间,系统调用动态链接器(ld-linux.so
)自动链接的过程。
举例描述:
静态连接库 libmyprintf.a
,那么 虚拟内存代码段中的 .rodata 节区
在链接后需要被重定位到一个绝对的虚拟内存地址,以便程序运行时能够正确访问该节区中的字符串信息。libc.so
中定义的函数,所以会在程序运行时通过 动态符号链接
找出 puts 函数
在 内存
中的地址,以便程序调用该函数。上面提到的四个步骤可以由 编程语言译器 gcc
来完成,gcc软件
通过 gcc这条命令
来实现各种功能,下面来看一下 gcc命令
的常用选项:
gcc test.c // 会默认生成a.out可执行程序
gcc
编译出来的默认文件名是 a.out
。gcc test.c -o test // 会生成名字是test可执行文件而不是默认的a.out
-o
把它重定向到一个输出文件里面。gcc -E test.c -o test.i //会生成test.i文件
-E
使用。.s
文件gcc -S test.c //会生成test.s文件
.o
文件。gcc -c test.c //会生成test.o文件
gcc -O1 test.c -o test
debug
调试信息,用于 gdb
调试gcc -pipe -o test test.c
#include
gcc test.c -include /root/file.h
-Idir :当你使用 #include”file”
的时候:
如果使用
-I
指定了目录,gcc/g++
会先在指定的目录查找;否则,在当前目录查找指定的头文件。
如果没有找到,回到默认的头文件目录查找。
-idirafter dir :在 -I
的目录里面查找失败,则到这个目录里面查找。
-llibrary :定制编译的时候使用的库。
gcc -lpthread test.c // 在编译的时候要依赖pthread这个库
-Ldir :指定编译的时候搜索库的路径。如果有自己的库,可以用它来定制搜索目录,否则编译器只在标准库目录里面找。dir
是目录的名字。
-M :生成文件关联信息。包含目标文件所依赖的所有源代码。
gcc -M hello.c
-M
一样,只不过忽略由 #include
所造成的依赖关系。-M
相同,只不过将输出导入到 .d
文件里面。-MM
相同,将输出导入到 .d
文件里面。gcc -shared test.c -o libtest.so // 编译动态库
推荐一个非常全的关于 Makefile
的文章:跟我一起学写 Makefile
在我们日常写代码中,一个工程的源文件不计其数, 按照类型、功能、模块等分别放在若干个目录中,这时候我们就可以利用 Makefile
来指定哪些文件先编译,哪些后编译,以及更复杂的操作。
make
是一个命令工具,它解释 Makefile
中的指令。我们只需要在 Makefile
里指定所有的操作,再用 make
这个操作,即可让整个工程自动编译。
makefile
的格式如下:
target : prerequisites
command
Object File
或 执行文件
。甚至还可以是一个 标签(Label
),如:clean
。target
的 依赖对象 。如果其中的 某个文件 要比 目标文件 要新,那么,目标文件 就被认为是 过时的 ,需要重新生成。target:prerequisites
在一行,那么,必须以 [Tab键]
开头,如果在一行,那么可以用分号做为分隔。一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。
执行 make 指令:
这样就生成了 .i,
、.s
、.o
、.out
文件。那么 make
是怎么运行的呢?
GNUmakefile
、makefile
和 Makefile
。其按顺序找这三个文件,一旦找到,就开始读取这个文件并执行。也可以给
make 命令
指定一个 特殊名字 的Makefile
。这需要使用make
的-f
或是--file
参数(--makefile
参数也行)。例如,我们有个Makefile
的名字是hchen.mk
,则可以这样执行make
命令:
make –f hchen.mk
如果在 make
的命令行中,不只一次地使用了 -f
参数,那么,所有指定的 Makefile
将会被连在一起传递给 make
执行。
target
(上面例子中的 test
),并把这个目标文件作为最终生成的文件。test
文件尚未生成;或是虽然 test
已经生成,但后面的依赖对象 test.o
文件的最后修改时间要比 test
这个文件新(可以用命令 touch
测试),那么,make
就会重新生成 test
这个文件。test
所依赖的 test.o
文件不存在,那么 make
会在当前文件中找目标文件为 test.o
的规则,如果找到则再根据那一个规则生成test.o
文件。test.o
的规则,则提前退出;否则,生成 test
文件并退出。这就是整个 make
的运行过程,make
会一层又一层地去找文件的依赖关系,直到:
make命令执行后有三个退出码:
0
:表示成功执行。1
:如果 make
运行时出现任何错误,返回 1
。2
:如果你使用了 make
的 -q
参数,导致一些目标不需要更新,那么返回 2
。而对于所定义的命令的错误,或是编译不成功,make根本不理。
可以通过通配符来简化命令行:
~
:Unix下, ~/test
表示当前用户的 $HOME
目录下的 test
目录。而 ~hchen/test
则表示用户 hchen
的宿主目录下的 test
目录。而在 Windows
或是 MS-DOS下
,用户没有宿主目录 ,那么波浪号所指的目录则根据环境变量 HOME
而定。(make支持UNIX下的通配符用法)*
:表示任意长度的字符串,*.c
表示所有后缀为c的文件。而当文件名中有通配符,如: ~
,那么可以用转义字符 \
,如 \~
来表示真实的 ~
字符。?
:表示任意一个字符串。shell 中的 自动化变量
(又名:特殊变量
) ,make 也是支持的,经常用到下面前三个自动化变量 :
$@
:目标对象 。在模式规则中,如果有多个目标,那么, $@
就是匹配于目标中模式定义的集合。$^
:所有 依赖对象 ,以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。$<
:所有 依赖对象 的 第一个 。如果依赖目标是以 模式(即 %
)定义的,那么 $<
将是符合模式的一系列的文件集。注意,其是一个一个取出来的。$?
:所有比 目标对象 新
的 依赖对象 的集合。以空格分隔。$+
: 这个变量很像 $^
,也是所有 依赖对象 的集合。只是它不去重。$%
:仅当 目标对象 是函数库文件中、表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o)
,那么, $%
就是 bar.o
, $@
就是 foo.a
。如果目标不是函数库文件(Unix下是 .a
,Windows下是 .lib
),那么,其值为空。$*
:这个变量表示目标模式中 %
及其之前的部分。(如果 目标对象 是 dir/a.foo.b
,并且 目标对象 的 模式 是 a.%.b
,那么, $*
的值就是 dir/a.foo
。)
$*
也就不能被推导出,但是,如果 目标文件 的后缀是 make
所识别的,那么 $*
就是除了后缀的那一部分。)例如:如果 目标对象 是
foo.c
,因为.c
是make
所能识别的后缀名,所以,$*
的值就是foo
。这个特性是GNU make
的,很有可能不兼容于其它版本的make
,所以,尽量避免使用$*
,除非是在 隐含规则 或是 静态模式 中。如果 目标对象 中的后缀是make
所不能识别的,那么$*
就是空值。
我们可以利用 自动化变量 简化 makefile
文件:
执行 make 命令:
我们还能进一步再简化,可以利用通配符来表示,在多个 目标对象 的 依赖对象 和 命令行 都相似时,利用通配符 %
来减少工作量,这样就可以不用一个个写出每个文件的生成规则了。
.PHONE: [命令] // 声明伪目标,无论目标是否最新,每次都重新生成。
举个 伪目标 的例子:
clean:
rm *.o temp
既然我们生成了许多编译文件,那么我们也应该提供一个清除它们的 目标 以备完整地重编译。 (以“make clean”来使用该目标)
之所以将 clean
称为 伪目标 , 是因为我们并不生成 clean
这个文件。伪目标 并不是一个 文件 ,只是一个 标签 ,由于 伪目标 不是 文件 ,所以 make
无法生成它的 依赖对象 ,无法决定它是否要执行 命令行 。我们只有显式地指明这个 目标 才能让其生效。当然,伪目标 的取名不能和 文件名 重名,不然其就失去了 伪目标 的意义了。
因此我们需要用 .PHONY
声明 伪目标 ,从而区分 伪目标 和 目标文件 。
.PHONY : clean
而只要有 .PHONY:clean
这个声明,不管是否有 clean
文件,只要执行 make clean
命令,就会运行 clean
。因此,我们要在声明后面跟上 clean
的具体内容:
.PHONY : clean
clean :
rm *.o temp
通常需要生成的程序不会设置伪对象,因为每个项目的构建需要很长的时间,所以尽可能判断不需要生成就不用重新生成。
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。
一个示例就是,如果你的 Makefile
需要一口气生成若干个可执行文件,但你只想简单地敲一个 make
完事,并且,所有的目标文件都写在一个 Makefile
中,那么你可以这样做:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
Makefile
中的第一个目标会被作为其默认目标。 我们声明了一个 all
的伪目标,其依赖于其它三个目标。由于 默认目标总是被执行的 ,而上面的 Makefile
文件中的第一个目标(默认目标) all
又是一个伪目标。因此 all 是一定会被执行的,但又因为伪目标只是一个标签不会生成文件,所以不会有 all
文件产生。于是,其它三个目标的规则总是会被执行。也就达到了我们一口气生成多个目标的目的。 .PHONY : all
声明 all
这个目标为 伪目标 。(注:这里的显式 .PHONY : all
不写的话一般情况也可以正确的执行,这样 make
可通过隐式规则推导出, all
是一个伪目标,执行 make
不会生成 all
文件,而是执行后面的多个目标。建议:显式写出是一个好习惯。)