Linux 工程编译调试Makefile及技巧


随着编程语言技术的不断发展,应用程序的开发过程也越来越简化。然而功能越是强大的工具,其内部结构也越复杂。高级编程语言的背后是一套复杂的编译系统。编译系统的任务是把高级语言编写的程序翻译成计算机可以直接运行的二进制文件。

4.1 GCC

在Linux平台上,最流行的编译系统是GCC(GNU Compile Collection)。GCC也是GNU发布的最著名的软件之一。GCC的功能非常强大,主要体现在两方面。

1) GCC可以为x86、ARM、MIPS等不同体系结构的硬件平台编译程序。

2) GCC可以编译C、C++、Pascal、Java等数十种高级语言。

GCC的这两项特性对嵌入式应用开发及其重要。此外,GCC的编译效率也是非常高的,一般要高出其他编译系统20%到30%左右。所以在嵌入式Linux开发领域,使用的基本上就是GCC编译系统。

在红旗Linux 6.0系统中已经集成了GCC,通过命令“gcc -v” 可以查看GCC中C编译器gcc的详细信息。

[root@localhost ~]#gcc -v

\Using built-in specs.

Target: x86-pc-linux

Configured with: ../configure--prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --enable-shared--enable-threads=posix --enable-checking=release --with-system-zlib--enable-__cxa_atexit --disable-libunwind-exceptions --enable-libgcj-multifile--disable-libmudflap --enable-languages=c,c++,java,fortran --disable-libgcj--with-cpu=generic --host=x86-pc-linux

Thread model: posix

gcc version 4.2.1

gcc命令的使用格式为:

 gcc [选项] [文件名] [选项] [文件名]

gcc命令拥有数量庞大的编译选项,按类型可以把选项分为以下几大类。

l  总体选项:用于控制编译的整个流程。

常用选项:

-c:对源文件进行编译或汇编。

-E:对源文件进行预处理。

-S:对源文件进行编译。

-o file:输出目标文件file。

-v:显示编译阶段的命令。

l  语言选项:用于支持各种版本的C语言程序。

常用选项:

-ansi:支持符合ANSI标准的C程序。

l  警告选项:用于控制编译过程中产生的各种警告信息。

常用选项:

-W:屏蔽所有的警告信息。

-Wall:显示所有类型的警告信息。

-Werror:出现任何警告信息就停止编译。

l  调试选项:用于控制调试信息。

常用选项:

-g:产生调试信息。

l  优化选项:用于对目标文件进行优化。

常用选项:

-O1:对目标文件的性能进行优化。

-O2:在-O1的基础上进一步优化,提高目标文件的运行性能。

-O3:在-O2的基础上进一步优化,支持函数集成优化。

-O0:不进行优化。

l  连接器选项:用于控制链接过程。

常用选项:

-static:使用静态链接。

-llibrary:链接library函数库文件。

-L dir:指定连接器的搜索目录dir

-shared:生成共享文件。

l  目录选项:用于指定编译器的文件搜索目录。

常用选项:

-Idir:指定头文件的搜索目录dir

-Ldir:指定搜索目录dir。

此外,还有配置选项等其他选项,这里不做介绍了。

编译系统本身是一种相当复杂的程序,编写甚至读懂这样的程序都是非常困难的。但是从事嵌入式Linux应用的开发人员都应掌握编译系统的基本原理和工作流程。

4.1.1 GCC工作流程

在C程序的编译过程中,依次要进行预处理、编译、汇编、链接四个阶段。这里通过编译C文件test.c来展示GCC的工作流程。

例如:

l  test.c

#include<stdio.h>

intmain()

{

   printf(“Helloworld!\n”);

   return 0;

}

1. 预处理阶段

由于在test.c中使用了头文件stdio.h,所以GCC在编译时首先要把头文件stdio.h中的内容加载到test.c中的首部。

在shell中输入命令“gcc-E test.c -o test.i”。其中,参数E告诉gcc命令只进行预编译,不做其他处理;参数o用来指明输出的文件名为test.i。命令运行完毕后就会产生一个名为test.i的文件。如下所示:

[root@localhost home]#gcc -E test.c -o test.i

[root@localhost home]#ls

test.c test.i

test.i文件的代码有一百多行,如下所示的是test.i文件的最后部分代码。

externchar *ctermid (char *__s) __attribute__ ((__nothrow__));

#820 "/usr/include/stdio.h" 3 4

externvoid flockfile (FILE *__stream) __attribute__ ((__nothrow__));

 

 

 

externint ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;

 

 

externvoid funlockfile (FILE *__stream) __attribute__ ((__nothrow__));

