柴米油鹽計劃

Kbuild 編譯 Linux 內核系列(六)

Author: Wei Yang
作者公眾號:楊小偉的世界

逐步地我們已經對 kbuild 有了一定的了解,知道了 kbuild 還會有自己定義的函數,以及這些函數會在一個類似 c 語言的頭文件中定義。做了這麼多準備,該是時候探索真正的代碼編譯了。先來看看一個 .o 文件是如何編譯的。

比如我們拿這個文件來做例子:

make mm/memblock.o

找到目標

有了剛才的經驗,這次我們順著之前的思路。先到根目錄的 Makefile 中找找線索。有時候找代碼除了經驗,還得有點運氣。不知道你這次有沒有找到呢?
單個文件的目標還是在根目錄 Makefile 文件中。

%.o: %.c prepare scripts FORCE
    $(Q)$(MAKE) $(build)=$(build-dir) $(target-dir)$(notdir $@)

你看,這格式其實和我們平時自己寫的規則是差不多的。 .o 的文件依賴於同名的 .c 文件,然後執行了一個命令來生成。不過呢,確實還多了些東西,包括一些特殊的依賴條件,以及這個命令長得也有點丑。
俗話說的好,惡人自有惡人磨,長得丑的代碼也有丑辦法來解讀。
把上面這段代碼添加一個井號。

%.o: %.c prepare scripts FORCE
    #$(Q)$(MAKE) $(build)=$(build-dir) $(target-dir)$(notdir $@)

再運行一次。

$ make mm/memblock.o
  CHK     include/config/kernel.release
  CHK     include/generated/uapi/linux/version.h
  CHK     include/generated/utsrelease.h
  CHK     include/generated/timeconst.h
  CHK     include/generated/bounds.h
  CHK     include/generated/asm-offsets.h
  CALL    scripts/checksyscalls.sh
# @make -f ./scripts/Makefile.build obj=mm mm/memblock.o

怎麼樣,通過顯示實際執行的命令,是不是你感覺熟悉了些?從顯示出的命令來看,生成 mm/memblock.o 這個目標文件是重新又調用了一次make,而這次使用的是 script/Makefile.build 這個規則文件,傳入的參數是 obj=mm ,目標還是 mm/memblock.o 。

你看,是不是又清晰了一些?

那串命令行

現在我們來看那串命令行究竟是如何展開的。

    $(Q)$(MAKE) $(build)=$(build-dir) $(target-dir)$(notdir $@)

第一招,我們先來拆分。這條命令中出現了好幾個變數, Q、MAKE、build、build-dir、target-dir 。前面兩個就是定義成 @ 和 make 的,基本問題不大也比較好理解。關鍵是後面三個。

build-dir & target-dir

可巧,最後兩個變數的定義就在這組規則的上方。我們拿來先看一下。

# Single targets
# ------------------------------------------------------------
# Single targets are compatible with:
# - build with mixed source and output
# - build with separate output dir 'make O=...'
# - external modules
#
#  target-dir => where to store outputfile
#  build-dir  => directory in kernel source tree to use

ifeq ($(KBUILD_EXTMOD),)
        build-dir  = $(patsubst %/,%,$(dir $@))
        target-dir = $(dir $@)
else
        zap-slash=$(filter-out .,$(patsubst %/,%,$(dir $@)))
        build-dir  = $(KBUILD_EXTMOD)$(if $(zap-slash),/$(zap-slash))
        target-dir = $(if $(KBUILD_EXTMOD),$(dir $<),$(dir $@))
endif

看注釋,這兩個變數一個是做編譯的路徑,一個是目標存放的路徑。

定義本身分為兩種情況
* 當 KBUILD_EXTMOD 為空,表示不是編譯內核模塊。
* 當 KBUILD_EXTMOD 不為空,表示是在編譯內核模塊。

這次我們並沒有編譯內核模塊,所以採用的是第一種情況的定義。從顯示出的命令來看 build-dir 和 target-dir 分別定義為 mm 和 mm/ 。說明指向的路徑是一樣的,只是相差了最後的一個 / 。

build

五個變數基本理解了四個,那現在來看看最後一個build。
通過我們比較丑的方法得到最後展開的命令來看,這個build變數包含的是

-f ./scripts/Makefile.build obj

關鍵它在哪裡呢?還記得上文找過的那個「頭文件」么?對了,還是在那 scripts/Kbuild.include 。

###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

瞧,是不是和我們估計得長得差不多。

再進一步

終於把第一步的命令看明白了,現在是時候研究規則文件 scripts/Makefile.build 是如何編譯出 mm/memblock.o 的了。

按照同樣的方法在 scripts/Makefile.build 文件中找 .o 的目標。找啊找啊找,終於看到一條像的了。

# Built-in and composite module parts
$(obj)/%.o: $(src)/%.c $(recordmcount_source) $(objtool_obj) FORCE
    $(call cmd,force_checksrc)
    $(call if_changed_rule,cc_o_c)

嗯,究竟是不是這條規則? 還記得我們的丑辦法么?留給大家自己試驗一次~
依賴條件我們暫時也不看了,不是關注我們的目標-- .o 文件是怎麼編譯出來的。看這個規則中的兩條命令,第一條是做代碼檢查的,那暫時也不關注吧。第二條看著名字就有點像, cc_o_c ,把 c 代碼通過 cc 製作成 o 文件。這個正是我們要關注的,就分析它了。

接近真相

上一節,我們把關注的焦點定在了這條語句。

    $(call if_changed_rule,cc_o_c)

是不是看著有點丈二和尚摸不少頭腦?先別著急,我們還是一點點來分析。

call 的用法在上一篇中也已經解釋了,其實就是調用 if_changed_rull 這個變數。既然如此,那我們就來找找這個變數唄~

再去我們的頭文件 scripts/Kbuild.include 中找找看。是不是踏破鐵鞋無覓處,得來全不費功夫。

# Usage: $(call if_changed_rule,foo)
# Will check if $(cmd_foo) or any of the prerequisites changed,
# and if so will execute $(rule_foo).
if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ),      \
    @set -e;                                                      \
    $(rule_$(1)), @:)

嗯,是不是千萬隻草泥馬又從心中奔騰而過了?剛才還覺得一切易如反掌,瞬間就變成了一頭霧水。其實我的內心也是崩潰的,不過沒辦法,這文章都寫了一大半了,總不能在這個時候停掉。自己挖的坑,硬著頭皮也得把它填上了。

靜下心來一看,這其實就是一個 if 語句。當條件為真,執行逗號之前的動作。當條件為假,則執行後面那個 @: 。然後你再對照一下注釋,誒,還真是這麼個理兒。關於後面這個 @: ,我也不是很確認是個神馬東西。然後查了一下 git 記錄,發現就是為了減少一些討厭的輸出消息的。具體說明可以看這個 kbuild: suppress annoying "... is up to date." message 。而後進一步發現 shell 中還有一個啥都不幹的冒號語句

感覺又有了一點點的小進步,是不是還是挺開心的。
這下我們把注意集中在 rule_$(1) ,仍然暫時先不看前置條件。剛才我們說了 if_changed_rule 是一個函數,傳入的參數剛是 cc_o_c 。你看這時候正好用上,在這裡展開後就是 rule_cc_o_c 。
這裡有點像回調函數的感覺, if_changed_rule 負責判斷有沒有前置條件是新的,是否需要重新生成目標。如果需要, if_changed_rule 就會調用所要求的函數再去生成目標。
這一路走來真是不容易。給自己倒杯咖啡,聽聽音樂,歇一歇。馬上就要看到最後的結果了。

雲開日出

經過對 if_changed_rule 的分析,我們停在了 rule_cc_o_c 上。現在的任務就是找到它。

