Makefile
1.1背景
在window上我们一般不用管源代码是如何生成可执行程序,从源代码到可执行程序的中间过程,一般有编译器帮你处理。也就是我们熟知的IDE(Integrated Development Envirment)。但是在linux上,这所有的一切都由我们自己处理。我们在linux上写完程序,一般使用gcc 或者 g++ 进行编译,生成 a.out可执行文件。但是有个问题,如果工程本身的源程序就有几百个。并且分布在不同文件夹下面,同时部分文件还依赖外部库。这个时候靠我们手动去gcc编译就显得不切实际。而makefile就是解决这种问题。当然现在流行cmake,其主要原理也是通过一些配置,帮我们生成makefile。这个也就不再我们讨论的范围内了。
1.2 gcc如何生成可执行文件
本来想直接切入主题谈如和写makefile,但是如果我们不懂gcc编译流程的话。后面可能会犯很多低级错误。首先程序编译是分为四步的,预处理、编译、汇编和连接。接下来我们分别说一下以下几步的具体内容。
1. 预处理
替换宏:会将我们编码中的 # 开头的代码。比如 #include<stdio.h>,#define MAX 5等,会把stdio.h 文件的内容替换到 #include<stdio.h> 处,其次会替换代码中所有的宏定义。
-
删注释:代码中的注释是帮助我们理解,但是没有必要生成到可执行文件中。由此在预处理阶段会将注释删除。
-
去除#ifdef 不符和要求的内容
#include<stdio.h> #define MAX 5 int main() { int a = MAX; /*为debug模式时 a = 1*/ #ifdef DEBUG a = 1; #endif printf("%d\n", a); return 0; }
以上是我们 temp.c 的内容。 我们使用 gcc -E temp.c -o temp.i 命令生成temp.i 预处理之后的文件
我们来查看temp.i 文件的内容
# 1 "temp.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "temp.cpp"
# 1 "/usr/include/stdio.h" 1 3 4
...................省略很多行...........................
...................省略很多行...........................
extern void funlockfile (FILE *__stream) throw ();
# 943 "/usr/include/stdio.h" 3 4
}
# 2 "temp.cpp" 2
int main()
{
int a = 5;
printf("%d\n", a);
return 0;
}
我们的以看出 #include<stdio.h> 被替换了。 int a = MAX; 被替换成 int a = 5。 #ifdef #endf 被去除掉。还有注释也没了。
[注意1:temp.i 的后缀不能更改。.i为某种格式。]
[拓展:预编译会对代码进行[词法分析],词法分析会识别出各个单次,确定单词的类型,将识别出的单词转换成统一词法单元(token)]
2.编译
编译主要解决的是将经过预处理的文件生成汇编代码。(以下几点仅为拓展,不要求理解)
-
词法分析:将单词序列组合成各类语法短语,如“程序”、“语句”和“表达式”等,分析源代码结果上是否正确
-
语义分析:对结构上正确的源程序进行上下文有关性质的审查
-
源代码优化
-
目标代码生成
-
目标代码优化
gcc -S temp.i -o test.s
好我们接下来执行上述命令,将预处理生成的 temp.i 文件生成汇编代码。
...................省略很多行...........................
...................省略很多行...........................
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $5, 28(%esp)
movl 28(%esp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8.4"
.section .note.GNU-stack,"",@progbits
以上为temp.i 生成的 test.s 汇编代码。由于水平不足,只能看懂一部分。当然也不需要看懂。
3.汇编
我们在第二步之后,将源代码转化为汇编代码。但是汇编代码机器也不能识别。我们要进一步转化,在汇编阶段我们将汇编代码转化为二进制代码
gcc -c test.c -o test.o
这也就是我们熟悉的.o结尾的中间代码。
4.连接
可是可执行的bin文件不一定可以执行,如果你调用了其他程序的函数,就需要将temp.o使用的先关函数正确连接。
gcc test.o -o test
[注意:本人之前犯了一个错误,总是把 gcc 中的 -c 参数理解为生成中间文件, 而 -o 理解为链接生成可执行文件。但事实上并非如此, -o 参数只是给生成的文件命名而已。望读者不要犯我出现的错误 ]
2 简单makefile编写
2.1 预备材料
接下来我们创建如下文件及文件夹。以下为其目录结构
|-- add
| |-- add_float.c
| |-- add.h
| `-- add_int.c
|-- main.c
|-- makefile
|-- sub
| |-- sub_float.c
| |-- sub.h
| `-- sub_int.c
add_float.c
#include "add.h"
/* add_float.c */
/* 浮点数求和函数*/
float add_float(float a, float b)
{
return a+b;
}
add_int.c
#include "add.h"
/* add_int.c */
/* 整数求和函数 */
int add_int(int a, int b)
{
return a + b;
}
main.c
#include "add/add.h"
#include "sub/sub.h"
#include<stdio.h>
int main(void)
{
int a =10, b = 12;
float x = 1.2345, y = 9.8765;
add_int(a,b);
printf("int a+b IS: %d \n",add_int(a,b));
printf("float x+y IS: %f \n",add_float(x,y));
return 0;
}
sub_float.c
/* sub_float.c */
/* 浮点数相减 */
float sub_float(float a, float b)
{
return a - b;
}
sub.h
/* sub.h */
#ifndef __SUB_H__
#define __SUB_H_
extern float sub_float(float a, float b);
extern int sub_int(int a, int b);
#endif /* sub.h */
sub_int.c
/* sub_int.c */
/* 整形数相减 */
int sub_int(int a, int b)
{
return a - b;
}
2.2 makefile基本规则
TARGET... : DEPENDEDS..
COMMAND
...
...
-
TARGET: 规则定义的目标。可以为文件名,也可以为一个动作。其也称之为伪目标
-
DEPENDEDS:执行该规则的依赖条件,即在执行COMMAND之前会检查这些依赖项是否存在
-
COMMAND:规则所执行的命令,即规则的动作。例如编译文件、生成库和进入目录等。
【注意1:所有的COMMAND必须以 一个 Tab 键开头。即COMMAND距离该行开始处为一个 Tab的距离,不能有空格】
【注意2:COMMAND执行前会根据文件的时间戳来判断是否执行该命令。一般来说如果目标文件的时间晚于依赖的时间,则该命令不会执行】
2.3 makefile最基本的写法
1 target:main.o add_float.o add_int.o sub_int.o sub_float.o
2 gcc -o target main.o add_float.o add_int.o sub_int.o sub_float.o
3
4 main.o:main.c
5 gcc -c main.c -o main.o
6
7 add_int.o:add/add_int.c
8 gcc -c add/add_int.c -o add_int.o -Iadd
9
10 add_float.o:add/add_float.c
11 gcc -c add/add_float.c -o add_float.o -Iadd
12
13 sub_int.o:sub/sub_int.c
14 gcc -c sub/sub_int.c -o sub_int.o -Isub
15
16 sub_float.o:sub/sub_float.c
17 gcc -c sub/sub_float.c -o sub_float.o -Isub
18
19 clean:
20 -rm -rf target main.o add_float.o add_int.o sub_int.o sub_float.o
以上就是makefile最简单的写法。好我们先对上面的makefile执行流程做一个简要的介绍
首先我们输入make指令时,就相当于执行了一条shell指令,比如我们输入 whereis make 我们就可以找到make的bin文件位置。执行make之后,它会在当前目录下搜索makefile或者Makefile的文件并执行。而我们执行make指令时,并没有加任何参数,由此它会默认执行第一伪目标(即target)。执行COMMAND前会先检查依赖是否存在。首先检查main.o,make会自动搜索后面的规则,于是就到了main.o伪目标(注意依赖找的是伪目标,并非真实文件),执行第五条指令。以此类推,所以依赖完成之后。执行第2条指令。
-rm 有人可能注意到前面多了 “-” 。其含义是,执行此指令,即使出错了,也不要报错
【题外话:本人关于gcc有很多想法并未得到"直接"证实,也就是说以上的过程并非一定正确。gcc】
2.4 makefile 使用变量
假如我们需要增加或者删除一些文件,那makefile基本就得重写。为了提高makefile的重用性,我们使用变量进行重写。
1 CC = gcc
2 TARGET = target
3 OBJS = main.o add/add_float.o add/add_int.o sub/sub_int.o sub/sub_float.o
4 CFLAGS = -Iadd
5 CFLAGS += -Isub
6
7 $(TARGET):$(OBJS)
8 $(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
9
10 $(OBJS):%.o:%.c
11 $(CC) -c $(CFLAGS) $< -o $@
12
13 clean:
14 -rm -rf $(OBJS)
当然对linux shell一点都不了解的人可能会看蒙掉。接下来我简述一下基本语法。
-
定义变量和使用变量:
- 定义 CC = gcc
- 使用 $(CC)
- " = " 为赋值
- " += " 累加
-
makefile的几个基本变量
- $@ 规则中的目标
- $^ 规则中的所有依赖
- $< 规则中的第一个依赖
-
$(OBJS):%.o:%.c 将OBJS中所有扩展名为.o的文件扩展成为 .c 的文件(记住就行)
【注意:其实makefile还有自动推导规则,但是本人觉得自动推导虽然会使makefile更加简洁,但是会带来其他问题,就是难以理解。所以本文不探究自动推导的规则】
2.5 makefile 使用变量 改进版
按照上述办法使用变量之后可以一定程度解决文件改动问题,但是如果添加文件,还是要进行较大的修改。因此我们使用linux的一些函数来进行改善。具体如下
1 CC = gcc
2 CFLAGS = -Iadd -Isub
3 DIRS = add
4 DIRS += sub
5 DIRS += .
6 OBJSDIR = objs
7 TARGET = target
8 CSOURCE = $(foreach dir, $(DIRS), $(wildcard $(dir)/*.c))
9 OBJS = $(CSOURCE:%.c=%.o)
10 RM = rm -f
11
12 $(TARGET):$(OBJS)
13 $(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
14
15 $(OBJS):%.o:%.c
16 $(CC) $(CFLAGS) -c $< -o $@
17
18 test:
19 @echo "OBJS = $(OBJS)"
20 @echo "CSOURCE = $(CSOURCE)"
21
22 clean:
23 -$(RM) $(TARGET)
24 -$(RM) $(OBJS)
我们可以执行 make test 来查看 OBJS中的变量。 echo 大家都知道。而@echo 中的 "@"含义是不打印此条命令。
-
$(foreach VAR, LIST, TEXT) 将LIST字符串中的一个空格分隔的单词,传给VAR然后执行TEXT指令,最后将执行的结果以字符串返回
-
$(wildcard DIR/*.c) 的含义是搜索DIR目录下所有以.c结尾的文件。
-
【拓展】 $(patsubst %.c,%.o, add.c) 将add.c替换成add.o
2.5 makefile 嵌套
当工程有很多,需要分别生成不同的文件,这时就需要makefile进行嵌套。
其实语法很简答
add:
cd add && $(MAKE)
等价于
add:
$(MAKE) -C add
由此我们在主目录下的 makefile 为
1 CC = gcc
2 TARGET = target
3
4 INCLUDE := -Iadd
5 INCLUDE += -Isub
6
7 CSOURCE += $(wildcard ./*.c)
8
9 COBJS = $(CSOURCE:.c=.o)
10
11 DIR = add
12 DIR += sub
13
14 GSOURCE = $(foreach dir, $(DIR), $(wildcard $(dir)/*.c))
15 GOBJS = $(GSOURCE:.c=.o)
16
17 LIBS = -lstdc++
18
19 all: ALLObject $(TARGET)
20
21 $(TARGET):$(COBJS)
22 $(CC) $^ -o $@ $(LIBS) $(GOBJS) $(INCLUDE)
23
24 ALLObject:
25 @for dir in $(DIR);do \
26 echo "$$dir"; \
27 $(MAKE) -C $$dir; \
28 done
29
30 $(COBJS):%.o:%.c
31 $(CC) -c $< -o $@
32
33 clean:
34 @for dir in $(DIR); do \
35 $(MAKE) -C $$dir clean; \
36 done
37 @rm -rf $(COBJS)
add 和 sub 目录下的makefile为
1 CC = gcc
2 CSOURCE = $(wildcard ./*.c)
3 COBJS = $(CSOURCE:.c=.o)
4
5 all:$(COBJS)
6
7 $(COBJS):%.o:%.c
8 $(CC) -c $< -o $@ $(CFLAGS)
9
10 clean:
11 @$(RM) $(COBJS)
执行主目录的makefile时,首先会检查 ALLObject 是否存在。显然它会转向执行
@for dir in $(DIR);do \
26 echo "$$dir"; \
27 $(MAKE) -C $$dir; \
28 done
for 循环,简单语法,记住就行。主要是 $(MAKE) -C $$dir 。我们之前说过,它就相当于cd $$dir && $(MAKE) 去执行dir目录下的makefile