1. 程式人生 > >全面解析Linux 核心 3.10.x

全面解析Linux 核心 3.10.x

一切手工技藝,皆由口傳心授 - 夏奈爾首席鞋匠

傳授手藝的同時,也傳遞了耐心、專注、堅持的精神,這是一切手工匠人所必須具備的特質。

貌似是從2.6開始,核心編譯就開始採用Kbuild體系!
Kbuild幾點觀念:
1.一個配置檔案對應一個自動包含的子目錄樹!
2.目標配置檔案模板是簡化Makefile的主要機制!
3.工具和SDK使得模板具有靈活性!
4.子Makefiles來實現非遞迴Makefile方法!
在編譯核心的時候會讀取兩次Makefile,先讀取頂層Makfile,讀到之後獲取到Kbuild的子Makefile來先編譯子Makefile.
核心中的Kbuild體系用到Makefile的5個部分!


a.頂層Makefile
核心頂層Makefile 位於核心原始碼的頂層目錄,它主要用於指定編譯核心目標檔案(vmlinux)模組(modules).選擇編譯核心或者模組,這個檔案會被首先讀取,並根據讀到的內容配置編譯環境變數.對於核心或驅動開發人員來說,這個檔案幾乎不用任何修改.
b.config
核心的配置檔案,當配置完menuconfig以後,就會在主目錄下生成一個.config檔案,此檔案一般Demo板都會提供一個參考的condfig(放在arcg/$(ARCH)/configs/),如我下面要使用的是(arch/mips/configs/nlm_xlp_config).
可以直接複製過來,*cp arch/mips/configs/nlm_xlp_config .config*

後續根據自己的需要可以對此檔案增刪修改!
c.arch/$(ARCH)/Makefile
具體體系架構下的頂層Make0file,位於ARCH/$(ARCH)/Makefile,是系統對應平臺的Makefile.
核心頂層Makefile會包含這個檔案來指定平臺相關資訊.這個就確定你的平臺資訊!
d.Kbuild 子 Makefiles
核心原始碼中大約有成百上千個這樣的檔案,一般是每個目錄一個Makefile,同它對應的有一個
Kconfig檔案,其內容是一些預設的編譯選項.每一個子目錄都有一個Kbuild Makefile 檔案,用
來執行從其上層目錄傳遞下來的命令.**注意****Kbuild Makefile 並不直接被當做Makefile 執行,而是從.config 檔案中提取資訊,生成Kbuild完成核心編譯所需的檔案列表.
e.scripts/Makefile.*

Kbuild使用到通用的規則等,面向所有的Kbuild Makefiles,包含了所有的定義、規則等!

編譯之處,很多初學者或者一般都總是會記得那麼幾個步驟:
make config/defconfig/menuconfig/xconfig 等
make && make modules_install && make install

真正去將每一個步驟細細研究的可能很少!
那麼我們來分解一下上述幾個步驟:
1.make menuconfig
一般情況下我們都使用次選項,基本上都工作在字元介面並。編譯配置,怎麼個配置法?
這裡值得一提的是,3.10.x版本menuconfig 介面已經變化了,
在上面的內容中我們大概介紹了下Kbuild檔案,首先我們檢視頂層Makefile的內容,看是否有menuconfig這個target呢?
尋找一圈並沒有發現menuconfig這個目標!為什麼呢? 不過的碼海中發現下面這幾句!
1.make *config

ifeq ("$(origin V)", "command line")           //make V=1 ...
  KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
  KBUILD_VERBOSE = 0
endif
srctree     := $(if $(KBUILD_SRC),$(KBUILD_SRC),$(CURDIR))
ifeq ($(KBUILD_VERBOSE),1)
    quiet =
    Q =
else
    quiet=quiet_
    Q = @
endif
# ===========================================================================
# *config targets only - make sure prerequisites are updated, and descend
# in scripts/kconfig to make the *config target

# Read arch specific Makefile to set KBUILD_DEFCONFIG as needed.
# KBUILD_DEFCONFIG may point out an alternative default configuration
# used for 'make defconfig'
include $(srctree)/arch/$(SRCARCH)/Makefile
export KBUILD_DEFCONFIG KBUILD_KCONFIG

config: scripts_basic outputmakefile FORCE
    $(Q)mkdir -p include/linux include/config
    $(Q)$(MAKE) $(build)=scripts/kconfig [email protected]

%config: scripts_basic outputmakefile FORCE
    $(Q)mkdir -p include/linux include/config
    $(Q)$(MAKE) $(build)=scripts/kconfig [email protected]

