你的位置:首页 > 操作系统

[操作系统]【LINUX网络编程】Makefile文件


《Linux网络编程》(第二版) 第2章的一些读书笔记 ↓

 

Makefile:在一个含有较多文件的工程中,定义一系列规则来指定编译文件的顺序,可用于管理工程。

Makefile指定了工程中的哪些源文件需要编译以及如何编译、需要创建那些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”,编译整个工程你所要做的唯一的一件事就是在shell 提示符下输入make命令,整个工程就完全自动编译。

 

首先来看一下Linux下的GCC(GNU Compiler Collection):这是一个工具集,包含gcc(跟大写的不一样), g++等编译器和ar, nm等工具集。

GCC编译器对程序的编译有4个阶段:预编译 -> 编译和优化 -> 汇编 -> 链接,下面以c代码作例子:

源代码(*.c) 【预编译 -E】 预处理后的代码(*.i) 【编译和优化 -S】 汇编代码(*.s) 【汇编 -c】 目标文件(*.o) 【链接】 可执行文件

预编译过程是将程序中引用的头文件包含进源代码中,并对一些宏进行替换

编译和优化通常是翻译成汇编语言,汇编与机器操作码之间有一对一的关系

目标文件是指经过编译器的编译和汇编生成的CPU可识别的二进制代码,但是其中的一些函数过程没有相关的指示和说明,所以一般不能执行

目标文件需要用某种方式组合起来才可以运行,这就是链接

 

一些命令选项(假设文件名为hello.c):

gcc hello.c  /* 生成可执行文件,名字为默认的 a.out。gcc后面的不只可以是*.c文件,同样也可以是*.o或其他,以下的选项也是如此,只要被处理的文件的阶段在生成文件的阶段之前即可 */

gcc -o hello hello.c  /* 生成可执行文件,名字接在-o后面,即hello */

gcc -E hello.c  /* 经过预编译的过程,默认名字格式,产生了hello.i */

gcc -S hello.c  /* 经过了预编译和编译优化的过程,名字为默认的 hello.s */

gcc -c hello.c  /* (常用) 经过了预编译和编译优化以及汇编的过程,产生了目标文件,名字为默认的 hello.o */

 

 

 

其他选项:

gcc -D宏名 , gcc -DOS_LINUX 则相当于在预编译的时候添加了 #define OS_LINUX,即当出现 #ifdef OS_LINUX 的时候,就会满足条件

gcc -Idir (大写i),将头文件的搜索路径扩大,包含dir目录 (后面会用到)

gcc -Ldir,将链接时使用的链接库搜索路径扩大,包含dir目录,gcc优先使用共享程序库

gcc -static,仅选用静态程序库进行链接

gcc -On,n为数字,优化程序执行速度和占用空间(同时会加长编译速度),常用的是2,gcc -O2

 

了解了一些基本选项之后,给出一个小项目的例子,分别用手动编译和Makefile“自动化编译”的方式来说明:

目录结构:

 

project/  main.c  add/    add_int.c    add_float.c  sub/    sub_int.c    sub_float.c

 

main.c:可以注意到add.h和sub.h并没有跟main.c同一级目录,这时就要用到 gcc -I目录 的选项了,扩大头文件搜索路径,才找到真正的*.h。至于没有定义的add_int等函数,需要在链接的时候同时包含有具体定义的.o文件

/* main.c */#include <stdio.h>#include "add.h"#include "sub.h"int main(void){	int a = 10, b = 12;	float x = 1.23456, y = 9.87654321;	printf("int a+b IS:%d\n",add_int(a,b));	printf("int a-b IS:%d\n",sub_int(a,b));	printf("float a+b IS:%f\n",add_float(x,y));	printf("float a-b IS:%f\n",sub_float(x,y));	return 0;}

add/add.h

/* add.h */#ifndef __ADD_H__#define __ADD_H__extern int add_int(int a, int b);extern float add_float(float a, float b);#endif

