最近工作編譯程序一直在用別人寫的Makefile,但是沒有系統(tǒng)的學(xué)習(xí)過,趁著放假學(xué)一波

0x00 Makefile 概述
一個(gè)企業(yè)級(jí)項(xiàng)目,通常會(huì)有很多源文件,有時(shí)也會(huì)按功能、類型、模塊分門別類的放在不同的目錄中,有時(shí)候也會(huì)在一個(gè)目錄里存放了多個(gè)程序的源代碼。
這時(shí),如何對(duì)這些代碼的編譯就成了個(gè)問題。Makefile 就是為這個(gè)問題而生的,它定義了一套規(guī)則,決定了哪些文件要先編譯,哪些文件后編譯,哪些文件要重新編譯。
整個(gè)工程通常只要一個(gè) make 命令就可以完成編譯、鏈接,甚至更復(fù)雜的功能。可以說(shuō),任何一個(gè) Linux 源程序都帶有一個(gè)Makefile 文件。
0x01 Makefile 的優(yōu)點(diǎn)
管理代碼的編譯,決定該編譯什么文件,編譯順序,以及是否需要重新編譯;節(jié)省編譯時(shí)間。如果文件有更改,只需重新編譯此文件即可,無(wú)需重新編譯整個(gè)工程;一勞永逸。Makefile 通常只需編寫一次,后期就不用過多更改。
0x02 編譯知識(shí)
Makefile最初是為了編譯C/C++而誕生的, 所以它里面的很多隱藏規(guī)則都是針對(duì) C/C++的。在講 Makefile 之前有必要對(duì) C/C++的編譯有一點(diǎn)了解

預(yù)處理器:將.c 文件轉(zhuǎn)化成 .i 文件,使用的 gcc 命令是:gcc –E,對(duì)應(yīng)于預(yù)處理命令 cpp;
編譯器:將.c/.h 文件轉(zhuǎn)換成.s 文件,使用的 gcc 命令是:gcc –S,對(duì)應(yīng)于編譯命令 cc –S;
匯編器:將.s 文件轉(zhuǎn)化成 .o 文件,使用的 gcc 命令是:gcc –c,對(duì)應(yīng)于匯編命令是 as;
鏈接器:將.o 文件轉(zhuǎn)化成可執(zhí)行程序,使用的 gcc 命令是: gcc,對(duì)應(yīng)于鏈接命令是 ld;
加載器:將可執(zhí)行程序加載到內(nèi)存并進(jìn)行執(zhí)行,loader 和 ld-linux.so。
0x03 Makefile規(guī)則
Target...: Prerequsites...
Command
Command
...
或
Targets: Prerequisites;Command
Command
...
下面會(huì)稱 Target 為目標(biāo), Prerequisites 為目標(biāo)依賴, Command 為規(guī)則的命令行
Command 必須以[Tab]開始, Command 可以寫成多行,通過來(lái)繼行,但行尾的后不能有空格。
規(guī)則包含了文件之間的依賴關(guān)系和更新此規(guī)則 target 所需要的 Command
targets 可以使用通配符, 如果格式是"A(M)"表示檔案文件(.a)中的成員“M”
在需要用本義的時(shí)候,使用兩個(gè)$$來(lái)表示。
當(dāng)規(guī)則的 target 是一個(gè)文件,它的任何一個(gè)依賴文件被修改后,在執(zhí)行 make <target>時(shí)這個(gè)目標(biāo)文件都會(huì)被重新編譯或重新連接。如果有必要此 target 的一個(gè)依賴文件也會(huì)被先重新編譯。
0x04偽目標(biāo)
Makefile 中把那些沒有任何依賴只有執(zhí)行動(dòng)作的目標(biāo)稱為“偽目標(biāo)“(Phony targets)
.PHONY : clean
clean :
-rm edit $(objects
通過.PHONY 將 clean 聲明為偽目標(biāo),避免當(dāng)目錄下有名為“clean”文件時(shí),clean 無(wú)法執(zhí)行
這樣的目標(biāo)不是為了創(chuàng)建或更新程序,而是執(zhí)行相應(yīng)動(dòng)作。
0x05自動(dòng)推導(dǎo)規(guī)則
在使用 make 編譯.c 源文件時(shí),編譯.c 源文件規(guī)則的命令可以不用明確給出。這是因?yàn)?make 本身存在一個(gè)默認(rèn)的規(guī)則,能夠自動(dòng)完成對(duì).c 文件的編譯并生成對(duì)應(yīng)的.o 文件。它執(zhí)行命令“cc -c”來(lái)編譯.c 源文件。在 Makefile 中我們只需要給出需要重建的目標(biāo)文件名(一個(gè).o 文件),make 會(huì)自動(dòng)為這個(gè).o 文件尋找合適的依賴文件(對(duì)應(yīng)的.c 文件。對(duì)應(yīng)是指:文件名除后綴外,其余都相同的兩個(gè)文件),而且使用正確的命令來(lái)重建這個(gè)目標(biāo)文件。
例如, 現(xiàn)在有三個(gè)文件 test.cpp, my.cpp, my.h

