抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

本文主要讲解了一些文件编译和Makefile语法,Makefile在Linux中经常使用到,熟悉了解它也是非常有必要。文章大概讲述了一些内容,具体部分可以通过下面的pdf文件学习。

通过网盘分享的文件:跟我一起写Makefile.pdf
链接: https://pan.baidu.com/s/1x_GA80c7LCa8UjdQPQcd4w?pwd=cbus 提取码: cbus

1 编译工具

高级语言需要通过专门的软件转换成机器语言,才能被计算机执行。这个转换软件统称编译工具。将高级语言编译成机器语言,大致需要经过预处理、编译、汇编和链接四个过程。
(1)预处理。在这一过程中,完成将#include 的文件嵌入、进行宏替换,以及其他宏语的的处理。
(2)编译。将预处理后生成的文件进行语法分析、翻译,转换成汇编语言。如果有文件输出,生成后缀为“.s”的汇编语言文件。
(3)汇编。将汇编语言翻译成机器语言,生成目标文件,通常文件名后缀是“.o”。目标文件中是可执行的代码,但不具备可执行文件的格式,因为操作系统不知道如何将它装入内存也不知道如何给它分配地址。
(4)链接。根据要求将若干目标文件组装在一起,填入正确的外部地址,再加上工具链中的启动文件,共同组合成一个可执行文件。

正常软件开发过程中,如果不是有意为之,生成的中间文件都不会保留,预处理的输出汇编语言文件和目标文件.o皆属于这类情况。但并非所有软件的开发过程都要经过上面四个步骤,例如,开发静态库只需要前三步,最后用库管理工具做个包装就行了;如果本身就是汇编语言源程序,汇编转换工作则是不必要的。

1.1 GCC 工具链

GCC是 Linux 系统首选的编译工具。虽然 GCC 最初是 C 语言的编译器,但目前已支持包括 C++、FORTRAN、Java 在内的多种编程语言。当我们提到 GCC 时,狭义上是指条命令,而广义上则是指一组编译工具的集合,它包含下面几部分内容。

(1)预处理器 cpp、C 编译器 gcc、C++编译器 g++、FORTRAN 编译器 gfortran 等。

(2)二进制代码处理工具:汇编器as,链接器1d,库管理工具a、mnlb,代码转换工具obidump 等。在编译过程中如果有链接需求,gcc 或 g++会自动调用链接器 ld。通常 C++源程序用 g++链接,因为默认的 gcc 不链接 C++库 stdc++。
(3)调试器 gdb,用于诊断程序的错误。
(4)C语言标准库 glibc,由若干库文件和启动文件组成。

(5)应用程序头文件。

1.2 GCC常用选项

选项 说明
-o fle 输出文件
-c 编译,生成.o目标文件
-E 预处理
-S 输出汇编语言程序.s
-g 为 gdb 生成源码级调试信息
-s 去除可执行程序中的所有符号表和定位信息
-p,-pg 生成供 gprof分析的代码
-DNAME 定义宏 NAME=1
-UNAME 取消 NAME 的宏定义
-Ipath 指定额外的头文件搜索路径 path
-Lpath 指定额外的库文件搜索路径 path
-library 链接指定的库,库名不含前缀 lib 和后缀.a/.so
-O 优化选项。从-O0到-O3,数值越大,优化程度越高(程序运行越快)。默认方式是-O0(编译耗时最少 )。-0s 以优化代码大小为目标
-shared 生成共享目标文件(或称动态库)
-static 使用静态链接方式,禁用共享连接
-pthread 链接 POSIX 线程库,等效于-pthread
-fPIC 生成位置独立代码。一些共享库需要此选项
-Wa, options options 是传递给汇编器的选项,多个选项之间用逗号分隔
-Wl,options options 是传递给链接器的选项,多个选项之间用逗号分隔
-W 打印警告信息
-w 不打印任何警告信息

1.3 代码分析工具

nm:打印目标文件符号列表,包括符号的值(大小或地址 )、类型和名称。

1
2
3
4
5
6
7
$ nm main.o
U atoi
U fibo
U GLOBAL_OFFSET_TABLE
0000000000000000 T main
U perror
U printf

