內核中單個.o文件的編譯過程

逐步地我們已經對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都包含使用。

好了,這下真的要歇一歇了。喫個大餐慰勞一下自己吧~

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