写Makefile是一个非常便利的编译方法,由于以前习惯把所有的代码都集中在一个文件中,体现不出make的优势,当把源代码拆分成若干个源文件,Makefile就显得必要了。以下是一份简单的Makefile的教程,参考自A Simple Makefile Tutorial。正如原文所说,这份教程只是打算让初学者快速入门,写自己的makefile,来维护中小型的项目。
一个简单的例子,用K&R C中4.5那个例子:主程序(main.c)、函数代码(getop.c, stack.c, getch.c)、头文件(calc.h)。
一般的,我们会使用
1
|
gcc -o calc main.c getch.c getop.c stack.c -I.
|
来编译。-I.是指gcc在当前目录(.)下寻找include文件。如果不用makefile,在测试-修改-调试过程中,如果我们不想重敲那条编译指令的话,我们必须不停地在终端中按上下键来寻找最后的那条编译指令。不幸的是,这种编译方法有两个缺陷:1. 当你把编译指令丢失或者换电脑的时候,这样效率会很低;2. 当我们只修改了一个.c文件时,每一次都将必须重新编译所有的文件,这非常耗时,不划算。现在是切入主题的时候了
最简单的makefile写法:
Version 1
1
2
|
calc: main.c getch.c getop.c stack.c
gcc -o calc main.c getch.c getop.c stack.c -I.
|
如果把这些语句写入一个叫Makefile或者makefile的文件,然后在终端中输入make,她将会按你在makefile中要求地编译。注意:第一行中并没有任何参数,只是在冒号(:)后列出编译中所需的文件,当第一行中的任何文件中更改时,make就知道calc需要重新编译了。现在我们已经解决了问题1,不用上下按箭头了,但是对于问题2依旧没有很好地解决。注意,非常重要:gcc前面必须有一个tab,在任何指令之前都要有一个tab,不然make就会罢工的。
让事情变得更有效率一点:
Version 2
1
2
3
4
5
|
CC = gcc
CFLAGS = -I.
calc: main.c getch.c getop.c stack.c
$(CC) -o calc main.c getch.c getop.c stack.c $(CFLAGS)
|
现在我们新定义了两个常量CC和CFLAGS。这些是与make交流的特殊的常量,让make知道我们要怎么编译.c文件。CC是C编译器所使用的,CFLAGS是编译用的参数。make会先分别编译.c文件,然后生成可执行文件calc。
这种形式的makefile在小项目中非常有效,但是有一个遗憾:include文件的变动。如果我们修改了calc.h文件,make是不会重新编译.c文件的,事实上我们需要重新编译。为了解决这一问题,我们必须告诉make所有的.c文件依赖于.h文件。我们可以在makefile中增加一条规则:
Version 3
1
2
3
4
5
6
7
8
9
|
CC = gcc
CFLAGS = -I.
DEPS = calc.h
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
calc: main.o getch.o getop.o stack.o
$(CC) -o calc main.o getch.o getop.o stack.o $(CFLAGS)
|
首先宏定义DEPS,声明.c文件所依赖的.h文件。然后我们定义一条规则,为所有的.c文件生成一个.o文件。规则描述:.o文件依赖于.c文件和DEPS中声明的.h文件,为了产生.o文件,make需要使用CC中声明的编译器来编译.c文件。-c 意味着产生object文件,-o $@ 意思是编译生成的文件用上面的%.o来命名,$< 指依赖关系中的第一项(%.c)CFLAGS的定义和之前一样。
最后为了简化,我们使用特殊的宏定义 $@ 和 $^ ,分别表示冒号(:)的左右两边。为了让make中所有的规则更具通用性,在Version 4中,我们把所有的include文件作为DEPS的一部分,所有的object文件作为OBJ的一部分:
Version 4
01
02
03
04
05
06
07
08
09
10
|
CC = gcc
CFLAGS = -I.
DEPS = calc.h
OBJ = main.o getch.o getop.o stack.o
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
calc: $(OBJ)
$(CC) -o $@ $^ $(CFLAGS)
|
如果我们想把.h文件放在include目录下,.c文件放在src目录下以及一些本地的库放在lib目录下,同时我们想把.o文件整理一下,避免整个目录的凌乱。在Version 5中,定义了include,lib的目录,并把object文件放到了src目录下的obj子目录,同时还包含了任何我们想要包含的库(比如说math库-lm)。这份makefile将放在src目录下。值得注意的是,在这一版本中添加了一条clean的规则,使make clean得以运行。目录结构如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
IDIR = .. /include
CC = gcc
CFLAGS = -I$(IDIR)
ODIR = obj
LDIR = .. /lib
LIBS = -lm
_DEPS = calc.h
DEPS = $(patsubst %,$(IDIR)/%,$(_DEPS))
_OBJ = main.o getch.o getop.o stack.o
OBJ = $(patsubst %,$(ODIR)/%,$(_OBJ))
$(ODIR)/%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
calc: $(OBJ)
gcc -o $@ $^ $(CFLAGS) $(LIBS)
.PHONY: clean
clean:
rm -f $(ODIR)/*.o *~ core $(IDIR)/*~
|
其中patsubst函数包含3个参数:需要匹配的式样,用什么来替换它,需要被处理的由空格分隔的字符串。
现在我们已经有了一个不错的makefile,根据这个,我们能维护中小型的工程。当然我们能增加一些更复杂的规则;甚至创造一些规则。更多关于makefile和make请参考GNU Make Manual。
这里需要再补充几点内容,这些都是开发中会遇到的问题,1常用内置函数;2 .自动化变量;3.动态库和静态库的调用:
名称:模式字符串替换函数——patsubst。
功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。
这里,<pattern>可以包括通配符“%”,表示任意长度的字串。如果<replacement>中也包含“%”,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。
(可以用“\”来转义,以“\%”来表示真实含义的“%”字符)
返回:函数返回被替换过后的字符串。
示例:
$(patsubst %.c,%.o, a.c b.c)
把字串“a.c b.c”符合模式[%.c]的单词替换成[%.o],返回结果是“a.o b.o”
$(wildcard PATTERN...)
在Makefile中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。
例:$(wildcard *.c)”来获取工作目录下的所有的.c文件列表
notdir
使用:SRC = $(notdir wildcard)
去除所有的目录信息,SRC里的文件名列表将只有文件名。
$@, $^, $<$@ 表示目标文件
$^ 表示所有的依赖文件
$< 表示第一个依赖文件
$? 表示比目标还要新的依赖文件列表
CFLAGS 表示用于 C 编译器的选项,
CXXFLAGS 表示用于 C++ 编译器的选项。
这两个变量实际上涵盖了编译和汇编两个步骤。
CFLAGS: 指定头文件(.h文件)的路径,如:CFLAGS=-I/usr/include -I/path/include。同样地,安装一个包时会在安装路径下建立一个include目录,当安装过程中出现问题时,试着把以前安装的包的include目录加入到该变量中来。
LDFLAGS:gcc 等编译器会用到的一些优化参数,也可以在里面指定库文件的位置。用法:LDFLAGS=-L/usr/lib -L/path/to/your/lib。每安装一个包都几乎一定的会在安装目录里建立一个lib目录。如果明明安装了某个包,而安装另一个包时,它愣是说找不到,可以抒那个包的lib路径加入的LDFALGS中试一下。
LIBS:告诉链接器要链接哪些库文件,如LIBS = -lpthread -liconv
简单地说,LDFLAGS是告诉链接器从哪里寻找库文件,而LIBS是告诉链接器要链接哪些库文件。不过使用时链接阶段这两个参数都会加上,所以你即使将这两个的值互换,也没有问题。
在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则。
target ... : prerequisites ...
command
...
...
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites就是,要生成那个target所需要的文件或是目标。
command也就是make需要执行的命令。(任意的Shell命令)
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
之前一直迷茫于冒号后面写什么,这下清楚了,冒号后面内容的作用决定是否需要重新执行command,那就意味着不写也是没关系了,但修改过就不能重新make了,除非先make clean一下。
背景:写这篇博客的原因是:最近在搞嵌入式,需要交叉编译opencv库文件,自己写Makefile,通过arm-linux-g++编译、链接、生成可执行文件,从而实现了移植的过程。平台是Toradex的Apalis TK1,三千多元,买回来我就后悔了,全是英文资料,还各种Bug,迟迟无法上手。早知如此,还不如直接买Nvidia的Jetson TK1呢。
书归正传,今天写一下Makefile文件中,动态链接库和静态链接库的生成与调用。
一、概念
动态链接库:是一种不可执行的二进制程序文件,它允许程序共享执行特殊任务所必需的代码和其他资源。Windows平台上动态链接库的后缀名是”.dll”,Linux平台上的后缀名是“.so”。Linux上动态库一般是libxxx.so;相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。
静态链接库:这类库的名字一般是libxxx.a;利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。
Makefile:利用IDE开发调试的人员可能对Makefile不大理解,其实Makefile就是完成了IDE中的编译、链接、生成等工作,并遵循shell脚本中变量定义与调用的规则。
二、编写Makefile实现编译与链接
1、准备文件
我们写一个简单的工程吧,此工程包含3个文件,分别是main.cpp,func.cpp和func.h。代码如下:
1)main.cpp源文件:包含入口函数 int main()。该源文件中添加了“func.h”头文件,在入口函数中调用func()函数。func函数中在func.cpp中定义,在func.h中原型声明。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
2)func.h头文件:对void func()函数进行原型声明。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
3)func.cpp源文件:对void func()函数的定义或实现。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
2、编写Makefile文件
1)定义变量
首先定义SOURCE,OBJS和TARGET变量,用于指代我们项目中的源文件、目标文件和可执行文件。
2) 设置编译参数
CC:配置编译器为g++,
LIBS:需要调用的链接库(-l开头,去掉lib和.so。例:对 libopencv_core.so链接库的调用要写作:-lopencv_core),
LDFLAGS:链接库的路径(-L开头),
INCLUDE:头文件的路径。
3)链接生成
此步骤生成可执行文件(ELF),链接需要用到目标文件,由下一步产生
4)编译
此步骤生成目标文件(.o)
5)清理
此步骤清理可执行文件和所有的目标文件
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
上述Makefile是将编译和链接两个步骤分开写的,我们同样可以直接从源文件生成可执行文件,自动进行编译链接等工作。
方法:将上述Makefile中的:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
修改为:
- 1
- 2
其他内容,不作变化。
6)编译、执行、清理
- 1
- 2
- 3
- 4
三、动态链接库的生成与调用
引言:仍然利用上文中的main.cpp, func.cpp和func.h文件。下面,我们将func.cpp源文件制作成动态链接库libfunc.so,然后调用该动态库对main.cpp进行编译链接。
1、动态链接库的生成
Makefile如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
执行make命令之后,就可以在当前目录生成libfunc.so的动态链接库了。
注意:动态链接库必须“以lib开头,以.so结尾”。
注:缺少 -fPIC错误
linux生成动态库时,如果遇到了relocation R_X86_64_32 against `.rodata‘ can not be used when making a shared object; recompile with -fPIC错误。
解决方法:
由于我的系统是AMD64位的,所以需要在编译的时候添加 -fPIC选项,重新清理编译。
例如:
- 1
- 2
- 3
- 4
- 5
- 6
2、动态链接库的调用
引言:在第一部分中,我们将main.o和func.o两个目标文件进行链接,便生成了main可执行文件。如果甲方并没有提供func.cpp和func.o,只是提供了libfunc.so这个链接库,我们如何生成可执行文件呢?下文就是讲述如何利用动态库链接生成可执行文件。
Makefile如下:
1)编译的时候需要通过INCLUDE指明头文件的路径
2)链接的时候需要通过LDFLAGS 和 LIBS指明动态库的路径和名称。这里需要注意的是,指明动态库名称时需要“掐头去尾”,例:我们需要用到 libfunc.so库,LIBS必须定义为 -lfunc 。
3)执行的时候,需要把libfunc.so动态库拷贝到系统环境变量包含的路径下(比如/lib或/usr/lib),这样程序在运行时才能调用到动态库。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
四、静态链接库的生成和调用
1、静态链接库的生成
引言:仍然利用上文中的main.cpp, func.cpp和func.h文件。下面,我们将func.cpp源文件制作成静态链接库func.a,然后调用该静态库对main.cpp进行编译链接。
Makefile如下:
注意:AR:配置链接器为ar
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
执行make命令之后,就可以在当前目录生成func.a的静态链接库了。
注意:静态链接库必须“以.a结尾”。
2、静态链接库的调用
引言:在第一部分中,我们将main.o和func.o两个目标文件进行链接,便生成了main可执行文件;第二部分,我们将main.o和libfunc.so进行链接,也可以生成main可执行文件。如果我们既没有func.o也没有func.so,该如何生成可执行文件呢?下文就是讲述如何利用静态库func.a链接生成可执行文件。
Makefile如下:
1)编译的时候需要通过INCLUDE指明头文件的路径
2)链接的时候需要通过LDFLAGS 和 LIBS指明静态库的路径和名称。这里不需要像动态库那样“掐头去尾”,直接写作func.a即可。
3)执行的时候,不需要拷贝func.a至环境变量包含的路径,直接执行即可。