超清晰的makefile解释、编写与示例

时间:2022-05-04
本文章向大家介绍超清晰的makefile解释、编写与示例,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Makefile范例教学

Makefile和GNU make可能是linux世界里最重要的档案跟指令了。编译一个小程式,可以用简单的command来进行编译;稍微复杂一点的程式,可以用shell script来帮忙进行编译。如今的程式(如Apache, Linux Kernel)可能动辄数百万行程式码,数万个标头档(headers)、库库(libraries)以及程式码(source code),如果只是针对几个档案进行修改,却要用shell script整个程式重新编译,不但浪费时间也相当没有效率。GNU make或是其他make程式的用途就在这里:当程式有些许变动时,我们需要一个程式帮助我们判断哪些需要重新编译,哪些不用;因此,撰写一个好的Makefile便是相当重要的能力。

不过话说回来,是不是每一只程式都需要一个Makefile呢?其实撰写Makefile是有益无害的,只是如果你的程式就只有两三个source codes需要编译,其实忘掉Makefile也没关系。本文的目的是希望以范例的方式能够让读者能看得懂,并且有能力撰写并修改Makefile,也顺便当作自己的笔记。

传统的编译:

gcc foo1.c -o foo1

事实上,上面的这个编译方式可以拆解成:

gcc foo1.c -c 
gcc foo1.o -o foo1

编译的过程是将原始码(foo1.c)先利用-c参数编译成.o(object file),然后再链结库库成为一个binary。-c即compile之意。

gcc foo1.c $SACLIB/sacio.a -O3 -g -Wall -ansi -o foo1

开始有趣了。编译的参数开始变多:

  • -c :编译但不进行链结,会产生一个跟原始码相同名称但副档名为.o的目的档。
  • -O :表示最佳化的程度

-O预设就是-O1,你可以指定成-O2或-O3,数字越大表示最佳化程度越好,但是会增加编译时间。

  • -g :把侦错资讯也编译进去

当你有需要使用GDB软体进行侦错,必须加入-g使GDB能够读取。一般情况下可以不用-g,因为他也会增加binary大小。

  • -Wall :显示警告讯息

使用这个参数会在编译时显示更多的警告讯息。这个参数相当有用,特别是找不到libs/headers之类的问题。

  • -ansi :使用相容ANSI标准的编译方式

ANSI是American National Standards Institute,即美国国家标准学会的简称。-ansi可以增加程式的可移植性。

  • 其中的$SACLIB就是一个变数名称,她必须被指定正确的值。

执行这个命令前必须先确定这个变数是有被指派正确的值才行。.a档是一个静态库(static library),关于静态跟共享的观念稍候解释。

再来更多吧!假设你今天要编译main这只程式,他的source codes有main.c, foo.c, target.h,并且需要/usr/local/moreFoo/lib/libpthread.so这个共享库,以及/usr/ local/moreFoo/include里面的headers;这么复杂的情况又该怎么作呢?

gcc main.c foo.c -I /usr/local/moreFoo/include -lpthread -L /usr/local/moreFoo/lib -O3 -ansi -o main

新的参数意义如下:

  • -I :需要include某些headers所在的目录

通常include目录都放置headers,利用-I使编译器知道去哪里找原始码里宣告的header。gcc预设会去寻找headers的目录大致有:

  • /usr/include
  • /usr/local/include
  • /usr/src/linux-headers-`uname -r`/include
  • /usr/lib/gcc/i486-linux-gnu/ UR_GCC_VERSION /include
  • 当前目录

因此,当原始码内有宣告

#include <fakeFoo.h> 

但fakeFoo.h并不在上述的资料夹内,就需要利用-I引导gcc找到她。至于target.h因为在当前目录,因此不必额外宣告。

当然,可以利用多个-I来指定多个headers的路径。

  • -l :表示编译过程需要一个library。