在上述的友好提示以及程式碼的明示下,我們大概已經明白!執行make defconfig後首先根本得到的SARCH(這個巨集其實就是指定的ARCH,此巨集可以手動指定,也可以通過make 傳參,如 make ARCH=mips CROSS_COMPILE=mips64-xxx-gcc,指定架構以及交叉鏈子),SARCH的值知道以啦,根據前面的知識,[email protected]表示目標檔案的完整名稱,%表示萬用字元,好,程式碼變變變:

include $(CURDIR)/arch/mips/Makefile
export KBUILD_DEFCONFIG KBUILD_KCONFIG

config: scripts_basic outputmakefile FORCE
    $(@) mkdir -p include/linux include/config   //Q = @
    $(@)  $(MAKE) $(build)=scripts/kconfig config

%config: scripts_basic outputmakefile FORCE
    $(@) mkdir -p include/linux include/config
    $(@)$(MAKE) $(build)=scripts/kconfig *config
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:
    $(Q)$(MAKE) $(build)=scripts/basic
    $(Q)rm -f .tmp_quiet_recordmcount\

上述target 翻譯過來就是編譯 scipt/basic/Makefile,此處就是編譯fixdep(其實是一個修復平臺包依賴關係的軟體),相信很多人以前編譯核心的時候都需要安裝一個叫build-essential的包,要不然就會出現以下錯誤
*make[1]: [scripts/basic/fixdep] Error 1****
而build-essential 的解釋是:Informational list of build-essential packages…so .are you ok?

# outputmakefile generates a Makefile in the output directory, if using a
# separate output directory. This allows convenient use of make in the
# output directory.
outputmakefile:
ifneq ($(KBUILD_SRC),)
    $(Q)ln -fsn $(srctree) source
    $(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile \
        $(srctree) $(objtree) $(VERSION) $(PATCHLEVEL)
endif

在看mkmakefile 同樣是scripts 下的檔案,不過此檔案就不叫編譯.
CONFIG_SHELL是神馬呢?看下面:

# SHELL used by kbuild
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
      else if [ -x /bin/bash ]; then echo /bin/bash; \
      else echo sh; fi ; fi)

上述程式碼號高大上啊,其實就是echo $$BASH (此處的意識是表示BASH的完整路徑,系統變數配置的) or(木有找到環境變數就只能簡單粗暴了) echo /bin/bash ..

ifeq ($(mixed-targets),1)
# ===========================================================================
# We're called with mixed targets (*config and build targets).
# Handle them one by one.

%:: FORCE
    $(Q)$(MAKE) -C $(srctree) KBUILD_SRC= [email protected]

上述程式碼中KBUILD_SRC 其實就arch/xxx ..
小技巧:
你可以執行make arch/mips 瞅瞅
遇到你任務想要make 的東東,就揍大膽的去試試吧
2.make menuconfig字元介面的實現
下面程式碼簡單的描述了menuconfig如如果將子目錄拉起來變成一個視覺化的字元介面的!

# Handle descending into subdirectories listed in $(vmlinux-dirs)
# Preset locale variables to speed up the build process. Limit locale
# tweaks to this spot to avoid wrong language settings when running
# make menuconfig etc.
# Error messages still appears in the original language

PHONY += $(vmlinux-dirs)
$(vmlinux-dirs): prepare scripts
    $(Q)$(MAKE) $(build)[email protected]

# Store (new) KERNELRELASE string in include/config/kernel.release
include/config/kernel.release: include/config/auto.conf FORCE
    $(Q)rm -f [email protected]
    $(Q)echo "$(KERNELVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))" > [email protected]

上述程式碼中其實最重要的就是vmlinux-dirs 目標,它描述了每個子目錄
vmlinux-dirs := (patsubst(filter %/, (inity)(init-m) \
(corey)(core-m) (driversy)(drivers-m) \
(nety)(net-m) (libsy)(libs-m)))
之後就是Kconfig 找Kconfig了!
每一個Kconfig檔案都對對應一個子目錄檔案的描述,Kconfig的三個選項* M []分別表示編譯到核心,編譯為模組,不編譯!
三個選項影響同級目錄Makefie檔案中的obj-y obj-$(CONFIG_XX_XX) 不編譯!
說到這裡,大概基本上把我的幾個疑點都搞清楚了。
3.手動修改ARCH
cp arch/mips/configs/xx_deconifg .config