第一列给出符号的值,第二列是符号的类型。由于目标文件还要经过链接才能将地址定位,因此函数 main 的地址并不是最终可执行程序的地址,不在 main.o 中实现的代码(包括未声明的符号)会被标记为U(未定义)。
下面是符导类型的字母含义。存在外部地址空间时,大写字母表示外部地址,小复字母表示局部地址。

符号 含义
A 符号的值是绝对的,在链接中不能改变
B,b 符号位于未初始化的数据段(BSS)
C 公用符号,位于未初始化的数据段,链接时才进行分配
D,d 符号位于已初始化的数据段
G,g 符号位于已初始化的数据段,用于小型化目标对象
I,i 表示对另一个符号的间接引用
N 表示这是一个调试(debugging)符号
R,r 符号位于只读数据区
S,s 符号位于非初始化数据区,用于小型化目标对象
T,t 符号位于代码区(textsection)
U 该符号在当前文件中未定义
u 该符号是唯一的全局符号(GNU扩展)
V,v 弱对象。弱对象链接到普通符号时,使用普通符号;链接未定义时,该符号为0;有的系统还对“v”指定了默认值
W,w 未被特别标记的弱符号
? 符号类型未知

readelf:显示ELF(Executable Linkable Format)文件信息。

objdump:显示目标文件信息,该命令常用于目标文件的反汇编。

objdump的常用选项:

选项 功能
-a 显示归档文件头信息
-D 反汇编
-d 仅反汇编指令部分
-f 显示目标文件头信息
-g 显示调试信息
-h 显示段头部信息
-i 显示处理器架构列表
-l 反汇编时,打印源程序对应行号
-m 指定用于反汇编的处理器架构
-s 显示一个段的完整内容
-S 如果可能,同时显示源代码和反汇编代码
-x 显示所有的头部信息

ar:归档工具,常用于静态库的创建和管理。

2 编译调试代码

2.1 编译和运行

编译过程如下:

1
2
3
$	gcc -c main.c
$ gcc -c demo.c
$ gcc -o demo main.o demo.o

gcc 的“-c”选项仅将源程序编译成目标程序而不继续生成可执行程序,默认生成的文件名后缀用.o替换。不能一次性将源程序编译成可执行程序时,一般需要使用“-c”选项。不使用“-E” “-S” “-c”选项时,gcc 会调用链接器尝试生成可执行程序,“-o”用于指明生成的文件名。不使用“-o”时,生成的可执行文件被命名为 a.out,但这种方式一般不采用,因为文件名缺乏个性,不具备识别意义。

运行过程如下:

1
$	./demo

对于简单的项目,gcc 可以将多个源程序一次性编译成可执行程序,而不需要生成中间的目标文件。对于分阶段编译的目标文件,gcc会自动识别合法文件格式并进行正确处理。如:

1
$	gcc -o demo main.c demo.c

1
$	gcc -o demo main.c demo.o

2.2 模块编译成库

2.2.1 静态库

2.2.2 动态库

2.3 gdb调试

3 Makefile

3.1 GNU Make

一个大型项目往往包含许多源程序文件,在项目开发过程中,程序员需要不厌其烦地反复输人编译命令。为了避免重复操作,可以将编译过程写进下面的脚本文件中,并为它加上可执行属性。之后就可以用一条简单的命令,build,sh 代替三行编译命令。

1
2
3
4
#/bin/sh
gcc -c main.c
gcc -c demo.c
gcc -o demo main.o demo.o

这种做法缺乏一定的灵活性。当在项目中增减文件时需要修改编译脚本,无形中增加了一项工作,而且不能节省编译的时间。例如,Linux 内核有上万个源代码文件,完整地编译一需要二三十分钟(取决于计算机的性能)。如果修改了其中的部分文件,再次编译时,并不需要将所有文件都重新编译一次,只需要编译修改过的文件,以及与修改过的文件存在依赖关察的文件。这样就可以大量节省编译时间,提高开发效率。
这样的软件开发方式可通过 GNU Make 实现。

3.2 Makefile基本结构

我们在该目录下另写一个文件 Makefle:

1
2
3
4
5
6
7
8
demo: demo.o main.o
gcc -o demo main.o fibo.o
demo.o: demo.c
gcc -c demo.c
main.o:main.c demo.h
gcc -c main.c
clean:
rm -f demo main.o demo.o

使用make命令执行编译:

