Linux內核啓動分析

作者:奮鬥的白楊

注:原創作品,轉載請註明出處

《Linux內核分析》 MOOC課程http://mooc.study.163.com/course/USTC-1000029000


一、構建自己的實驗環境

1. 在Linux14.04上配置32位程序的運行環境。

apt-get install lib32z1 lib32ncurses5-dev lib32bz2-1.0 libc6-dev-i386
2. 安裝Qemu

apt-get install qemu # install QEMU
ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu

3. 搭建MenuOS運行環境

# 下載內核源代碼編譯內核
cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar
cd linux-3.18.6
make i386_defconfig
make # 一般要編譯很長時間,少則20分鐘多則數小時
 
# 製作根文件系統
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git  # 如果被牆,可以使用附件menu.zip 
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
 
# 啓動MenuOS系統
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

4. 上面編譯的Linux內核沒有debug信息,需要在原來配置的基礎上重新配置,使之攜帶調試信息。

make menuconfig




然後重新make,生成帶調試信息的Kernel。


二、使用GDB跟蹤調試內核

第一步:用QEMU加載Linux3.18的內核

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 關於-s和-S選項的說明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,則可以使用-gdb tcp:xxxx來取代-s選項

其中-S選項是開啓了一個gdb server。

關於qemu一些選項的詳細說明,可以使用qemu --help來查看。



第二步:使用GDB進行調試跟蹤。

關於GDB的使用,可以參考下面的表:

 命令  解釋  示例
file <文件名> 加載被調試的可執行程序文件。
因爲一般都在被調試程序所在目錄下執行GDB,因而文本名不需要帶路徑。
(gdb) file gdb-sample
r Run的簡寫,運行被調試的程序。
如果此前沒有下過斷點,則執行完整個程序;如果有斷點,則程序暫停在第一個可用斷點處。
(gdb) r
c Continue的簡寫,繼續執行被調試程序,直至下一個斷點或程序結束。 (gdb) c
b <行號>
b <函數名稱>
b *<函數名稱>
b *<代碼地址>

d [編號]

b: Breakpoint的簡寫,設置斷點。兩可以使用“行號”“函數名稱”“執行地址”等方式指定斷點位置。
其中在函數名稱前面加“*”符號表示將斷點設置在“由編譯器生成的prolog代碼處”。如果不瞭解彙編,可以不予理會此用法。

d: Delete breakpoint的簡寫,刪除指定編號的某個斷點,或刪除所有斷點。斷點編號從1開始遞增。

(gdb) b 8
(gdb) b main
(gdb) b *main
(gdb) b *0x804835c

(gdb) d

s, n s: 執行一行源程序代碼,如果此行代碼中有函數調用,則進入該函數;
n: 執行一行源程序代碼,此行代碼中的函數調用也一併執行。

s 相當於其它調試器中的“Step Into (單步跟蹤進入)”;
n 相當於其它調試器中的“Step Over (單步跟蹤)”。

這兩個命令必須在有源代碼調試信息的情況下纔可以使用(GCC編譯時使用“-g”參數)。

(gdb) s
(gdb) n
si, ni si命令類似於s命令,ni命令類似於n命令。所不同的是,這兩個命令(si/ni)所針對的是彙編指令,而s/n針對的是源代碼。 (gdb) si
(gdb) ni
p <變量名稱> Print的簡寫,顯示指定變量(臨時變量或全局變量)的值。 (gdb) p i
(gdb) p nGlobalVar
display ...

undisplay <編號>

display,設置程序中斷後欲顯示的數據及其格式。
例如,如果希望每次程序中斷後可以看到即將被執行的下一條彙編指令,可以使用命令
“display /i $pc”
其中 $pc 代表當前彙編指令,/i 表示以十六進行顯示。當需要關心彙編代碼時,此命令相當有用。

undispaly,取消先前的display設置,編號從1開始遞增。

(gdb) display /i $pc

(gdb) undisplay 1

i Info的簡寫,用於顯示各類信息,詳情請查閱“help i”。 (gdb) i r
q Quit的簡寫,退出GDB調試環境。 (gdb) q
help [命令名稱] GDB幫助命令,提供對GDB名種命令的解釋說明。
如果指定了“命令名稱”參數,則顯示該命令的詳細說明;如果沒有指定參數,則分類顯示所有GDB命令,供用戶進一步瀏覽和查詢。
(gdb) help display

 

我們新打開一個Terminal


gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加載符號表
(gdb)target remote:1234 # 建立gdb和gdbserver之間的連接,按c 讓qemu上的Linux繼續運行
(gdb)break start_kernel # 斷點的設置可以在target remote之前,也可以在之後
設置好斷點後,按c[Enter],讓程序執行到斷點位置。


使用list命令查看start_kernel附近的代碼。


在rest_init處,我們設置第二個斷點。


此處是start_kernel最後調用的一個函數。