-l pthread代表需要一个名为lib pthread .so的库。

  • -L :需要额外链结库库所在的目录

有时候程式码经常会呼叫一些函数(methods, functions或是subroutines),而这些函数是使用其他人预先写好的、已经编译成库(例如libpthread.so)供人使用的话,我们就不必自己从头写过。gcc预设会去找库的目录大致有:

  • /lib
  • /usr/lib
  • /lib/modules/`uname -r`/kernel/lib
  • /usr/src/linux-headers-`uname -r`/lib
  • /usr/local/lib
  • 当前目录

因此编译时,利用-L指定目录告诉编译器可以该路径下寻找libpthread.so。因此,若使用了-l,则必须确定所使用的lib有在预设寻找的目录中,否则就必须利用-L来指定路径给编译器。

当然,可以利用多个-L来指定多个lib路径。

静态、共享与动态链结库库 我们已经知道:轮子不必重复发明--人家写好的方法我们可以直接拿来用。不过很多时候,这些方法可能因为某些因素,希望提供给别人使用却又不希望公布原始码,这时候编译成libraries是最好的选择。

1.静态库(static libraries)

静态库其实就是将一系列.o档打包起来,因此她可以直接视为一个巨大的.o档。打造出一个静态库的方法很简单:

gcc operator.c -c 
ar crsv liboperator.a operator.o

或者

gcc -static operator.c -l operator

两种方法皆能产生liboperator.a。假设这个静态库在/usr/local/foo/lib/里,编译时要与静态库作链结也很容易:

gcc main.c /usr/local/foo/lib/liboperator.a -o main

把静态库当成一般的.o档一起纳入binary,也可以像这样:

gcc main.c -L /usr/local/foo/lib -loperator -o main

静态库将所有的功能全部打包在一起,因此binary会变得很巨大,但是执行这个程式的所有功能都已满足,不会再有libraries相依性的问题。但是缺点在于当某些libraries的功能有所更新时,这个程式就必须重新编译,无法享受到即时更新的优点。通常商业软体以及嵌入式系统等功能异动较少的程式,会倾向使用静态库。

2共享库(shared libraries)

共享库跟静态库的观念刚好相反,程式在执行时必须能够找到相依的库,否则执行时会出现错误讯息。制作一个共享库的方法也很简单:

gcc -shared operator.c -o liboperator.so

或是先编译出目的档再进行链结:

gcc -c operator.c 
gcc -shared operator.o -o liboperator.so

产生出liboperator.so。假设这个共享库在/usr/local/foo/lib/里,使用共享库进行链结也很容易:

gcc main.c /usr/local/foo/lib/liboperator.so -o main

也可以像这样:

gcc main.c -L /usr/local/foo/lib -loperator -o main

共享库在程式启动时期会检查是否存在。以一个分别链结了静态库与共享库的binary而言,执行的结果大有差别。以静态库链结的main程式可以顺利执行,但是假设系统预设寻找库库的路径里找不到liboperator.so,以共享库链结的main程式则会出现错误讯息:

./main : error while loading shared libraries: liboperator.so: cannot open shared object file: No such file or directory

 这时解决的方法有四种: 

(1)把liboperator.so复制或是作一个连结到/usr/lib里。

(2)修改/etc/ld.so.conf,把/usr/local/foo/lib加进系统libraries的搜寻范围内。

(3)设定LD_LIBRARY_PATH变数,累加该路径进来:

如果你不是系统管理员,前两个方法根本没办法执行。我们只好自己加到~/.profile里:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/foo/lib

(4)改用静态库进行链结。

共享库经常出现在开放原始码的linux世界里,由于使用所有库皆是共享的,因此许多程式都可以重复利用既有的功能;有新功能或是bug也能简单的替换掉该库,所有程式都可以即时享受到这样的改变,也是最为常见的库型态。

3.动态库(dynamic libraries)

