Makefile的介紹與工作原理


第一部分、概述


什麼是 makefile?

或許很多 Winodws 的程序員都不知道這個東西,因爲那些 Windows的 IDE 都爲你做了這個工作,但我覺得要作一個好的和 professional 的程序員,makefile還是要懂。特別在Linux下的軟件編譯,你就不能不自己寫 makefile 了,會不會寫 makefile,從一個側面說明了一個人是否具備完成大型工程的能力。 因爲,makefile 關係到了整個工程的編譯規則。一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile 定義了一系列的規則來指定,哪些文件需要先編譯,哪些文件需要後編譯,哪些文件需要重新編譯,甚至於進行更復雜的功能操作,因爲makefile 就像一個 Shell 腳本一樣,其中也可以執行操作系統的命令。 makefile 帶來的好處就是——“自動化編譯”,一旦寫好,只需要一個 make 命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。make 是一個解釋 makefile 中指令的命令工具,一般來說,大多數的 IDE 都有這個命令,比如:Delphi 的 make,Visual C++的 nmake,Linux 下 GNU 的 make。可見,makefile 都成爲了一種在工程方面的編譯方法。

當然,不同產商的 make 各不相同,也有不同的語法,但其本質都是在“文件依賴性”上做文章,這裏,我僅對 GNU 的 make 進行講述。在這篇文檔中,將以 C/C++的源碼作爲我們基礎,所以必然涉及一些關於 C/C++的編譯的知識,相關於這方面的內容,還請各位查看相關的編譯器的文檔。這裏所默認的編譯器是UNIX 下的 GCC 和 CC。


第二部分、關於程序的編譯和鏈接


在此,我想多說關於程序編譯的一些規範和方法,一般來說,無論是 C、C++、還是 pas,首先要把源文件編譯成中間代碼文件在 Windows 下也就是 .obj 文件,UNIX 下是 .o 文件,即 Object File,這個動作叫做編譯(compile)然後再把大量的 Object File 合成執行文件,這個動作叫作鏈接(link)

編譯時,編譯器需要的是語法的正確,函數與變量的聲明的正確鏈接時,通常是你需要告訴編譯器頭文件的所在位置(頭文件中應該只是聲明,而定義應該放在 C/C++文件中),只要所有的語法正確,編譯器就可以編譯出中間目標文件。一般來說,每個源文件都應該對應於一箇中間目標文件(O 文件或是 OBJ 文件)。鏈接時,主要是鏈接函數和全局變量,所以,我們可以使用這些中間目標文件(O 文件或是 OBJ文件)來鏈接我們的應用程序。鏈接器並不管函數所在的源文件,只管函數的中間目標文件(Object File)。

在大多數時候,由於源文件太多,編譯生成的中間目標文件太多,而在鏈接時需要明顯地指出中間目標文件名,這對於編譯很不方便,所以,我們要給中間目標文件打個包,在 Windows 下這種包叫“庫文件”(Library File),也就是 .lib 文件,在 UNIX下,是 Archive File,也就是 .a 文件

總結: 源文件首先會生成中間目標文件,再由中間目標文件鏈接生成執行文件。在編譯時,編譯器只檢測程序語法,和函數、變量是否被聲明。如果函數未被聲明,編譯器會給出一個警告,但可以生成 Object File。而在鏈接程序時,鏈接器會在所有的 Object File 中找尋函數的實現,如果找不到,那到就會報鏈接錯誤碼(Linker Error),在 VC 下,這種錯誤一般是:Link 2001 錯誤,意思說是說,鏈接器未能找到函數的實現。你需要指定函數的Object File.


第三部分、 Makefile 介紹


一、 Makefile 的規則


在講述這個 Makefile 之前,還是讓我們先來粗略地看一看 Makefile 的規則。

target : prerequisites ...
	command
...
...

