linux_insides_cn之一:內核引導過程

1、內核引導過程

1. 從引導加載程序內核

問題:linux

  • 底層是如何工做的
  • 程序是如何運行的
  • 如何在內存定位的
  • 內核是如何管理進程和內存的
  • 網絡堆是如何在底層工做的

過程:
按下電源開關 ——> 主板發送信號給電源 ——> 電源收到信號給電腦供電 ——> 主板收到「電源備妥信號」 ——> 嘗試啓動CPU ——> CPU復位全部寄存器數據,並設置預約值
注意:
處理器開始在「實模式」工做,它有20位的尋址總線,尋址空間是0~2^20(1MB),但它的寄存器卻只有16位(2^16即64KB),因此實模式使用「段式內存管理」來管理整個內存空間。
替代方法:chrome

PhysicalAddress = Segment * 16 + offset

但:數組

>>> hex((0xffff << 4) + 0xffff)
'0x10ffef'

已經超出1MB範圍。既然實模式下, CPU 只能訪問 1MB 地址空間,0x10ffef變成有A20缺陷的0x00ffef(CPU只有20位,最高位將被捨棄)網絡

  • CS:代碼段寄存器
  • IP:指令指針寄存器

CS:IP 兩個寄存器指示了 CPU 當前將要讀取的指令的地址 電腦復位後,CPU寄存器中的預約義數據:app

IP          0xfff0
CSselector    0xf000
CSbase        0xffff000

邏輯地址: CS:IPide

0xffff0000:0xfff0
>>> 0xffff0000 + 0xfff0
'0xfffffff0'

這個地方是復位向量(Reset vector)。這是CPU在重置後指望執行的第一條指令的內存地址。它包含一個jump指令,這個指令一般指向BIOS入口點。
在初始化和檢查硬件以後,須要尋找到一個可引導設備。可引導設備列表存儲在在 BIOS 配置中, BIOS 將根據其中配置的順序,嘗試從不一樣的設備上尋找引導程序。對於硬盤,BIOS 將嘗試尋找引導扇區。
一個真實的啓動扇區包含了分區表,已經用來啓動系統的指令,而不是像咱們上面的程序,只是輸出了一個感嘆號就結束了。從啓動扇區的代碼被執行開始,BIOS 就將系統的控制權轉移給了引導程序。
實模式下的1MB地址空間分配表:函數

0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table 實模式中斷向量表
0x00000400 - 0x000004FF - BIOS Data Area BIOS數據區
0x00000500 - 0x00007BFF - Unused 未被使用
0x00007C00 - 0x00007DFF - Our Bootloader 咱們的引導加載程序
0x00007E00 - 0x0009FFFF - Unused
0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory 視頻RAM(VRAM)存儲器
0x000B0000 - 0x000B7777 - Monochrome Video Memory 單色視頻存儲器
0x000B8000 - 0x000BFFFF - Color Video Memory 彩色視頻存儲器
0x000C0000 - 0x000C7FFF - Video ROM BIOS 視頻ROMD的BIOS 
0x000C8000 - 0x000EFFFF - BIOS Shadow Area BIOS陰影區
0x000F0000 - 0x000FFFFF - System BIOS 系統BIOS

問題:CPU 執行的第一條指令是在地址0xFFFFFFF0處,這個地址遠遠大於0xFFFFF ( 1MB )。那麼實模式下的 CPU 是如何訪問到這個地址的呢?文檔coreboot給出了答案:spa

0xFFFE_0000 - 0xFFFF_FFFF: 128 kilobyte ROM mapped into address space

0xFFFFFFF0這個地址被映射到了 ROM,所以 CPU 執行的第一條指令來自於 ROM,而不是RAM。
小總結:操作系統

1. CPU寄存器復位(0xffff0)
2. 讀取ROM指令(jump)
3. 跳轉至BIOS入口點
4. 尋找可引導設備(可引導程序,可引導扇區)

拓展知識:指針

  • boot protocol

遺留問題:

  • BIOS設置存儲在哪裏?

2. 引導程序

