iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Software Development

30 天精通 C 語言建置與除錯:從 Makefile 到 CMake 跨平台實戰系列 第 15

[Day 15] [make] 繼續看懂遞迴make在做什麼...

  • 分享至 

  • xImage
  •  

延續之前文章中的內容...

[Day 14] 看過有一個makefile的專案了? 那我們來看看兩個以上makefile專案怎麼寫
上一篇講了如何建構含有多個makefile的專案,今天我們會繼續學會怎麼解讀子資料夾calc以及app裡面的訊息。

專案的結構

Day14_recursive_make/
├── Makefile            # 頂層:遞迴呼叫子專案
├── calc/
│   ├── Makefile        # 產生 libcalc.a
│   ├── inc/
│   │   └── calc.h
│   └── src/
│       ├── add.c
│       ├── sub.c
│       └── mul.c
└── app/
    ├── Makefile        # 連結 libcalc.a → app
    └── main.c

主要的流程:

make all
   │
   ├── target: calc
   │        └── (進入 ./calc → build libcalc.a)
   │
   └── target: app (依賴 calc)
            └── (進入 ./app → build bin/app,連結 libcalc.a)

專有名詞必了解

  1. .o → 物件檔 (object file) : 編譯器 gcc -c 的輸出結果
    把 xxx.c 轉成機器碼,但還沒連結成最終程式,一個 .o 只對應一個 .c
    如果只改了一個檔案,只需要重編那個 .c → .o,不用重建整個專案。
    之後所有 .o 再一起交給連結器 (ld 或 gcc) → 產生執行檔或靜態/動態庫
    gcc -c src/add.c -o obj/add.o
    輸出就是 obj/add.o

  2. .d → 相依檔 (dependency file): 編譯器用 -MMD -MP 自動產生的副產品
    主要是用來記錄這個 .o 需要哪些 .h 檔
    目的是讓 Makefile 知道:如果某個 header 有改動,要重編依賴它的 .o,意思是:只要 src/add.c 或 inc/calc.h 有修改,obj/add.o 就要重編。
    obj/add.o: src/add.c inc/calc.h
    所以makefile通常會有這行:-include $(DEPS) : 主要是把所有.d 列出來,讓 make 自動追蹤誰依賴誰

繼續看董makefile裡面的資訊...

calc/Makefile (靜態函式庫)

現在請先用回顧之前文章中的訊息,嘗試看懂下面的makefile,應該用Day 13 14 的資訊就好了]
CC      := gcc
CFLAGS  := -Wall -Wextra -O2 -g -Iinc -MMD -MP
AR      := ar
ARFLAGS := rcs