target 也就是一個目標文件,可以是 Object File,也可以是執行文件。還可以是一個標籤(Label),對於標籤這種特性,在後續的“僞目標”章節中會有敘述。prerequisites 就是,要生成那個 target 所需要的文件或是目標command 也就是 make 需要執行的命令。 (任意的 Shell 命令)。

這是一個文件的依賴關係,也就是說,target 這一個或多個的目標文件依賴於prerequisites 中的文件其生成規則定義在 command 中。說白一點就是說,prerequisites中如果有一個以上的文件比 target 文件要新的話,command 所定義的命令就會被執行。這就是 Makefile 的規則。也就是 Makefile 中最核心的內容


二、一個示例


make 命令執行時,需要一個 Makefile 文件,以告訴 make 命令需要怎麼樣的去編譯和鏈接程序。
首先,我們用一個示例來說明 Makefile 的書寫規則。以便給大家一個感興認識。這個示例來源於 GNU 的 make 使用手冊,在這個示例中,我們的工程有 8 個 C 文件,和 3 個頭文件,我們要寫一個 Makefile 來告訴 make 命令如何編譯和鏈接這幾個文件。我們的規則是:

 1. 如果這個工程沒有編譯過,那麼我們的所有 C 文件都要編譯並被鏈接。
2. 如果這個工程的某幾個 C 文件被修改,那麼我們只編譯被修改的 C 文件,並鏈接目標程。
3. 如果這個工程的頭文件被改變了,那麼我們需要編譯引用了這幾個頭文件的 C 文件,並鏈接目標程序。

只要我們的 Makefile 寫得夠好,所有的這一切,我們只用一個 make 命令就可以完成,make 命令會自動智能地根據當前的文件修改的情況來確定哪些文件需要重編譯,從而自己編譯所需要的文件和鏈接目標程序。


正如前面所說的,如果一個工程有 3 個頭文件,和 8 個 C 文件,我們爲了完成前面所述的那三個規則,我們的 Makefile 應該是下面的這個樣子的。

edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
	cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

main.o : main.c defs.h
	cc -c main.c

kbd.o : kbd.c defs.h command.h
	cc -c kbd.c

command.o : command.c defs.h command.h
	cc -c command.c

display.o : display.c defs.h buffer.h
	cc -c display.c

insert.o : insert.c defs.h buffer.h
	cc -c insert.c

search.o : search.c defs.h buffer.h
	cc -c search.c

files.o : files.c defs.h buffer.h command.h
	cc -c files.c

utils.o : utils.c defs.h
	cc -c utils.c

clean :
	rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

我們可以把這個內容保存在文件爲“Makefile”或“makefile”的文件中,然後在該目錄下直接輸入命令“make”就可以生成執行文件 edit。如果要刪除執行文件和所有的中間目標文件,那麼,只要簡單地執行一下“make clean”就可以了

在這個 makefile 中,目標文件(target)包含執行文件 edit 和中間目標文件(*.o)依賴文件(prerequisites)包含:就是冒號後面的那些 .c 文件和 .h 文件。每一個 .o 文件都有一組依賴文件,而這些 .o 文件又是執行文件 edit 的依賴文件。依賴關係的實質上就是說明了目標文件是由哪些文件生成的,換言之,目標文件是哪些文件更新的。在定義好依賴關係後,後續的那一行定義瞭如何生成目標文件的操作系統命令,一定要以一個 Tab 鍵作爲開頭

記住,make 並不管命令是怎麼工作的,他只管執行所定義的命令。make 會比較 targets 文件和prerequisites 文件的修改日期,如果 prerequisites 文件的日期要比 targets 文件的日期要新,或者 target 不存在的話,那麼,make 就會執行後續定義的命令。
這裏要說明一點的是,clean 不是一個文件,它只不過是一個動作名字,有點像 C 語言中的 lable 一樣,其冒號後什麼也沒有,那麼,make 就不會自動去找文件的依賴性,也就不會自動執行其後所定義的命令。要執行其後的命令,就要在 make 命令後明顯得指出這個lable 的名字,即“make clean”。這樣的方法非常有用,我們可以在一個 makefile 中定義不用的編譯或是和編譯無關的命令,比如程序的打包,程序的備份,等等。