#850 "/usr/include/stdio.h" 3 4

 

# 2"test.c" 2

intmain()

{

  printf(“Hello world!\n”);

  return 0;

}

2. 编译阶段

编译阶段是整个编译过程中最复杂的一个阶段。这里拿自然语言的翻译过程作个对比。比如在把“I love China”翻译成中文前,需要依次完成以下几个步骤:

1)考察这个句子中每个单词的拼写是不是正确。

2)考察整个句子的语法(比如主谓宾、定状补的结构等)是不是正确。

3)考察整个句子的语义是不是正确。

只有以上三个步骤都正常通过了,才能保证句子被正确翻译。同样,高级编程语言的编译阶段也必须实现这三个步骤。

1) 步骤1称为词法分析,主要负责检查关键字、标识符等是否正确。

2) 步骤2称为语法分析,主要负责检查程序中语句的语法是否正确。

3) 步骤3称为语义分析,主要负责检查程序中语句的逻辑意义是否正确。

在shell中输入命令“gcc-S test.i -o test.s”。其中,参数S告诉gcc命令只进行编译,不做其他处理。命令运行完毕后就会产生一个名为test.s的汇编文件。如下所示:

[root@localhost home]#gcc -S test.i -o test.s

[root@localhost home]#ls

test.c test.i test.s

在学习使用汇编语言编程的时候,对照C文件和其汇编程序是很好的办法。如下所示的是test.s的代码。

    .file    "test.c"

     .section .rodata

.LC0:

     .string  "Hello world!"

     .text

.globl main

     .type    main, @function

main:

     leal 4(%esp), %ecx

     andl $-16, %esp

     pushl    -4(%ecx)

     pushl    %ebp

     movl %esp, %ebp

     pushl    %ecx

     subl $4, %esp

     movl $.LC0, (%esp)

     call puts

     movl $0, %eax

     addl $4, %esp

     popl %ecx

     popl %ebp

     leal -4(%ecx), %esp

     ret

     .size    main, .-main

     .ident   "GCC: (GNU) 4.2.1"

     .section .note.GNU-stack,"",@progbits

注意,以上所示的汇编代码是针对x86平台的。

3. 汇编阶段

汇编阶段的任务是把汇编程序翻译成CPU可以识别的二进制文件,该文件又称为目标文件。

在shell中输入命令“gcc -c test.s -o test.o”,其中,参数c告诉gcc命令只进行汇编,不做其他处理。命令运行完毕后就会产生一个名为test.o的目标文件。如下所示:

[root@localhost home]#gcc -c test.s -o test.o

[root@localhost home]#ls

test.c test.i test.o test.s

在Windows系统中,目标文件的后缀是obj。

4. 链接阶段

目标文件虽然已经可以被CPU直接识别,但是单个目标文件一般是无法运行的。原因在于一个程序往往是由多个源文件组成的,每个源文件只对应一个目标文件。也许有人会问,test程序不就只有一个源文件test.c吗,为什么也不能直接运行呢?原因是test.c使用了stdio.h对应的库函数,所以必须要把test.o文件和函数库文件链接在一起才能运行。

链接阶段的任务就是把程序中所有的目标文件和所需的库文件都链接在一起,最终生成一个可以直接运行的文件,称为可执行文件。

在shell中输入命令“gcc test.o -o test”,运行完毕后就会产生一个名为test的可执行文件。输入命令“./test”执行该文件,就可以得到test文件的运行结果“Hello world!”。 如下所示:

[root@localhost home]#gcc test.o -o test

[root@localhost home]#./test

Hello world!

gcc命令生成的可执行文件的有以下三种格式。

1)a.out(Assemblerand Link editor output);

2)COFF(Commonobject file format);

3)ELF(Executableand linkable format);

其中,a.out和COFF格式都是比较老的格式,现在Linux平台上可执行文件的主流格式是ELF。

4.1.2 Glibc

从逻辑功能上看,程序的主体是由一系列函数组成的。所以说编写程序的主要工作之一就是实现函数。为了有效降低编程的工作量,编程系统会把一些非常基本、常用的函数集中到函数库中实现。比如信息的打印函数、文件的打开或关闭函数、内存空间的申请与释放函数、数学计算函数等。当程序需要使用到函数库中的某个函数时,就可以直接从库中调用。就好比建造房屋,建筑队并不需要从头开始制造砖瓦和水泥,而只需要从原材料市场购买就可以了。

