逐步學習嵌套Makefile

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語句一樣,很難受。可能我還是小白的原因,我相信只要矜持學習,一切都會變得簡單起來的。

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