三、 make 是如何工作的


在默認的方式下,也就是我們只輸入 make 命令。那麼:

  1. make 會在當前目錄下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它會找文件中的第一個目標文件(target),在上面的例子中,他會找到“edit”這個文件,並把這個文件作爲最終的目標文件
  3. 如果 edit 文件不存在, 或是 edit 所依賴的後面的 .o 文件的文件修改時間要比 edit這個文件新,那麼,他就會執行後面所定義的命令來生成 edit 這個文件。
  4. 如果 edit 所依賴的.o 文件也存在,那麼 make 會在當前文件中找目標爲.o 文件的依賴性,如果找到則再根據那一個規則生成.o 文件
  5. 當然,你的 C 文件和 H 文件是存在的啦, 於是 make 會生成 .o 文件, 然後再用 .o 文件生命 make 的終極任務,也就是執行文件 edit 了。

這就是整個 make 的依賴性,make 會一層又一層地去找文件的依賴關係,直到最終編譯出第一個目標文件。在找尋的過程中,如果出現錯誤,比如最後被依賴的文件找不到,那麼make 就會直接退出,並報錯,而對於所定義的命令的錯誤,或是編譯不成功,make 根本不理。make 只管文件的依賴性,即,如果在我找了依賴關係之後,冒號後面的文件還是不在,那麼對不起,我就不工作啦。

通過上述分析,我們知道,像 clean 這種,沒有被第一個目標文件直接或間接關聯,那麼它後面所定義的命令將不會被自動執行,不過,我們可以顯示要 make 執行。即命令——“make clean”以此來清除所有的目標文件,以便重編譯。於是在我們編程中,如果這個工程已被編譯過了,當我們修改了其中一個源文件,比如file.c,那麼根據我們的依賴性,我們的目標 file.o 會被重編譯(也就是在這個依性關係後面所定義的命令),於是 file.o 的文件也是最新的啦,於是 file.o 的文件修改時間要比edit 要新,所以 edit 也會被重新鏈接了(詳見 edit 目標文件後定義的命令)。而如果我們改變了“command.h”,那麼,kdb.o、command.o 和 files.o 都會被重編譯,並且,edit 會被重鏈接。


四、 makefile 中使用變量


在上面的例子中,先讓我們看看 edit 的規則:

edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
	cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o

我們可以看到[.o]文件的字符串被重複了兩次, 如果我們的工程需要加入一個新的[.o]文件,那麼我們需要在兩個地方加(應該是三個地方,還有一個地方在 clean 中)。當然,我們的 makefile 並不複雜,所以在兩個地方加也不累,但如果 makefile 變得複雜,那麼我們就有可能會忘掉一個需要加入的地方,而導致編譯失敗。所以,爲了 makefile 的易維護,在 makefile 中我們可以使用變量。makefile 的變量也就是一個字符串,理解成 C 語言中的宏可能會更好。

比如,我們聲明一個變量,叫 objects,只要能夠表示 obj 文件就行了。我們在 makefile 一開始就這樣定義:

objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

於是,我們就可以很方便地在我們的 makefile 中以“$(objects)”的方式來使用這個變量了,於是我們的改良版 makefile 就變成下面這個樣子:

objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

edit : $(objects)
	cc -o edit $(objects)
	
main.o : main.c defs.h
	cc -c main.c

kbd.o : kbd.c defs.h command.h
	cc -c kbd.c

command.o : command.c defs.h command.h
	cc -c command.c

display.o : display.c defs.h buffer.h
	cc -c display.c

insert.o : insert.c defs.h buffer.h
	cc -c insert.c

search.o : search.c defs.h buffer.h
	cc -c search.c

files.o : files.c defs.h buffer.h command.h
	cc -c files.c

utils.o : utils.c defs.h
	cc -c utils.c

