【LINUX網路編程】Makefile檔案,網路編程makefile
《Linux網路編程》(第二版) 第2章的一些讀書筆記 ↓
Makefile:在一個含有較多檔案的工程中,定義一系列規則來指定編譯檔案的順序,可用於管理工程。
Makefile指定了工程中的哪些源檔案需要編譯以及如何編譯、需要建立那些庫檔案以及如何建立這些庫檔案、如何最後產生我們想要的可執行檔。為工程編寫Makefile 的好處是能夠使用一行命令來完成“自動化編譯”,編譯整個工程你所要做的唯一的一件事就是在shell 提示符下輸入make命令,整個工程就完全自動編譯。
首先來看一下Linux下的GCC(GNU Compiler Collection):這是一個工具集,包含gcc(跟大寫的不一樣), g++等編譯器和ar, nm等工具集。
GCC編譯器對程式的編譯有4個階段:先行編譯 -> 編譯和最佳化 -> 彙編 -> 連結,下面以c代碼作例子:
原始碼(*.c) 【先行編譯 -E】 預先處理後的代碼(*.i) 【編譯和最佳化 -S】 彙編代碼(*.s) 【彙編 -c】 目標檔案(*.o) 【連結】 可執行檔
先行編譯過程是將程式中引用的標頭檔包含進原始碼中,並對一些宏進行替換
編譯和最佳化通常是翻譯成組合語言,彙編與機器作業碼之間有一對一的關係
目標檔案是指經過編譯器的編譯和彙編產生的CPU可識別的二進位代碼,但是其中的一些函數過程沒有相關的指示和說明,所以一般不能執行
目標檔案需要用某種方式組合起來才可以運行,這就是連結
一些命令選項(假設檔案名稱為hello.c):
gcc hello.c /* 產生可執行檔,名字為預設的 a.out。gcc後面的不只可以是*.c檔案,同樣也可以是*.o或其他,以下的選項也是如此,只要被處理的檔案的階段在組建檔案的階段之前即可 */
gcc -o hello hello.c /* 產生可執行檔,名字接在-o後面,即hello */
gcc -E hello.c /* 經過先行編譯的過程,預設名字格式,產生了hello.i */
gcc -S hello.c /* 經過了先行編譯和編譯最佳化的過程,名字為預設的 hello.s */
gcc -c hello.c /* (常用) 經過了先行編譯和編譯最佳化以及彙編的過程,產生了目標檔案,名字為預設的 hello.o */
其他選項:
gcc -D宏名 , gcc -DOS_LINUX 則相當於在先行編譯的時候添加了 #define OS_LINUX,即當出現 #ifdef OS_LINUX 的時候,就會滿足條件
gcc -Idir (大寫i),將標頭檔的搜尋路徑擴大,包含dir目錄 (後面會用到)
gcc -Ldir,將連結時使用的連結庫搜尋路徑擴大,包含dir目錄,gcc優先使用共用程式庫
gcc -static,僅選用靜態程式庫進行連結
gcc -On,n為數字,最佳化程式執行速度和佔用空間(同時會加長編譯速度),常用的是2,gcc -O2
瞭解了一些基本選項之後,給出一個小項目的例子,分別用手動編譯和Makefile“自動化編譯”的方式來說明:
目錄結構:
project/ main.c add/ add_int.c add_float.c sub/ sub_int.c sub_float.c
main.c:可以注意到add.h和sub.h並沒有跟main.c同一級目錄,這時就要用到 gcc -I目錄 的選項了,擴大標頭檔搜尋路徑,才找到真正的*.h。至於沒有定義的add_int等函數,需要在連結的時候同時包含有具體定義的.o檔案
/* main.c */#include <stdio.h>#include "add.h"#include "sub.h"int main(void){int a = 10, b = 12;float x = 1.23456, y = 9.87654321;printf("int a+b IS:%d\n",add_int(a,b));printf("int a-b IS:%d\n",sub_int(a,b));printf("float a+b IS:%f\n",add_float(x,y));printf("float a-b IS:%f\n",sub_float(x,y));return 0;}
add/add.h
/* add.h */#ifndef __ADD_H__#define __ADD_H__extern int add_int(int a, int b);extern float add_float(float a, float b);#endif
add/add_int.c
/* add_int.c */int add_int(int a, int b){return a+b;}
add/add_float.c
/* add_float.c */float add_float(float a, float b){return a+b;}
sub目錄略。
根據書上的代碼,將c檔案轉成目標檔案,然後連結上。其中,如果在編譯main.c的時候,由於標頭檔找不到,所以必須添加上 -Iadd 和 -Isub 才可以找到,書上的例子有誤。
可以看到,編譯一個小的項目就需要如此多的步驟:
gcc -o add/add_int.o -c add/add_int.c gcc -o add/add_float.o -c add/add_float.c gcc -o sub/sub_float.o -c sub/sub_float.c gcc -o sub/sub_int.o -c sub/sub_int.c gcc -o main.o -c main.c -Iadd -Isubgcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o ./cacu
雖然,可以通過gcc的預設規則,使用如下命令也可以產生可執行檔:
gcc -o cacu1 add/add_int.c add/add_float.c sub/sub_int.c sub/sub_float.c main.c -Iadd -Isub
但是當頻繁修改源檔案或者當項目中的檔案比較多,關係比較複雜的時候,用gcc直接編譯就會變得非常困難。
於是我們應該採用Makefile檔案的編寫,通過make命令將多個檔案編譯為可執行檔。
make通過解析Makefile檔案中的規則,可以自動執行相應的指令碼,以下就是一個簡單的Makefile檔案:
# 第一行item的":"左邊為make命令在正確操作之後預設產生的檔案,故產生cacu2,依賴於":"右邊的檔案。# 每一個item都是一個規則# 在從左往右掃描的過程中,如果不存在某個檔案,則跳到產生它的規則# 注意底下的一行,它是以tab鍵開頭的,不能是空格,表示滿足所有依賴的情況下,執行那幾行指令cacu2:add_int.o add_float.o sub_int.o sub_float.o main.ogcc -o cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.oadd_int.o:add/add_int.c add/add.hgcc -o add/add_int.o -c add/add_int.cadd_float.o:add/add_float.c add/add.hgcc -o add/add_float.o -c add/add_float.csub_int.o:sub/sub_int.c sub/sub.hgcc -o sub/sub_int.o -c sub/sub_int.csub_float.o:sub/sub_float.c sub/sub.hgcc -o sub/sub_float.o -c sub/sub_float.cmain.o:main.c add/add.h sub/sub.hgcc -o main.o -c main.c -Iadd -Isub# 清理的規則,可以使用make clean來主動執行clean:rm -f cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o
,在安裝了make的情況下,通過編寫Makefile後執行make命令,系統會自動執行gcc的一些命令,產生了cacu2這個可執行檔;然後make clean之後,便刪除了本次make產生的內容。
回到Makefile檔案中,事實上預設情況下,make會直接執行第一個規則,即cacu2的規則。系統先檢查依賴,並在成功之後執行下述命令:
$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)
如何理解?上面的命令其實等價於下面這一條,只是用了變數擴充
gcc -o cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o -Iadd -Isub -O2
CC = gcc , TARGET = cacu2 , OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o , CFLAGS = -Iadd -Isub -O2
同理,make clean命令是這樣的:
-$(RM) $(TARGET) $(OBJS)
等價於rm -f cacu2 add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o,注意到RM變數前面有個 “-” 號,它表示當操作失敗時不報錯,命令繼續執行。
總結一下要注意的點:
Makefile中使用變數:
為啥引入變數? 在某個規則中添加一項依賴,既要在依賴項中填寫,又要在命令列中填寫,很不方便。
變數有3種:
1、預定義變數:Makefile中已經定義的變數,使用者可以直接使用這些變數,不用進行定義。(圖來自此連結) 使用方法與自訂變數一樣,見3
2、自動變數:在編譯語句中,會經常出現目標檔案和依賴檔案,自動變數代表這些目標檔案和依賴檔案。
3、使用者自訂變數:
定義方法: 變數名 = 值。 eg, OBJS = add_int.o add_float.o ... # 一行代表一個變數
使用方法: $(變數名)。 eg, $(OBJS)
/* 此處重寫一份Makefile */
# 定義一些變數CC = gccCFLAGS = -Iadd -Isub -O2TARGET = cacu3OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o# $(CC) -o $(TARGET) $(OBJS) $(CFLAGS)$(TARGET):$(OBJS)$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)$(OBJS):%o:%c$(CC) -o $@ -c $< $(CFLAGS)clean:-$(RM) $(TARGET) $(OBJS)
在此處運用了定義變數的方式,簡化了文本的維護。
$(CC) -o $@ -c $< $(CFLAGS) 採用了所謂的“靜態模式”的規則,相當於是多條規則,規則如上面所示。
搜尋路徑:
在大的系統中,存在很多目錄,手動用-I的方法添加目錄不方便,所以用到了VPATH變數,它可以自動找到指定檔案的目錄並添加到檔案上。
使用方法:VPATH = path1:path2:...:.
用冒號(:)隔開,記得在最後加上目前的目錄 .
這樣會出現一個問題:目標檔案會放到當前的目錄下,汙染環境!所以,自訂一個變數,建立以該變數為名字的目錄,將*.o檔案放進去。
/* 此處再重寫一份Makefile */
# 發現一個嚴重的錯誤,因為Makefile檔案行末沒有終結符,所以最好不要盲目在後面放注釋,而應該換行。。# 定義一些變數,並引入VPATHCC = gccCFLAGS = -Iadd -Isub -O2# 將所有的目標檔案放置在這個檔案夾中OBJSDIR = objsVPATH = add:sub:.TARGET = cacu4# OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.oOBJS = add_int.o add_float.o sub_int.o sub_float.o main.o# 定義了VPATH之後,OBJS中的檔案夾名就不用寫了# 先檢查目錄是否存在$(TARGET):$(OBJSDIR) $(OBJS)$(CC) -o $(TARGET) $(OBJSDIR)/*.o $(CFLAGS)$(OBJS):%o:%c$(CC) -o $(OBJSDIR)/$@ -c $< $(CFLAGS)# mkdir -p 建立所有遺失的父目錄$(OBJSDIR):mkdir -p ./$@clean:-$(RM) $(TARGET) $(OBJSDIR)/*.o
自動推導規則:使用命令make編譯副檔名為c的C語言檔案的時候,源檔案的編譯規則不用明確給出。按照預設的規則,通過依賴中的.o檔案,找到對應的.c檔案將其編譯成目標檔案用於滿足依賴。
# 使用命令make編譯副檔名為c的C語言檔案的時候,源檔案的編譯規則不用明確給出。(會使用一個預設的編譯規則) -- make的隱含規則CC = gccCFLAGS = -Iadd -Isub -O2VPATH = add:sub:.TARGET = cacu5OBJS = add_int.o add_float.o sub_int.o sub_float.o main.o$(TARGET):$(OBJS)$(CC) -o $(TARGET) $(OBJS) $(CFLAGS)clean:-$(RM) $(TARGET) $(OBJS)
遞迴make:
當有多人在多個目錄進行程式開發,並且每個人負責一個模組,而檔案在相對獨立的目錄中,這時由同一個Makefile維護代碼的編譯會十分蹩腳。
1、遞迴調用的方式:make命令有遞迴調用的作用,它可以遞迴調用每個子目錄的Makefile。
假設目錄add和sub中都有Makefile,則可以用以下兩種方法(第二種比較好):
add: cd add && $(MAKE)
add: $(MAKE) -C add
都表示進入add目錄,然後執行make命令
2、總控Makefile:調用$(MAKE) -C即可。如果總控Makefile中的一些變數需要傳遞給下層的Makefile,可以使用export命令。
以下是總控的Makefile代碼實現:
export CC = gccCFLAGS = -Iadd -Isub -O2TARGET = cacu6export OBJSDIR = ${shell pwd}/objs$(TARGET):$(OBJSDIR) main.o$(MAKE) -C add$(MAKE) -C sub$(CC) -o $(TARGET) $(OBJSDIR)/*.omain.o:main.c$(CC) -o $(OBJSDIR)/$@ -c $^ $(CFLAGS)$(OBJSDIR):mkdir -p $(OBJSDIR)clean:-$(RM) $(TARGET) $(OBJSDIR)/*.o
以上代碼與書上有一些不同,注意CC需要export到子Makefile中,否則會預設使用cc而不是gcc
${shell pwd}表示執行shell中的pwd命令,即擷取當前路徑
產生的目標檔案(*.o)全都放進項目目錄的objs目錄下
以下是子目錄Makefile:
add/Makefile:
OBJS = add_int.o add_float.oall:$(OBJS)$(OBJS):%o:%c$(CC) -o $(OBJSDIR)/$@ -c $< -O2clean:-$(RM) $(OBJS)
sub/Makefile:
OBJS = sub_int.o sub_float.oall:$(OBJS)$(OBJS):%o:%c$(CC) -o $(OBJSDIR)/$@ -c $< -O2clean:-$(RM) $(OBJS)
結果如:
3、Makefile中的函數
- 擷取匹配模式的檔案名稱 wildcard:尋找目前的目錄下所有符合模式PATTERN的檔案名稱,傳回值是以空格分割的(符合模式的)檔案名稱列表。 原型:$(wildcard PATTERN) , 如:$(wildcard *.c)
- 模式替換函數 patsubst:相當於replace函數,尋找字串text中按照空格分開的詞,將符合模式的字串替換成別的字串。 原型:$(patsubst pattern, replacement, text), 如:$(patsubst %.c, %.o, $(wildcard *.c) )
- 迴圈函數 foreach:在LIST中取出每個用空格分割的VAR單詞,然後執行TEXT運算式,處理結束後輸出。 原型:$(foreach VAR, LIST, TEXT), 如:$(foreach dir, $(DIRS), $(wildcard $(dir)/*.c) )
以下是根據書上的Makefile檔案改寫的一個用函數的Makefile檔案:
CC = gccCFLAGS = -Iadd -Isub -O2TARGET = cacu7DIR = add sub .FILES = $(foreach dir, $(DIR), $(wildcard $(dir)/*.c) )OBJS = $(patsubst %c, %o, $(FILES))$(TARGET):$(OBJS) main.o$(CC) -o $@ $^ -O2$(OBJS):%o:%c$(CC) -o $@ -c $< $(CFLAGS)clean:-$(RM) $(TARGET) $(OBJS)
----- 分割線 -----
以上的Makefile例子和整個項目代碼可以參見:TyrusChin - Github