动态库跟共享库非常类似,唯一的差别在于程式执行时期并不会去检查该库是否存在,而是程式执行到某功能时才进行检查。这种动态载入的技术最常用在浏览器或是大型程式的外挂程式,当有需要用到这个功能时才载入进来。

制作一个动态库比较麻烦。

gcc -c -fPIC operator.c  gcc -shared operator.o -o liboperator.so

其中的-fPIC是产生position-independent code,也可以用-fpic。详细的用法已经超过笔者的理解范围,撰写呼叫动态库的程式码也需要传入相关参数。关于更多dynamic libraries的用法请参考这里

迈向Makefile之路 说了这么多,尚未触碰到Makefile本身。以上的范例如果都够清楚了,接下来的Makefile才能够继续学习下去喔! 以下是Makefile的基本组成:

# comments

#一些变数宣告

 

target1 : dependencies of target1

< TAB > command ;

target2 : dependencies of target2

< TAB > command ;

...

...

clean :

< TAB > rm - rf *. o    

需注意command那行前面必须是一个tab键,不能是tab键以外的任何空格。 用第一个简单的范例来说明:假设你需要执行

gcc main.c foo1.c foo2.c -o main

才能编译出main这只程式,则Makefile会像是:

#example 1:

#usage: make main OR make

main : main . o foo1 . o        

    gcc main . o foo1 . o - o main  

main . o : main . c             

    gcc main . c - c          

foo1 . o : foo1 . c             

    gcc foo1 . c - c          

clean :                 

    rm - rf main . o foo1 . o      

make读取此Makefile的流程如下:

1.由于没有变数宣告的部份,程式进入点为line 4,target即为main。main需要main.o跟foo1.o这两个目的档;如果gcc找得到这两个目的档,才会开始执行line 5的命令。很不巧,gcc无法找到这两个档案(因为还没有编译过!),因此gcc会寻找第一个dependency,也就是main.o,接续line 6。

2.到了line 6找到了main.o,他的dependency是main.c。main.c就在这个目录下,因此gcc终于可以执行第一个command(也就是line 7),产生main.o并回到line 4。

3.有了main.o,gcc会回到line 4继续寻找第二个dependency--foo1.o:于是进入line 8,找到了foo1.c,执行line 9的命令产生了foo1.o。

4.很高兴的再次回到line 4,发现此时所有dependencies都满足了,终于可以开始进行真正的链结工作,也就是line 5,把所有的obj链结成main这只程式。

这个例子里,make的效果等同于make main;可以不用指定main的原因是make会预设读第一个target。假设你输入make foo1.o,当然就只会执行line 8这行命令。如果程式码稍作修改,则编译出来的obj档也会有所不同(这是一个标准的废话>.<);此时有必要先清除某些(或全部的obj档)。 如果我们下一个make clean的指令,则程式会跑到line 10;发现clean这个target并没有dependency,而且也没有clean这个档案,此时这个项目称为假项目(fake entry)。没有相依的档案,因此可以快乐的执行line 11,把main.o跟foo1.o删除。 第二个范例:假设你需要执行 gcc main.c foo.c clean.c -I /usr/foo/include -lpthread -L /usr/foo/lib -O3 -ansi -o main,且目录下包含target.h才能编译出main这只程式,则Makefile会像是:

#example 2

#usage: make main OR make

CC = gcc                    #欲使用的C compiler

CFLAGS = - O3 - ansi          #欲使用的参数

INC = - I / usr / foo / include   #include headers的位置

LIB = - L / usr / foo / lib       #include libraries的位置

main : main . o foo1 . o                    

    $ { CC } main . o foo1 . o $ { CFLAGS } $ { INC } $ { LIB } - o main

main . o : main . c target . h                    

    $ { CC } main . c $ { CFLAGS } $ { INC } $ { LIB } - lpthread - c  

foo1 . o : foo1 . c target . h                    

    $ { CC } foo1 . c $ { CFLAGS } $ { INC } $ { LIB } - c        