add/add_int.c

/* add_int.c */int add_int(int a, int b){	return a+b;}

add/add_float.c

/* add_float.c */float add_float(float a, float b){	return a+b;}

sub目录略。

根据书上的代码,将c文件转成目标文件,然后链接上。其中,如果在编译main.c的时候,由于头文件找不到,所以必须添加上 -Iadd 和 -Isub 才可以找到,书上的例子有误。

可以看到,编译一个小的项目就需要如此多的步骤:

gcc -o add/add_int.o -c add/add_int.c gcc -o add/add_float.o -c add/add_float.c gcc -o sub/sub_float.o -c sub/sub_float.c gcc -o sub/sub_int.o -c sub/sub_int.c gcc -o main.o -c main.c -Iadd -Isubgcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o ./cacu

虽然,可以通过gcc的默认规则,使用如下命令也可以生成可执行文件:

gcc -o cacu1 add/add_int.c add/add_float.c sub/sub_int.c sub/sub_float.c main.c -Iadd -Isub

但是当频繁修改源文件或者当项目中的文件比较多,关系比较复杂的时候,用gcc直接编译就会变得非常困难。

 

于是我们应该采用Makefile文件的编写,通过make命令将多个文件编译为可执行文件。

make通过解析Makefile文件中的规则,可以自动执行相应的脚本,以下就是一个简单的Makefile文件:

# 第一行item的":"左边为make命令在正确操作之后默认生成的文件,故生成cacu2,依赖于":"右边的文件。# 每一个item都是一个规则# 在从左往右扫描的过程中,如果不存在某个文件,则跳到生成它的规则# 注意底下的一行,它是以tab键开头的,不能是空格,表示满足所有依赖的情况下,执行那几行指令cacu2:add_int.o add_float.o sub_int.o sub_float.o main.o	gcc -o cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.oadd_int.o:add/add_int.c add/add.h	gcc -o add/add_int.o -c add/add_int.cadd_float.o:add/add_float.c add/add.h	gcc -o add/add_float.o -c add/add_float.csub_int.o:sub/sub_int.c sub/sub.h	gcc -o sub/sub_int.o -c sub/sub_int.csub_float.o:sub/sub_float.c sub/sub.h	gcc -o sub/sub_float.o -c sub/sub_float.cmain.o:main.c add/add.h sub/sub.h	gcc -o main.o -c main.c -Iadd -Isub# 清理的规则,可以使用make clean来主动执行clean:	rm -f cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

如图所示,在安装了make的情况下,通过编写Makefile后执行make命令,系统会自动执行gcc的一些命令,生成了cacu2这个可执行文件;然后make clean之后,便删除了本次make生成的内容。

回到Makefile文件中,事实上默认情况下,make会直接执行第一个规则,即cacu2的规则。系统先检查依赖,并在成功之后执行下述命令:

$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)

如何理解?上面的命令其实等价于下面这一条,只是用了变量扩展

gcc -o cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o -Iadd -Isub -O2

CC = gcc , TARGET = cacu2 , OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o , CFLAGS = -Iadd -Isub -O2

同理,make clean命令是这样的:

-$(RM) $(TARGET) $(OBJS)

等价于rm -f cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o,注意到RM变量前面有个 “-” 号,它表示当操作失败时不报错,命令继续执行。

 