# CROSS_COMPILE specify the prefix used for all executables used
# during compilation. Only gcc and related bin-utils executables
# are prefixed with $(CROSS_COMPILE).
# CROSS_COMPILE can be set on the command line
# make CROSS_COMPILE=ia64-linux-
# Alternatively CROSS_COMPILE can be set in the environment.
# A third alternative is to store a setting in .config so that plain
# "make" in the configured kernel build directory always uses that.
# Default value for CROSS_COMPILE is not to prefix executables
# Note: Some architectures assign CROSS_COMPILE in their arch/*/Makefile
ARCH        ?= $(SUBARCH)
CROSS_COMPILE   ?= $(CONFIG_CROSS_COMPILE:"%"=%)  

這裡預設的SUBARCH 一般指的是你主機的環境架構!CROSS_COMPILE也是!
那麼我手動將這個地方改一下…

ARCH        ?= mips
CROSS_COMPILE   ?= mips64-xx-linux-      

Ps.這裡我暫時不將交叉鏈子寫出來!一般的鏈子命名都是arch-廠商-linux-.
總結:
其實你想要的東西都已經在你的面前,相信自己,你可以!其他的細節請詳細參考Makefile,世上無難事,只怕就心人!
最後說一句,百度這種東西做為新手,或者搜搜生活還可以,至於你都已經看核心了嘛。還是Google 或者直接就是Source Codinng..
做為一個技術人,你漸漸的應該選擇不去使用這種東西,除了多讀書外,多讀原始碼,多讀原始碼,多讀原始碼!重要的事情說三遍!

上面大抵說明了一件事情那就是怎麼編譯!到這裡其實就可以make 了,孤孤單單的一個command.你可以make ||make ARCH=xx CROSS_COMPILE=xxxxx || make -j n || make V=n || make dir/ make dir/xx.i 等等等等!
一個 make 可以變著花樣玩..
我關心的不是make本身 ,而是make 核心的過程..
從make 開始 — 到生成映象檔案..此過程我是非常感興趣,那麼我們就去研究吧啊..哈哈!
首先基本的編譯都是要經過以下幾個階段

核心編譯流程大抵也如此:
預編譯 – *.i <這部分也一般會被省略,但是可以作為除錯的手段來使用>
編譯 – *.s <這部一般會被省略,這部分可能用處並不是很大,因為如果要看彙編除錯的話,使用除錯工具會事半功倍,如kdb,kgdb等>
彙編 – *.o
子目錄小連結 – built-in.o
總連結 – vmlinux

上面我們將ARCH以及CROSS修改完畢以後,就可以執行第一步!
make menuconfig 儲存退出這就是將.config 配置儲存!
然後我們 make menuconfig V=1看看都幹了些什麼,和我們上述分析的是否有差別.
有幾個小地方我這裡暫時不做解釋,到後面我們在來解釋。
留點懸念核心小工具番外篇 - 核心中script的幾個作用

到這一步其實大抵都明白,不就單獨編譯每個被選中的.c麼,然後生成 -o 麼!是的,這裡就坐了這樣的事情,但是你知道這麼多目錄順序是什麼嗎?
這裡我就強調一下編譯的目錄..
讓我們靜靜的在看一段Makefile 中的片段:

# ===========================================================================
# Build targets only - this includes vmlinux, arch specific targets, clean
# targets and others. In general all targets except *config targets.

ifeq ($(KBUILD_EXTMOD),)
# Additional helpers built in scripts/
# Carefully list dependencies so we do not try to build scripts twice
# in parallel
PHONY += scripts
scripts: scripts_basic include/config/auto.conf include/config/tristate.conf \
     asm-generic
    $(Q)$(MAKE) $(build)=$(@)

# Objects we will link into vmlinux / subdirs we need to visit
init-y      := init/
drivers-y   := drivers/ sound/ firmware/
net-y       := net/
libs-y      := lib/
core-y      := usr/

ifeq ($(KBUILD_EXTMOD),)
core-y      += kernel/ mm/ fs/ ipc/ security/ crypto/ block/

init-y      := $(patsubst %/, %/built-in.o, $(init-y))
core-y      := $(patsubst %/, %/built-in.o, $(core-y))
drivers-y   := $(patsubst %/, %/built-in.o, $(drivers-y))
net-y       := $(patsubst %/, %/built-in.o, $(net-y))
libs-y1     := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2     := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y      := $(libs-y1) $(libs-y2)

so…
編譯順序>
init - usr – arch/mips — kernel —- mm —– fs —— ipc ——- security ——– crypto ——— block ———- drivers ———– sound ———— firmware ————- net ————- lib
觀光完畢…
Ps. 如果不信的話,自己去看時間戳.. ls –full-time 可以檢視時間戳哦,精確到毫秒!
Ps1.其實編譯順序不一定非的按照如此,你使用mae -j n 的時候就不是如此順序,具體自己可去檢視,順便思考一下!