每种高级编程语言都有各自的函数库。比如C语言的C 库、Visual C++的MFC、Java的JFC等。函数库中的函数都是由经验丰富的资深程序员编写的,具有出色的运行性能和工作效率。所以说,函数库的使用不光减少了编程的工作量,还能有效提高程序的性能和健壮性。在面向对象语言中,函数被封装在类中,所以函数库就演变成了类库,但其原理和机制是类似的。

函数库的使用方式分为静态链接和动态链接两种。静态链接是指编译系统在链接阶段把程序的目标文件和所需的函数库文件链接在一起,这样生成的可执行文件就可以在没有函数库的情况下运行。就好比火箭把燃料和氧料装在一起,就可以在没有空气的太空中飞行。动态链接是指编译系统在链接阶段并不把目标文件和函数库文件链接在一起,而是等到程序在运行过程中需要使用时才链接函数库。

使用静态链接方式产生的可执行文件体积较大,但运行效率较高。而使用动态链接方式产生的可执行文件由于没有库文件,所以体积较小。但由于需要动态加载函数库,所以运行效率要低一点。

在具体应用时,如果有多个源文件都需要调用函数库时,那么应该选择动态链接的方式。而只有少数源文件需要调用函数库时,应该选择静态链接的方式。可以被静态链接的函数库称为静态库,可以被动态链接的函数库称为动态库,或者共享库。

Glibc(GNU Library C)是GNU推出的C语言函数库。Glibc符合ISO C (International Standard for the C programming language)和POSIX(Portable Operating System Interfacefor Computer Environments)标准。其中,ISO C定义了C函式库的标准格式,POSIX定义了不同计算平台应该遵守的C函数库标准,是ISO C标准的扩充。因此Glibc可以在各种不同体系结构的计算平台上使用。

Glibc中包含了大量的函数库,其中libc是最基本的函数库,每个C程序都需要使用libc库。此外,常用的还有数学库libm、加密库libcrypt、POSIX线程库libpthread、网络服务库libnsl、IEEE浮点运算库libieee等。Glibc库为C程序提供了大量功能强大的函数,包括输入输出函数、字符串处理函数、数学函数、中断处理函数、错误处理函数、日期时间函数等等。

C程序在调用Glibc中的函数库时,需要引用与函数库对应的头文件。比如stdio.h、string.h、time.h等。这些头文件都存放在/usr/include目录下。同时,在编译命令中需要加入某些函数库的链接参数(在函数库的使用文档中会列出具体的链接库名称参数),并使用符号“-l”进行连接。比如libm库的链接参数为m,libpthread库的链接参数为pthread等。

例如:

l  test.c:

#include<stdio.h>

#include<math.h>

intmain()

{

  printf(“%d\n”, sin(0));

  return 0;

}

编译命令如下所示:

[root@localhost home]# gcc test.c -o test -lm

[root@localhost home]# ./test

0

在红旗Linux 6.0系统中,Glibc分布在/lib和/usr/lib目录下,其中/lib目录中的函数库文件主要是给/bin目录下的系统程序使用的,/usr/lib目录中的函数库文件主要是给/usr目录下的用户程序使用的。如下所示的是/usr/lib目录下的部分png函数库文件。

libpng.a

libpng.la

libpng.so

libpng.so.3

libpng.so.3.16.0

其中,后缀为a的是静态库文件,后缀为la的是用来记录库文件信息的动态库文件,后缀为so的是动态库文件。libpng.so.3.16.0是真正的png动态库文件,而libpng.so.3是指向libpng.so.3.16.0动态库文件的符号链接文件。

[root@localhost lib]# file libpng.so.3