clean :                             

    @ rm - rf *. o                        

这里宣告了四个变数,在Makefile里变数可以用$(VAR)或是${VAR}来表示皆可。但是为了跟shell script视觉上有所区隔,我个人建议尽量使用${VAR}来表示。 跟刚刚的Makefile其实是大同小异,只是利用变数使make更加的灵活;执行的流程可参考上一个范例。唯一值得注意的是在line 15的command前我用了一个@符号,这个意思是用来表示不把执行命令输出到萤幕,仅输出结果的意思。make预设会把命令跟结果都输出到萤幕,利用@可简化输出,使make的结果更简洁一点点。

如果你仔细观察这两个范例,会发现其实这个档案本身有太多东西是重复的了。例如line {8,9}重复了main.o, foo1.o;line {10,11}重复了mian.c,而line {12,13}重复了foo1.c。想想这只是一个极小的程式,他的Makefile就要如此巨大,往后如果开发出数百个方法的中型程式,那么Makefile可能会写到手软;更可怕的是程式的架构如果一改变,Makefile写错得机会会非常高。

在继续第三个范例之前,我们来思考一个问题。如果你有foo{1,2,3...100}.c,要把他们写进Makefile里,定义targer: depencency然后定义command,加起来总共要两百行,这实在不是绝妙的方法;make的开发者也想到了这点,因此make有隐含规则(implicit rules):

  main.o: main.c

      gcc main.c -c

可以隐含简化成

  main.o: main.c

或者是当你根本没有定义main.o这个target时,make会自动找main.c来编译。这是个好消息,但是我们编译程式通常会夹带大量参数,光是使用隐含规则是不够用的;因此我们有需要去自订一个隐含规则。 第三个范例: 

#example 3

#usage: make main OR make

SHELL = / usr / bin / bash         #宣告command所使用的shell环境为bash

CC = gcc                                #欲使用的C compiler

CFLAGS = - O3 - ansi              #欲使用的参数

INC = - I / usr / foo / include  

LIB = - L / usr / foo / lib      

.SUFFIXS : . c . cpp . f77 . f       #加入所列副档名到隐含规则里

main : main . o foo1 . o foo2 . o                                    

    $ { CC } main . o foo1 . o $ { CFLAGS } $ { INC } $ { LIB } - o $@

%. o : %. c target . h                      

    $ { CC } $< $ { CFLAGS } $ { INC } $ { LIB } - lpthread - c          

.PHONY : clean

clean :                             

    @ rm - rf *. o                                            

这个Makefile看起来开始吓人了!首先宣告这个Makefile所使用到的command是bash的语法。如果不需告则预设是sh,但是linux的sh就是bash,因此如果你是csh的拥护者,请你一定要宣告她。并且要注意的是,在GNU make里,变数与变数值之间可以有空格(VAR = value,这个习惯跟csh一样)也可以没有空格(VAR=vlaue,这个习惯跟bash一样);不过如果在其他平台,如Solaris、HPUX或是AIX,很可能要使用具有空格的形式宣告才行。为了Makefile的可移植性,建议使用具有空格的表示方法。 SUFFIXS与PHONY都是变数,代表隐含、内定的target。例如宣告了.c, .cpp, .f77, .f这些副档名到SUFFIXS变数,是告诉make这些副档名也要加入隐含规则的行列。事实上,.c, .cpp, .o都已经在make的隐含规则里了,再次宣告只是为了让阅读者更加明确知道这些档案会被隐含规则处理。而PHONY变数则是让make知道该target不是某个档案,只是一个标记。假设make跑到line 15,发现没有dependency,而工作目录内恰好有一个clean的档案,make会认为无条件需求而不去执行我们所要求的clean的动作;为了解决这个极少发生的窘境,细心的开发者还是会把PHONY变数加进Makefile里。 line 11所出现的$@以及line 13出现的$<称为自动变数,$@代表target本身,$<代表第一个dependency。line 12大量出现的%则是样式规则,她就是帮助我们简化Makefile最好的朋友。 