1
2
3
4
$ make
gcc -c main.c
gcc -c demo.c
gcc -o demo main.o demo.o

GNU Make根据一个脚本文件,按文件的时间截建立依赖关系,并根据给定的规则生成目件,默认情况下,GNU Makefile 是 GNU Make 的第一顺位脚本,接下来依次是makefile和Makefile。Linux系统习惯使用 Makefle,是因为 Linux 系统中的文件通常以小写字母命名,首字母大写的文件名在众多文件中比较容易找到。如果不接受默认,make可使用选项”-f“指定脚本文件。
Makefle 文件中描述了文件的生成规则。一个 Makefle 中可以有多项规则,每项规则由目标、依赖文件和动作三部分组成。目标和依赖文件之间用冒号“:”分开,多个依赖文件之间用空格分开。如果依赖文件比较多,可以多行书写,并使用“\”作为换行符。

目标既可以是由动作生成的文件,也可以是一个单纯的字符串标号,如“clean”这样的目标被称为伪目标。GNU Make 允许多目标,但使用同一个规则描述多个目标的依赖关系比较复杂,而且容易产生规则定义不明,应避免采用这样的形式。
目标行下面的命令被称为动作。Makefle 语法要求动作前面必须是制表符,不能用多个空格代替。如果不喜欢制表符引导动作的格式,也可以重新定义变量“RECIPEPREFIX”。一般情况下,达成一个目标使用一个动作;如果有多个动作,可以按动作的先后顺序写在依赖关系的下面。这种做法本身没有问题,只是有可能导致多余的重复动作。
通常,GNU Make 在执行动作之前会把要执行的命令行输出到标准输出设备(回显)。很多情况下,回显命令行意义并不大,而且多少会增加一些编译的时间。如果在动作前面加一个字符“@”,则该命令行就不会显示出来。典型的做法是在使用 echo 命令输出信息时禁用回显功能,否则会使得终端显示的信息看上去比较怪异。其他命令前面也可以加“@”,以避免过多的无用信息于扰屏幕。不回显的缺点是,如果在编译一个项目的过程中出了问题,可能不容易找到问题出在哪一步。

Make 的选项很多,常用的有下面几个。

  • -f file:指定文件 fle 作为 make 的脚本文件。
  • -C dir:进入目录 dir,执行该目录下的 GNU Make 脚本。在一个大型项目中,各个模块被组织在不同的子目录中,每个目录都可能有一个 Makefle,用于指导一个模块的编译过程。
  • -j jobs:指定可并行工作的数量,在多核处理器中,此选项可以大大加快编译速度。
  • -p:打印规则和变量。

默认参数情况下,make 会执行第一个目标的动作。因此,在编辑 Makefle 时通常会把实现项目的最终目标(又叫终极目标)的规则写在最前面,这样就不用在命令行中为 make 指定参数。如果明确指定了参数,GNU Make 就以该参数为目标。

除了终极目标所在的规则以外,其他规则在 Makefle 文件中的顺序无关紧要。

3.3 GNU Make基本规则

下面是一个典型的规则:

1
2
main.o: main.c dmeo.h
gcc -c main.c

一个规则描述了以下内容:

1.如何确定目标文件是否过期(需要重建目标)。过期是指目标文件(这里是 main.o)不存在或者目标文件的时间戳比依赖文件中的任何一个(这里是 main.c或者 demo.h)都要早。

2.如何重建目标文件 main.o。这个规则中使用 gcc 编译器且没有明确用到依赖文件 demo.h。如果文件 main.c中已经包含这个头文件,将它列作目标的依赖是合理的。

规则的书写有两种形式。

1
目标:依赖;动作

或者

1
2
目标:依赖
动作

规则的中心思想是:目标文件的内容是由依赖文件决定的,依赖文件的任何一处改动,都将导致目前已经存在的目标文件的内容过期。规则中的命令为重建目标提供了方法,这些命令运行在系统 shell 之上。

3.3.1 变量

以上的Makefle 只是机械地重复了键盘命令,对每个“.o”文件都要手工建立一个规则。随着源文件数量的增加,编辑 Makefile 也成了一项额外的负担。GNU Make 内建的变量和规则可以帮助我们简化 Makefile 的编写。
GNU Make 有三类变量:预定义变量(内部变量)、自动化变量和定义变量。
GNU Make 内部定义了一些变量,下表是其中比较常用的部分。内部变量可以使用 make的选项“-p”打印出来。如果存在内部变量,建议尽量使用它们,这可以使 Makefile 变得更加规范。此外,所有环境变量都将作为 GNU Make 的预定义变量。

