Linux操作系統學習筆記(二)內核運行

前言

  上文中,我們分析了從按下電源鍵到BootLoader完成加載的過程。加載完成之後,就要正式啓動Linux內核了,而在這之前首先要完成從實模式到保護模式的切換。本文主要分析以下幾部分內容

  • 新舊中斷的交替
  • 打開A20
  • 進入main函數
  • 內核初始化

  其實整個過程中還有很多內容,比如檢查各種硬件設備等,在此略過不提。下面就開始潛入Linux源碼的海洋暢遊啦。

新舊中斷的交替

  在實模式下的中斷顯然不可以和保護模式的中斷同日而語,因此我們需要關閉舊的中斷(cli)並確立新的中斷(sti)。main函數能夠適應保護模式的中斷服務體系被重建完畢纔會打開中斷,而那時候響應中斷的服務程序將不再是BIOS提供的中斷服務程序,取而代之的是由系統自身提供的中斷服務程序。

  cli、sti總是在一個完整操作過程的兩頭出現,目的是避免中斷在此期間的介入。接下來的代碼將爲操作系統進入保護模式做準備。此處即將進行實模式下中斷向量表和保護模式下中斷描述符表(IDT)的交接工作。試想,如果沒有cli,又恰好發生中斷,如用戶不小心碰了一下鍵盤,中斷就要切進來,就不得不面對實模式的中斷機制已經廢除、保護模式的中斷機制尚未完成的尷尬局面,結果就是系統崩潰。cli、sti保證了這個過程中,IDT能夠完整創建,以避免不可預料中斷的進入造成IDT創建不完整或新老中斷機制混用。

#boot/setup.s
……
do _move:
mov es,ax!destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax!source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
jmp do_move

  如上代碼主要完成了一項工作:將位於0x10000處的內核程序複製至內存地址起始位置0x00000處。在上一節我們分析了實模式中的存儲分佈圖,在此位置原來存放着由BIOS建立的中斷向量表及BIOS數據區。這個複製動作將BIOS中斷向量表和BIOS數據區完全覆蓋,使它們不復存在。這樣做的好處如下:

  1. 廢除BIOS的中斷向量表,等同於廢除了BIOS提供的實模式下的中斷服務程序。
  2. 收回剛剛結束使用壽命的程序所佔內存空間。
  3. 讓內核代碼佔據內存物理地址最開始的、天然的、有利的位置。

  此時,重要角色要登場了,他們就是中斷描述符表IDT和全局描述符表GDT

  • GDT(Global Descriptor Table,全局描述符表),在系統中唯一的存放段寄存器內容(段描述符)的數組,配合程序進行保護模式下的段尋址。它在操作系統的進程切換中具有重要意義,可理解爲所有進程的總目錄表,其中存放每一個任務(task)局部描述符表(LDT, Local Descriptor Table)地址和任務狀態段(TSS, Task Structure Segment)地址,完成進程中各段的尋址、現場保護與現場恢復。GDTR是GDT基地址寄存器,當程序通過段寄存器引用一個段描述符時,需要取得GDT的入口,GDTR標識的即爲此入口。在操作系統對GDT的初始化完成後,可以用LGDT(Load GDT)指令將GDT基地址加載至GDTR。

  • IDT(Interrupt Descriptor Table,中斷描述符表),保存保護模式下所有中斷服務程序的入口地址,類似於實模式下的中斷向量表。IDTR(IDT基地址寄存器),保存IDT的起始地址。

  32位的中斷機制和16位的中斷機制,在原理上有比較大的差別。最明顯的是16位的中斷機制用的是中斷向量表,中斷向量表的起始位置在0x00000處,這個位置是固定的;32位的中斷機制用的是中斷描述符表(IDT),位置是不固定的,可以由操作系統的設計者根據設計要求靈活安排,由IDTR來鎖定其位置。GDT是保護模式下管理段描述符的數據結構,對操作系統自身的運行以及管理、調度進程有重大意義。

  此時此刻內核尚未真正運行起來,還沒有進程,所以現在創建的GDT第一項爲空,第二項爲內核代碼段描述符,第三項爲內核數據段描述符,其餘項皆爲空。IDT雖然已經設置,實爲一張空表,原因是目前已關中斷,無需調用中斷服務程序。此處反映的是數據“夠用即得”的思想。

創建這兩個表的過程可理解爲是分兩步進行的:

  1. 在設計內核代碼時,已經將兩個表寫好,並且把需要的數據也寫好。此處的數據區域是在內核源代碼中設定、編譯並直接加載至內存形成的一塊數據區域。專用寄存器的指向由程序中的lidt和lgdt指令完成
  2. 將專用寄存器(IDTR、GDTR)指向表。

A20

  A20啓用是一個標誌性的動作,由上文提到的lzma_decompress.img 調用 real_to_prot啓動。打開A20,意味着CPU可以進行32位尋址,最大尋址空間爲4 GB。注意圖1-19中內存條範圍的變化:從5個F擴展到8個F,即0xFFFFFFFF(4 GB)。

  實模式下,當程序尋址超過0xFFFFF時,CPU將“回滾”至內存地址起始處尋址(注意,在只有20根地址線的條
件下,0xFFFFF+1=0x00000,最高位溢出)。例如,系統的段寄存器(如CS)的最大允許地址爲0xFFFF,指令指針(IP)的最大允許段內偏移也爲0xFFFF,兩者確定的最大絕對地址爲0x10FFEF,這將意味着程序中可產生的實模式下的尋址範圍比1 MB多出將近64 KB(一些特殊尋址要求的程序就利用了這個特點)。這樣,此處對A20地址線的啓用相當於關閉CPU在實模式下尋址的“回滾”機制。如下所示爲利用此特點來驗證A20地址線是否確實已經打開。注意此處代碼並不在此時運行,而是在後續head運行過程中爲了檢測是否處於保護模式中使用。

