1. 程式人生 > >作業系統實戰之CPU的模式切換與Linux上的實現

作業系統實戰之CPU的模式切換與Linux上的實現

概述

這兒我們以x86-64為例,通常情況下我們使用的都是壓縮核心,也就是經過壓縮的核心,核心外面被添加了一段自解壓程式。對於壓縮核心,從載入程式引導後首先執行的是那段位元組壓程式,其入口為arch/x86/boot/compressed/head_64.S中的startup_32。從那兒開始,將會配置解壓核心所需要的環境並解壓和跳轉到核心。

Grub引導到核心啟動各階段CPU的控制暫存器狀態如下表:
階段 CR0 CR3 CR4 EFER
GRUB到自解壓前 0x60000011 0x000000000000 0x00000000 0x00000000
自解壓程式配置環境後 0x80000011 0x000002328000 0x00000020 0x00000500
自解壓跳轉到核心後 0x80000011 0x000002328000 0x00000020 0x00000500
核心配置新環境後 0x80050033 0x000001df6000 0x000000a0 0x00000d01
Grub引導到核心啟動各階段CPU的控制暫存器狀態如下表:
階段 CS DS gdtr idtr ldtr
GRUB到自解壓前 0x0010 0x0018 0x00000000000010b0 0x0000000000000000 0x0000
自解壓程式配置環境後 0x0010 0x0018 0x0000000001639400 0x0000000000000000 0x0000
自解壓跳轉到核心後 0x0010 0x0000 0x0000000001639400 0x0000000000000000 0x0000
核心配置新環境後 0x0010 0x0000 0xffffffff81d74000 0x0000000000000000 0x0000
從上表可知,kernel引導經歷了幾次CPU模式配置,自解壓前的配置是為了滿足解壓核心程式碼的環境要求,核心啟動後由對頁表和CPU模式進行了新的配置,以滿足核心在虛擬地址下的執行需求。我們接下來具體看一下它是怎麼完成這些的。

CPU模式的配置

載入程式完成了對核心的載入與跳轉,同時還進行了一些必要的環境配置以滿足其自身執行的要求和所要載入的核心的引導要求。從上表可以看到,載入程式將CR0的值配置成了0x60000011。
參考以上對於控制暫存器的介紹,由CR0的值我們可以確定Grub已經使得CPU進入了32位保護模式,但是還沒開啟分頁功能;接著在自解壓程式裡,CPU開啟了分頁,CR3儲存了頁目錄的基地址,也關閉了在上一階段還開啟著的快取記憶體相關的特性。同時還設定了CR4中的PAE標誌位打開了實體地址擴充套件,EFER暫存器的LME和LMA標誌位使得CPU進入64位模式。最後有配置了CR0暫存器,打開了一些新的特性,同時還設定了CR4暫存器的PGE標誌位啟用了全域性頁表。以上就是對於linux從引導到啟動經歷的幾次CPU模式切換。
x86架構CPU上電執行第一條指令後就進入了真實模式,真實模式之後有32位保護模式,64位保護模式,長模式等,我們接下來看看CPU的這些模式是如何相互切換的。

真實模式到保護模式的切換

真實模式下的CPU定址能力是有限的,也限制了CPU的許多能力,所以在上電後我們需要配置CPU使得它變得更強大。
從真實模式到保護模式的切換比較簡單,一步就可完成,那就是將CR0.PE位置1即可進入保護模式,但是有一點需要注意的是,保護模式下采用的是分段機制進行記憶體定址,所以如果沒有配置號分段機制需要的段描述符表就冒然進入保護模式系統必然會跑飛的。至於具體的程式碼實現可以參照如下例子:

保護模式到分頁模式