在a中我們看到了編譯每個檔案匯聚成N多個.o檔案,然後.o 檔案又被第二次匯聚成built-in.o檔案!
built-in.o 檔案是怎麼生成的呢?
我們以頂層目錄的usr為例子(因為只有一個.c檔案麼!)

  gcc -Wp,-MD,usr/.gen_init_cpio.d -Wall -Wmissing-prototypes -Wstrict-prototypes -O2 -fomit-frame-pointer -std=gnu89     -o usr/gen_init_cpio usr/gen_init_cpio.c
/bin/bash /home/dev/share/hewen/kernel/linux-3.10.92/scripts/gen_initramfs_list.sh -l -d > usr/.initramfs_data.cpio.d
  /bin/bash /home/dev/share/hewen/kernel/linux-3.10.92/scripts/gen_initramfs_list.sh -o usr/initramfs_data.cpio   -d
  mips64-nlm-linux-gcc -Wp,-MD,usr/.initramfs_data.o.d  -nostdinc -isystem /opt/Mips_Cross/toolchains_bin/mipscross/linux/bin/../lib/gcc/mips64-nlm-linux/4.6.1/include -I/home/dev/share/hewen/kernel/linux-3.10.92/arch/mips/include -Iarch/mips/include/generated  -Iinclude -I/home/dev/share/hewen/kernel/linux-3.10.92/arch/mips/include/uapi -Iarch/mips/include/generated/uapi -I/home/dev/share/hewen/kernel/linux-3.10.92/include/uapi -Iinclude/generated/uapi -include /home/dev/share/hewen/kernel/linux-3.10.92/include/linux/kconfig.h -D__KERNEL__ -DVMLINUX_LOAD_ADDRESS=0xffffffff80100000 -DDATAOFFSET=0  -D__ASSEMBLY__  -mno-check-zero-division -mabi=64 -G 0 -mno-abicalls -fno-pic -pipe -msoft-float -ffreestanding  -I/home/dev/share/hewen/kernel/linux-3.10.92/arch/mips/include/asm/mach-netlogic -I/home/dev/share/hewen/kernel/linux-3.10.92/arch/mips/include/asm/netlogic -march=xlp -I/home/dev/share/hewen/kernel/linux-3.10.92/arch/mips/include/asm/mach-generic -msym32 -DKBUILD_64BIT_SYM32 -gdwarf-2      -DINITRAMFS_IMAGE="usr/initramfs_data.cpio"   -c -o usr/initramfs_data.o usr/initramfs_data.S
   mips64-xxx-linux-ld  -m elf64btsmip   -r -o usr/built-in.o usr/initramfs_data.o

好吧,正如你所知道的,我使用了make usr V=1
重點在最後一行,mips64-xxx-linux-ld -m elf64btsmip -r -o usr/built-in.o usr/initramfs_data.o
發現了嘛?
四個引數..一一說明:
-m Set emulation
elf64btsmip 模擬型別
-r Generate relocatable output
-o Set output file name
簡單的表示就是將*.o 檔案轉變為指定模擬模式的built-in.o檔案!

有了built-in.o以後,就可以進行連結了!
連結指導檔案是arch/mips/kernel/vmlmux.lds.S
vmlinux.lds.S 在編譯的過程中被處理為vmlinux.lds
vmlinux.lds.S 中將vmlinux的Section 進行了詳細的劃分!

使用readelf -S vmlinux 可以檢視所有的section,下面列出一部分section

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         ffffffff80100000  00004000
       000000000053a058  0000000000000000  AX       0     0     32
  [ 2] __ex_table        PROGBITS         ffffffff8063a060  0053e060
       0000000000006870  0000000000000000   A       0     0     8
  [ 3] .notes            NOTE             ffffffff806408d0  005448d0
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .rodata           PROGBITS         ffffffff80641000  00545000
       000000000019d878  0000000000000000   A       0     0     256
  [ 5] .pci_fixup        PROGBITS         ffffffff807de878  006e2878
       0000000000001998  0000000000000000   A       0     0     8
  [ 6] __ksymtab         PROGBITS         ffffffff807e0210  006e4210
       000000000000c9d0  0000000000000000   A       0     0     8
  [ 7] __ksymtab_gpl     PROGBITS         ffffffff807ecbe0  006f0be0
       0000000000006a20  0000000000000000   A       0     0     8
  [ 8] __kcrctab         PROGBITS         ffffffff807f3600  006f7600
       00000000000064e8  0000000000000000   A       0     0     8
  [ 9] __kcrctab_gpl     PROGBITS         ffffffff807f9ae8  006fdae8
       0000000000003510  0000000000000000   A       0     0     8
  [10] __ksymtab_strings PROGBITS         ffffffff807fcff8  00700ff8
       0000000000015e3e  0000000000000000   A       0     0     1
  [11] __param           PROGBITS         ffffffff80812e38  00716e38
       0000000000000fa0  0000000000000000   A       0     0     8
  [12] __modver          PROGBITS         ffffffff80813dd8  00717dd8