libpng.so.3: symbolic link to `libpng.so.3.16.0'

[root@localhost lib]# file libpng.so.3.16.0

libpng.so.3.16.0: ELF32-bit LSB shared object, Intel 80386, version 1 (SYSV),

stripped

使用动态链接方式编译程序时,动态库的符号链接文件会写入二进制文件中。这样,程序在运行时就可以通过符号链接文件找到指定的动态库文件了。

这里以编译test.c为例,使用“gcc test.c -o test”命令编译生成可执行文件test。通过file命令可以查看test文件的相关信息,如下所示。

[root@localhost home]# file test

test: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), for GNU/Linux

2.6.9, dynamically linked (uses shared libs), for GNU/Linux2.6.9, not stripped

其中,“dynamically linked (uses sharedlibs)”表明test文件使用了动态链接库。test文件的大小是5KB左右。

通过选项static可以使用静态链接方式对程序进行编译。输入命令“gcc -static test.c -o test”生成可执行文件test,如下所示。

[root@localhost home]# file test

test: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), for GNU/Linux

2.6.9, statically linked, for GNU/Linux 2.6.9, not stripped

其中,“statically linked”表明test文件使用了静态链接库。可以看到,test文件的大小达到了540KB左右。

4. 2 工程管理器

在实际的开发过程中,仅仅通过使用gcc命令对程序进行编译是非常低效的。原因主要有以下两点。

1)程序往往是由多个源文件组成的,源文件的个数越多,那么gcc的命令行就会越长。此外,各种编译规则也会加大gcc命令行的复杂度。所以在开发调试程序的过程中,通过输入gcc命令行来编译程序是很麻烦的。

2)在程序的整个开发过程中,调试的工作量占到了整体工作量的70%以上。在调试程序的过程中,每次调试一般只会修改部分源文件。而在使用gcc命令行编译程序时,gcc会把那些没有被修改的源文件一起编译,这样就会影响编译的总体效率。

为了提高编译程序的效率,很多基于Windows平台上的开发工具都提供了工程管理器。用户只需要点击一个“make”按钮就可以启动工程管理器对整个程序进行自动编译。在整个编译的过程中是不需要人工干预的。这种工程管理器形象的称为全自动工程管理器。

GCC提供了半自动化的工程管理器Make。所谓半自动化是指在使用工程管理器前需要人工编写程序的编译规则。所有的编译规则都保存在Makefile文件中。全自动化的工程管理器在编译程序前会自动生成Makefile文件。

Make工程管理器的优越性具体体现在以下两个方面。

(1)使用方便

通过命令“make”就可以启动Make工程管理器对程序进行编译,所以不再需要每次都输入gcc命令行。Make启动后会根据Makefile文件中的编译规则命令自动对源文件进行编译和链接,最终生成可执行文件。

(2)调试效率高

为了提高编译程序的效率,Make会检查每个源文件的修改时间(时间戳)。只有在上次编译之后被修改的源文件才会在接下来的编译过程中被编译和链接,这样就能避免多余的编译工作量。为了保证源文件具有正确的时间戳,必须保证操作系统时间的正确性(注意VMWare虚拟机的CMOS时间是否正确)。

4.2.1 Makefile

Make工程管理器是完全根据Makefile文件中的编译规则命令进行工作的。Makefile 文件由以下三项基本内容组成。

1)需要生成的目标文件(target file)。

2)生成目标文件所需要的依赖文件(dependency file)。

3)生成目标文件的编译规则命令行(command)。

这三项内容按照如下格式进行组织:

target file :dependency file

command

其中,Makefile规定在书写command命令行前必须加一个<Tab>键。

Make工程管理器在编译程序时会检查每个依赖文件的时间戳,一旦发现某个依赖文件的时间戳比目标文件要新,就会执行目标文件的规则命令来重新生成目标文件。这个过程称为目标文件的依赖规则检查。依赖规则检查是Make工程管理器的最核心的工作任务之一。下面以编译程序test(由a.c、b.c和b.h组成)为例来描述Make的工作过程。例如:

l  a.c:

#include “b.h”

int main()

{

  hello();

  return 0;

}

l  b.h:

voidhello();

l  b.c:

#include “stdio.h”

void hello()

{

  printf(“hello”);

}

l  Makefile:

test : a.o b.o

cc -o test a.o b.o

a.o : a.c b.h

cc -c a.c

b.o : b.c

cc -c b.c

Make工程管理器编译test程序的过程如下:

(1)Make工程管理器首先会在当前目录下读取Makefile文件。

(2)查找Makefile文件中的第一个目标文件(在本例中为test),该文件也是Make工程管理器本次编译任务的最终目标。

(3)把目标文件test的依赖文件当作目标文件进行依赖规则检查。这是一个递归的检查过程。在本例中就是依次把a.o和b.o作为目标文件来检查各自的依赖规则。Make会根据以下三种情况进行处理:

1) 如果当前目录下没有或缺少依赖文件,则执行其规则命令生成依赖文件(比如缺少a.

文件,则执行命令“cc -c a.c”生成a.o)。

2) 如果存在依赖文件,则把其作为目标文件来检查依赖规则(假设a.c比a.o新,则执

行命令“cc -c a.c”更新a.o)。

3) 如果目标文件比所有依赖文件新,则不做处理。

(4)递归执行第三步后,就会得到目标文件test所有最新的依赖文件了。接着Make会根据以下三种情况进行处理:

1) 如果目标文件test不存在(比如第一次编译),则执行规则命令生成test。

2) 如果目标文件test存在,但存在比test要新的依赖文件,则执行规则命令更新test。

3) 目标文件test存在,且比所有依赖文件新,则不做处理。

下面通过make的运行结果来印证上述流程。

(1)第一次编译时,由于没有test、a.o和b.o,Make会先执行命令“cc -c a.c”生成a.o;然后接着执行命令“cc -c b.c”生成b.o;最后执行命令“cc -o test a.o b.o”生成test文件。如下所示:

[root@localhost home]#make

cc –c a.c

cc –c b.c

cc –o test a.o b.o

(2)如果修改了a.c文件,Make会先执行命令“cc-c a.c”生成a.o;由于b.o没有修改,所以Make就接着执行命令“cc -o test a.o b.o”生成test文件。如下所示:

[root@localhost home]#make

cc –c a.c

cc –o test a.o b.o

(3)如果删除了b.o文件,由于a.o没有修改,所以Make就先执行命令“cc -c b.c”生成b.o;然后接着执行命令“cc -o test a.o b.o”生成test文件。如下所示:

[root@localhost home]#make

cc –c b.c

cc –o test a.o b.o

(4)如果再运行一次make时,因为所有的源文件都没有改动,所以Make不会有任何动作。如下所示:

[root@localhost home]#make

make: “test”是最新的。

4.2.2 Makefile 特性介绍

源文件数量越是多的程序,其编译规则就会越复杂,导致Makefile文件也越复杂。为了简化Makefile的编写,丰富编译程序的方法和手段。Makefile提供了很多类似高级编程语言的语法机制。

1. 变量

在Makefile文件中,存在着大量的文件名,而且这些文件名都是重复出现的。所以在源文件比较多的情况下,很容易发生遗漏或写错文件名。而且一旦源文件的名称发生了变化,还容易造成与其他文件名不一致的错误。于是,Makefile提供了变量来代替文件名。变量的使用方式为:

$(变量名)

例如:

obj = a.o b.o

 

test : $(obj)

cc -o test $(obj)

a.o : a.c b.h

cc -c a.c

b.o : b.c

cc -c b.c

该Makefile使用了变量obj来代替“a.o b.o”。当源文件名发生改动或增删源文件时,只要对变量obj的值进行相应的修改就可以了,这样可以避免文件名不一致或遗漏的错误。Makefile中变量的命名可以使用字符、数字和下划线,但要注意变量名对大小写是敏感的。

此外,Make工程管理器提供了灵活的变量定义方式,具体有以下几种实现方式。

(1)通过“=”来实现

例如:

a1= $(a2)

a2= $(a3)

a3= a.o

这种方式下变量a1的值是a.o,也就是说前面的变量可以通过后面的变量来定义。但使用这种方式定义变量时,要防止出现死循环的情况。

(2)通过“:=”来实现

例如:

a1:= a.o

a2:= $(a1) b.o

这种方式下变量a1的值是a.o,变量a2的值是a.o b.o。

例如:

a1:= $(a2) b.o

a2:= a.o

这种方式下变量a1的值是b.o,而不是“a.o b.o”。也就是说前面的变量不能通过后面的变量来定义。

(3)通过“+=”来实现

例如:

a1= a.o

a1+= b.o

这种方式下变量a1的值是“a.ob.o”。也就是说“+=”可以实现给变量追加值。等同于如下示例:

a1= a.o

a1:= $(a1) b.o

可以看到,Makefile的“+=”和C语言中的“+=”是非常相似的。

(4)通过“?=”来实现

例如:

a1:= a.o

a1?=b.o

这种方式下变量a1的值是a.o,而不是b.o。也就是说,如果变量a1已经在前面定义过了,那么后面的定义就无效了。

    以上所介绍的变量都是全局变量,也就是在整个Makefile文件中都可以访问的。

2. 自动推导

为了进一步简化Makefile的书写,Make工程管理器提供了自动推导的功能。自动推导功能默认每个目标文件都有一个与之对应的依赖文件。比如a.o文件有依赖文件a.c与之对应)。这样在Makefile中就不需要指定与目标文件对应的依赖文件名了。此外,自动推导功能还能推导出与目标文件对应的基本编译规则命令。比如a.o文件的规则命令为“gcc –c –o a.c”。

例如:

obj = a.o b.o

 

test : $(obj)

cc -o test $(obj)

a.o : b.h

结果为:

[root@localhost home]#make

cc –c –o a.o a.c

cc –c –o b.o b.c

cc –o test a.o b.o

可以看到,Makefile分别推导出了目标文件a.o和b.o的规则命令“cc -c-o a.o a.c”与“cc -c -o b.o b.c”。

3.  伪目标

伪目标不是真正的目标文件,所以通过伪目标可以让Make工程管理器只执行规则命令,

而不用创建实际的目标文件。伪目标的使用方式为:

make 伪目标名

由于伪目标不是真正的目标文件,只是一个符号。为了不和真实的目标文件混淆,最好使用“.PHONY”对伪目标进行标识。

例如:

obj = a.o b.o

 

.PHONY : all

all : test $(obj)

 

test : $(obj)

    cc –o test $(obj)

 

.PHONY : clean

clean :

    rm –rf test $(obj)

 

test_dir =/home/t_d

.PHONY : install

install :

    mkdir $(test_dir)

    cp test $(test_dir)

 

.PHONY : uninstall

uninstall :

    rm -rf $(test_dir)

(1)all

运行命令“make all”后,Make会把all看成是最终的目标。由于伪目标和真实目标一样都有依赖文件,所以Make会更新all的依赖文件test、a.o和b.o。如下所示:

[root@localhost home]#make all

cc –c –o a.o a.c

cc –c –o b.o b.c

cc –o test a.o b.o

(2)clean

运行命令“make clean”后,Make会执行命令“rm -rf  test $(obj)”。这样test、a.o和b.o文件就全被删除了。如下所示:

[root@localhost home]#make clean

rm –rf test a.o b.o

(3)install

运行命令“make clean”后,Make会顺序执行命令“mkdir $(test_dir)”和“cp test $(test_dir)”,把test文件复制到test_dir变量指定的目录中去(这里只是模拟安装过程,并不是真正的实现安装方法)。如下所示:

[root@localhost home]#make install

mkdir /home/t_d

cp test /home/t_d

(4)uninstall

运行命令“make clean”后,Make会执行命令“rm -rf $(test_dir)”。这样就可以把变量test_dir指定的目录以及目录中的文件全部删除。如下所示:

 [root@localhosthome]#make uninstall

rm -rf /home/t_d

在Makefile文件中,伪目标是非常有用的。比如在递归编译、并行编译等场合中, 使用伪目标可以方便地控制编译过程。

4.  文件查找

为了便于管理和组织,程序的源文件都根据功能的不同放置在不同的子目录中。但是源文件被分散存储之后,Makefile又如何才能找到这些源文件呢?Makefile提供了以下两种方法。

(1)VPATH

VPATH是一个特殊变量。Make在当前路径找不到源文件的情况下就会自动到VPATH中指定的路径中去寻找。VPATH的使用方法为:

VPATH = 目录 : 目录 …

例如:

VPATH=/a : /b

Make会在当前路径找不到文件时按照顺序依次查找/a和/b目录。

(2)vpath

和VPATH不同的是,vpath并不是变量而是关键字。其作用和VPATH类似,但使用方式更加灵活。vpath的使用方法为:

vpath 模式 目录: 目录 …

例如:

vpath %.c /a : /b

Make会在当前路径找不到文件时按照顺序依次查找/a和/b目录中所有的C文件。vpath也可以对不同的路径采用不同的搜素模式。

例如:

vpath %.c /a

vpath %.h /b

Make会在当前路径找不到源文件时先查找/a目录下的C文件,然后再查找/b目录下的头文件。

例如:

首先在/home目录下新建一个目录b,然后把b.c文件放入目录b中。

VPATH = /home/b

obj = a.o b.o

 

.PHONY : all

all : test $(obj)

 

test : $(obj)

    cc –o test $(obj)

结果为:

[root@localhost home]#make

cc –c –o a.o a.c

cc –c –o b.o /home/b/b.c

如果把“VPATH= /home/b”修改成“vpath %.c /home/b”,运行结果是一样的。

5.  嵌套执行

如果把所有源文件的编译规则命令都写在一个Makefile中,会造成Makefile过于臃肿,为编写和修改带来了很大的不便。解决这个问题的办法是把Makefile分解成多个子Makefile,并放置到程序的每个子目录中,每个子Makefile文件负责所在目录下源文件的编译工作。

Make工程管理器会首先读取程序根目录下的Makefile文件(总控Makefile),然后再去读取各个目录中的子Makefile文件。这个过程就称为Make的嵌套执行。

嵌套执行的使用方法为:

cd 子目录 && $(MAKE)

或:

$(MAKE) –c子目录

例如:

首先在/home/b目录下新建一个子Makefile,如下所示:

b.o : b.c

cc -c –o b.o b.c

然后修改/home目录中的总控Makefile,如下所示:

VPATH = /home/b

obj = a.o b.o

 

.PHONY : all

all : test $(obj)

 

test : $(obj)

    cc –o test a.o b/b.o

 

b.o :b.c

    cd b && make

结果为:

[root@localhost home]#make

cc –c –o a.o a.c

cd b && make

make[1] : Entering directory ‘/home/b’

cc –c –o b.o b.c

make[1] : Leaving directory ‘/home/b’

cc –o test a.o b/b.o

可以看到,Make产生a.o之后会进入/home/b目录读取子Makefile,编译产生b.o之后再退出该目录。

在使用嵌套编译时,上层Makefile把编译任务下发给各个下层Makefile进行处理。就好比公司的总经理管理部门经理,再由部门经理去管理每个员工。

    总控Makefile中的变量可以通过“export 变量”的方式传递到各级子Makefile中,但不会覆盖子Makefile中的变量。也可以通过“unexport 变量”的方式不让变量传递到各级子Makefile中。

6.  条件判断

和C语言的条件编译类似,Make工程管理器也可以在运行时对条件进行判断,然后进入

条件分支继续编译。条件判断的书写格式如下所示:

条件表达式

如果真执行的文本段

endif

或者:

条件表达式

如果真执行的文本段

else

如果假执行的文本段

endif

条件表达式有以下四种格式:

ifeq(参数1,参数2)

作用:比较参数1和参数2的值是否相同,相同为真,相异为假

ifneq(参数1,参数2)

作用:比较参数1和参数2的值是否相同,相异为真,相同为假

ifdef(参数)

作用:参数非空为真,空为假

ifndef(参数)

作用:参数空为真,非空为假

例如:

a1= a.o

a2= b.o

ifeq ($(a1), $(a2))

a1=x.o

else

a2=y.o

变量a1的值是a.o, 变量a2的值是y.o。

7.  函数

对于编程语言来说,函数的作用是非常重要的。为此,Make工程管理器也引入了函数机制,以丰富Make控制编译过程的方法。和变量一样,函数也用符号$进行标识,其使用格式为:

$(函数名 参数,参数…)

其中函数名和参数之间用空格隔开,参数与参数之间用逗号隔开。下面简单介绍一些常用的基本函数。

l  subst

格式:$(subset 参数1,参数2,参数3)

功能:把参数3中的参数1替换成参数2

返回值:被替换后的参数3

例如:

result := $(substchina, the world, I love China)

result的值为“I love the world”。

l  patsubst

格式:$(patsubset模式参数,参数1,参数2)

功能:把参数2中符合模式参数的单词(单词是指参数中被空格隔开的字符串)替换成参数1

返回值:被替换后的参数2

例如:

result :=$(patsubst %.c, %.o, x.c y.c)

result的值为“x.o y.o”。

l  wildcard

格式:$(wildcard模式参数)

功能:列出当前目录下所有符合模式参数的文件名

返回值:当前目录下所有符合模式参数的文件名

例如:

result := $(wildcard*.c)

result的值为当前目录下所有的C文件名。

l  strip 参数

格式:$(strip 参数)

功能:去掉参数中开头和结尾的空格

返回值:被去掉空格的参数

例如:

result :=$(strip   China    )

result的值为“China”。

l  findstring

格式:$(findstring 参数1,参数2)

功能:在参数2中查找参数1

返回值:如果找到返回参数1,如果没找到返回空

例如:

result :=$(findstring me, you and me)

result的值为“me”。

result :=$(findstring she, you and me)

result的值为“”。

l  filter

格式:$(filter模式参数,参数1)

功能:从参数1中筛选出符合模式参数的字符串

返回值:符合参数模式的字符串

例如:

a := x.c y.c z.h

result := $(filter%.c, $(a))

result的值为“x.c y.c”。

l  addsuffix

格式:$(addsuffix 参数1,参数2)

功能:在参数2中的每个单词加上后缀参数1

返回值:加上后缀的所有单词

例如:

result := $(addsuffix.c, x y)

result的值为“x.c y.c”。

l  addprefix

格式:$(addprefix 参数1,参数2)

功能:在参数2中的每个单词加上前缀参数1

返回值:加上前缀的所有单词

例如:

result :=$(addprefix src/, x.c y.c)

result的值为“src/x.c src/y.c”。

l  foreach

格式:$(foreach 变量参数,参数1,表达式)

功能:循环取出参数1中的单词赋给变量参数,然后运行表达式

返回值:表达式的运行结果

例如:

a:= x y z

result := $(foreachb, $(a), $(b).c)

result的值为“x.c y.c z.c”。

注意,b在这里是一个临时的变量。

l  call

格式:$(call 变量参数,参数…)

功能:循环把参数依次赋给变量参数中的$(1)、$(2)…

返回值:赋值后的变量值

例如:

a:= $(2) $(1)

result := $(call$(a), x y)

result的值为“yx”。

l  if

格式:$(if 条件参数,执行参数)

功能:如果条件参数非空,运行执行参数部分

返回值:条件参数非空,返回执行参数部分

例如:

result := $(if China,world)

result的值为“world”。

格式:$(if 条件参数,执行参数1,执行参数2)

功能:如果条件参数非空,运行执行参数1部分;反之运行执行参数2部分

返回值:条件参数非空,返回执行参数1;反之返回执行参数2

例如:

a:=

result := $(if$(a), China,world)

result的值为“world”。

l  dir

格式:$(dir 参数)

功能:从参数中取出目录部分

返回值:目录部分

例如:

result:=$(dir/home/test/a.c)

result的值为“/home/test/”。

l  error

格式:$(error 参数)

功能:停止“Make”运行并显示参数

返回值:参数

例如:

result:=$(errorerror occure!)

result的值为“error occure!”。

l  warning

格式:$(warning 参数)

功能:“Make”运行时显示参数

返回值:参数

例如:

result:=$( warningwarning occure!)

result的值为“warning occure!”。

4.3 Makefile的自动生成

使用Make工程管理器虽然能大幅提高编译和调试程序的效率,但Makefile文件的编写仍然是件非常麻烦的事。那么可不可以自动生成Makefile文件呢?

在Linux平台上广泛使用autotools工具来实现Makefile文件的自动生成。Autotools是由一系列工具组成的,每个工具完成一个阶段的任务,最后生成一个完整的Makefile文件。下面以4.1.1节中的test.c文件为例来介绍Autotools工具自动生成Makefile的过程(为避免干扰,把test.c文件放在/home/auto目录下)。

(1)autoscan

autoscan会在当前的目录中搜索源文件并生成autoscan.log和configure.scan两个文件。其中,autoscan.log是日志文件。configure. scan是一种脚本配置文件,里面定义了一些宏定义(“#”开头的是注释)。宏定义的意义如下所示:

l  AC_PREREQ :声明要求的autoconf版本,本文件的版本是“2.59”。

l  AC_INIT :声明要生成程序的基本信息(包的全称、版本号以及发送出错信息的邮箱)。

l  AC_CONFIG_SRCDIR :检查指定的源文件和目录是否存在与有效。

l  AC_CONFIG_HEADER :用于生成config.h文件。

l  AC_PROG_CC :用于确定C编译器。

l  AC_PROG_MAKE_SET :用于确定Make的设置。

此外,还需要人工添加以下两个宏定义:

l  AM_INIT_AUTOMAKE :声明要生成程序的基本信息(程序名称和版本号,本例默认为“1.0”)。

l  AC_CONFIG_FILES :用于生成相应的Makefile文件。

如下所示的是修改后的configure.scan源代码。

#                                              -*- Autoconf -*-

#Process this file with autoconf to produce a configure script.

 

AC_PREREQ(2.59)

AC_INIT(FULL-PACKAGE-NAME,VERSION, BUG-REPORT-ADDRESS)

AM_INIT_AUTOMAKE(test,1.0)

AC_CONFIG_SRCDIR([test.c])

AC_CONFIG_HEADER([config.h])

 

#Checks for programs.

AC_PROG_CC

 

#Checks for libraries.

 

#Checks for header files.

 

#Checks for typedefs, structures, and compiler characteristics.

 

#Checks for library functions.

AC_CONFIG_FILES(Makefile)

AC_OUTPUT

最后把修改完的configure.scan文件重命名为configure.in。

(2)aclocal

运行aclocal会生成aclocal.m4文件和autom4te.cache文件夹。主要用来处理configure.in中的宏定义。

(3)autoconf

运行autoconf会生成configure文件。

(4)autoheader

运行autoheader会生成config.h.in文件。

(5)automake

运行automake前,需要先创建Makefile.am文件。在Makefile.am文件中需要定义以下三个宏定义:

l  AUTOMAKE_OPTIONS :用于设置automake的选项。automake共提供了三种软件等级,分

别是foreign、gnu和gnits,默认等级为gnu。本例使用foreign等级,该级别只检测必须的文件。

l  bin_PROGRAMS :声明要产生的二进制文件名。如果要产生多个二进制文件,每个文件

名用空格隔开。本例中的二进制文件名为test。

l  test_SOURCES :声明产生test文件所需要的源文件。如果有多个源文件,则必须把所

有的源文件都列出来,并用空格隔开。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com 联系我们: