操作系统(5)实验0——makefile的写法

2021-10-15 16:35:45 浏览数 (1)

之前GCC那部分我提到过,gcc啥啥啥啥傻傻的那个指令只能够编译简单的代码,如果要干大事(例如突然心血来潮写个c开头的神经网络库之类的),还是要写Makefile来编译。其实在Windows下通常用IDE,例如Visual Studio,那个所谓的“项目”就有点像Makefile。通常Windows系统下这类IDE会自动帮你配置了编译时需要的东西,而Linux环境下我们需要自己来写Makefile来实现IDE的效果,听起来会麻烦点,实际上掌握了技巧之后就那样。

这部分实验指导书里面写的东西不多,但是我觉得有必要详细拿出来讲讲,毕竟很有用。

前提与假设

这里假设使用的make是GNU的make(不同厂商的make对应的makefile写法不一样,make可以理解为根据makefile来编译链接程序的工具)。下面我们通过一个简单的例子来看makefile的具体作用、功能,以及使用方式。

简单的例子

假设当前目录下有以下文件:

当你想要编译的时候,就会使用下面的指令:

代码语言:javascript复制
gcc -o hellomake hellomake.c hellofunc.c -I.

上面指令就是指定输出结果名字为hellomake,编译两个*.c文件,而-I用来高数gcc在同一个目录下寻找用到的链接文件(.h头文件),其实这个选项还可以指定gcc去别的目录下寻找别的库,详情可以看这里。上面的-I.不是误写,不要忽略掉.,这是指定在当前目录搜索链接库的意思。

但是这只是在写小工程的时候才可以这么干,如果你要编译一个有20 个.c文件,链接需要30 个库的工程呢?很显然直接使用gcc相关指令来编译是不现实的,而且每次编译都要重新输入那么一长串指令,很麻烦。那么,我们这时候想到的第一个办法就是,直接将这个指令保存为一个文件,用的时候直接复制出来运行不就简单很多了?

而makefile恰好就有这个功能,你只需要将这个指令直接输入到makefile中,在直接用到的时候直接使用指令make,工具make就会直接帮你运行makefile中的这个命令。此时makefile应该这样写:

代码语言:javascript复制
hellomake: hellomake.c hellofunc.c hellomake.h
	gcc -o hellomake hellomake.c hellofunc.c -I.

当直接使用指令make并且没有提供任何参数的时候,make就会直接执行第一条规则,因为我们这里只有一条,所以使用指令make就会直接执行我们想要让他执行的编译指令了。注意gcc前面有一个tab,不能少了这个tab

既然提到了规则,那么就介绍下makefile中的规则写法:

在规则中,如果是第一次执行规则或者执行规则对应的prerequisites中的文件被更新了,那么在执行规则的时候才会运行规则对应的command,否则就不会执行。例如已经执行过一次hellomake规则了,如果没有更改hellomake.c、hellofunc.c、hellomake.h的话,那么再执行一次hellomake规则是不会执行下面的gcc ...指令的;如果更改了hellomake.c,例如修改了printf里面的内容,那么再执行一次make,就会调用规则对应的指令gcc ...,更新编译出来的程序。

基本上到这里就可以明白Makefile的基本规则了,后面的就基本上是锦上添花的功能而已。

引入变量

假设我们突然想要换一个编译器,或者改一改编译时候的参数设置,那么我们最好定义一些变量来实现这样的功能,直接上makefile内容:

代码语言:javascript复制
CC=gcc
CFLAGS=-I.

hellomake: hellomake.o hellofunc.o
     $(CC) -o hellomake hellomake.o hellofunc.o

这个还是很容易懂得,可能你会觉得CFLAGS那里有点奇怪,其实这是默认给C编译器(这里是gcc)指定额外配置用的,是make工具规定的。另外还有CXXFLAGSCPPFLAGS,前者用来给C 编译器指定额外参数,后者同时给C和C 编译器指定额外参数。不过有些时候CPPFLAGS只给C 编译器指定额外参数,所以具体情况还是要具体分析。注意-o后面的名字,一定是.o的,这和上面有点不一样,但是结果和上面一样。之所以放在规则里面(prerequisites部分)以及command里面,是因为这样可以让make知道在编译出hellomake之前要先编译后面的.o文件对应的.c部分,即能够让编译器理解它们之间的依赖关系。

但是这个makefile其实有一个问题,那就是如果修改了.h文件,那么再一次make的时候是不会编译的,因为make此时没有追踪相关的.h文件的变化。所以我们需要加上相关的规则来实现对.h文件的追踪。

代码语言:javascript复制
CC=gcc
CFLAGS=-I.
DEPS = hellomake.h

%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: hellomake.o hellofunc.o 
	$(CC) -o hellomake hellomake.o hellofunc.o 

重点在那个DEPS和后面的%.o ...那部分,主要讲解%.o这部分规则。首先,我们这里定义的是一个适用于所有.o为结尾的规则,我们将对应的.c结尾的文件二号(DEPS)对应的文件放在prerequisites那部分,这样make就会去追踪这些文件的变化。最后在command部分,-c意思是让编译器编译出.o文件,-o @意思是将编译出来的文件用规则左侧的名字规则来命名(例如hellomake.o),最后的

但是还是麻烦,所以我们进一步“抽象”,把hellomake那一条规则改改

代码语言:javascript复制
CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o 

%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
	$(CC) -o $@ $^ $(CFLAGS)

@代表:左边,^代表右边(看键盘上的位置就知道哪个左哪个右了,这个不用记)。这样我们只需要改OBJ就可以应付更加复杂的项目了。

引入目录

但是上面那样虽然makefile看起来还好,在项目的目录里面就显得比较杂乱,各种头文件和源代码混杂在一起显得比较没条理,所以我们通常都会将头文件集中放在一个文件夹里面,将源代码集中放在一个文件夹里面。

代码语言:javascript复制
IDIR =../include
CC=gcc
CFLAGS=-I$(IDIR)

ODIR=obj
LDIR =../lib

LIBS=-lm

_DEPS = hellomake.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))

_OBJ = hellomake.o hellofunc.o 
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))


$(ODIR)/%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

hellomake: $(OBJ)
	$(CC) -o $@ $^ $(CFLAGS) $(LIBS)

.PHONY: clean

clean:
	rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~ 

注意,这个makefile文件时放在项目文件夹下的src文件夹里面的。大概是xx(项目名)/src这样子。LIBS用来引入其他的库(通常安装在系统中,这里可以直接引用),例如这里用了-lm来引用数学的库。patsubst用来替换通配符,即变量中的%,最终会被替换成具体的文件名。

.PHONY用来避免文件使用make clean的时候真的去make一个名为clean的文件,用来确保确实跑的是定义好了的clean规则。这里的clean规则用来删掉编译出来的东西。其他的基本和上面的一样,所以就不多说了。

大概就这些,其实这个要深入进去的话还有很多可以说的,但是今天就记到这里。

参考

跟我一起写 Makefile(一) A Simple Makefile Tutorial Makefile cheatsheet速成用的cheatsheet,不过不建议一开始就看这个 CFLAGS, CCFLAGS, CXXFLAGS - what exactly do these variables control? Makefile之patsubst

0 人点赞