总结一下要注意的点:

  1. Makefile由若干条规则组成,默认执行第一条,规则的基本格式是:
    TARGET...:DEPENDEDS(依赖)...  COMMAND  ...  ... # 一行一条,如果一条命令一行不够写,则需要用反斜杠分节

      

  2. 命令行必须以Tab键(不能是空格)开始,make程序把出现在一条规则之后的所有连续的以Tab键开始的行作为命令行处理
  3. 依赖项之间的顺序按照自左向右的顺序检查或者执行,如果检查无此文件时,则需要跳到相应(同名)的规则去产生这个文件,但如果没有这样一个规则,则报错。
  4. make命令执行的时候会根据文件的时间戳判定是否执行相关的命令,并且执行依赖于此项的规则。例如在某次make之后只修改main.c文件,再次用make命令编译的时候,就会只编译main.c并且执行规则,重新连接程序。
  5. 执行非默认的规则:make XXX(规则名),如make cacu
  6. 模式匹配 (使用自动变量来简化规则书写),具体在后面有说到,以下是一个例子:
    main.o:main.c  gcc -o main.o -c main.c -Iadd -Isub# 可以替换成以下写法main.o:%o:%c  gcc -o $@ -c $< -Iadd -Isub# %o:%c 表示将TARGET域的扩展名替换成.c,$@表示TARGET域的内容,$<表示依赖项的结果

      

Makefile中使用变量:

为啥引入变量? 在某个规则中添加一项依赖,既要在依赖项中填写,又要在命令行中填写,很不方便。

变量有3种:

  1、预定义变量:Makefile中已经定义的变量,用户可以直接使用这些变量,不用进行定义。(图来自此链接) 使用方法与自定义变量一样,见3

    

  2、自动变量:在编译语句中,会经常出现目标文件和依赖文件,自动变量代表这些目标文件和依赖文件。

    

  3、用户自定义变量:

    定义方法: 变量名 = 值。 eg,  OBJS = add_int.o add_float.o ...   # 一行代表一个变量

    使用方法: $(变量名)。  eg,  $(OBJS)

 

/* 此处重写一份Makefile */

# 定义一些变量CC = gccCFLAGS = -Iadd -Isub -O2TARGET = cacu3OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o# $(CC) -o $(TARGET) $(OBJS) $(CFLAGS)$(TARGET):$(OBJS)	$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)$(OBJS):%o:%c	$(CC) -o $@ -c $< $(CFLAGS)clean:	-$(RM) $(TARGET) $(OBJS)

在此处运用了定义变量的方式,简化了文本的维护。

$(CC) -o $@ -c $< $(CFLAGS)  采用了所谓的“静态模式”的规则,相当于是多条规则,规则如上面所示。

 

搜索路径:

在大的系统中,存在很多目录,手动用-I的方法添加目录不方便,所以用到了VPATH变量,它可以自动找到指定文件的目录并添加到文件上。

使用方法:VPATH = path1:path2:...:.

用冒号(:)隔开,记得在最后加上当前目录 . 

这样会出现一个问题:目标文件会放到当前的目录下,污染环境!所以,自定义一个变量,创建以该变量为名字的目录,将*.o文件放进去。

/* 此处再重写一份Makefile */

# 发现一个严重的错误,因为Makefile文件行末没有终结符,所以最好不要盲目在后面放注释,而应该换行。。# 定义一些变量,并引入VPATHCC = gccCFLAGS = -Iadd -Isub -O2# 将所有的目标文件放置在这个文件夹中OBJSDIR = objsVPATH = add:sub:.TARGET = cacu4# OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.oOBJS = add_int.o add_float.o sub_int.o sub_float.o main.o	# 定义了VPATH之后,OBJS中的文件夹名就不用写了# 先检查目录是否存在$(TARGET):$(OBJSDIR) $(OBJS)	$(CC) -o $(TARGET) $(OBJSDIR)/*.o $(CFLAGS)$(OBJS):%o:%c	$(CC) -o $(OBJSDIR)/$@ -c $< $(CFLAGS)# mkdir -p 创建所有遗失的父目录$(OBJSDIR):	mkdir -p ./$@clean:	-$(RM) $(TARGET) $(OBJSDIR)/*.o

 

自动推导规则:使用命令make编译扩展名为c的C语言文件的时候,源文件的编译规则不用明确给出。按照默认的规则,通过依赖中的.o文件,找到对应的.c文件将其编译成目标文件用于满足依赖。

