GNU Make 中文手册v3.8 学习( 1/3 )

来源:互联网 发布:新媒体公众号网络培训 编辑:程序博客网 时间:2024/05/21 08:44

GNU Make 中文手册v3.8 学习( 1/3 )

  • 这个手册翻译整理: 徐海兵, 先表示一些尊敬. “本人在工作之余,花了18个多月时间完成对“info make”的翻译整理,完成这个中文版手册”, 工夫不负有心人.
  • 链接器将.o文件中使用的函数和其它.o或者库文件中的相关符号进行合并,对所有文件中的符号进行重新安排(重定位),并链接系统相关文件(程序启动文件等)最终生成可执行程序。
  • Linux中,共享库文件格式通常为“ELF”格式。共享库已经具备了可执行条件。
  • 模块中各个成员的地址(变量引用和函数调用)都是相对地址。使用此共享库的程序在运行时,共享库被动态加载到内存并和主程序在内存中进行连接。多个可执行程序可共享库文件的代码段(多个程序可以共享的使用库中的某一个模块,共享代码,不共享数据)。另外共享库的成员对象可被执行(由libdl.so提供支持)
  • 在make读取Makefile以后会建立一个编译过程的描述数据库。此数据库中记录了所有各个文件之间的相互关系,以及它们的关系描述(要是我能"看"到这个数据库,就能使用程序trace,你知道一个大型的project的makefile有多复杂)

 

  • 当使用make工具进行编译时,工程中以下几种文件在执行make时将会被编译(重新编译):
    1. 所有的源文件没有被编译过,则对各个C源文件进行编译并进行链接,生成最后的可执行程序;
    2. 每一个在上次执行make之后修改过的C源代码文件在本次执行make时将会被重新编译;
    3. 头文件在上一次执行make之后被修改。则所有包含此头文件的C源文件在本次执行make时将会被重新编译。(这就是为什么头文件修改后,很多文件都会被重新compile的原因)

 

  • 首先书写时,可以将一个较长行使用反斜线(/)来分解为多行,这样可以使我们的Makefile书写清晰、容易阅读理解。但需要注意:反斜线之后不能有空格(这也是大家最容易犯的错误,错误比较隐蔽)
  • 编译.c源文件规则的命令可以不用明确给出。这是因为make本身存在一个默认的规则,能够自动完成对.c文件的编译并生成对应的.o文件。它执行命令“cc -c”来编译.c源文件。
  • 对一个目标文件是“N.o”,倚赖文件是“N.c”的规则,完全可以省略其规则的命令行,而由make自身决定使用默认命令。此默认规则称为make的隐含规则。
  • 书写规则建议的方式是:单目标,多依赖。就是说尽量要做到一个规则中只存在一个目标文件,可有多个依赖文件。尽量避免多目标,单依赖的方式。这样后期维护也会非常方便,而且Makefile会更清晰、明了。
  • 在这个Makefile中,根据依赖而不是目标对规则进行分组(Wrong!)。上例的Makefile就可以这样来实现:
    #sample Makefile
    objects = main.o kbd.o command.o display.o /
                   insert.o search.o files.o utils.o
    edit : $(objects)
              cc -o edit $(objects)
    $(objects) : defs.h
    kbd.o command.o files.o : command.h
    display.o insert.o search.o files.o : buffer.h