- test.cpp
#include <iostream>
#include "my.h"
int main(int argc, char * argv[]) {
int a = 100, b = 101;
std::cout << "this code is for test makefile" << std::endl;
std::cout << xadd(a, b) << std::endl;
}
- my.h
#ifndef _MY_H_
#define _MY_H_
int xadd(const int x, const int y);
#endif
- my.cpp
#include "my.h"
int xadd(const int x, const int y)
{
return x + y;
}
對(duì)于上邊的例子,此默認(rèn)規(guī)則就使用命令“gcc -c test.cpp -o test.o”來(lái)創(chuàng)建文件“main.o”。對(duì)一個(gè)目標(biāo)文件是“N.o”,倚賴文件是“N.c”的規(guī)則,完全可以省略其規(guī)則的命令行,而由 make 自身決定使用默認(rèn)命令。此默認(rèn)規(guī)則稱為 make 的隱含規(guī)則。
test: test.cpp my.o
gcc -c -o test test.cpp
my.o: my.cpp my.h
gcc -c -o my.o my.cpp
clean :
rm test my.o
也可以用隱式規(guī)則
test: test.cpp my.o
my.o: my.cpp my.h
clean :
rm test my.o
效果是一樣的
這里要說(shuō)明一點(diǎn)的是, clean 不是一個(gè)文件,它只不過是一個(gè)動(dòng)作名字,有點(diǎn)像c語(yǔ)言中的label一 樣,其冒號(hào)后什么也沒有,那么,make就不會(huì)自動(dòng)去找它的依賴性,也就不會(huì)自動(dòng)執(zhí)行其后所定義的命令。 要執(zhí)行其后的命令,就要在make命令后明顯得指出這個(gè)label的名字。這樣的方法非常有用,我們可以在一 個(gè)makefile中定義不用的編譯或是和編譯無(wú)關(guān)的命令,比如程序的打包,程序的備份,等等。
0x06 規(guī)則書寫建議
書寫規(guī)則建議的方式是:?jiǎn)文繕?biāo),多依賴。就是說(shuō)盡量要做到一個(gè)規(guī)則中只存在一個(gè)目標(biāo)文件,可有多個(gè)依賴文件。盡量避免使用多目標(biāo),單依賴的方式。
0x07 makefile 工作原理文和件搜索順序
在默認(rèn)的方式下,也就是我們只輸入 make 命令。那么,
- 首先會(huì)搜索目錄下的
GNUmakefile,makefile,Makefile文件,或者make -f從指定文件讀取
2.找到makefile后首先從第一個(gè)target開始,如果生成target依賴別的目標(biāo)就遞歸從依賴開始
例如:上面的例子中,首先準(zhǔn)備編譯生成目標(biāo)test,發(fā)現(xiàn)依賴my.o沒有生成,就向下找my.o的生成,發(fā)現(xiàn)my.o的資源my.cpp,my.h已經(jīng)就緒了,就先編譯出my.o,回到test,發(fā)現(xiàn)test.cpp和my.o全部就緒,使用規(guī)則Command生成目標(biāo)test
這就是整個(gè)make的依賴性,make會(huì)一層又一層地去找文件的依賴關(guān)系,直到最終編譯出第一個(gè)目標(biāo)文件。在 找尋的過程中,如果出現(xiàn)錯(cuò)誤,比如最后被依賴的文件找不到,那么make就會(huì)直接退出,并報(bào)錯(cuò),而對(duì)于所 定義的命令的錯(cuò)誤,或是編譯不成功,make根本不理。make只管文件的依賴性,即,如果在我找了依賴關(guān)系 之后,冒號(hào)后面的文件還是不在,那么對(duì)不起,我就不工作啦。
通過上述分析,我們知道,像clean這種,沒有被第一個(gè)目標(biāo)文件直接或間接關(guān)聯(lián),那么它后面所定義的命 令將不會(huì)被自動(dòng)執(zhí)行,不過,我們可以顯示要make執(zhí)行。即命令—— make clean ,以此來(lái)清除所有 的目標(biāo)文件,以便重編譯。
0x08 makefile中使用變量
我們可以看到 .o 文件的字符串被重復(fù)了兩次,如果我們的工程需要加入一個(gè)新的 .o 文件, 那么我們需要在兩個(gè)地方加(應(yīng)該是三個(gè)地方,還有一個(gè)地方在clean中)。
當(dāng)然,我們的makefile并不復(fù) 雜,所以在兩個(gè)地方加也不累,但如果makefile變得復(fù)雜,那么我們就有可能會(huì)忘掉一個(gè)需要加入的地方, 而導(dǎo)致編譯失敗。所以,為了makefile的易維護(hù),在makefile中我們可以使用變量。makefile的變量也 就是一個(gè)字符串,理解成C語(yǔ)言中的宏可能會(huì)更好。
比如,我們聲明一個(gè)變量 obj,表示所有obj文件,在makefile的一開始就定義
obj = my.o
maincpp = test.cpp
于是,我們就可以很方便地在我們的makefile中以 $(obj) 的方式來(lái)使用這個(gè)變量了,于是 我們的改良版makefile就變成下面這個(gè)樣子:
obj = my.o
maincpp = test.cpp
test :$(maincpp) $(obj)
my.o: my.cpp my.h
clean:
rm $(obj)
于是如果有新的 .o 文件加入,我們只需簡(jiǎn)單地修改一下 obj 變量就可以了。
關(guān)于變量更多的話題,我會(huì)在后續(xù)給你一一道來(lái)。
0x09 另類風(fēng)格的makefiles
既然我們的make可以自動(dòng)推導(dǎo)命令,那么我看到那堆.o 和 .h 的依賴就有點(diǎn)不爽,那么多的重復(fù)的 .h ,能不能把其收攏起來(lái),好吧,沒有問題,這個(gè)對(duì)于make來(lái)說(shuō)很容易,誰(shuí)叫它提供了自動(dòng) 推導(dǎo)命令和文件的功能呢?來(lái)看看最新風(fēng)格的makefile吧。
obj = my.o
maincpp = test.cpp
test :$(maincpp) $(obj)
$(obj): my.h
clean:
rm $(obj) test
這種風(fēng)格,讓我們的makefile變得很簡(jiǎn)單,但我們的文件依賴關(guān)系就顯得有點(diǎn)凌亂了。魚和熊掌不可兼得。 還看你的喜好了。我是不喜歡這種風(fēng)格的,一是文件的依賴關(guān)系看不清楚,二是如果文件一多,要加入幾個(gè) 新的.o 文件,那就理不清楚了。
0x10 清空目標(biāo)文件的規(guī)則
每個(gè)Makefile中都應(yīng)該寫一個(gè)清空目標(biāo)文件( .o 和執(zhí)行文件)的規(guī)則,這不僅便于重編譯,也很 利于保持文件的清潔。這是一個(gè)“修養(yǎng)”(呵呵,還記得我的《編程修養(yǎng)》嗎)。一般的風(fēng)格都是:
clean:
rm test $(obj)
更為穩(wěn)健的做法是:
.PHONY: clean
clean:
rm test $(obj)
0x11 Makefile的文件名
默認(rèn)的情況下,make命令會(huì)在當(dāng)前目錄下按順序找尋文件名為“GNUmakefile”、 “makefile”、“Makefile”的文件,找到了解釋這個(gè)文件。在這三個(gè)文件名中,最好使用“Makefile” 這個(gè)文件名,因?yàn)?,這個(gè)文件名第一個(gè)字符為大寫,這樣有一種顯目的感覺。最好不要用“GNUmakefile”, 這個(gè)文件是GNU的make識(shí)別的。有另外一些make只對(duì)全小寫的“makefile”文件名敏感,但是基本上來(lái)說(shuō), 大多數(shù)的make都支持“makefile”和“Makefile”這兩種默認(rèn)文件名。
當(dāng)然,你可以使用別的文件名來(lái)書寫Makefile,比如:“Make.Linux”,“Make.Solaris” ,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的-f和--file參數(shù)
make -f Makefile.Linux
make -f Makefile.mac
0x12 引用其他的Makefile
在Makefile使用include 關(guān)鍵字可以把別的Makefile包含進(jìn)來(lái),這很像C語(yǔ)言的 #include ,被包含的文件會(huì)原模原樣的放在當(dāng)前文件的包含位置。 include 的語(yǔ)法是:
include <filename>
filename 可以是當(dāng)前操作系統(tǒng)Shell的文件模式(可以包含路徑和通配符)。
在include 前面可以有一些空字符,但是絕不能是 Tab 鍵開始。 include 和 <filename> 可以用一個(gè)或多個(gè)空格隔開。舉個(gè)例子,你有這樣幾個(gè)Makefile: a.mk 、 b.mk 、 c.mk ,還有一個(gè)文件叫 foo.make ,以及一個(gè)變量 $(bar) ,其包含 了 e.mk 和 f.mk ,那么,下面的語(yǔ)句:
include foo.make *.mk $(bar)
等價(jià)于
include foo.make a.mk b.mk c.mk e.mk f.mk
- 如果make執(zhí)行時(shí),有
-I或--include-dir參數(shù),那么make就會(huì)在這個(gè)參數(shù)所指定的目 錄下去尋找。
2.如果目錄 <prefix>/include (一般是: /usr/local/bin 或 /usr/include )存在的話,make也會(huì)去找。
如果有文件沒有找到的話,make會(huì)生成一條警告信息,但不會(huì)馬上出現(xiàn)致命錯(cuò)誤。它會(huì)繼續(xù)載入其它的 文件,一旦完成makefile的讀取,make會(huì)再重試這些沒有找到,或是不能讀取的文件,如果還是 不行,make才會(huì)出現(xiàn)一條致命信息。如果你想讓make不理那些無(wú)法讀取的文件,而繼續(xù)執(zhí)行,你可以 在include前加一個(gè)減號(hào)“-”。如:
-include <filename>
0x13 環(huán)境變量MAKEFILES
如果你的當(dāng)前環(huán)境中定義了環(huán)境變量 MAKEFILES ,那么,make會(huì)把這個(gè)變量中的值做一個(gè)類似于 include 的動(dòng)作。這個(gè)變量中的值是其它的Makefile,用空格分隔。只是,它和 include 不 同的是,從這個(gè)環(huán)境變量中引入的Makefile的“目標(biāo)”不會(huì)起作用,如果環(huán)境變量中定義的文件發(fā)現(xiàn) 錯(cuò)誤,make也會(huì)不理。
但是在這里我還是建議不要使用這個(gè)環(huán)境變量,因?yàn)橹灰@個(gè)變量一被定義,那么當(dāng)你使用make時(shí), 所有的Makefile都會(huì)受到它的影響,這絕不是你想看到的。在這里提這個(gè)事,只是為了告訴大家,也許 有時(shí)候你的Makefile出現(xiàn)了怪事,那么你可以看看當(dāng)前環(huán)境中有沒有定義這個(gè)變量。
0x14 變量定義及賦值:
變量直接采用賦值的方法即可完成定義,如:
INCLUDE = ./include/
變量取值:
用括號(hào)括起來(lái)再加個(gè)美元符,如:
`FOO = $(OBJ)`
系統(tǒng)自帶變量:
通常都是大寫,比如 CC、PWD、CFLAG,等等。
有些有默認(rèn)值,有些沒有。比如常見的幾個(gè):
CPPFLAGS : 預(yù)處理器需要的選項(xiàng) 如:-I
CFLAGS:編譯的時(shí)候使用的參數(shù) –Wall –g -c
LDFLAGS :鏈接庫(kù)使用的選項(xiàng) –L -l
變量的默認(rèn)值可以修改,比如 CC 默認(rèn)值是 cc,但可以修改為 gcc:CC=gcc
0x15 函數(shù)
Makefile 也為我們提供了大量的函數(shù),同樣經(jīng)常使用到的函數(shù)為以下兩個(gè)。需要注意的是,Makefile 中所有的函數(shù)必須都有返回值。在以下的例子中,假如目錄下有 main.c、func1.c、func2.c 三個(gè)文件。
通配符:
用于查找指定目錄下指定類型的文件,跟的參數(shù)就是目錄+文件類型,比如:
src = $(wildcard ./src/*.c)
這句話表示:找到 ./src 目錄下所有后綴為 .c 的文件,并賦給變量 src。
命令執(zhí)行完成后,src 的值為:main.c func1.c fun2.c。
patsubst:
匹配替換,例如以下例子,用于從 src 目錄中找到所有 .c 結(jié)尾的文件,并將其替換為 .o 文件,并賦值給 obj。
obj = $(patsubst %.c ,%.o ,$(src))
命令執(zhí)行完成后,obj 的值為 main.o func1.o func2.o。
特別地,如果要把所有 .o 文件放在 obj 目錄下,可用以下方法:
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src))
更多可以參考https://seisman.github.io/how-to-write-makefile/overview.html