Linux有多種引導程序,好比GRUB 2和syslinux.
當 BIOS 已經選擇了一個啓動設備,而且將控制權轉移給了啓動扇區中的代碼(boot.img,很是簡單隻作必要初始化),跳轉到 GRUB 2's core image(diskboot.img,通常是在磁盤上存儲在啓動扇區以後到第一個可用分區以前)去執行。
core image 的初始化代碼會把整個 core image (包括 GRUB 2的內核代碼和文件系統驅動)引導到內存中。引導完成以後,grub_main將被調用。
grub_main 初始化控制檯,計算模塊基地址,設置 root 設備,讀取 grub 配置文件,加載模塊。最後,將 GRUB 置於 normal 模式,在這個模式中,grub_normal_execute (from grub-core/normal/main.c) 將被調用以完成最後的準備工做,而後顯示一個菜單列出所用可用的操從引導加載程序內核1做系統。當某個操做系統被選擇以後,grub_menu_execute_entry 開始執行,它將調用 GRUB的boot命令,來引導被選中的操做系統。
正如kernel boot protocol 所描述的,引導程序必須填充 kernel setup header (位於 kernelsetup code 偏移0x01f1處)的必要字段。
當 bootloader 完成任務,將執行權移交給 kernel.
小總結:

// 讀取可引導程序,包括:
1. 啓動扇區代碼(boot.img),必要的初始化,跳轉至 GRUB 2's core image
2. core image 的初始化代碼將core image(內核代碼、文件系統驅動)引導到內存中
3. 引導完成後,調用grub_main,初始化控制檯、計算模塊基地址、設置root設備、讀取grub配置文件、加載模塊。將GRUB置於normal模式,最後調用grub_normal_execute顯示可用的操做系統
4. 當操做系統被選擇以後,grub_menu_execute_entry 開始執行,將調用GRUB的 boot 命令,引導被選中的操做系統
5. 當 bootloader完成任務後,將執行權移交給kernel

遺留問題:

  • bootloader做用是什麼?

3. 內核設置

從技術上說,內核尚未被運行起來,由於首先咱們須要正確設置內核,啓動內存管理,進程管理等等。
而內核設置代碼的運行起點是 _start函數,但在其開始以前,還有不少代碼(bootloader)。去除這些做爲 bootloader 使用的代碼,真正的內核代碼就從_start開始了。其餘的 bootloader (grub2 and others) 知道 _start 所在的位置(從MZ頭開始偏移0x200字節),因此這些 bootloader 就會忽略全部在這個位置前的代碼(這些以前的代碼位於.bstext段中),直接跳轉到這個位置啓動內核。從引導加載程序內核。
_start 開始就是一個 jmp 語句,短跳轉至 start_of_setup - 1f。在_start標號以後的第一個標號爲1的代碼段中包含了剩下的 setup header 結構。在標號爲1的代碼段結束以後,緊接着就是標號爲start_of_setup的代碼段(這個代碼段位於.entrytext代碼區,這個代碼段中的第一條指令其實是內核開始執行以後的第一條指令)
從start_of_setup標號開始的代碼須要完成下面這些事情:

  • 將全部段寄存器的值設置成同樣的內容
  • 設置堆棧
  • 設置bss(靜態變量區)
  • 跳轉到main.c開始執行代碼

(1)段寄存器設置

  • 代碼段寄存器CS(Code Segment) 存放當前正在運行的程序代碼所在段的段基址,表示當前使用的指令代碼能夠從該段寄存器指定的存儲器段中取得,相應的偏移量則由IP提供。
  • 數據段寄存器DS(Data Segment) 指出當前程序使用的數據所存放段的最低地址,即存放數據段的段基址。
  • 堆棧段寄存器SS(Stack Segment) 指出當前堆棧的底部地址,即存放堆棧段的段基址。
  • 附加段寄存器ES(Extra Segment) 指出當前程序使用附加數據段的段基址,該段是串操做指令中目的串所在的段。

將DS和ES段寄存器的內容設置同樣:

movw %ds, %ax
movw %ax, %es
sti

將DS和CS段寄存器設置相同的值:

pushw %ds   //將DS寄存器的值入棧
pushw $6f   //將標號爲6的代碼段地址入棧
lretw      //執行lretw指令將把標號爲6的內存地址放入ip寄存器

(2)設置堆棧

絕大部分的 setup 代碼都是爲 C 語言運行環境作準備。在設置了ds和es寄存器以後,接下來step的代碼將檢查ss寄存器的內容,若是寄存器的內容不對,那麼將進行更正:

movw %ss, %dx
cmpw %ax, %dx  //比較ss是否等於ax
movw %sp, %dx  //將sp值保存到dx
je 2f          //兩數相等跳轉至標號2段代碼
2: andw $~3, %dx //將dx寄存器的值(就是當前sp寄存器的值)4字節對齊
   jnz 3f        //檢查是否爲0,不爲0跳轉
   movww $0xfffc, %dx //爲0表示棧區已滿,須要將sp重置至棧底一個字節前 0xfffc
3: movw %ax, %ss      //由於ss和ax相等,因此設置ss棧底地址爲0x10000
   movzwl %dx, %esp  //不爲0保留當前sp的值
   sti

特別地,當 ss != ds 時,要先將setup code 的結束地址 _end 寫入 dx寄存器,而後再檢查 loadflags 中是否設置了 CAN_USE_HEAP 標誌。

1)CAN_USE_HEAP被置位

movw heap_end_ptr, dx
overflow dx + STACK_SIZE, CF_flag //判斷dx是否在棧頂,意思是若是在棧頂 0+STACK_SIZE=ss jn 2f

2)CAN_USE_HEAP沒被置位

movw dx + STACK_SIZE, dx //同理 jmp 2f

(3)BSS段設置

在咱們正式執行C代碼以前,還有2件事情須要完成:

  • 設置正確的BSS段
  • 檢查 magic 簽名

首先檢查 magic 簽名 setup_sig,若是簽名不對,直接跳轉到 setup_bad 部分執行代碼:

cmpl $0x5a5aaa55, setup_sig
jne setup_bad

若是 magic 簽名是對的,那麼咱們只要設置好 BSS 段就能夠考試執行C代碼了。
BSS 段用來存儲那些沒有被初始化的靜態變量。對於這個段使用的內存, Linux 首先使用下面的代碼將其所有清零:

movw $__bss_start, %di
movw $_end+3, %cx
xorl %eax, %eax
subw %di, %cx
shrw $2, %cx
rep; stopsl

在這段代碼中,首先將__bss_start地址放入di寄存器,而後將_end + 3(4字節對齊)地址放入cx,接着使用xor指令將ax寄存器清零,接着計算 BSS 段的大小(cx -di),讓後將大小放入cx寄存器。接下來將cx寄存器除4,最後使用rep; stosl指令將ax寄存器的值(0)寫入寄存器整個 BSS 段。
示意圖

  • BSS段 :一般是指用來存放程序中 未初始化的全局變量、靜態變量(全局變量未初始化時默認爲0)的一塊內存區域

  • 數據段 :一般是指用來存放程序中 初始化後的全局變量和靜態變量

  • 代碼段 :一般是指用來存放程序中 代碼和常量

  • 堆 :一般是指用來存放程序中 進程運行時被動態分配的內存段 ( 動態分配:malloc / new,者動態釋放:free / delete)

  • 棧 :一般是指用來存放程序中 用戶臨時建立的局部變量、函數形參、數組(局部變量未初始化則默認爲垃圾值)也就是說咱們函數括弧「{}」中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除 之外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中。因爲棧的先進後出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。它是由操做系統分配的,內存的申請與回收都由OS管理。

(4)跳轉到main函數

到目前爲止,咱們完成了堆棧和 BSS 的設置,如今咱們能夠正式跳入main()函數來執行 C代碼了:

calll main

小總結:

1. 執行_start函數
2. 開始是一個jmp語句,跳轉至start_of_setip - 1f (該代碼段中包含了剩下的setup header結構)
3. 在 start_of_setip - 1f 代碼段結束以後,執行 start_of_setup 的代碼段,實際上這是內核開始執行以後的第一條指令
4. start_of_setup 代碼開始後,將完成如下工做:
- 將全部段寄存器的值設置成同樣的內容
- 設置堆棧
- 設置bss(靜態變量區)
- 跳轉到main.c開始執行代碼

遺留問題:

  • 爲何要將全部段寄存器的值設置成同樣?
  • 設置堆棧時,只設置了ss與sp的值,沒有設置堆的操做?

main() 函數定義在 arch/x86/boot/main.c, 下一篇文章將會詳細介紹在Linux內核設置過程當中調用的第一個C代碼(main()),也將介紹諸如 memset, memcpy, earlyprintk 這些底層函數的實現,敬請期待!

相關文章
相關標籤/搜索