# 使用命令make编译扩展名为c的C语言文件的时候,源文件的编译规则不用明确给出。(会使用一个默认的编译规则) -- make的隐含规则CC = gccCFLAGS = -Iadd -Isub -O2VPATH = add:sub:.TARGET = cacu5OBJS = add_int.o add_float.o sub_int.o sub_float.o main.o$(TARGET):$(OBJS)	$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)clean:	-$(RM) $(TARGET) $(OBJS)

 

递归make:

当有多人在多个目录进行程序开发,并且每个人负责一个模块,而文件在相对独立的目录中,这时由同一个Makefile维护代码的编译会十分蹩脚。

 

1、递归调用的方式:make命令有递归调用的作用,它可以递归调用每个子目录的Makefile。

假设目录add和sub中都有Makefile,则可以用以下两种方法(第二种比较好):

add:  cd add && $(MAKE)

add:  $(MAKE) -C add

都表示进入add目录,然后执行make命令

 

2、总控Makefile:调用$(MAKE) -C即可。如果总控Makefile中的一些变量需要传递给下层的Makefile,可以使用export命令。

以下是总控的Makefile代码实现:

export CC = gccCFLAGS = -Iadd -Isub -O2TARGET = cacu6export OBJSDIR = ${shell pwd}/objs$(TARGET):$(OBJSDIR) main.o	$(MAKE) -C add	$(MAKE) -C sub	$(CC) -o $(TARGET) $(OBJSDIR)/*.omain.o:main.c	$(CC) -o $(OBJSDIR)/$@ -c $^ $(CFLAGS)$(OBJSDIR):	mkdir -p $(OBJSDIR)clean:	-$(RM) $(TARGET) $(OBJSDIR)/*.o

以上代码与书上有一些不同,注意CC需要export到子Makefile中,否则会默认使用cc而不是gcc

${shell pwd}表示执行shell中的pwd命令,即获取当前路径

生成的目标文件(*.o)全都放进项目目录的objs目录下

以下是子目录Makefile:

add/Makefile:

OBJS = add_int.o add_float.oall:$(OBJS)$(OBJS):%o:%c	$(CC) -o $(OBJSDIR)/$@ -c $< -O2clean:	-$(RM) $(OBJS)

sub/Makefile:

OBJS = sub_int.o sub_float.oall:$(OBJS)$(OBJS):%o:%c	$(CC) -o $(OBJSDIR)/$@ -c $< -O2clean:	-$(RM) $(OBJS)

 结果如下图:

 

3、Makefile中的函数

  • 获取匹配模式的文件名 wildcard:查找当前目录下所有符合模式PATTERN的文件名,返回值是以空格分割的(符合模式的)文件名列表。    原型:$(wildcard PATTERN) , 如:$(wildcard *.c)
  • 模式替换函数 patsubst:相当于replace函数,查找字符串text中按照空格分开的词,将符合模式的字符串替换成别的字符串。    原型:$(patsubst pattern, replacement, text), 如:$(patsubst %.c, %.o, $(wildcard *.c) )
  • 循环函数 foreach:在LIST中取出每个用空格分割的VAR单词,然后执行TEXT表达式,处理结束后输出。        原型:$(foreach VAR, LIST, TEXT), 如:$(foreach dir, $(DIRS), $(wildcard $(dir)/*.c) )

以下是根据书上的Makefile文件改写的一个用函数的Makefile文件:

CC = gccCFLAGS = -Iadd -Isub -O2TARGET = cacu7DIR = add sub .FILES = $(foreach dir, $(DIR), $(wildcard $(dir)/*.c) )OBJS = $(patsubst %c, %o, $(FILES))$(TARGET):$(OBJS) main.o	$(CC) -o $@ $^ -O2$(OBJS):%o:%c	$(CC) -o $@ -c $< $(CFLAGS)clean:	-$(RM) $(TARGET) $(OBJS)

 

----- 分割线 -----

 

以上的Makefile例子和整个项目代码可以参见:TyrusChin - Github