如果CR0.PG=0,分頁機制就沒有啟動,處理器將會把線性地址當做實體地址處理。CR4.PAE、IA32_EFER.LME、CR0.WP、CR4.PSE、CR4.PGE、CR4.SMEP、CR4.SMAP和IA32_EFER.NXE也被處理器忽略,但當CR0.PG=1時,只要CR0.PE=1分頁機制就被開啟。分頁有三種模式,
  • 32位分頁模式

    如果CR0.PG=1與CR4.PAE=0,CPU就會採用32位分頁模式

  • PAE分頁模式

    如果CR0.PG=1,CR4.PAE=1和IA32_EFER.LME=0,CPU就會採用 PAE分頁模式

  • IA-32e分頁模式

    如果CR0.PG=1,CR4.PAE=1和IA32_EFER.LME=1,CPU就會採用 IA-32e分頁模式

    下圖展示了開啟與切換分頁模式的轉換圖
    這裡寫圖片描述

    這張圖清晰展示了個分頁模式是怎麼切換的,其中順著箭頭不能相關聯的模式之間不能切換,這就影響到了我們設定控制暫存器標誌位的先後順序。比如在32位分頁模式下,如果我們設定IA32_EFER.LME標誌位為1,那就會產生一個常規保護異常(#GP(0)),
    各個分頁模式下的一些控制行為由如下的控制位決定,CR0.WP、 CR4.PSE、CR4.PGE、CR4.PCIDE、CR4.SMEP、CR4.SMAP、CR4.PKE、IA32_EFER.NXE。
    <

ENTRY(startup_32)
    /* Load new GDT with the 64bit segments using 32bit descriptor */
    leal    gdt(%ebp), %eax
    movl    %eax, gdt+2(%ebp)
    lgdt    gdt(%ebp)

    /* Enable PAE mode */
    movl    %cr4, %eax
    orl $X86_CR4_PAE, %eax
    movl    %eax, %cr4

 /*
  * Build early 4G boot pagetable
  */
    /* Initialize Page tables to 0 */
    leal    pgtable(%ebx), %edi
    xorl    %eax, %eax
    movl    $(BOOT_INIT_PGT_SIZE/4), %ecx
    rep stosl

    /* Build Level 4 */
    leal    pgtable + 0(%ebx), %edi
    leal    0x1007 (%edi), %eax
    movl    %eax, 0(%edi)

    /* Build Level 3 */
    leal    pgtable + 0x1000(%ebx), %edi
    leal    0x1007(%edi), %eax
    movl    $4, %ecx
1:  movl    %eax, 0x00(%edi)
    addl    $0x00001000, %eax
    addl    $8, %edi
    decl    %ecx
    jnz 1b

    /* Build Level 2 */
    leal    pgtable + 0x2000(%ebx), %edi
    movl    $0x00000183, %eax
    movl    $2048, %ecx
1:  movl    %eax, 0(%edi)
    addl    $0x00200000, %eax
    addl    $8, %edi
    decl    %ecx
    jnz 1b

    /* Enable the boot page tables */
    leal    pgtable(%ebx), %eax
    movl    %eax, %cr3

    /* Enable Long mode in EFER (Extended Feature Enable Register) */
    movl    $MSR_EFER, %ecx
    rdmsr
    btsl    $_EFER_LME, %eax
    wrmsr

    /* After gdt is loaded */
    xorl    %eax, %eax
    lldt    %ax
    movl    $__BOOT_TSS, %eax
    ltr %ax
    pushl   $__KERNEL_CS
    leal    startup_64(%ebp), %eax
    pushl   %eax

    /* Enter paged protected Mode, activating Long Mode */
    movl    $(X86_CR0_PG | X86_CR0_PE), %eax /* Enable Paging and Protected mode */
    movl    %eax, %cr0

    /* Jump from 32bit compatibility mode into 64bit mode. */
    lret
ENDPROC(startup_32)
從以上程式碼可看出,Linux先設定了CR4.PAE,然後再設定IA-32e.EFER.LME,最後設定CR0.PG,這個路徑正好對應上面模式轉換圖切換到IA-32e分頁模式的切換路徑,當然PAE與LME設定的先後是無影響的。

這裡寫圖片描述

上圖展示了CR3及各級頁表項的結構,這可以幫助我們瞭解頁表構建過程中為什麼需要填充那些值。有興趣的可以參照上圖的結構定義對照著去解析上面程式碼中頁表構建過程每一步的意義。
我們通過最初的觀察發現linux啟動過程中經歷了幾次控制暫存器值的變化,查閱程式碼會發現,從開啟IA-32e分頁模式後,CPU就一直工作在了這個模式,但是還有很多功能特性沒有開啟,所以在之後的程式碼中逐步配置並打開了相應的功能特性,如CR4.PGE,CR4.PSE等,正是因為這樣才出現了進入IA-32e分頁模式後控制暫存器仍然在發生變化。
對於cpu來講,進入IA-32e分頁模式後,大體的配置基本就已經完成了,但是對於linux來講還有一個最重要的步驟就是使得核心程式碼執行在核心地址空間,也就是我們所謂的核心虛擬地址空間。這關鍵的步驟其實就是通過建立相應的頁表對映結構完成的,核心程式碼實際存放的實體地址不變,但在頁表對映中將其對映到核心虛擬地址空間中,這樣在用線性地址定址是就能夠找到對應的實體地址。
以上就是Linux在平臺初始化過程中做的最重要的工作,當然還有中斷門,系統呼叫門的設定,這將在以後說到。每個平臺可能硬體特性有差別,但一些最基本的處理原則應該都是一致的,那就是記憶體定址,因為只有能夠完全操作記憶體後,我們才能依靠CPU去做更多的事。