例子中头文件“defs.h”作为所有.o文件的依赖文件。其它两个头文件作为其对应规则的目标中所列举的所有.o文件的依赖文件。
但是这种风格的Makefile并不值得我们借鉴。问题在于:同时把多个目标文件的依赖放在同一个规则中进行描述(一个规则中含有多个目标文件),这样导致规则定义不明了,比较混乱。建议大家不要在Makefile中采用这种方式了书写。否则后期维护将会是一件非常痛苦的事情。

  • “include”指示符告诉make暂停读取当前的Makefile,而转去读取“include”指定的一个或者多个文件,完成以后再继续当前Makefile的读取。
    通常指示符“include”用在以下场合:
    1. 有多个不同的程序,由不同目录下的几个独立的Makefile来描述其重建规则。它们需要使用一组通用的变量定义或者模式规则。
    2. 当根据源文件自动产生依赖文件时;我们可以将自动产生的依赖关系保存在另外一个文件中,主Makefile使用指示符“include”包含这些文件。这样的做法比直接在主Makefile中追加依赖文件的方法要明智的多。其它版本的make已经使用这种方式来处理。(我想看看这方面的例子)
    当在这些目录下都没有找到“include”指定的文件时,make将会提示一个包含文件未找到的告警提示,但是不会立刻退出。而是继续处理Makefile的后续内容。当完成读取整个Makefile后,make将试图使用规则来创建通过指示符“include”指定的但未找到的文件,当不能创建它时(没有创建这个文件的规则),make将提示致命错误并退出。
  • 通常我们在Makefile中可使用“-include”来代替“include”,来忽略由于包含文件不存在或者无法创建时的错误提示(“-”的意思是告诉make,忽略此操作的错误。make继续执行)。

 

  • Makefile中符号“$”有特殊的含义(表示变量或者函数的引用),在规则中需要使用符号“$”的地方,需要书写两个连续的(“$$”)。
  • Maekfile中表示文件名时可使用通配符。可使用的通配符有:“*”、“?”和“[…]”。
    在Makefile中这些统配符并不是可以用在任何地方,Makefile中统配符可以出现在以下两种场合:
    1. 可以用在规则的目标、依赖中,make在读取Makefile时会自动对其进行匹配处理(通配符展开);
    2. 可出现在规则的命令中,通配符的通配处理是在shell在执行此命令时完成的。
    除这两种情况之外的其它上下文中,不能直接使用通配符。而是需要通过函数“wildcard”来实现。
    比如变量定义中使用的通配符不会被统配处理(因此在变量定义中不能使用通配符,否则在某些情况下会出现非预期的结果,下一小节将会详细讨论)。在Makefile有这样一个变量定义:“objects = *.o”。它表示变量“objects”的值是字符串“*.o”(并不是期望的空格分开的.o文件列表)。当需要变量“objects”代表所有.o文件列表示,需要使用函数“wildcard”(objects = $(wildcar *.o))。
    在规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN...) 。
  • 在Makefile中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函数会忽略模式字符并返回空。
  • 一般我们可以使用“$(wildcard *.c)”来获取工作目录下的所有的.c文件列表。复杂一些用法;可以使用“$(patsubst %.c,%.o,$(wildcard *.c))”,首先使用“wildcard”函数获取工作目录下的.c文件列表;之后将列表中所有文件名的后缀.c替换为.o。这样我们就可以得到在当前目录可生成的.o文件列表。因此在一个目录下可以使用如下内容的Makefile来将工作目录下的所有的.c文件进行编译并最后连接成为一个可执行文件:
    #sample Makefile
    objects := $(patsubst %.c,%.o,$(wildcard *.c))
    foo : $(objects)
            cc -o foo $(objects)

 

  • 使用伪目标有两点原因:
    1. 避免在我们的Makefile中定义的只执行命令的目标(此目标的目的为了执行执行一些列命令,而不需要创建这个目标)和工作目录下的实际文件出现名字冲突。
    2. 提高执行make时的效率,特别是对于一个大型的工程来说,编译的效率也许你同样关心。
    如:在make的并行和递归执行过程中。此情况下一般会存在一个变量,定义为所有需要make的子目录。对多个目录进行make的实现方式可以是:在一个规则的命令行中使用shell循环来完成。如下:
    SUBDIRS = foo bar baz
    subdirs:
          for dir in $(SUBDIRS); do /
              $(MAKE) -C $$dir; /
         done
    但这种实现方法存在以下几个问题:
    1. 当子目录执行make出现错误时,make不会退出。就是说,在对某一个目录执行make失败以后,会继续对其他的目录进行make。在最终执行失败的情况下,我们很难根据错误提示定位出具体是在那个目录下执行make时发生错误。这样给问题定位造成了很大的困难。为了解决这个问题,可以在命令行部分加入错误监测,在命令执行错误后主动退出。不幸的是,如果在执行make时使用了“-k”选项,此方式将失效。
    2. 另外一个问题就是使用这种shell的循环方式时,没有用到make对目录的并行处理功能,由于规则的命令是一条完整的shell命令,不能被并行处理。 有了伪目标之后,我们可以用它来克服以上实现方式所存在的两个问题。
    SUBDIRS = foo bar baz
    .PHONY: subdirs $(SUBDIRS)
    subdirs: $(SUBDIRS)
    $(SUBDIRS):
           
    $(MAKE) -C $@
    foo: baz
    上边的实现中有一个没有命令行的规则“foo: baz”,此规则用来限制子目录的make顺序。它的作用是限制同步目录“foo”和“baz”的make过程(在处理“foo”目录之前,需要等待“baz”目录处理完成)。提醒大家:在书写一个并行执行make的Makefile时,目录的处理顺序是需要特别注意的。

    在broadcom的project中,clean的工作就是用了SUBDIRS类似的用法:
    SUBDIRS_APP = $(SUBDIRS_BROADCOM) $(SUBDIRS_OPENSOURCE) $(SUBDIRS_ALPHASOURCE)
    SUBDIRS = $(foreach dir, $(SUBDIRS_APP), $(shell if [ -d "$(dir)" ]; then echo $(dir); fi))
    而后面subdirs: $(patsubset %, _dir_%, $(SUBDIRS))
    $(patsubset %, _dir_%, $(SUBDIRS)):
                 $(MAKE) -C $(patsubset _dir_%, %, $@) $(TGT)

     

    • make存在一个内嵌隐含变量“RM”,它被定义为:“RM = rm –f”。

     

    • 静态模式规则是这样一个规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。静态模式规则比多目标规则更通用,它不需要多个目标具有相同的依赖。但是静态模式规则中的依赖文件必须是相类似的而不是完全相同的。
      首先,我们来看一下静态模式规则的基本语法:
      TARGETS ...: TARGET-PATTERN: PREREQ-PATTERNS ...
                           COMMANDS
      首先在目标模式和依赖模式中,一般需要包含模式字符“%”。在目标模式(TAGET-PATTERN)中“%”可以匹配目标文件的任何部分,模式字符“%”匹配的部分就是“茎”。目标文件和目标模式的其余部分必须精确的匹配。看一个例子:目标“foo.o”符合模式“%.o”,其“茎”为“foo”。而目标“foo.c”和“foo.out”就不符合此目标模式。
      每一个目标的依赖文件是使用此目标的“茎”代替依赖模式(PREREQ-PATTERNS)中的模式字符“%”而得到。例如:上边的例子中依赖模式(PREREQ-PATTERNS)为“%.c”,那么使用“茎”“foo”替代依赖模式中的“%”得到的依赖文件就是“foo.c”。需要明确的一点是:在模式规则的依赖列表中使用不包含模式字符“%”也是合法的。代表这个文件是所有目标的依赖文件。
      我们来看一个例子,它根据相应的.c文件来编译生成“foo.o”和“bar.o”文件:
      objects = foo.o bar.o
      all: $(objects)
      $(objects): %.o: %.c
            $(CC) -c $(CFLAGS) $< -o $@
      如果存在一个文件列表,其中一部分符合某一种模式而另外一部分符合另外一种模式,这种情况下我们可以使用“filter”函数来对这个文件列表进行分类,在分类之后对确定的某一类使用模式规则。例如:
      files = foo.elc bar.o lose.o
      $(filter %.o,$(files)): %.o: %.c
            
      $(CC) -c $(CFLAGS) $< -o $@
      $(filter %.elc,$(files)): %.elc: %.el
            
      emacs -f batch-byte-compile $<
      其中;$(filter %.o,$(files))的结果为“bar.o lose.o”。“filter”函数过滤不符合“%.o”模式的文件名而返回所有符合此模式的文件列表。
      我们通过另外一个例子来看一下自动环变量“$*”在静态模式规则中的使用方法:
      bigoutput littleoutput : %output : text.g
               
      generate text.g -$* > $@
      当执行此规则的命令时,自动环变量“$*”被展开为“茎”。在这里就是“big”和“little”。

     

    • 典型的用法是在使用“echo”命令输出一些信息时。如:
      @echo 开始编译XXX模块......
      执行时,将会得到“开始编译XXX模块......”这条输出信息。如果在命令行之前没有字符“@”,那么,make的输出将是:
      echo编译XXX模块......
      编译XXX模块......
    • 另外,如果使用make的命令行参数“-n”或“--just-print”,那么make执行时只显示所要执行的命令,但不会真正的去执行这些命令。只有在这种情况下make才会打印出所有make需要执行的命令,其中也包括了使用“@”字符开始的命令。这个选项对于我们调试Makefile非常有用,使用这个选项我们可以按执行顺序打印出Makefile中所有需要执行的所有命令。
    • 命令的执行
      规则中,当目标需要被重建时。此规则所定义的命令将会被执行,如果是多行命令,那么每一行命令将在一个独立的子shell进程中被执行(就是说,每一行命令的执行是在一个独立的shell进城中完成)。因此,多行命令之间的执行是相互独立的,相互之间不存在依赖(多条命令行的执行为多个相互独立的进程)。
      在Makefile中书写在同一行中的多个命令属于一个完整的shell命令行,书写在独立行的一条命令是一个独立的shell命令行。因此:在一个规则的命令中,命令行“cd”改变目录不会对其后的命令的执行产生影响。就是说其后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。如果要实现这个目的就不能把“cd”和其后的命令放在两行来书写。而应该把这两条命令写在一行上,用分号分隔。这样它们才是一个完整的shell命令行。如:
      foo : bar/lose
           
      cd bar; gobble lose > ../foo

     

     

    • 中断make的执行
      make在执行命令时如果收到一个致命信号(终止make),那么make将会删除此过程中已经重建的那些规则的目标文件。其依据是此目标文件的当前时间戳和make开始执行时此文件的时间戳是否相同。
      删除这个目标文件的目的是为了确保下一次make时目标文件能够被正确重建。其原因我们上一节已经有所讨论。假设正在编译时键入“Ctrl-c”,此时编译器已经开始写文件“foo.o”,但是“Ctrl-c”产生的信号关闭了编译器。这种情况下文件“foo.o”可能是不完整的,但这个内容不完整的“foo.o”文件的时间戳比源程序‘foo.c’的时间戳新。如果在make收到终止信号后不删除文件“foo.o”而直接退出,那么下次执行make时此文件被认为已是最新的而不会去重建它。最后在链接生成终极目标时由于某一个.o文件的不完整,可能出现一堆令人难以理解的错误信息,或者产生了一个不正确的终极目标。
      所以在重新make之前,一定要make clean一下.

     

    • 上层make过程要将所执行的Makefile中的变量传递给子make过程,需要明确地指出。在GNU make中,实现此功能的指示符是“export”。当一个变量使用“export”进行声明后,变量和它的值将被加入到当前工作的环境变量中,以后在make执行的所有规则的命令都可以使用这个变量。而当没有使用指示符“export”对任何变量进行声明的情况下,上层make只将那些已经初始化的环境变量(在执行make之前已经存在的环境变量)和使用命令行指定的变量(如命令“make CFLAGS +=-g”或者“make –e CFLAGS +=-g”)传递给子make程序,
    • 存在两个特殊的变量“SHELL”和“MAKEFLAGS”,对于这两个变量除非使用指示符“unexport”对它们进行声明,它们在整个make的执行过程中始终被自动的传递给所有的子make。
    • 当不希望将一个变量传递给子make时,可以使用指示符“unexport”来声明这个变量。格式如下:
      unexport VARIABLE ...

     

    • 一个不带任何参数的指示符“export”指示符:
      export
      含义是将此Makefile中定义的所有变量传递给子make过程。
      需要说明的是:单独使用“export”来导出所有变量的行为是老版本GNU make所默认的。但是在新版本的GNU make中取消了这一默认的行为。因此在编写和老版本GNU make兼容的Makefile时,需要使用特殊目标“.EXPORT_ALL_VARIABLES”来代替“export”,

     

    • -w选项
      在多级make的递归调用过程中,选项“-w”或者“--print-directory”可以让make在开始编译一个目录之前和完成此目录的编译之后给出相应的提示信息,方便开发人员跟踪make的执行过程。例如,在目录“/u/gnu/make”目录下执行“make -w”,将会看到如下的一些信息:
      在开始执行之前我们将看到:
      make: Entering directory `/u/gnu/make'.
      而在完成之后我们同样将会看到:
      make: Leaving directory `/u/gnu/make'.
      通常,选项“-w”会被自动打开。在主控Makefile中当如果使用“-C”参数来为make指定一个目录或者使用“cd”进入一个目录时,“-w”选项会被自动打开。主控make可以使用选项“-s”(“--slient”)来禁止此选项。另外,make的命令行选项“--no-print-directory”,将禁止所有关于目录信息的打印。

    • 变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。Makefile传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义定义的一般变量(例如:目标文件列表objects)使用小写方式,而对于一些参数列表(例如:编译选项CFLAGS)采用大写方式
    • 当我们定义了一个变量之后,就可以在Makefile的很多地方使用这个变量。变量的引用方式是:“$(VARIABLE_NAME)”或者“${ VARIABLE_NAME }”来引用一个变量的定义。

     

    • 注意:Makefile中在对一些简单变量的引用,我们也可以不使用“()”和“{}”来标记变量名,而直接使用“$x”的格式来实现,此种用法仅限于变量名为单字符的情况。另外自动化变量也使用这种格式。对于一般多字符变量的引用必须使用括号了标记,否则make将把变量名的首字母作为作为变量而不是整个字符串(“$PATH”在Makefile中实际上是“$(P)ATH”)。这一点和shell中变量的引用方式不同。shell中变量的引用可以是“${xx}”或者“$xx”格式。但在Makefile中多字符变量名的引用只能是“$(xx)”或者“${xx}”格式。

    • 一般在我们书写Makefile时,各部分变量引用的格式我们建议如下:
      1. make变量(Makefile中定义的或者是make的环境变量)的引用使用“$(VAR)”格式,无论“VAR”是单字符变量名还是多字符变量名。
      2. 出现在规则命令行中shell变量(一般为执行命令过程中的临时变量,它不属于Makefile变量,而是一个shell变量)引用使用shell的“$tmp”格式
      3. 对出现在命令行中的make变量我们同样使用“$(CMDVAR)” 格式来引用
      例如:
      # sample Makefile
      ……
      SUBDIRS := src foo
      .PHONY : subdir
      Subdir :
           
      @for dir in $(SUBDIRS); do    /
                  
      $(MAKE) –C $$dir || exit 1; /
            
      done
      ……

     

    • 下边我们来看一个复杂一点的例子。分析一下直接展开式变量定义(:=)的用法,这里也用到了make的shell函数和变量“MAKELEVEL”(此变量在make的递归调用时代表make的调用深度)。
      其中包括了对函数、条件表达式和系统变量“MAKELEVEL”的使用:
      ifeq (0,${MAKELEVEL})
             cur-dir     := $(shell pwd)
             whoami    := $(shell whoami)
             host-type := $(shell arch)
           
      MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
      endif
      第一行是一个条件判断,如果是顶层Makefile,就定义下列变量。否则不定义任何变量。第二、三、四、五行分别定义了一个变量,在进行变量定义时对引用到的其它变量和函数展开。最后结束定义。利用直接展开式的特点我们可以书写这样一个规则:
      ${subdirs}:
             
      ${MAKE} cur-dir=${cur-dir}/$@ -C $@ all
      它实现了在不同子目录下变量“cur_dir”使用不同的值(为当前工作目录)。
    • 在复杂的Makefile中,推荐使用直接展开式变量。因为这种风格变量的使用方式和大多数编程语言中的变量使用方式基本上相同。它可以使一个比较复杂的Makefile在一定程度上具有可预测性。而且这种变量允许我们利用之前所定义的值来重新定义它(比如使用某一个函数来对它以前的值进行处理并重新赋值),此方式在Makefile中经常用到。尽量避免和减少递归式变量的使用。

     

     

    • “自动化变量”,诸如“$^”等。规则命令行中的自动化变量“$^”代表所有通过目录搜索得到的依赖文件的完整路径名(目录 + 一般文件名)列表。“$@”代表规则的目标。
      所以对于一个规则我们可以进行如下的描述:
      foo.o : foo.c
               cc -c $(CFLAGS) $^ -o $@
    原创粉丝点击