clean :
	rm edit $(objects)

於是如果有新的 .o 文件加入,我們只需簡單地修改一下 objects 變量就可以了。


五、讓 make 自動推導


GNU 的 make 很強大,它可以自動推導文件以及文件依賴關係後面的命令,於是我們就沒必要去在每一個[.o]文件後都寫上類似的命令,因爲,我們的 make 會自動識別,並自己推導命令。只要 make 看到一個[.o]文件,它就會自動的把[.c]文件加在依賴關係中如果 make找到一個 whatever.o,那麼 whatever.c,就會是 whatever.o 的依賴文件。並且 cc -c whatever.c 也會被推導出來,於是,我們的 makefile 再也不用寫得這麼複雜。我們的是新的 makefile 又出爐了。

objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o

edit : $(objects)
	cc -o edit $(objects)
	
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
	rm edit $(objects)

這種方法,也就是 make 的“隱晦規則”。上面文件內容中,“.PHONY”表示,clean是個僞目標文件。


六、清空目標文件的規則


每個 Makefile 中都應該寫一個清空目標文件(.o 和執行文件)的規則,這不僅便於重編譯,也很利於保持文件的清潔。這是一個“修養”。

一般的風格都是:
clean:
	rm edit $(objects)
	
更爲穩健的做法是:
.PHONY : clean
clean :
	-rm edit $(objects)

前面說過,.PHONY 意思表示 clean 是一個“僞目標”, 。而在 rm 命令前面加了一個小減號的意思就是,也許某些文件出現問題,但不要管,繼續做後面的事。當然,clean 的規則不要放在文件的開頭,不然,這就會變成 make 的默認目標,相信誰也不願意這樣。不成文的規矩是——“clean 從來都是放在文件的最後”。


第四部分、Makefile 總述



一、 Makefile 裏有什麼?


Makefile 裏主要包含了五個東西:顯式規則、隱晦規則、變量定義、文件指示和註釋

  • 顯式規則:如何生成一個或多個的目標文件。這是由 Makefile 的書寫者明顯指出,要生成的文件,文件的依賴文件,生成的命令。
  • 隱晦規則:由於我們的 make 有自動推導的功能,所以隱晦的規則可以讓我們比較粗糙地簡略地書寫 Makefile,這是由 make 所支持的。
  • 變量的定義:在 Makefile 中我們要定義一系列的變量,變量一般都是字符串,這個有點 C 語言中的宏,當 Makefile 被執行時,其中的變量都會被展開到相應的引用位置上。
  • 文件指示:其包括了三個部分,一個是在一個 Makefile 中引用另一個 Makefile,就像 C 語言中的include 一樣;另一個是指根據某些情況指定Makefile 中的有效部分,就像 C 語言中的預編譯#if 一樣;還有就是定義一個多行的命令。
  • 註釋:Makefile 中只有行註釋,和 UNIX 的 Shell 腳本一樣,其註釋是用“#”字符,這個就像 C/C++中的“//”一樣。如果你要在你的 Makefile 中使用“#”字符,可以用反斜槓進行轉義,如:“\ #”。最後,還值得一提的是,在 Makefile 中的命令,必須要以[Tab]鍵開始

二、 Makefile 的文件名


默認的情況下,make 命令會在當前目錄下按順序找尋文件名爲“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解釋這個文件。在這三個文件名中,最好使用“Makefile”這個文件名,因爲,這個文件名第一個字符爲大寫,這樣有一種顯目的感覺。最好不要用“GNUmakefile”,這個文件是 GNU 的 make 識別的。有另外一些 make 只對全小寫的“makefile”文件名敏感,但是基本上來說,大多數的 make 都支持“makefile”和“Makefile”這兩種默認文件名。當 然 , 你 可 以 使 用 別 的 文 件 名 來 書 寫 Makefile , 比 如 : “ Make.Linux” ,“Make.Solaris”,“Make.AIX”等,如果要指定特定的 Makefile,你可以使用 make 的“-f”和“–file”參數,如:make -f Make.Linux 或 make --file Make.AIX。


