基於x86體系結構分析linux的啟動過程
僅考慮32位體系結構
不考慮多核多處理器
要求1:分析流程按照開機-->BIOS-->grub-->Linux的順序進行,到start_kernel結束
第一步——>載入BIOS:
開啟計算機電源後,計算機會首先載入BIOS資訊,這是因為BIOS中包含了CPU的相關資訊、裝置啟動順序資訊、硬碟資訊、記憶體資訊、時鐘資訊、PnP特性等等。BIOS所做的工作如下:
1.檢測連線硬體,比如顯示卡,記憶體,磁碟等等,檢測的目的是以後把這些裝置資訊提供給作業系統;
2.尋找啟動磁碟,每一種BIOS都會有開機啟動選單,可以在選單裡設定以哪個裝置啟動系統
比如:光碟機,硬碟,網路等等,這個選單可以設定多個選項,依照設定次序在裝置上尋找啟動資訊;
讀取所指定的硬碟上的MBR(主引導記錄,在硬碟上第0磁軌第一個扇區),並將其拷貝到0x7c00地址所在的實體記憶體(RAM)中,這部分內容其實就是Boot Loader,即作業系統核心映像,對應到電腦上的lilo或者grub.
第二步——>grub
grub就是在作業系統核心執行之前執行的一段小程式。通過這段小程式,我們可以初始化硬體裝置、建立記憶體空間的對映圖,從而將系統的軟硬體環境帶到一個合適的狀態,以便為最終呼叫作業系統核心做好一切準備。
grub是通過將兩階段的引導載入程式轉換成三階段的引導載入程式來實現載入Linux核心功能的。階段 1 (MBR)引導了一個階段 1.5 的引導載入程式,它可以理解包含 Linux 核心映像的特殊檔案系統。這方面的例子包括reiserfs_stage1_5(要從 Reiser 日誌檔案系統上進行載入)或 e2fs_stage1_5(要從 ext2 或 ext3 檔案系統上進行載入)。當階段 1.5 的引導載入程式被載入並執行時,階段 2 的引導載入程式就可以進行載入了。當階段 2 載入之後,GRUB 就可以在請求時顯示可用核心列表(在 /etc/grub.conf 中進行定義,同時還有幾個軟符號連結/etc/grub/menu.lst 和 /etc/grub.conf)。將第二階段的引導載入程式載入到記憶體中之後,就可以對檔案系統進行查詢了,並將預設的核心映像和 initrd 映像載入到記憶體中。當這些映像檔案準備好之後,階段 2 的引導載入程式就可以呼叫核心映像了。
Linux的引導扇區內容是採用組合語言編寫的程式,其原始碼在arch/i386/boot中(不同體系的CPU有其各自的boot目錄),它最重要的兩個程式檔案為:
◎bootsect.S,引導扇區的主程式,彙編後的程式碼不超過512位元組,即一個扇區的大小;
◎setup.S, 引導輔助程式;
下面分析這兩個檔案到底做了什麼:
Bootsect首先將“自己”從被RAM的0x7c00處搬到0x90000處,然後建立執行環境,即將DS,ES,SS都指向0x90000處,與CS看齊,同時初始化SP;然後將setup讀到0x90200處(setup的image將會讀入至程式所指定的記憶體絕對地址0x90200處),列印“Loading”,讀入核心(vmlinuz)到0x100000(bzImage)處,然後跳到setup(boot/Setup.S)處執行。Setup部分首先設定一些系統的硬體裝置(例如建立idt, gdt表),然後將核心從0x10000處移至0x1000處,這時系統轉入保護模式,開始執行位於0x1000處的程式碼,正式進入核心。
第三步——>載入linux核心
首先是核心的加壓縮,ox1000處的程式碼來自於arch/i386/boot/compressed/head.S,它用來初始化暫存器和呼叫decompress_kernel()(這個函式將核心vmlinuz解壓到0x100000)程式,解壓後的資料被裝入到0x100000處,而arch/x86/kernel/head.S中的startup_32的地址正是0x100000,所以現在從這個startup_32還是執行,它實現瞭如下功能:
1.首先將ds,es,fs,gs指向系統資料段KERNEL_DS(KERNEL_DS 在asm/segment.h中定義,表示全域性描述符表中中的第三項)。
2 資料段全部清空。
3 setup_idt為一段子程式,將中斷向量表全部指向ignore_int函式,該函式打印出: unknown interrupt 當然這樣的中斷處理函式什麼也幹不了。
4 察看資料線A20是否有效,否則迴圈等待(地址線A20是x86的歷史遺留問題,決定是否能訪問1M以上記憶體)。
5 拷貝啟動引數到0x5000頁的前半頁,而將setup.s取出的bios引數放到後半頁。
6 檢查CPU型別。
7 初始化頁表,只初始化最初幾頁。
1>將swapper_pg_dir(0x2000)和pg0(0x3000)清空swapper_pg_dir作為整個系統的頁目錄;
2>將pg0作為第一個頁表,將其地址賦到swapper_pg_dir的第一個32位字中;
3>同時將該頁表項也賦給swapper_pg_dir的第3072個入口,表示虛擬地址0xc0000000也指向pg0;
4>將pg0這個頁表填滿指向記憶體前4M;
5>進入分頁方式;
8 裝入新的gdt和ldt表。
9 重新整理段暫存器ds,es,fs,gs。
10 使用系統堆疊,即預留的0x6000頁面。
11 執行start_kernel函式,這個函式是第一個C編制的 函式。
到此處為止,核心又有了一個新的開始。
要求2:首先給出Linux映像的make過程分析,說明grub將跳轉到哪個Linux原始檔中的哪處開始執行
Makefile的主要流程如下:
1.使用命令列或者圖形介面配置工具,對核心進行裁減,生成.config配置檔案;
2. 儲存核心版本資訊到 include/linux/version.h;
3. 產生符號連結 include/asm,指向實際目錄 include/asm-$(ARCH);
4. 為最終目標檔案的生成進行必要的準備工作;
5. 遞迴進入 /init 、/core、 /drivers、 /net、 /lib等目錄和其中的子目錄來編譯生成所有的目標檔案;
6. 連結上述過程產生的目標檔案生成vmlinux,vmlinux存放在核心程式碼樹的根目錄下;
7. 最後根據 arch/$(ARCH)/Makefile檔案定義的後期編譯的處理規則建立最終的映象bootimage,包括建立引導記錄、準備initrd映象和相關處理。
Make命令首先從頂層的makefile中開始執行,這個makefile產生vmlinux檔案和核心模組(modules):
只使用make命令,即沒有任何引數的情況之下,make會執行的是Makefile檔案中的預設規則,即all:vmlinux這個規則。
然後找到vmlinux目標:
這樣可以看出vmlinux的依賴的幾項內容了,對應這樣的幾個依賴檔案,分別進行分析。
這些就是以上三個依賴項的定義了。
l vmlinux-lds的定義已經很清楚了,就是對應目錄arch/x86/kernel/下的vmlinux.lds了
l 至於vmlinux-init的定義,就得到arch/x86/makefile檔案中去看了,因為頂層的Makefile檔案把這個Makefile檔案也include進去了。
其中head-y如下:
head-y:=arch/x86/kernel/head_$(BITS).o
head-y+=arch/x86/kernel/head$(BITS).o
head-y+=arch/x86/kernel/head.o
head-y+=arch/x86/kernel/init_task.o
至於BITS,按照要求之考慮32為的情況,即把BITS代換為32就可以了。所以,head-y有三個重要的檔案組成,即head_32.S,head32.c,init_task.o檔案。這也說明了其是與體系結構相關的。
其中init-y如下:
這樣可以看出,init-y是與體系結構無關的。一種涉及到了一個patsubs函式替換的工作。
其中還有core-y如下:
core-y := usr/
……
core-y += kernel/ mm/ fs/ ipc/ security/crypto/block/
……
vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y)$(init-m)\
$(core-y) $(core-m) $(drivers-y)$(drivers-m) \
$(net-y) $(net-m) $(libs-y)$(libs-m)))
core-y := $(patsubst %/, %/built-in.o,$(core-y))
vmlinux-main:=$(core-y) $(libs-y) $(drivers-y) $(net-y)
經過分析,分析core-y的定義,分析core-y中既包含體系結構相關的,又包含體系結構無關的內容。
其中libs-y如下:
至於$(vmlinux-lds)$(vminux-init)$(vmlinux-main)這三個,都是依賴於$(vmlinux-dirs)的。而vmlinux-dirs又是依賴於上面說到的init-y,init-m,core-y,core-m,dirvers-m,ner-y,net-m,libs-y,l
libs-m等,並且定義時候要對這些檔案進行排序和過濾,即使用了sort和filter函式,通過這些就可以生成想要的一些檔案見了。
接下來,就應該是編譯過程中的連結操作了。即把生成的這些檔案連結起來,生成最終的目標檔案才行。在Makefile檔案中,使用瞭如下的call操作,分別呼叫相關的連結規則。
$(callif_changed_rule,vmlinux__)
$(call cmd,vmlinux__)
其中又有quiet_cmd_vmlinux。
在確定了$(vmlinux-lds)$(vminux-init)$(vmlinux-main)都成功生成了之後,才可以執行該操作函式的。quiet_cmd_vmlinux這個主要是用來在編譯時進行顯示用的,可以看出顯示結果為LD target列表,真正的命令為cmd_vmlinux__,通過這個命令將變數vmlinux-init和vmlinux-main指定的目標連結成vmlinux檔案。連結指令碼由vmlinux-lds指定,即 –T 後跟連線的指令碼。
綜上,這樣編譯vmlinux的主要流程如下:
最後生成在核心程式碼樹根目錄下的vmliux目標檔案。
若是makebzImage,則可以在arch/x86/makefile檔案中找到相應規則,
然後可以看到再到arch/x86/boot/makefile檔案中的bzImage,
可以看出bzImage和vmliunx.bin和setup.bin都相關,而setup.bin由set.elf轉化而來,setuo.elf是由一系列.o檔案連結而成,vmlinux.bin則是由compressed/vmlinux經過objcopy轉化而來的,在arch/x86/boot/compressed的目錄下,分析其中的Makefile檔案了。其中:
$(obj)/vmlinux:$(src)/vmlinux_$(BITS).lds$(obj)/head_$(BITS).o $(obj)/misc.o $(obj)/piggy.oFORCE
則vmlinux又由head_32.o misc.o piggy.o經過vmlinux_32.lds連結組成的。
misc.o的作用就是一個解壓縮的功能。而piggy.o是由vmlinux.scr和vmlinux.bin.gz經過ld連結生成。vmlinux.bin.gz是vmlinux.bin經過gzip壓縮之後生成,而vmlinux.bin是由頂層vmlinux經過objcopy得到得。
之後,利用objcopy把arch/x86/boot/cmpressed目錄下的vmlinux檔案轉換成二進位制的vmlinux檔案,儲存在arch/x86/boot/目錄下了。
接著,利用build工具把arch/x86/boot/cmpressed目錄下的setup.bin和vmlinux.bin拼接成bzImage.這樣就生成了bzImage.
Grub將跳轉到arch/x86/boot/compressed/head_32.S的startup_32處開始執行。
要求3:從make過程,給出Linux中的啟動相關的幾個關鍵原始檔的執行順序
1. arch/i386/boot/header.S;
2. arch/i386/boot/main.c;
3. arch/i386/boot/compressed/head_32.S;
4. arch/i386/kernel/head_32.S;
5. init/main.c
要求4:從第一個原始檔開始給出主要流程,到start_kernel結束
1.header.S的主要流程:
① 設定setup header引數部分
② start_of_setup
1):設定堆疊
2):檢查setup中的標籤
3):清除BSS段
4):呼叫C入口main
2.boot/main.c的主要流程
copy_boot_params();複製 boot header到"zeropage"
validate_cpu();確保支援當前執行的CPU
set_bios_mode();告訴BIOS什麼CPU我們將要去執行
detect_memory();檢測Memory
keyboard_set_repeat();設定鍵盤 repeatrate (Why ?)
set_video();設定 Video mode
query_mca();獲得 MCA 資訊
query_ist();獲得 Query IntelSpeedStep (IST) 資訊
query_apm_bios();獲得APM 資訊
query_edd();獲得EDD資訊
go_to_protected_mode();最後一件事,也是最重要的一件事,進入保護模式
3.boot/compressed/head_32.S的主要流程
在保護模式下,首先到0x00001000處,即startup_32處開始執行
1) 首先初始化段暫存器和臨時堆疊;
2) 清除eflags暫存器的所有位;
3) 將_edata和_end區間的所有核心未初始化區填充0;
4) 呼叫decompress_kernel( )函式解壓核心映像。首先顯示"Uncompressing Linux..."資訊,解壓完成後顯示 "OK, booting the kernel."。核心解壓後,如果時低地址載入,則放在0x00100000位置;否則解壓後的映像先放在壓縮映像後的臨時快取裡,最後解壓後的映像被放置到物理位置0x00100000處;
5) 跳轉到0x00100000實體記憶體處執行;
4.kernel/head_32.S
該函式未Linux第一個程序建立執行環境,操作如下:
1) 初始化ds,es,fs,gs段暫存器的最終值;
2) 用0填充核心bss段;
3) 初始化swapper_pg_dir陣列和pg0包含的臨時核心頁表:
4) 建立程序0idle程序的核心模式的堆疊;
5) 再次清除eflags暫存器的所有位;
6) 呼叫setup_idt()用非空的中斷處理函式填充IDT表;
7) 將從BIOS獲取的系統引數傳遞到作業系統的第一個頁面幀;
8) 識別處理器的模式;
9) 將GDT和IDT表的地址載入到gdtr和idtr暫存器中;
10) 跳轉到start_kernel函式,這個函式是第一個C編制的函式,核心又有了一個新的開始。
5.init/main.c
主要是執行start_kernel函式:
2) 呼叫build_all_zonelists函式初始化記憶體區;
3) 呼叫page_alloc_init()和mem_init()初始化夥伴系統分配器;
4) 呼叫trap_init()和init_IRQ()對中斷控制表IDT進行最後的初始化;
5) 呼叫softirq_init() 初始化TASKLET_SOFTIRQ和HI_SOFTIRQ;
6) Time_init()對系統日期和時間進行初始化;
7) 呼叫kmem_cache_init()初始化slab分配器;
8) 呼叫calibrate_delay()計算CPU時鐘頻率;
通過呼叫kernel_thread()啟動程序1init程序的核心執行緒,然後該執行緒再建立其他的核心執行緒執行/sbin/init程式。