文章目錄
c文件的編譯原理
- 原理流程
說到Makefile,不得不談一談C源文件的編譯過程啦,整個過程使用下圖足以說明一切。
- 功能
- 上述編譯流程針對每個源文件而言,在一個項目的編譯工作中,一般使用linux GNU make管理和構建自己的工程,如果不使用make,可想而知,編譯上百個源文件得編譯到何年馬月,所以Makefile對於一個linux開發人員是何等的重要。
什麼是Makefile
1、make如何工作的
make工具依賴Makefile或着makefile文件,執行make命令時,make會去當前工作目錄下尋找Makefile或makefile文件,然後根據目標和依賴項按順序執行任務。首次make將執行所有任務,如果後面某個依賴項被更新,make只執行被更新過的依賴項,減少不必要的系統開銷,節省編譯時間。
2、爲什麼要使用Makefile
Makefile的最大作用就是自動化編譯,只要寫好了Makefile文件,終端鍵入make命令就可以執行Makefile文件裏面的指令。在實際項目中,一個工程包含幾十個,甚至幾百個源文件,如果手動gcc編譯一個個源文件,即使你有使不完的力氣,和你協作共事的小夥伴也沒有那個耐心等你gcc編譯完所有的源文件。所以,Makefile不僅可以提高個人的專業技能,更重要的是大大提高了工作的效率。
一次編寫Makefile,終生受用,源文件被修改後,只需一鍵make就搞定。
3、Makefile與shell
Makefile類似於shell腳本,都可以執行操作系統的命令。Makefile和shell是兩種不同的腳本,Makefile專門用於編譯的專用工具,shell相當於一個命令行終端,是一個通用工具。當然用shell腳本也可以實現自動化編譯,但相對Makefile來說,無論從執行效率、編程難易上來說都顯得複雜很多。所以在工程編譯工作中,開發人員都比較喜歡使用Makefile,有時結合shell腳本做一些簡單的工作,比如程序穩定性測試、黑白盒測試、壓力測試等。
Makefile中的shell命令必須以[tab]鍵開頭。
Makefile的核心思想
target...: dependency...
command
...
...
Makefile的核心思想如上面的代碼所示,其實很簡單,主要由目標項、依賴項和shell命令組成。
-
目標項(target)
目標項執行這條shell命令要生成的目標文件,比如gcc hello.c -o hello,hello就是一個目標項。這個目標項不一定是最終的可執行文件hello,也可使是中間目標文件hello.o,還可以是一個標籤,如僞目標。 -
依賴項(dependency)
依賴項就是生成這個目標項所必要的文件。 -
shell命令(command)
shell對於我們來說再熟悉不過了,我們在linux終端使用的一切命令都是shell命令。
逐步瞭解Makefile
初始版示例
-
工作目錄下的文件
對client.h文件的說明:在這個目錄下,我將所有頭文件都包含在client.h中,單獨一個文件管理着所有文件,當增加頭文件時,只需在這個文件中添加,不必去c源文件中添加。
-
Makefile文件
1
2 client_main:client_main.o domain_parse.o get_temp.o log.o
3 gcc -o client_main client_main.o domain_parse.o get_temp.o log.o
4
5 client_main.o:client_main.c client.h
6 gcc -c client_main.c
7
8 domain_parse.o:domain_parse.c client.h
9 gcc -c domain_parse.c
10
11 get_temp.o:get_temp.c client.h
12 gcc -c get_temp.c
13
14 log.o:log.c client.h
15 gcc -c log.c
16
17 clean:
18 rm -rf client_main.o domain_parse.o get_temp.o log.o
解析:
2、5、8、11、14、17一共有6個目標,其中第17行clean不是總目標的依賴項,make不會自動執行,只有輸入
make clean才執行它。
第一步:輸入make命令後,make機制開始在當前目錄下尋找Makefile文件,並找到總目標,如client_main。
第二步:然後按總目標的依賴項的先後順序找到依賴項,如client_main.o。
第三步:如果該依賴項也是一個目標項,程序就跳到相應依賴項作爲目標項的地方,如第5行client_main.o:。
第四步:如果新依賴項滿足新目標項,程序開始執行shell命令,如gcc -c client_main.c 。
第五步:重複第二步~第五步,直到所有任務執行完成。
-
make結果
此時可以看到目錄中多了 *.o文件和client_main,make並沒有自動執行clean,因爲clean並不是總目標裏面的依賴項。所以手動make clean
此時make刪除所有.o文件。
改進版
一級示例
1
2 OBJS=client_main.o domain_parse.o get_temp.o log.o
3
4 client_main:$(OBJS)
5 gcc -o client_main $(OBJS)
6
7 client_main.o:client.h
8
9 domain_parse.o:client.h
10
11 get_temp.o:client.h
12
13 log.o:client.h
14
15 .PHONY:clean
16 clean:
17 -rm client_main $(OBJS)
從上面的Makefile文件中可以看出,代碼比初始版本簡潔許多,在此版本中增加了以下內容:
-
變量
類似於c語言中的宏定義,它代表文本字串。第2行定義OBJS變量並賦值爲所有的*.o文件,在後面的語句中所有使用.o文件的地方用$(OBJS)代替,這樣做的好處是,增加或刪除某個.o文件時,只需修改變量OBJS的值。 -
自動推導功能
make會根據.o文件自動推到出.c文件,並根據總目標中的shell命令執行次級目標任務,比如在此版本中將所有次級目標的依賴文件.c文件刪除。 -
僞目標
僞目標不是文件,可有可無依賴文件,是一個標籤,第15行.PHONY是僞目標關鍵詞,clean使用僞目標的原因是,如果當前工作目下有一個文件名爲clean,執行make就會出錯,所以使用僞目標用於出區分。 -
-rm
如果文件出錯,不管,繼續往後執行。
二級示例
1 OBJS:=$(patsubst %.c,%.o,$(wildcard *.c))
2 CC:=gcc
3 CFLAGS:=-g
4 TARGETS:=client_main
5 RM:=-rm
6 #終極目標
7 $(TARGETS):$(OBJS)
8 $(CC) -o $@ $^ $(CFLGS) $(LDFLAGES)
9
10 #清除命令
11 .PHONY:clean cleanall
12 clean:
13 $(RM) $(OBJS)
14 cleanall:
15 $(RM) $(OBJS) $(TARGETS)
解析:
- 第1行 調用函數將當前工作目錄下的所有c文件替換爲中間目標文件.o文件。簡單對函數說明一下:
1、patsubst
調用:$(patsubst pattern,replacement,text)
參數:查找text中的單詞(單詞以“空格”、“Tab”或“回車”“換行”分隔)是否符合模式pattern,如果匹
配的話,則以replacement替換。這裏,pattern可以包括通配符“%”,表示任意長度的字串。如果
replacement中也包含“%”,那麼,replacement中的這個“%”將是pattern中的那個“%”所代表的字
串。(可以用“\”來轉義,以“%”來表示真實含義的“%”字符)
返回 :函數返回被替換過後的字符串。
2、wildcard 獲取當前目錄下的所有c文件
- 第2行~第5行:對環境變量重新定義,使之不適用默認值
- 第7行:總目標和依賴
- 第8行:執行shell命令。這裏去掉了一級版本中的語句,通過使用make的隱含規則,自動推導c文件所需要的頭文件和編譯總目標需要的中間目標文件。
- 第11行~第15行:使用僞目標做刪除文件處理,在這裏主要刪除中間目標文件和最終的可執行文件。
高級版
前面的示例是在同一個目錄下編寫Makefile,但在linux內核源碼中,有多個子目錄,每個子目錄下存在着一個管理文件的Makefile,決定哪些文件需要編譯,哪些文件不需要編譯。在頂層根目錄下有一個總控Makefile,這個Makefile的作用就是管理着其他子目錄下的Makefile。
- 先在根目錄下創建目錄樹和定義好測試文件。比如:
- 簡單介紹一下目錄中的文件,print1.c、print2.c、print3.c三個文件內程序是一樣的,函數名和頭文件不同而已:
#include "../../include/print1.h"
void print1(void)
{
printf("here is print1\n");
}
- print1、print2、print3目錄下是Makefile是一樣的,用於管理自己目錄下c文件的編譯:
#遍歷目錄下的c文件
SRC:=$(wildcard *.c)
#更改文件名後綴
OBJS:=$(patsubst %.c,%.o,$(SRC))
../../$(OBJS_DIR)/$(OBJS):$(SRC)
$(CC) -c $^ -o $@
- src目錄下的Makefile用於管理自己的子目錄,決定make應該進去哪個子目錄裏面,在這一層中子目錄是print1、print2、print3:
#定義子目錄
SUBDIRS := print1 print2 print3
all:compile_src
#set命令-e,若指令傳回值不等於0,則立即退出shell
#for循環依次進入子目錄
compile_src:
set -e; for i in $(SUBDIRS); do $(MAKE) -C $$i; done
- 頂層中的Makefile,也可以叫它總控Makefile,它控制着子目錄中make的走向,功能和src中的Makefile是一樣的:
#定義gcc
CC := gcc
#定義子目錄
SUBDIRS := main src obj
#定義bin目錄
BIN_DIR := bin
#定義最終得可執行文件
BIN := my_app
#定義.o文件目錄
OBJS_DIR := obj
#定義清除命令
RM := rm
#傳參下一層Makefile
export CC OBJS_DIR BIN_DIR BIN
#總目標
all:check_bin compile_src
#創建bin目錄
check_bin:
mkdir -p $(BIN_DIR)
#編譯子目錄中的源碼
compile_src:
set -e; for i in $(SUBDIRS); do $(MAKE) -C $$i; done
#清除文件
.PHONY:clean cleanall
clean:
$(RM) -rf $(OBJS_DIR)/*.o
cleanall:
$(RM) -rf $(OBJS_DIR)/*.o $(BIN_DIR)/$(BIN)
- 說一下export
export的作用是總控Makefile向下一層Makefile中傳遞參數。
調用格式
export <variable …>
在本示例中總控Makefile
export CC OBJS_DIR BIN_DIR BIN
下層的Makefile加$直接使用,不必向c語言中一樣定義參變量。
- 測試
在頂層目錄下輸入make,回車
運行一下最終的可執行文件
總結一下
通過學習了Makefile,我想說一下自己的感受,Makefile說簡單也簡單,說難也難,說簡單是因爲它的編程思想簡單,一個目標項,一個依賴項,再加上shell命令,它沒有c/c++、java中的算法,也沒有很多的函數。說難是因爲平時自己寫程序都是幾個文件而已,少則一個c文件,多則十幾個,寫個Makefile很簡單,但是有的工程,目錄加子目錄幾十個,文件上百個,可能每個目錄下都有Makefile,一層嵌套一層,就像在c程序中大量使用go to語句一樣,很難受。可能我還是小白的原因,我相信只要矜持學習,一切都會變得簡單起來的。