三、引用其它的 Makefile


在 Makefile 使用 include 關鍵字可以把別的 Makefile 包含進來,這很像 C 語言的#include,被包含的文件會原模原樣的展開在當前文件的包含位置。include 的語法是:include <filename>,filename 可以是當前操作系統 Shell 的文件模式(可以保含路徑和通配符) 在 include前面可以有一些空字符,但是絕不能是[Tab]鍵開始。include 和可以用一個或多個空格隔開。

舉個例子,你有這樣幾個Makefile:a.mk、b.mk、c.mk,還有一個文件叫foo.make,以及一個變量$(bar),其包含了 e.mk 和 f.mk,那麼,下面的語句:include foo.make *.mk $(bar)等價於:include foo.make a.mk b.mk c.mk e.mk f.mk,make命令開始時,會把尋找 include 所指出的其它 Makefile,並把其內容安置在當前的位。就好像 C/C++的#include 指令一樣。如果文件都沒有指定絕對路徑或是相對路徑的話,make 會在當前目錄下首先尋找,如果當前目錄下沒有找到,那麼,make 還會在下面的幾個目錄下找

  • 如果 make 執行時,有“-I”或“--include-dir”參數,那麼 make 就會在這個參數所指定的目錄下去尋找。

  • 如果目錄<prefix>/include(一般是:/usr/local/bin 或/usr/include)存在的話,make也會去找。如果有文件沒有找到的話,make 會生成一條警告信息,但不會馬上出現致命錯誤。它會繼續載入其它的文件,一旦完成 makefile 的讀取,make 會再重試這些沒有找到的或是不能讀取的文件;如果還是不行,make纔會出現一條致命信息。如果你想讓 make不理那些無法讀取的文件,而繼續執行,你可以在include 前加一個減號“-”。如: -include <filename>其表示,無論 include 過程中出現什麼錯誤,都不要報錯繼續執行。和其它版本 make兼 容的相關命令是 sinclude,其作用和這一個是一樣的。


四、環境變量 MAKEFILES


如果你的當前環境中定義了環境變量 MAKEFILES,那麼,make 會把這個變量中的值做一個類似於 include 的動作。這個變量中的值是其它的 Makefile,用空格分隔。只是, 它和 include不同的是,從這個環境變中引入的 Makefile 的“目標”不會起作用,如果環境變量中定義的文件發現錯誤,make 也會不理。但是在這裏我還是建議不要使用這個環境變量,因爲只要這個變量一被定義,那麼當你使用 make 時, 所有的 Makefile 都會受到它的影響, 這絕不是你想看到的。在這裏提這個事,只是爲了告訴大家,也許有時候你的 Makefile 出現了怪事,那麼你可以看看當前環境中有沒有定義這個變量。


五、 make 的工作方式


GNU 的 make 工作時的執行步驟入下: (想來其它的 make 也是類似)

	1、讀入當前的 Makefile。
	2、讀入被 include 的其它 Makefile。
	3、初始化文件中的變量。
	4、推導隱晦規則,並分析所有規則。
	5、爲所有的目標文件創建依賴關係鏈。
	6、根據依賴關係,決定哪些目標要重新生成。
	7、執行生成命令。

1-5 步爲第一個階段,6-7 爲第二個階段。第一個階段中,如果定義的變量被使用了,那麼,make 會把其展開在使用的位置。但 make 並不會完全馬上展開,make 使用的是拖延戰術,如果變量出現在依賴關係的規則中,那麼僅當這條依賴被決定要使用了,變量纔會在其內部展開。當然,這個工作方式你不一定要清楚,但是知道這個方式你也會對 make 更爲熟悉。有了這個基礎,後續部分也就容易看懂了。

想了解更多關於makefile的知識可參考我的下一篇博客Makefile的書寫規則

文章內容來自陳皓的《跟我一起寫makefile》,轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章