预定义变量 含义 默认值
AR 归档维护程序名 ar
AS 汇编程序名 as
CC C 编译器名 cc
CXX C++编译器名 g++
CPP C预编译器名 $(CC) -E
FC FORTRAN编译器名 f77
LEX Lex到C语言转换器 lex
PC Pascal语言编译器 pc
RM 删除 rm-f
YACC Yacc的C解析器 yacc
YACCR Yacc的Ratfor解析器 yacc-r
TEX tex编译器(生成.dvi) tex
选项/参数 含义 默认值
ARFLAGS 归档维护程序的选项 rv
ASFLAGS 汇编程序的选项 (空)
CFLAGS C 编译器的选项 (空)
LDFLAGS 链接器(如ld)的选项 (空)
CPPFLAGS C 预编译的选项 (空)
CXXFLAGS C++编译器的选项 (空)
FFLAGS FORTRAN 编译器的选项 (空)
LFLAGS Lex 解析器选项 (空)
PFLAGS Pascal语言编译器选项 (空)
YFLAGS Yacc 解析器选项 (空)

第二类变量形式是自动化变量,它们是会随上下文关系发生变化的一类变量,在 Makefle中有非常重要的作用。

符号 含义
$@ 规则中的目标文件名
$< 规则的第一个依赖文件名
$^ 规则的所有依赖文件列表(不包括重复的文件名),以空格分开
$+ 和$^类似,但保留重复出现的文件
$? 所有比目标文件更新的依赖文件列表,以空格分开
$% 当目标是静态库时,表示库的一个成员名
$* 模式规则中的主干,即“%”所代表的部分,一般表示不包含扩展名的文件名

表中列出的也是 System V 中的 make 自动化变量。除此以外,GNU Make 还使用两个特殊的字母“D”和“F”对自动化变量进行了扩展,分别表示目录(Directory)和文件(File)。如果目标是一个包含完整路径的文件名,则$(@D)表示目标的目录部分(不包括最后的斜线),$(@F)表示目标的文件名部分;如果目标仅仅是文件名,则$(@D)就是“(当前目录)。这类自动化变量的形式有以下几种。

  • $(@D)、$(@F ):目标的目录部分和文件名部分。
  • S(* D)、$(*F):模式规则主干的目录部分和文件部分(可能不包括扩展名)。
  • S(%D)、$(%F):静态库目标中文件成员的目录部分和文件名部分。
  • $(<D )、$(<F ):第一个依赖文件的目录部分和文件名部分。
  • $(^D)、$(^F ):所有依赖文件的目录部分和文件名部分,不包括重复的文件。
  • S(+D)、$(+F ):所有依赖文件的目录部分和文件名部分,允许重复的文件。
  • S(?D)、$(?F ):比目标文件更新的依赖文件目录部分和文件名部分。

第三种形式是用户定义的变量,它可以随 make 命令导人,也可以在 Makefile 文件中定义,

利用 GNU Make 提供的变量资源,编译一个C 语言文件的规则可以写成:

1
2
main.o:main.c demo.h
$(cc)-c $<

引用变量的方法是在变量名前面加“$”(自动化变量已经有了$,不需额外再加 )。如果变量名由多个字母组成,需要用括号“()”或“{}”把变量名括起来,否则会以第一个字母作为变量名。变量 CC 是C语言编译器命令,默认是 c(UNIX 系统的编译器名)出于兼容性考虑,Ubuntu 系统中已将命令 cc 链接到 gcc。如果要明确编译器命令,可以在Makefle 文件中的第一个引用之前定义它:

1
CC = gcc

或者随 make 命令定义:

1
$CC = gcc make

定义变量的目的,一方面是为了符号统一,另一方面是为了便于替换。例如针对 Arm平台的编译项目,只需要修改 Makefile 一处,将变量 CC 重新定义成 arm-linux-gcc 即可其他地方不需要做任何改动。