這次比較簡單,定義就在 scripts/Makefile.build 中。

define rule_cc_o_c
    $(call echo-cmd,checksrc) $(cmd_checksrc)     \
    $(call cmd_and_fixdep,cc_o_c)                 \
    $(cmd_modversions_c)                          \
    $(cmd_objtool)                                \
    $(call echo-cmd,record_mcount) $(cmd_record_mcount)
endef

又是一長串是不是? 不過你有沒有發現熟悉的 cc_o_c 的身影?而且也是作為一個自定義的函數的參數?那我們來猜一下,這個 cmd_and_fixdep 函數也會在某個地方調用參數所指定的一個函數來完成生成目標的動作。
那它在哪呢? 對了,和 if_changed_rule 一樣,也是在 scripts/Kbuild.include 文件中。

cmd_and_fixdep =                          \
    $(echo-cmd) $(cmd_$(1));              \
    ...

為了不鬧心,我就複製一下最關鍵的部分吧。你看,還是調用到了我們的參數。這次展開後就是 cmd_cc_o_c 了。它還是在 scripts/Makefile.build 文件中。

cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<

好了,可以鬆一口氣了。如果用過 makefile 的童鞋,看到這個是不是覺得很親切?這就是我們平時手動編譯的時候輸入的命令。

At last, you got it.

整頓行囊

剛才在探索如何編譯單個 .o 文件的道路上一路飛奔,雖然終於我們找到了那條最最最後的語句在哪裡,但是估計我們自己的行囊丟了一地。中間打下的江山可能自己也記不太清楚了。
為了更好的理解 kbuild ,在下一次我們出發探索其他複雜的目標前,我們有必要在此總結這次探索的收穫,就好像千里行軍總得找個好地方,安營紮寨,整頓行囊,這樣才能走得更遠,更穩。

執行順序

從文件執行順序上整理如下。

    Makefile
    ---------------
    %.o: %.c
        make -f scripts/Makefile.build obj=mm mm/memblock.o

    scripts/Makefile.build
    ---------------
    $(obj)/%.o: $(src)/%.c
        $(call if_changed_rule,cc_o_c)

    scripts/Makefile.build
    ---------------
    rule_cc_o_c
        $(call cmd_and_fixdep,cc_o_c)

    scripts/Makefile.build
    ---------------
    cmd_cc_o_c
        $(CC) $(c_flags) -c -o $@ $<

這麼看或許可以更清楚一些。對單個 .o 文件的編譯,一共是四個步驟。在我們平時寫的 makefile 中,可能到第二步就可以直接寫上 cmd_cc_o_c 的命令了。而在內核中,又多出了兩步為了檢查有沒有依賴關係的更新及其他的一些工作。
在探索的過程中或許會有總也沒有盡頭的感覺,而現在整理完一看,一共也就四個層次,是不是覺得好像也沒有那麼難了?是不是覺得不過如此了?

scripts/Makefile.build

這個文件幾乎包含了所有重要的規則, rule_cc_o_c、 cmd_cc_o_c 都在這個文件中定義。以後我們會看到,凡事要進行編譯工作,都會使用這個規則文件。

scripts/Kbuild.include

這個文件包含了一些有意思的函數, if_changed_rule, cmd_and_fixdep 。而且這個文件被根目錄 Makefile 和 scripts/Makefile.build 都包含使用。
好了,這下真的要歇一歇了。吃個大餐慰勞一下自己吧~


本文是 LinuxStory 柴米油鹽計劃的投稿文章,由 LinuxStory 整理髮布。
原作者署名為:Wei Yang 。
轉載請註明出處,否則必究相關責任。
本文鏈接:https://linuxstory.org/kbuild-linux-6/

對這篇文章感覺如何?

太棒了
1
不錯
0
愛死了
0
不太好
0
感覺很糟
0
這裡是柴米油鹽計劃投稿的發布賬號。

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據