#boot/head.s
……
xorl %eax,%eax
1:incl%eax#check that A20 really IS enabled
movl %eax,0x000000#loop forever if it isn't
cmpl %eax,0x100000
je 1b
……

  A20如果沒打開,則計算機處於20位的尋址模式,超過0xFFFFF尋址必然“回滾”。一個特例是0x100000會回滾到0x000000,也就是說,地址0x100000處存儲的值必然和地址0x000000處存儲的值完全相同。通過在內存0x000000位置寫入一個數據,然後比較此處和1 MB(0x100000,注意,已超過實模式尋址範圍)處數據是否一致,就可以檢驗A20地址線是否已打開。

進入main函數

  這裏涉及到一個硬件知識:在X86體系中,採用的終端控制芯片名爲8259A,此芯片,是可以用程序控制的中斷控制器。單個的8259A能管理8級向量優先級中斷,在不增加其他電路的情況下,最多可以級聯成64級的向量優先級中斷系統。CPU在保護模式下,int 0x00~int 0x1F被Intel保留作爲內部(不可屏蔽)中斷和異常中斷。如果不對8259A進行重新編程,int 0x00~int 0x1F中斷將被覆蓋。例如,IRQ0(時鐘中斷)爲8號(int 0x08)中斷,但在保護模式下此中斷號是Intel保留的“Double Fault”(雙重故障)。因此,必須通過8259A編程將原來的IRQ0x00~IRQ0x0F對應的中斷號重新分佈,即在保護模式下,IRQ0x00~IRQ0x0F的中斷號是int 0x20~int 0x2F。

  setup程序通過下面代碼將CPU工作方式設爲保護模式。這裏涉及到一個CR0寄存器:0號32位控制寄存器,放系統控制標誌。第0位爲PE(Protected Mode Enable,保護模式使能)標誌,置1時CPU工作在保護模式下,置0時爲實模式。將CR0寄存器第0位(PE)置1,即設定處理器工作方式爲保護模式。CPU工作方式轉變爲保護模式,一個重要的特徵就是要根據GDT決定後續執行哪裏的程序。前文提到GDT初始時已寫好了數據,這些將用來完成從setup程序到head程序的跳轉。

#boot/setup.s
mov ax,#0x0001!protected mode(PE)bit
lmsw ax!This is it!
jmpi 0,8!jmp offset 0 of segment 8(cs)

  head程序是進入main之前的最後一步了。head在空間創建了內核分頁機制,即在0x000000的位置創建了頁目錄表、頁表、緩衝區、GDT、IDT,並將head程序已經執行過的代碼所佔內存空間覆蓋。這意味着head程序自己將自己廢棄,main函數即將開始執行。具體的分頁機制因爲較爲複雜,所以打算放在後續介紹內存管理的部分再單獨介紹

  head構造IDT,使中斷機制的整體架構先搭建起來(實際的中斷服務程序掛接則在main函數中完成),並使所有中斷服務程序指向同一段只顯示一行提示信息就返回的服務程序。從編程技術上講,這種初始化操作,既可以防止無意中覆蓋代碼或數據而引起的邏輯混亂,也可以對開發過程中的誤操作給出及時的提示。IDT有256個表項,實際只使用了幾十個,對於誤用未使用的中斷描述符,這樣的提示信息可以提醒開發人員注意錯誤。

  除此之外,head程序要廢除已有的GDT,並在內核中的新位置重新創建GDT。原來GDT所在的位置是設計代碼時在setup.s裏面設置的數據,將來這個setup模塊所在的內存位置會在設計緩衝區時被覆蓋。如果不改變位置,將來GDT的內容肯定會被緩衝區覆蓋掉,從而影響系統的運行。這樣一來,將來整個內存中唯一安全的地方就是現在head.s所在的位置了。

  下來步驟主要包括

  1. 初始化段寄存器和堆棧
  2. 清零eflag寄存器以及內核未初始化數據區
  3. 調用decompress_kernel()解壓內核映像並跳轉至0X00100000處。
  4. 段寄存器初始化爲最終值並填充BSS字段爲0
  5. 初始化臨時內核頁表

  最終完成了分頁機制初始化後,PG(Paging) 標誌位將會置1,表示地址映射模式採取分頁機制,最終跳轉至main函數,內核開始初始化工作。

內核初始化

  注意,至此爲止,我們尚未打開中斷,而必須通過main函數完成一系列的初始化後纔會打開新的中斷,從而使內核正式運行起來。該部分主要包括:

  1. 爲進程0建立內核態堆棧
  2. 清零eflags寄存器
  3. 調用setup_idt()用空的中斷處理程序填充IDT
  4. 把BIOS中獲得的參數傳遞給第一個頁框
  5. 用GDT和IDT表填充寄存器

  完成這些之後,內核就正式運行,開始創建0號進程了。

總結

  本文介紹了實模式到保護模式的整個切換過程,完成了內核的加載並開始正式準備創建0號進程。後續將繼續分析啓動內核創建0號、1號、2號進程的整個過程。本文介紹過程中忽略了很多彙編代碼以及一些雖然很重要但是不屬於基本流程的知識,有興趣瞭解的可以根據文中鏈接、文末的源碼和參考資料進行更深入的學習研究。

源碼資料

[1] GURB 2

[2] syslinux

參考資料

[1] Linux-insides

[2] 深入理解Linux內核源碼

[3] Linux內核設計的藝術

[4] 極客時間 趣談Linux操作系統

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