GNU Make 对变量的命名规则没有太多的限制,但尽量不要取字母、数字和下划线以外的字符,因为它们有可能具有特定含义。GNUMake 的传统做法是全部使用大写字母命名变量。

变量有以下三种赋值方式。

1
2
3
FOO	= 	bar
FOO := bar
FOO ?= bar

第一种形式称为递归展开式变量;第二种形式称为直接展开式变量:第三种形式称为条件赋值,即在 FO0 此前没有赋值的情况下才会对它赋值,否则保留原来的值不变。

使用递归展开式变量的优点是,可以引用之前没有定义的、但可能在后续部分定义的变量这给编写 Makefle 带来了一定的灵活性,但需要注意避免循环嵌套(即后续部分定义的变量又引用了前面的变量 )。
变量定义时,赋值号前后可以有空格(这与命令行定义变量的要求不同),但后面的空格是不能忽略的。在 Makefle 中,所有的尾部空格都不能被忽略(另一个容易出错的地方是换行符后面出现无用的空格 )。

3.3.2 隐含规则

使用 GNU Make 内建的隐含规则,不需要在 Makefile 中明确给出重建某一个目标的命令甚至可以不需要规则。make 会自动根据已存在(或者可以被创建)的源文件类型来启动相应的动作。隐含规则为 GNU Make 提供了重建一类目标文件的通用方法。

如果不考虑编译 main.o和 demo.o 的选项,我们不需要为这两个规则指定动作,甚至都不需要为 main.o 和 demo.o 建立规则,只需要一个生成 demo的链接规则就足够了。.c文件到.o文件的生成关系就是 GNU Make 的隐含规则之一,它被定义为:

1
$(CC)$(CFLAGS) $(CPPFLAGS) -c $<

每一个内建的隐含规则中都存在一对“目标:依赖”关系,而且同一个目标可以对应多个依赖。例如:一个.o文件的目标既可以由 C 编译器编译对应的.c 源文件得到,也可以由FORTRAN 编译器编译.f源文件得到。GNU Make 会根据不同的源文件选择不同的编译器。对于 demo.c,使用的就是C编译器。如果同时还存在一个 dmeo.f文件,问题就会变得比较复杂。

下面是 GNU Make 内建的一些较常用的隐含规则。

(1)编译C程序。demo.o由demo.c生成,执行命令为:

1
$(CC)$(CELAGS)$(CPPFLAGS)-c <

(2)编译 C++程序。dmeo.o由 demo.cc、demo.C或 demo.cpp生成,执行命令为:

1
§(CXX)$(CFLAGS)$(CPPELAGS)-c $<

C++源文件的后缀不建议使用.C,因为会在 VFAT 文件系统上和 C 源文件混淆。

(3)汇编和需要预处理的汇编程序。如果需要执行预处理,demo.s由 demo.S 生成,执行命令为:

1
$(CPP)$(CPPFLAGS)

如果 demo.s 是不需要预处理的汇编源文件,通过命令

1
$(AS)$(ASFLAGS)

生成 demo.o。

(4)链接单一的o目标文件或编译单一的c文件。dmeo由 demo.o或 demo.c生成,执行的命令都是:

1
$(CC)$(LDFLAGS)$^(OADLIBES)$(LDLIBS)-o $@

此规则仅适用于由一个目标文件或源文件直接产生可执行文件的情况,并且在 Windows平台中是无效的,因为 Windows 平台对可执行文件名后缀有要求。

当需要由多个源文件来共同创建一个可执行文件时,需要在 Makefle 中增加隐含规则的依赖文件,如:

1
dmeo: demo.o main.o

3.3.3 模式规则

一个项目中的.。文件编译到。文件的规则如果是统一的,利用模式规则可以将多个相同的规则写在一起。模式规则使用模式字符“%”匹配文件名或文件名的一部分。

如规则 demo.o:demo.c可以写成“%.o:%.c”。此时,由于规则中没有明确的文件名,动作必须使用自动化变量表示:

1
2
%.o:%.c
$(CC) -c $<

该动作在目标文件被依赖时发生(包括用 make 命令指定目标时,如 make demo.o)。依赖文件中模式字符“%”的取值由目标的“%”决定,即如果目标是 main.o,则依赖文件就是 main.c。