三、Linux內核啓動分析

  ● 計算機的啓動過程概述
      ○ x86 CPU啓動的第一個動作CS:EIP=FFFF:0000H(換算爲物理地址爲000FFFF0H,因爲16位CPU有20根地址線),即BIOS程序的位置。http://wenku.baidu.com/view/4e5c49eb172ded630b1cb699.html
      ○ BIOS例行程序檢測完硬件並完成相應的初始化之後就會尋找可引導介質,找到後把引導程序加載到指定內存區域後,就把控制權交給了引導程序。這裏一般是把硬盤的第一個扇區MBR和活動分區的引導程序加載到內存(即加載BootLoader),加載完整後把控制權交給BootLoader。
      ○ 引導程序BootLoader開始負責操作系統初始化,然後起動操作系統。啓動操作系統時一般會指定kernel、initrd和root所在的分區和目錄,比如root (hd0,0),kernel (hd0,0)/bzImage root=/dev/ram init=/bin/ash,initrd (hd0,0)/myinitrd4M.img
      ○ 內核啓動過程包括start_kernel之前和之後,之前全部是做初始化的彙編指令,之後開始C代碼的操作系統初始化,最後執行第一個用戶態進程init。

      ○ 一般分兩階段啓動,先是利用initrd的內存文件系統,然後切換到硬盤文件系統繼續啓動。initrd文件的功能主要有兩個:1、提供開機必需的但kernel文件(即vmlinuz)沒有提供的驅動模塊(modules) 2、負責加載硬盤上的根文件系統並執行其中的/sbin/init程序進而將開機過程持續下去


四、道生一、一生二、二生三、三生萬物

當Power on PC時,BIOS的代碼開始執行,然後是Linux初始化的代碼,這其中大約很長一段時間Linux都沒有進程這一概念,但是這不影響CPU執行它的二進制代碼。如果不是多任務以及進程調度的需要,Linux內核可以一直這樣走下去。
但是因爲多任務的需求,Linux必須能支持任務這一特性,任務即進程,或者更簡單地說由task_struct對象實例所代表的一段代碼的集合,用以完成特定的任務。所以Linux內核初始化過程中必須爲進程以及進程調度做準備。

init_task進程在Linux中屬於一個比較特殊的進程,它是內核開發者人爲製造出來的,而不是其他進程通過do_fork來完成。init_task對象的初始化在內核代碼中由下面代碼來完成:

<arch/x86/kernel/init_task.c>
struct task_struct init_task = INIT_TASK(init_task);

如果仔細考察INIT_TASK宏的細節,會發現很多有趣的東西,比如inti_task所對應的內核棧,在INIT_TASK宏中由下列代碼指定:

.stack        = &init_thread_info
可以猜想init_task進程的內核棧一定是通過靜態方式分配的,事實上也的確如此:
<arch/x86/kernel/init_task.c>
union thread_union init_thread_union __init_task_data =
    { INIT_THREAD_INFO(init_task) };
init_thread_info定義中的__init_task_data表明該內核棧所在的區域位於內核映像的init data區,我們可以通過編譯完內核後所產生的System.map來看到該變量及其對應的邏輯地址:
root@build-server:/boot# cat System.map-3.1.6 | grep init_thread_union
ffffffff81a00000 D init_thread_union
這意味着init_task.stack = 0xffffffff81a00000.

Linux在無進程概念的情況下將一直從初始化部分的代碼執行到start_kernel,然後再到其最後一個函數調用rest_init。

從rest_init開始,Linux開始產生進程,因爲init_task是靜態製造出來的,pid=0,它試圖將從最早的彙編代碼一直到start_kernel的執行都納入到init_task進程上下文中。在rest_init函數中,內核將通過下面的代碼產生第一個真正的進程(pid=1):

<init/main.c>
static noinline void __init_refok rest_init(void)
{
    ...
    kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
    ...
    cpu_idle();
}
kernel_init函數最有意思的地方在於它會通過調用kernel_execve來執行根文件系統下的/sbin/init文件(所以此前系統根文件系統必須已經就緒),kernel_execve對用戶空間程序/sbin/init的調用發起自int $0x80,這是個從內核空間發起的系統調用,與call_usermodehelper函數本質上是完全一樣的。

而此時init_task的任務基本上已經完全結束了,它將淪落爲一個idle task,事實上在更早前的sched_init()函數中,通過init_idle(current, smp_processor_id())函數的調用就已經把init_task初始化成了一個idle task,init_idle函數的第一個參數current就是&init_task,在init_idle中將會把init_task加入到cpu的運行隊列中,這樣當運行隊列中沒有別的就緒進程時,init_task(也就是idle task)將會被調用,它的核心是一個while(1)循環,在循環中它將會調用schedule函數以便在運行隊列中有新進程加入時切換到該新進程上。


五、Init進程分析

init process 是 Linux 系統的第一個用戶態進程,那自然沒有父親。它是由 Linux 內核直接啓動的。


start_kernel()是內核的彙編與C語言的交接點,在該函數以前,內核的代碼都是用匯編寫的,完成一些最基本的初始化與環境內核代碼載入內存並解壓縮(現在的內核一般都經過壓縮),CPU 的最基本初始化,爲 C 代碼的運行設置環境(C 代碼的運行是有的,比如 stack 的設置等)。這裏一個不太確切的比喻是 start_kernel()就像是 C 代碼中的 main()。我們知道對應用程序員是他的入口,但實際上程序的入口是被包在了C庫中,在鏈接階段,linker 會把它鏈接入你的程序中。而它的任務中有一項就是爲
行環境。main()中的 argc,argv 等都不是平白無故來的,都是在調用 main()以前的代碼做的準備。


在 start_kernel()中 Linux 將完成整個系統的內核初始化。內核初始化的最後一步就是啓動 init 進程這個所有進程的祖先。


參考:init進程詳解


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