1.缺少main.o时,make跳到line 12进行我们所自订的隐含规则进行编译:此时的%.o就是main.o,%.c就是main.c。line 13的$<代表main.c,执行完毕会产生main.o。

2.发现还是缺少foo1.o,make再次跳到line 12:此时的%.o就是foo1.o,%.c就是foo1.c。line 13的$<代表foo1.c,执行完毕会产生foo1.o。

3.发现还是缺少foo2.o,make再次跳到line 12:此时的%.o就是foo2.o,%.c就是foo2.c。line 13的$<代表foo2.c,执行完毕会产生foo2.o。因此,利用隐含规则,不但可以应付更复杂的架构,也可以使Makefile更容易阅读、维护。我们来看一个真实的Makefile:

#------------------------------------------------- ----------------------------

UP_CC := g ++

BIN = $ ( PWD ) /../../ bin

COPT =- g

CINC = - I . - I $ ( PWD ) /../ PPP - I $ ( PWD ) /../ include

CLIB = - L $ ( PWD ) /../ lib - largtable2 - lfftw3

OBJS = gwlCreateAxis . o gwlSignalGen . o gwlCft . o gwlCwt . o gwlConvert . o gwlIwt . o gwlDiffeoLin . o gwlSignalSum . o gwlSignalRead . o gwlCwtMaxLine . o gwlET2D . o gwlET2DFilter . o gwlET3D . o gwlET3DFilter . o gwlDispModel . o gwlDiffeoDisp . o gwlAutoCorr . o gwlTransFK . o gwlOptiSP . o gwlOptiSI . o gwlSignalFilter . o gwlNNpred . o gwlWavelets . o

EXE = $ ( OBJS :. o = )

#------------------------------------------------- ----------------------------

# clear suffix list and set new one

.SUFFIXES :

.SUFFIXES : . cpp . o

#------------------------------------------------- ----------------------------

all : shell qwtplot installshell installqwtplot

shell : $ ( OBJS ) $ ( EXE )

installshell :

    cp - f $ ( EXE ) $ ( BIN )

qwtplot : gwlPlot . o gwlPlot

installqwtplot :

    cp - f gwlPlot $ ( BIN )

gwlPlot . o : gwlPlot . cpp

    $ ( QTDIR ) / bin / moc gwlPlot . cpp - o gwlPlot_moc . cpp ;

    $ ( UP_CC ) - c - g - Wno - deprecated gwlPlot . cpp $ ( CINC ) - I $ ( QTDIR ) / include

gwlPlot : gwlPlot . o

    $ ( UP_CC ) - g   $@ . o $ ( COPT ) - L $ ( PWD ) /../ lib - L $ ( QTDIR ) / lib - largtable2 - lfftw3 - lqwt - o $@

. cpp . o :

    $ ( UP_CC ) - c - Wno - deprecated $< $ ( CINC )

$ ( EXE ) : $ ( OBJS )

    $ ( UP_CC )  $@ . o $ ( COPT ) $ ( CLIB ) - o $@

clear :

    rm - f $ ( OBJS ) gwlPlot . o

clean : clear

    rm - f $ ( EXE ) gwlPlot

dependencies . make :

    touch dependencies . make