文件名中的模式字符“%”可以匹配任何非空字符串,除模式字符以外的部分则要求一致。例如:“%.c”匹配所有以.c结尾的文件,“s%.c”匹配所有以字母s开头且后缀为.c的文件。由“%”匹配的对应依赖文件必须在规则给定的动作执行之前存在。

模式规则中的依赖文件也可以不包含“%”的匹配文件,此时表示所有符合条件的目标文件的依赖都相同。这也是一种比较常见的模式规则形式。

3.3.4 后缀规则

后缀规则可以视作是模式规则的一种变体:当在一个模式中除使用“%”匹配的文件以外不存在其他依赖文件时,目标可以写成源文件和目标文件名后缀的连体。如“.c.o”即表示“%.o:%.c”。

后缀规则中不允许出现依赖文件,否则就成了一个普通的规则。

GNU Make 处理各种规则优先级顺序的原则是:明规则的优先级高于隐规则。因此,如果在 Makefle 中存在明确定义了的规则,则该规则优先执行:如果同时存在普通规则和模式规则,则执行普通规则的动作。

3.4 GNU Make的依赖

GNUMake根据依赖文件产生目标文件。依赖文件列表清所明确,有助于减少不必要的编译工作,但不正确的依赖关系容易造成编译错误,因此正确地建立依赖关系是Makefile中重要的环节。

1
2
3
4
5
6
7
8
demo: demo.o main.o
gcc -o demo main.o fibo.o
demo.o: demo.c
gcc -c demo.c
main.o:main.c demo.h
gcc -c main.c
clean:
rm -f demo main.o demo.o

以上Makefile,除了main.o对main.c的明确依赖以外,还增加了一个依赖文件demo.h,因为main.c源文件包含且属于该项目。但在查阅文件demo.h之前,我们并不知道demo.h是否也包含了其他文件。即使包含了其他头文件、还要确定条件编译时该包含命令是否生效。这同样需要耗费大量的精力去分析。并且在开发过程中,包含的头文件会发生变化,因此频繁修改Makefle 也不是一个好的策略。

gcc的选项”-MM”或“-M”可以接 Makefile的规则格式生成C 语音中#include的文件列表,包括嵌套的#include。选项”-MM”仅列出那些用双引号包含的“.h”,文件,不列用<…>包含的文件:

1
2
$ gcc -MM main.c
main.o: main.c demo.h

而使用选项“-M”时,会看到长长的文件清单,它列出所有被包含的头文件,包括<…>格式和”…”格式。

使用gcc的选项“-MM”很容易把它们分开。利用这一功能,我们可以给 main.o增加一个依赖文件 main.d,同时为 main.d 建立规则:

1
2
3
4
main.o:main.c | main.d
main.d: main.c
$(CC)-MM -o $@ $<
-include main.d

依赖列表文件“|”后面的文件称为弱依赖,对于生成目标文件来说,弱依赖只要求存在,而不检查其时间戳。即使该文件比目标文件新,也不会触发下面的动作。“include”是 GNU Mak的命令,它的功能与 语言的#include 指示符完全相同,即把其他文件的内容嵌在本文件中,它常用于包含其他 GNU Make 格式的文件。
include 前面的“-”符号是 GNU Make 的一项功能,表示忽略此命令导致的错误。此功能也可用于规则指定的动作。一般情况下,一个命名执行错误将导致 make 过程中断。但对于一些无伤大雅的错误(如删除一个不存在的文件),也可以通过这种方法忽略,书写的时候,”-”写在命令之前、制表符之后。

3.4.1 伪目标

clean 不是一个需要生成的文件,也没有出现在其他规则的依赖列表中,它不依赖任何文件,但它必须明确地通过make命令的参数执行。GNU Make把没有任何依赖关系,只有执行动作的目标称为伪目标。

伪目标的处理比较特殊:如果目录里真的存在clean这样一个文件(即使这个文件于本项目无关),由于他没有任何依赖,GNU Make看不到比他还旧的文件,实现clean的动作就永远不会执行。为了解决这个问题,我们将clean作为一个特殊目标,“.PHONY”的依赖。

1
.PHONY: clean

这样clean就成为一个伪目标。无论在当前目录下是否存在clean这个文件,输入make clean后,下面的动作就会被执行。

3.4.2 条件判断

3.4.3 内建函数

3..4.4 静态库的更新

评论