為什麼要說section 呢?
其實section 就是核心比較重要的全域性符號資訊!每個section都有Address + Offset.
注意有的section地址為空,基本都是一些除錯符號資訊! 關於section 這部分其實可以和反彙編等知識組成非常複雜的高階技術!

[MIPS的入口地址]

你只需要知道是MIPS預設地址是0xBFC00000,此地址在無快取的KSEG1的地址區域內,對應的實體地址是0x1FC00000!具體參考體系架構番外篇 - MIPS基本地址空間,即CPU從0x1FC00000開始取第一條指令,這個地址在硬體上已經確定為FLASH的位置,boot將vmlinux(核心映象) 拷貝到RAM中某個空閒地址處,然後一般有個記憶體移動操作,目的地址已經指定:
arch/mips/Makefile

#
# Automatically detect the build format. By default we choose
# the elf format according to the load address.
# We can always force a build with a 64-bits symbol format by
# passing 'KBUILD_SYM32=no' option to the make's command line.
#
ifdef CONFIG_64BIT
  ifndef KBUILD_SYM32
    ifeq ($(shell expr $(load-y) \< 0xffffffff80000000), 0)
      KBUILD_SYM32 = y
    endif
  endif

  ifeq ($(KBUILD_SYM32)$(call cc-option-yn,-msym32), yy)
    cflags-y += -msym32 -DKBUILD_64BIT_SYM32
  else
    ifeq ($(CONFIG_CPU_DADDI_WORKAROUNDS), y)
      $(error CONFIG_CPU_DADDI_WORKAROUNDS unsupported without -msym32)
    endif
  endif
endif

vmlinux.lds

OUTPUT_ARCH(mips)
ENTRY(kernel_entry)   #Ps....
PHDRS {
 text PT_LOAD FLAGS(7); /* RWX */
 note PT_NOTE FLAGS(4); /* R__ */
}
 jiffies = jiffies_64;
SECTIONS
{
 . = 0xffffffff80100000;
 /* read-only */
 _text = .; /* Text and read-only data */
 .text : {
  . = ALIGN(8); *(.text.hot) *(.text) *(.ref.text) *(.devinit.text) *(.devexit.text) *(.text.unlikely)
  . = ALIGN(8); __sched_text_start = .; *(.sched.text) __sched_text_end = .;
  . = ALIGN(8); __lock_text_start = .; *(.spinlock.text) __lock_text_end = .;
  . = ALIGN(8); __kprobes_text_start = .; *(.kprobes.text) __kprobes_text_end = .;

最終地址會被編譯寫入到vmlinux.lds,此檔案最終會以引數 -Xlinker –script -Xlinker vmlinux.lds的形式傳給mips64-xxx-linux-gcc,最終會被連結器mips64-xx-linux-ld來進行操作。mips64-xx-linux-ld會將 .text section的地址連結到 0xFFFFFFFF80100000!見上述section描述!boot會將核心移到實體地址0x00100000處。
核心ELF檔案的入口地址(Entry point),即 boot搬移完核心後,直接跳轉到的地址,由mips64-xx-linux-ld寫入ELF的頭中。使用mips-xxx-linux-readeld -h vmlinux 可檢視header資訊:

  Class:                             ELF64
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           MIPS R3000
  Version:                           0x1
  Entry point address:               0xffffffff8062c7f0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          79212296 (bytes into file)
  Flags:                             0x808e0001, noreorder, xlp, mips64r2
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         2
  Size of section headers:           64 (bytes)
  Number of section headers:         33
  Section header string table index: 30  

之後依次用下面的方法嘗試設定入口點,一直到成功時候才停止!

1. 命令列選項 -e entry
2. 指令碼中的 ENTRY(symbol)
3. 如果有定義 start 符號,則使用start符號(symbol)
4. 如果存在 .text 節,則使用第一個位元組的地址。
5. 地址0

如當前核心使用的就是ENTRY(symbol),在連結vmlinux.lds的時候設定了核心的entry,ENTRY(kernel_entry)!