gnitnaw
twlkh
這章的重點在bootloader之後,也就是kernel開始接手系統後會發生的事。寫心得的同時我會儘量使用RPi kernel(v.4.4.31)驗證本書的內容。如果有興趣補充的請自便(為方便編輯,加入comment後請留下大名)。
如果從kernel編譯過程來看vmlinux是如何組成的話:
這裡列出比較重要的幾個:
檔案 | 功能 | RPi source |
---|---|---|
vmlinux |
vmlinux 是ELF格式binary檔案,為最原始也未壓縮的kernel鏡像。 |
./vmlinux |
System.map |
在符號名稱與它們的記憶體位置間的查詢表格 | ./System.map |
Image |
vmlinux 經過objcopy 處理,把代碼從中抽出(去除註解或debugging symbols)以用於形成可執行的機器碼。不過Image 此時還不能直接執行,需加入metadata資訊。 |
./arch/arm/boot/Image |
head.o |
ARM特有的code,用來接受從bootloader送來的系統控制權,source code head.S 是用組語(arm-assembly)寫成。 |
./arch/arm/boot/compressed/head.S |
piggy.gzip |
被gzip壓縮的Image | ./arch/arm/boot/compressed/piggy.gzip |
piggy.gzip.o |
用組語寫成,可被用來連結到別的物件,例如piggy.gzip 。 |
./arch/arm/boot/compressed/piggy.gzip.S |
misc.o |
用來解壓縮。 | ./arch/arm/boot/compressed/misc.c |
compressed/vmlinux |
結合System.map 等檔案並產生鏡像檔,意義跟一開始的vmlinux 不太一樣。 |
./arch/arm/boot/compressed/vmlinux |
zImage |
最後產生的鏡像檔,已經被壓縮過。 | ./arch/arm/boot/zImage |
以下是我實際用RPi驗證的結果(make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage V=1
)。
vmlinux被objcopy包到Image:
Image
被gzip
壓縮成piggy.gzip
,這是說如果gzip
不成功就砍掉piggy.gzip
?
然後piggy.gzip.S
被生成(不過我看不懂這個步驟)。
這個piggy.gzip.S
檔案內容如下,內含剛剛提到的piggy.gzip
。
input_data
和input_data_end
這兩個label是用來當boundary的(介於這兩個label的才是要載入的binary檔案)。大概可以知道,這個piggy.gzip.S
的功能就是把piggy.gzip
提供給"kernel鏡像載入前"的啟動程序(其命名piggy帶有揹負之意)。
書中有一段我不太懂:
It is triggered by the .incbin assembler preprocessor directive, which can be viewed as the assembler's version of a #include file. In summary, the net result of this assembly language file is to contain the compressed binary kernel image as a payload within another imagethe bootstrap loader.
.incbin 是把 binary file 變成程式的 data 的一種方法(assembly directive)。舉例來說用這方法你可以把一個圖檔內容變成程式的data array。在這邊的用法就是把壓縮完的 kernel 變成第二階段的 vmlinux 裡的一個 data(可透過變數 input_data 來讀取)。
如果bootloader是前半段,那bootstrap loader就是後半段,其作用主要在於:
bootloader與bootstrap loader的不同在於:前者在電源被打開始開始啟動,並不依賴kernel;後者則處於bootloader結束與kernel介入之前的過渡期。我們可以把bootstrap loader的流程概括如下:
head.o
(與相關檔案)接住了控制權,然後針對不同的處理器開始了其初始化流程(打開特殊指令或cache、關掉中斷等)。misc.o
根據piggy.gzip.S
找到並解壓縮Image。我看了一下我的RPi的dmesg,並沒有如書中提到有decompress_kernel的相關訊息(dmesg的第1條訊息是從start_kernel
來的,就算開了initcall_debug
也一樣),不過原始檔misc.c
裏面的確有這個function
不過我覺得奇怪,bootstrap loader的過程怎麼會顯示在dmesg裡,如果bootstrap真的是介於bootloader跟kernel介入之間的過渡期的話,那它怎麼有機會管到pr_info
那去?kernel應該還沒有機會初始關於pr_info
的設定不是嗎?
然後dmesg裏面的確有kernel version string,這行是對應到start_kernel
中的pr_notice("%s", linux_banner)
:
這邊的#1
是Build number,每重新compile一次就會被./scripts/mkversion
增加1,可用make mrproper
使之重新回到1。
但是我在原始檔的init/main.c
裏面的start_kernel裡並沒有看到相關部份:
// scripts/mkcompile_h
Linux啟動流程大概是這樣:
當head.o從bootstrap loader
拿到控制權,kernel是處於真實模式(real mode),可以直接讀取physical memory。RPi的head.o
內容可以在./arch/arm/kernel/head.S
找到,以後可以根據此檔案研究一下低階的ARM操作。此module主要功能為:
start_kernel()
。以下是我在head-common.S
(被include進head.S
)找到的相關片段:由於真實模式可被定址的記憶體只有1MB,所以head.o
能做的事不多,如果不是非常必要,最好不要嘗試去動這個步驟,不然容易因為page fault
發生意想不到的問題,真的要更改的話請盡可能在MMU啟動之後進行,難度會降低很多。
./init/main.c
的主要函式為start_kernel()
,這個函式會呼叫setup_arch(&command_line)函式(定義在./arch/arm/kernel/setup.c
)來設定整個架構,包括辨別CPU並將其進階功能初始化。此函式的頭兩行:
這個setup_processor()
是用來確認CPU ID而且顯示其資訊。dmesg
可看到:
setup_arch
最後會做一些machine-dependent的初始化,至於會怎麼做不同結構有不同作法。ARM的話它會指到./arch/arm/mach-*
去抓該結構對應的檔案(使用第4章說的.config去設定Makefile),MIPS的作法也類似,Power Architecture則會放在platforms
資料夾。
一樣,RPi的dmesg
bootloader
把控制權交給bootstrap loader
時會附送已經規範好的kernel命令列 (kernel command line)。如果你是用grub
或lilo
等多重開機程式(其實他們也是bootloader
),可以用鍵盤選擇作業系統,這就相當於用kernel命令列去改變boot參數。./Documentation/kernel-parameters.txt
中有註明已經定義且可用的參數(不過需注意因為kernel更新太快Documentation的資訊會跟不上),開發者也可自行定義參數。
在RPi,/boot/cmdline.txt
可用來輸入kernel command line,例如:dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
為了要讓kernel message早點生效(這樣有問題才知道),console initialization必須愈早做愈好。此物件為printk.o
,原始檔在./kernel/printk/printk.c
,其中的函式console_setup(char *str)
便是console initialization。以下為console_setup(char *str)
的原始碼,可以看到最後__setup("console=", console_setup)
把console
定義成buf
。
這個__setup
的Macro是定義在./include/linux/init.h
:
所以__setup("console=", console_setup)
會被前置處理為__setup_param("console=", console_setup, console_setup, 0)
,然後變成
我看不懂__aligned(1)
和__used
的作用。
__attribute__ ((__used__))
__attribute__
來設置屬性。如以下連結介紹defined but not used
or set but not used
),但在某些情況下這是我們預期的行為時,可設置 __used
這個屬性告知編譯器,希望保留變數不被最佳化移除,設置後也不會吐出這個 compiling warning (或是 error)了。如以下連結的討論第2行去掉對齊設定就變成
所以這兩行code做的就是:
__setup_str_console_setup[]
的區域字串常數為"console"
;struct obs_kernel_param
的靜態變數__setup_console_setup
,裏面含有三個元件:剛剛創的字串常數__setup_str_console_setup[]
,一個指向setup function的函數指標(此例來說就是console_setup
),一個flag。.init.setup
部位。以下是從./init/main.c
摘錄出如何處理struct obs_kernel_param
物件的函式,我以註解的方式說明我對這code的理解。
在./include/linux/init.h
定義的有關init
的macro有:
含有這些macro的變數(__init.data
)或函式(__init
)作kernel做完這個module_init function過後就會釋放其佔有的資源,也就是說在boot步驟結束前就會被清除,可以從dmesg
中看到相關訊息。
obsolete_
開頭的函式是屬於module_param*
macro(參見./include/linux/moduleparam.h
),見Ch8。
在./init/main.c
中,要初始化子系統:
timer
或console
已經被main.c預設初始化。__setup
。./arch/arm/kernel/setup.c
的customize_machine(void)
。上一節說的arch_initcall
一樣是定義在./include/linux/init.h
:
所以arch_initcall(customize_machine)
經過首次預處理後會變成__define_initcall(customize_machine, 3)
,再次預處理然後去掉__section(.initcall3.init)
(這表示相關變數會放在此section)相關部份就變成:
書中是說會變成static initcall_t __initcall_customize_machine = customize_machine
,跟我判斷的不太一樣耶。
要注意的是.initcallN.init
這部份,這個N
代表初始化的順序(level),在__define_initcall
的程式碼中可發現,arch_initcall
是排在core_initcall
等後面。
不管是__setup
還是__initcall
,這些macro是提供一個可以初始子系統,並且在初始完成後釋放占用的記憶體的機制。
__initcall
macro是在kernel 2.6之後才開始加入,至於之前是使用device_initcall
macro (level=6)。
./init/main.c
中的start_kernel
函式跑完基本的初始化程序後,函式最後就會呼叫rest_init()
函式產生第1個thread,就是init()
。由於它是第1個process,所以它的PID=1
。這個process是所有user space process
的老爸,換言之,所有的user space process
皆是由init()
用fork()
分出來的。以下是rest_init()
的內容:
kernel_init
thread被建立了以後,此函式先呼叫的是kernel_init_freeable()
函式,然後kernel_init_freeable()
中間會呼叫do_basic_setup
,do_basic_setup
中間又會呼叫do_initcalls()
,這個函式就是用來處理之前提到的__initcall
的,會依序處理在不同.initcallN.init
的參數。
RPi可以在/boot/cmdline.txt
內加入initcall_debug=1
,把debug info.打開,重新開機後用dmesg
就能看了。
書中所說的這部份init_post()
在已經被併到kernel_init
最後面了,內容如下:
對照一下RPi的ps -ef
:
可以知道,kernel_init
執行到/sbin/init
後(link到/lib/systemd/systemd
)就一直run沒有停下來。當系統必須做其他的事(例如某user要執行程式)時,此process(或某個子process)就會fork()出一個分身去處理,證據如下:
bootloader
(儲存在ROM裡):讀取SD並載入第2階段的bootloader (/boot/bootcode.bin
)到L2 cache。bootcode.bin
將SDRAM啟動並繼續第3階段的bootloader:讀取GPU firmware(/boot/start.elf
)。start.elf
讀取config.txt
,cmdline.txt
和kernel.img
(即zImage),以啟動bootstrap Loader。