SRCS    := $(wildcard src/*.c)
OBJS    := $(patsubst src/%.c,obj/%.o,$(SRCS))
DEPS    := $(patsubst src/%.c,dep/%.d,$(SRCS))

LIBDIR  := ../bin
LIB     := $(LIBDIR)/libcalc.a

.PHONY: all clean distclean
all: $(LIB)

$(LIB): $(OBJS) | $(LIBDIR)
	$(AR) $(ARFLAGS) $@ $(OBJS)

obj/%.o: src/%.c | obj dep
	$(CC) $(CFLAGS) -c $< -o $@
	@mv $(@:.o=.d) dep/

$(LIBDIR) obj dep:
	@mkdir -p $@

-inc $(DEPS)

clean:
	@rm -rf obj dep

distclean: clean
	@rm -f $(LIB)

解答
  1. 變數區:統一定義編譯器、旗標與檔案清單
CC      := gcc
CFLAGS  := -Wall -Wextra -O2 -g -Iinc -MMD -MP
AR      := ar
ARFLAGS := rcs

SRCS    := $(wildcard src/*.c)
OBJS    := $(patsubst src/%.c,obj/%.o,$(SRCS))
DEPS    := $(patsubst src/%.c,dep/%.d,$(SRCS))

LIBDIR  := ../bin
LIB     := $(LIBDIR)/libcalc.a

CC:C 編譯器。
CFLAGS:

  • -Wall -Wextra 開所有常用警告

  • -O2 最佳化。

  • -g 附上除錯資訊

  • -I 會把 加到 include path 清單的最前面
    當程式碼有 #include "calc.h" 或 #include <calc.h> 時,編譯器就會依照以下順序搜尋:

    1. -I 指定的路徑(依照順序)
    2. 預設的系統路徑(例如 /usr/include、/usr/local/include)
    3. gcc -Iinc -c src/add.c 代表編譯器在處理 add.c 裡的 #include "calc.h" 時,會先到 inc/ 目錄找
  • AR/ARFLAGS:建靜態函式庫用的工具與旗標:
    r 插入 / 更新檔案、c 建立(抑制警告)、s 產生索引(ar 對 archive 內符號建立索引,連結較快)

  • SRCS:掃所有 src/xxx.c

  • OBJS:把 src/xxx.c 對應成 obj/xxx.o

  • DEPS:把 src/xxx.c 對應成 dep/xxx.d(等等會把編譯器輸出的 .d 搬移到這裡)

  • LIBDIR/LIB:輸出靜態庫的位置:../bin/libcalc.a

  1. 預設目標與 .PHONY
.PHONY: all clean distclean
all: $(LIB)

預設 make 就是 make all,而 all 依賴 $(LIB),也就是「把 libcalc.a 做出來」
下面有clean disclean
3. 產出 lib(彙整所有 .o)

$(LIB): $(OBJS) | $(LIBDIR)
	$(AR) $(ARFLAGS) $@ $(OBJS)

一般相依:$(LIB) 依賴所有 $(OBJS)。任何一個 .o 更新都會重新打包。
order-only 相依:| $(LIBDIR) 表示要先確保 ../bin 存在,但資料夾時間戳變化不會觸發重建
這個 [Day 14] 有提到避免資料夾本身變動造成不必要的重編
4. 編譯規則(pattern rule)+ 產生 .d 檔

obj/%.o: src/%.c | obj dep
	$(CC) $(CFLAGS) -c $< -o $@
	@mv $(@:.o=.d) dep/

order-only 相依 : | obj dep 會先建立 obj/ 與 dep/ 兩個資料夾。

$@ = 目標(例如 obj/xxx.o)

$(@:.o=.d) = 把副檔名 .o 換成 .d → obj/xxx.d

@mvdep/ → dep/xxx.d
這樣 .d 全部集中在 dep/,乾淨好管理

app/Makefile (應用程式)

問題

CC     := gcc
CFLAGS := -Wall -Wextra -O2 -g -I../calc/inc -MMD -MP

SRCS   := main.c
OBJS   := $(patsubst %.c,obj/%.o,$(SRCS))
DEPS   := $(patsubst %.c,dep/%.d,$(SRCS))

BINDIR := ../bin
TARGET := $(BINDIR)/app

LDFLAGS := -L$(BINDIR) -lcalc

.PHONY: all clean distclean
all: $(TARGET)

$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS)

obj/%.o: %.c | obj dep
	$(CC) $(CFLAGS) -c $< -o $@
	@mv $(@:.o=.d) dep/

$(BINDIR) obj dep:
	@mkdir -p $@

-inc $(DEPS)

clean:
	@rm -rf obj dep

distclean: clean
	@rm -f $(TARGET)

解答

前面重複的就不講了

  1. 連結規則(把 .o 連成可執行檔)
$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS)

  • 一般相依:$(TARGET) 依賴所有 $(OBJS)。
    order-only 相依:| $(BINDIR) 表示只保證 ../bin 存在,不因目錄時間戳改變而強制重建

  • libcalc.a 的相依
    這裡雖然用 -lcalc 連結,但 Make 看不到 libcalc.a 是一個「檔案相依」。
    因為這邊的步驟是頂層 Makefile 會先遞迴進 calc/ 把 libcalc.a 建好,再進 app/ 連結,所以沒問題
    但如果是單獨在 app/ 目錄下執行 make 也要自動建好 libcalc.a,可以這裡補上顯式相依:

$(TARGET): $(OBJS) ../bin/libcalc.a | $(BINDIR)

這樣可以統一由頂層make管理會比較方便。
2. 編譯規則(pattern rule)+ 產生與整理 .d

obj/%.o: %.c | obj dep
	$(CC) $(CFLAGS) -c $< -o $@
	@mv $(@:.o=.d) dep/

  • %.c 編成 obj/%.o
  • | obj dep 先確保兩個資料夾存在
  • 用 -MMD -MP,GCC 會把對應的 .d 產生在 與 .o 同一路徑(這裡是 obj/)
    • $@ = 目標(例:obj/main.o)
    • $(@:.o=.d) → obj/main.d
    • mv 到 dep/ → dep/main.d
    • 讓所有 .d 集中在 dep/,更乾淨(跟 calc/ 的做法一樣)

上一篇
[Day 14][make] 看過有一個makefile的專案了? 那我們來看看兩個以上makefile專案→瞭解子 makefile 與遞迴 make
系列文
30 天精通 C 語言建置與除錯:從 Makefile 到 CMake 跨平台實戰15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言