dep :

    $ ( UP_CC ) $ ( COPT ) $ ( CINC ) - MM *. cpp > dependencies . make                                

    这个Makefile写得相当的完整且严谨,是个很值得我们学习的好范例。首先,作者使用了变数替换(Variable substitution),也就是UP_CC := g++。这个替换的条件是:如果UP_CC没有被设定值,则自动设定变数值为g++。其次,作者使用了巨集以及巨集变数替换(Macro Variables Substitution)的语法:以此范例来说,是设定一个OBJS的巨集,包含了所有的.o档。     巨集变数替换的语法巧妙的使用在定义EXE变数这一行。请注意:=之 ​​间的字串:假设我定义了一个巨集JX = foo1.jx foo2.jx foo3.jx foo4.jx,然后我可以利用JAVA = ${JX : .jx = .java}定义foo{1,2,3,4}.java,利用CPP = ${JX: .jx = .cpp}定义foo{1,2,3,4}.cpp这些档案。因此本例的$(OBJ : .o = )表示EXE巨集的所有档案都是没有副档名的,可以预见他会利用这个技巧来产生binaries -- gwlCreateAxis.o产生gwlCreateAxis,gwlSignalGen.o产生gwlSignalGen ,以此类推。     line 45的.cpp.o :是%.cpp: %.o的缩写,千万别误会这是一个标记或假项目喔!line 48作者自订了一个隐含方法,与前例的line 12是相同的意义。    作者很谨慎的利用SUFFIXES(SUFFIXS也适用在GNU make)来定义隐含规则的list。第一个.SUFFIXES:后面没有接任何东西,表示清空suffix清单;第二个.SUFFIXES:接了.cpp .o两个副档名,告诉make只需要关心这两者就好,其他不必花费心思隐含编译。只是gcc在编译时,-I以及-L预设都会把当前目录包 ​​含进去,其实-I.是多余的;但是整体的撰写方式算是严谨清晰且流畅!  了解了target与dependency的关系以及隐含规则的符​​号之后,还剩下一个大重点:command。每行command都是一次独立的command,彼此毫无关联性。如果要具有连贯性的command必须写成连续的一行,例如:

%. o : %. c

    $ { CC } $< - c ; cd / somewhere ; ./ configure -- prefix $ ( PWD ) ; rm - rf . libs $ { PWD } / share $ { PWD } / bin ;

如果你写成

%. o : %. c

    $ { CC } $< - c ; cd / somewhere ;

    ./ configure -- prefix $ ( PWD ) ;

    rm - rf . libs $ { PWD } / share $ { PWD } / bin ;

会变成进入到/somewhere之后,整个shell就结束并重新执行./configure --prefix $(PWD)。但是根据原意,$(PWD)是/somewhere而非其他任何路径,因此整个command会造成预期外的结果。较恰当的写法应该是:

%. o : %. c

    $ { CC } $< - c ; cd / somewhere ; 

    ./ configure -- prefix $ ( PWD ) ; 

    rm - rf . libs $ { PWD } / share $ { PWD } / bin ;

利用跳脱字元把所有command当成同一行,在阅读上也具有排版的效果。是否发现在Makefile里我们并没有定义PWD这个变数值?那是因为Makefile可以直接存取环境变数。但是每个人针对某个程式所设定环境变数不尽相同,如果有使用到非系统面的环境变数,则还是要宣告在Makefile里比较恰当。例如以下这个不好的真实范例:

CFLAGS = - O $ { GMT_INC }

    pssac : pssac . o sacio . o

    $ ( LINK . c ) - o $@ $@ . o sacio . o $ ( GMT_LIBS )

 

clean :

    rm - f pssac *. o

首先,将${VAR}跟$(VAR)混合使用是容易造成混淆的;第二,使用者并不知道什么是LINK.c,从这个名称实在无法猜出到底该给她什么变数值。第三,GMT_INC与GMT_LIBS看起来像是某个程式的headers与libraries路径,但是要碰巧有个使用者跟作者一样有相同的环境变数名称,是很困难的。第四,其实可以利用$<来代替$@.o,因为他是第一个dependency;第五,sacio.o是作者给的,并不是使用者可以自己编译的。这会造成不同平台的使用者极大的困扰--即使她所有的变数都辛苦的解决了,但是她的硬体是sparc,若这个sacio.o是linux x86或其他平台上编译的,那么这个程式根本就不可能编译成功。