Makefile:简介

Make 可以简化编译过程,如果有一个近百个源文件的项目,如果有个文件更改后工程需要重新编译,那么一直用gcc -c a.c这些个命令敲来敲去会屎人的。运行make时候,他会寻找指定目录下(默认是 .)的 Makefile 文件并且分析依赖关系进行必要的编译。

Makefile文件的基本格式很简单:

目标文件: 依赖文件1 依赖文件2 依赖文件3 。。。。
[tab]编译命令

他的意思是目标文件是依赖于冒号后面几个文件的,如果这些依赖文件有更新的,那么其目标文件也需要更新。

Makefile 中可能有很多以上条目,他们共同组成了一个有向无回路图(DAG图),这样可以传递依赖。make 命令会把 Makefile 文件的第一个目标文件作为默认目标,当执行 make 命令时,make 会考察这个目标文件的依赖关系,进行编译。也可以指定,比如这个 Makefile:

main: a.o b.o
[tab]gcc -o main a.o b.o
a.o : a.c c.h
[tab]gcc -c a.c -o a.o
b.o: b.c c.h
[tab]gcc -c b.c -o b.o
// [tab]的意思是这里用tab字符代替,不能有其他的什么字符

在命令行里执行make,分析关系并生成main,如果是make a.o那么他只会编译到 a.o 。

当然我们还可以设定伪目标,比如:

clean:
[tab]rm a.o b.o

这样执行make clean的时候就把.o文件清除了,这里不会生成什么文件,只进行一些操作,更清楚的做法是在前面加上以下语句:

.PHONY : clean install dest [其他伪目标]

下面来说下变量,Makefile 里的变量按惯例是大写,包括数字字母下划线。当我们需要一个变量的值的时候,通常用 ${NAME} 或者 $(NAME)。他有好几种变量定义的方法。

首先是常规法,就是A=content,等号两边可以有空格,和shell不一样。

其次是递归法,比如A=$(B),B=$(C),C=haha,那么当寻找A的定义的时候就会去找B,然后再找C,变量展开的时候就是当他被引用的时候,这种方法效率比较低,因为如果他引用了函数,那么每次展开都要调用函数,而且可能会出现无限递归(A=$(B),B=$(A))

然后是直接展开法。这个很容易理解,就像是c语言是按照顺序执行的,当变量定义的时候这个变量就已经展开了(如果他引用了变量A,引用的是他定义时候A的值),当被引用的时候就直接用他代表的字符串替代。但是他用的不是等号 是 := ,比如 A:=hello,A:=$(B)

还有嵌套定义: A=B,B=haha,V=$($(A))类似于这种的V的值是haha

最后是替换引用定义,他会替换后缀,有个例子很好 foo := a.o b.o c.o ,bar := $(foo:.o=.c),我们可以知道bar的值就是a.c b.c c.c

变量还有分类:

1.预定义变量,当使用隐式规则的时候他会派上用场,常用的有以下几个:

CC   c编译器的名称(默认gcc?)
CPP    c预编译器名称(默认$(CC) -E)
CXX c++编译器的名称(默认g++)
CFLAGS c编译器选项,无默认值
CXXFLAGS c++编译器选项,无默认值

2.自动变量,常用有以下:

$@:表示当前规则中的完整目标文件名
$*:不包含扩展名的目标文件名
$<:当前规则中第一个依赖文件名
$^:当前规则所有文件列表
$%:当目标为库文件时,表示库文件名

3.环境变量,Makefile对环境变量是可见的,可以引用.

Makefile还有个常用的东东就是隐式规则,make会自己推导.比如说

c:a.o b.o
[tab]gcc -o c a.o b.o

这时我们可以省略下面的命令,直接用第一行就行。make自动分析生成a,此时预定义变量就有用了,CC,CFLAGS等也派上了用场。

由于把握不了隐式规则的底线和能力,我还是觉得隐式规则应用的不要太多太复杂影响阅读为好。。

make的工作过程大概是以下几步

  • 读取Makefile,根据make的选项查找Makefile 初始化Makefile,将Makefile中的变量进行替换,如果Makefile中包含其他文件,则加载他
  • 解释规则,对其中的执行规则进行解析,推导隐藏规则,为目标建立关系链
  • 分析变更,根据依赖关系和时间戳,判断有木有变化。
  • 执行。

编译的基本流程

基本过程是以下四步:

  1. c(.c) 和 c++(.cc, .cpp, .cxx) 的源文件
gcc -E a.c -o a.i   // 如果不加-o参数,gcc会把处理过的源文件放到标准输出中

2.预处理后的源文件。c源文件预处理后后缀为 .i , c++为 .ii 。

gcc -S a.i  //会在当前文件夹下生成a.s

3.编译后生成的汇编源代码。后缀为 .s , .S 。

gcc -c a.s
//只进行汇编生成目标文件,.o结尾的目标文件可以用
//(ar crv libabc.a a.o b.o c.o )打包成形如lib×××.a的静态库

4.目标文件与库文件进行链接,生成可执行文件。

gcc a.o //在当前文件夹下生成a.out

其中任何一种状态,用 gcc 如果不加 -c , -E , -S 选项都会直接生成可执行文件,如果加上了选项,可以由之前任一状态生成所需要的文件(如 gcc -S a.c 可以直接生成 a.s,gcc -c a.i 可以直接生成 a.o )。如果是c++直接换用g++命令就行。

另外 gcc -v 可以输出编译过程的配置和版本信息。

gcc 警告提示

-fsyntax-only   检查程序中的语法错误,不产生输出信息
-w 禁止所有警告信息
-Wunused 声明了木有用
-Wmain main函数定义不常规
-Wall 提供所有警告
-pedantic-errors 允许ansi c标准列出的全部信息

其他常用选项

  • -g 加入调试信息,gdb调试的时候要用。
  • -On 优化选项。这里的n可以用0-3来替代。数字越大优化效果越好,-O0表示不进行优化。优化可能针对硬件进行优化,也可能针对代码优化(删除公共表达式,循环优化,删除无用信息)。优化可能大大增加编译时间和内存,他通常会将循环或函数展开,使他们以内联的方式进行,不是通过函数调用,这样可以显著提高性能,不过调试最好不要用优化选项。
  • -l 指定要用到的库,注意这里之后要加的是库的名字,如果是多线程,可能要用到pthread库,那么此时就要加上 -lpthread ,这样gcc就会到库目录中找名为libpthread.so(lib×××.so)的文件,如果是静态库的话是libpthread.a( lib×××.a)(貌似gcc先找动态库,再找静态库?)。
  • -L 指定所需要的库所在的文件夹。系统先寻找标准位置,再寻找指定位置(标准库一般在/lib或/usr/lib)。
  • -I 指定头文件的寻找路径。先找标准的,后找指定的(标准的一般在/usr/include)。
  • -static 只用静态库,再拿上面那个例子,如果加上-static,系统就会只寻找libpthread.a文件。
  • -shared 生成动态库(共享库)文件,形如 libxxx.so (gcc -shared dang.o -o libdang.so)

(注意: 原文的链接在 这里 )

 游戏发展史:Valve Go - EBNF 

Comments