Linux kernel boot process——從實模式(real mode)到保護模式(protected mode),再到分頁(paging)

來源:互聯網
上載者:User

        本文簡要介紹X86-32架構下的Linux kernel被boot loader(如grub)載入到記憶體後,如何從最初的實模式,切換到保護模式,並開啟分頁機制。本文不涉及boot loader如何將核心載入到記憶體,因為這是boot loader的事,跟核心自己無關(當然他們之間一定要有種事先約定的協議來溝通)。因為啟動代碼並不經常變化,所以對這部分的分析基本適用於較早的2.6.24至現在的3.0.4版本。為了簡化起見,我們主要關注不啟動PAE機制的一般情況。看這篇文章前,先確定你對實模式,保護模式及分頁機制的基本原理有瞭解。

        先來看boot loader將核心載入到什麼位置。很形象的解釋了核心在實體記憶體中的位置。

        總的來說,核心分為兩部分,arch/x86/boot目錄下的代碼被載入到實體記憶體的第一個1M空間內,即X至X + 0x8000處,這部分代碼稱為setup代碼,它是16位實模式代碼。另一部分,也是核心的主要部分被載入到實體記憶體的第一個1M空間之後,在核心完全啟動前,這部分代碼還是被壓縮的。有一點需要注意的是,載入到第一個1M空間之後的核心代碼的位移地址在連結過程中被指定為起始於0xc0100000。

        核心從x86/boot/header.S檔案中的_start標號開始運行,此時CPU處於實模式。我們可以注意到,在_start標號以前還有一段代碼,位於bootsect_start標號和_start標號之間,總長度剛好為512位元組,而且最後兩位元組為0xAA55,這段代碼就是經典的開機磁區代碼。不過現代Linux核心不再支援從開機磁區開始運行,如果你研讀這段代碼你可以發現它只是列印了bugger_off_msg處的字串,該字串提示使用者:“不再支援直接從磁碟片啟動,請安裝boot loader"。

         位於_start標號處的第一條指令是一條直接用2進位機器碼編寫的段內跳轉指令。

 112                .byte   0xeb            # short (2-byte) jump
 113                .byte   start_of_setup-1f
 114 1:

        這條指令會跳轉到後面的start_of_setup標號處,我們會看到這兩個標號之間有一大串變數,這些變數是用於核心的載入以及初始化過程的,它們的值有的由編譯核心過程中build.c工具程式寫入,有的由boot loader在載入核心時寫入,這些就是前面說的boot loader與核心之間溝通的協議。我們忽略這些值的初始化過程,並預設核心被成功載入後這些值都是可用的。

        從start_of_setup標號開始,核心進行了一些簡單的初始化,如劃分堆棧(stack)和堆(heap)空間,我們來看位於末端的幾行代碼。

 292 # Zero the bss
 293        movw    $__bss_start, %di
 294        movw    $_end+3, %cx
 295        xorl    %eax, %eax
 296        subw    %di, %cx
 297        shrw    $2, %cx
 298        rep; stosl
 299
 300 # Jump to C code (should not return)
 301        calll   main

        292-298行代碼用於將.bbs段初始化為0。我們知道,在普通應用程式開發的概念中,來源程式中未初始化的全域變數被放在.bss段,這個段中的資料會在程式被載入器(loader,注意,這裡不是指前面說的boot loader,而是用來將普通應用程式載入入作業系統中啟動並執行載入器)載入記憶體時初始化為0,可是我們現在研究的是作業系統核心代碼,這個時候連作業系統都沒啟動,更不用說載入器了,所以我們需要在這裡自己初始化.bss資料。這裡的兩個__bss_start和_end標號並不是定義在代碼中,而是定義在x86/boot/setup.ld的串連指令碼中。
        300行的call指令使核心跳轉到x86/boot/main.c中的void main(void)函數。不錯,我們進入了C代碼,不過這裡還是以實模式運行C代碼。main函數中還調用了其它一些初始化函數,我們重點關注main函數調用的最後一個函數go_to_protected_mode(),顧名思義,此函數使核心切換到保護模式。該函數位於x86/boot/pm.c檔案中,它在調用enable_a20()開啟A20地址線後,依次調用了如下三個函數:

 122        setup_idt();
 123        setup_gdt();
 124        protected_mode_jump(boot_params.hdr.code32_start,
 125                            (u32)&boot_params + (ds() << 4));

        這三個函數依次構建IDT表,GDT表,以及切換到保護模式。下面是setup_gdt()函數:

  66 static void setup_gdt(void)
  67 {
  68        /* There are machines which are known to not boot with the GDT
  69           being 8-byte unaligned.  Intel recommends 16 byte alignment. */
  70        static const u64 boot_gdt[] __attribute__((aligned(16))) = {
  71                /* CS: code, read/execute, 4 GB, base 0 */
  72                [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
  73                /* DS: data, read/write, 4 GB, base 0 */
  74                [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
  75                /* TSS: 32-bit tss, 104 bytes, base 4096 */
  76                /* We only have a TSS here to keep Intel VT happy;
  77                   we don't actually use it for anything. */
  78                [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
  79        };
  80        /* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead
  81           of the gdt_ptr contents.  Thus, make it static so it will
  82           stay in memory, at least long enough that we switch to the
  83           proper kernel GDT. */
  84        static struct gdt_ptr gdt;
  85
  86        gdt.len = sizeof(boot_gdt)-1;
  87        gdt.ptr = (u32)&boot_gdt + (ds() << 4);
  88
  89        asm volatile("lgdtl %0" : : "m" (gdt));
  90 }

        此段代碼很簡單,它初始化了一個boot_gdt數組,也就是傳說中的GDT表,然後初始化了一個gdt變數,該變數指定了GDT表的物理地址與長度,最後通過彙編指令lgdtl將GDT表地址與長度載入到GDTR寄存器。顯然,這時核心只定義了三個段,一個程式碼片段,一個資料區段,一個TSS段。程式碼片段和資料區段都被定義為從0x00000000開始的4G線性地址空間。

        有個細節需要注意,在87行指定GDT表物理地址時加上了”額外“的ds()<<4,這是因為到目前為止,核心依然運行在實模式,CPU還是通過”段基址*16+位移地址“來定址,&boot_gdt返回的是boot_gdt數組頭在目前資料區段的位移,在加上"ds()<<4"後,得到boot_gdt數組頭的線性地址,而且,由於沒有開啟分頁機制,線性地址等於物理地址。

       在GDT表初始化完成後,核心調用protected_mode_jump()函數切換到保護模式,這個函數位於x86/boot/pmjump.S中,不錯,這又是一段彙編代碼,此後一段時間內,核心都會在彙編代碼與C代碼直接來回跳轉。代碼如下:

  26 GLOBAL(protected_mode_jump)
  27        movl    %edx, %esi              # Pointer to boot_params table
  28
  29        xorl    %ebx, %ebx
  30        movw    %cs, %bx
  31        shll    $4, %ebx
  32        addl    %ebx, 2f
  33        jmp     1f                      # Short jump to serialize on 386/486
  34 1:
  35
  36        movw    $__BOOT_DS, %cx
  37        movw    $__BOOT_TSS, %di
  38
  39        movl    %cr0, %edx
  40        orb     $X86_CR0_PE, %dl        # Protected mode
  41        movl    %edx, %cr0
  42
  43        # Transition to 32-bit mode
  44        .byte   0x66, 0xea              # ljmpl opcode
  45 2:      .long   in_pm32                 # offset
  46        .word   __BOOT_CS               # segment
  47 ENDPROC(protected_mode_jump)

        在分析代碼前先介紹下這裡彙編調用C語言函數時的參數傳遞機制,我們知道普通的函數調用規範(std,cdecl)採用堆棧傳遞參數,即從右往坐依次將參數壓入堆棧。而在這裡,核心中arch/boot下的代碼採用另外一種規範(fastcall):三個及三個以內的參數從左往右依次通過EAX,EDX,ECX寄存器傳遞參數,多於三個的參數通過堆棧傳遞。

        這樣,結合go_to_protected_mode()函數中的代碼,我們知道在protected_mode_jump()函數中,%eax寄存器中的值為boot_params.hdr.code32_start,%edx中的值為&boot_params+ds()<<4。這裡比較重要的是boot_params.hdr.code32_start參數,該參數就是前面提到的被壓縮的部分核心代碼的起始地址。心細的讀者可能猜到,該boot_params.hdr.code32_start就是在x86/boot/header.S中的code32_start變數,對,前面提到的main函數會將boot/header.S中從hdr標號處開始的變數拷貝的boot_params.hdr中,不過這個值不再是0,因為在核心被boot
loader載入入記憶體後,boot_loader會將該值修改為被壓縮核心真正所在的記憶體位置。

       進入protected_mode_jump()函數後,29-32行代碼做了一件非常”詭異“的事,它將%cs寄存器的值左移4位後加到位於標號2的變數處,而該變數之前儲存的的in_pm32標號的位移,這樣,在加上%cs<<4後,該處的變數就變成了in_pm32的線性地址了。這段代碼的作用在後面顯現。先看39-41行的代碼,該代碼設定了%cr0寄存器中的PE標誌,即開啟了保護模式的標誌。

        隨即,高潮來了,44-46行跟前面boot/header.S中的_start標號處的跳轉指令一樣,也是直接用2進位編寫的跳轉指令,不過這裡的是段間跳轉指令,45行的變數指定的是目的地址的位移,由於29-32行代碼的作用,該位移等於in_pm32標號在實模式下的線性地址,事實上,我們知道線性地址可以看做段基址為0的位移地址,bingo!,根據前面初始化的GDT表,46行的__BOOT_CS選擇符非常”巧合“的指定目的地址的段基址正好是0,所以此條段間跳轉指令”碰巧“跳轉到in_pm32標號處!所以,從現在開始核心開始在保護模式下運行,並跳轉到in_pm32處。

        in_pm32中最後的指令為:

 76        jmpl    *%eax                   # Jump to the 32-bit entrypoint

        該指令使核心跳轉到%eax指定的代碼處,該代碼位於x86/boot/compressed/head_32.S中的startup_32標號處。該程式碼片段的主要作用是將記憶體中的被壓縮核心解壓縮,然後跳轉到x86/kernel/head_32.S中的startup_32標號處。不錯,有兩個startup_32,不過這兩個標號並不衝突,因為這兩部分代碼並不是同時連結。在這段代碼中,核心會建立一個臨時核心頁表(provisional kernel page tables),並開啟分頁機制。

 202page_pde_offset = (__PAGE_OFFSET >> 20);
 203
 204        movl $pa(__brk_base), %edi
 205        movl $pa(initial_page_table), %edx
 206        movl $PTE_IDENT_ATTR, %eax
 207 10:
 208        leal PDE_IDENT_ATTR(%edi),%ecx          /* Create PDE entry */
 209        movl %ecx,(%edx)                        /* Store identity PDE entry */
 210        movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */
 211        addl $4,%edx
 212        movl $1024, %ecx
 213 11:
 214        stosl
 215        addl $0x1000,%eax
 216        loop 11b
 217        /*
 218         * End condition: we must map up to the end + MAPPING_BEYOND_END.
 219         */
 220        movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
 221        cmpl %ebp,%eax
 222        jb 10b
 223        addl $__PAGE_OFFSET, %edi
 224        movl %edi, pa(_brk_end)
 225        shrl $12, %eax
 226        movl %eax, pa(max_pfn_mapped)

        此代碼構造的頁表將分別映射從線性地址0x00000000和__PAGE_OFFSET開始的線性空間,並將這兩部分線性空間映射到同一物理空間。先介紹代碼中關鍵宏和標號的定義。

        1)宏__PAGE_OFFSET預設定義為0xc0000000,從__PAGE_OFFSET開始的線性地址空間即為核心空間。

        2)202行的宏page_pde_offset定義為虛擬位址__PAGE_OFFSET對應的page directory entry,這樣看可能更清楚些:page_pde_offset = (((__PAGE_OFFSET) >> (10 + 12)) << 2)。

        3)__brk_base標號為頁表的起始地址,跟前面提到的__bss_start類似,該標號由kernel/vmlinux.lds連結指令碼定義。

        4)inital_page_table標號定義在kernel/head_32.S檔案的後面,後面預留了4K空間,它被用作整個臨時頁表的頁目錄表(page directory table)。

        5)宏PTE_IDENT_ATTR和PDE_IDENT_ATTR分別為頁表項和頁目錄項中的頁面屬性。

        6)宏pa()就是著名的核心空間線性地址到物理地址的轉換函式,實際上等同於以下定義:#define pa(X) ((x) - __PAGE_OFFSET)。當然還有一個相反的轉換函式。

                此宏的作用是讓該部分核心代碼在開啟分頁前能正確訪問記憶體。前面提到,這部分核心被載入到實體記憶體第一個1M空間之後,但它的位移地址在連結過程中被指定為起始於0xc0100000。所以在開啟分頁前,核心代碼訪問記憶體需要將各個標號地址減去0xc00000000。實際上,我們可以將pa()函數看作一個簡單的分頁機制。

        7)MAPPING_BEYOND_END  + pa(_end)為構造的頁表所要映射的物理空間的結束位址。

        下面是代碼的詳細分析。

        204-205行,將%edi初始化為第一張頁表的物理地址,將%edx初始化為頁目錄表中第一個頁目錄項的物理地址。

        208行,在%ecx中產生中構造一個頁目錄項,其中頁表地址欄位為%edi的值,屬性欄位為PDE_IDENT_ATTR

        209行,將%ecx中的頁目錄項填充到%edx指向的的頁目錄項中,顯然這是從線性地址0x00000000開始映射實體記憶體。

        210行,將同樣的%ecx中的頁目錄項填充到(page_pde_offset + %edx)指向的頁目錄項中,顯然這是從線性地址__PAGE_OFFSET開始映射實體記憶體,可見兩部分線性空間映射到同一物理空間。

        211行,將%edx加4,即將其指向下一個頁目錄項。

        212-216行,此迴圈填充%edi指向的頁表。206行將%eax初始化為頁表項屬性值PTE_IDENT_ATTR。212行指定此迴圈迭代1024次。215行則在每次迭代填充一個頁表項後將%eax中頁表項的物理頁地址欄位加0x1000,顯然此迴圈由低地址到高地址依次映射物理頁。此迴圈結束後%edi剛好為下一張頁表的物理地址。

        220-222行,如果填充的最後一個頁表項小於$pa(_end) + MAPPING_BEYOND_END + PTE _IDENT_ATTR,則回到10標號處再構建一張頁表,否則映射結束。

        223-224行,將_brk_end標號處的變數設定為整個頁表的末端線性地址。

        225-226行,將max_pfn_mapped標號處的變數設定為此頁表所映射的物理頁總數。

        到這裡,臨時頁表就構造完畢了,核心之後會構造一個最終的頁表(final kernel page tables),此頁表在x86/mm/init_32.c中的kernel_physical_mapping_init()函數中構造。主要代碼如下,刪除了部分我們不關注的代碼。

 279repeat:
 280        pages_2m = pages_4k = 0;
 281        pfn = start_pfn;
 282        pgd_idx = pgd_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);
 283        pgd = pgd_base + pgd_idx;
 284        for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
 285                pmd = one_md_table_init(pgd);
 286
 287                if (pfn >= end_pfn)
 288                        continue;
 293                pmd_idx = 0;
 295                for (; pmd_idx < PTRS_PER_PMD && pfn < end_pfn;
 296                     pmd++, pmd_idx++) {
 297                        unsigned int addr = pfn * PAGE_SIZE + PAGE_OFFSET;
 298
 330                        pte = one_page_table_init(pmd);
 331
 332                        pte_ofs = pte_index((pfn<<PAGE_SHIFT) + PAGE_OFFSET);
 333                        pte += pte_ofs;
 334                        for (; pte_ofs < PTRS_PER_PTE && pfn < end_pfn;
 335                             pte++, pfn++, pte_ofs++, addr += PAGE_SIZE) {
 336                                pgprot_t prot = PAGE_KERNEL;
 337                                /*
 338                                 * first pass will use the same initial
 339                                 * identity mapping attribute.
 340                                 */
 341                                pgprot_t init_prot = __pgprot(PTE_IDENT_ATTR);
 342
 343                                if (is_kernel_text(addr))
 344                                        prot = PAGE_KERNEL_EXEC;
 345
 346                                pages_4k++;
 347                                if (mapping_iter == 1) {
 348                                        set_pte(pte, pfn_pte(pfn, init_prot));
 349                                        last_map_addr = (pfn << PAGE_SHIFT) + PAGE_SIZE;
 350                                } else
 351                                        set_pte(pte, pfn_pte(pfn, prot));
 352                        }
 353                }
 354        }
 355        if (mapping_iter == 1) {
 363                /*
 364                 * local global flush tlb, which will flush the previous
 365                 * mappings present in both small and large page TLB's.
 366                 */
 367                __flush_tlb_all();
 368
 369                /*
 370                 * Second iteration will set the actual desired PTE attributes.
 371                 */
 372                mapping_iter = 2;
 373                goto repeat;
 374        }

        Linux將分頁機制抽象為四層模型,PGD(page global directory),PUD(page upper directory),PMD(page middle directory)和PT(page table)。32位架構核心在不開啟PAE情況下只需要兩層模型,PGD和PT。此時,PUD和PMD被摺疊(folded),即PGD,PUD和PMD三者相同。在詳細解釋代碼前,先介紹幾個關鍵宏和函數。

       1)宏pgd_index(vaddr)返回線性地址vaddr對應的pgd目錄項的地址。

       2)函數one_md_table_init(pgd)建立並返回pgd目錄項對應的下一級PMD表地址,在32位架構核心中,此函數只是簡單返回pgd,即PMD表被摺疊入PGD表。

       3)函數one_page_table_init(pmd)建立並返回pmd目錄項對應的下一級PT表地址。

       4)函數set_pmd(pmd, pmde)將pmd目錄項填充為pmde。

       5)函數set_pte(pte, ptee)將pte表項填充為ptee。

       6)宏PTRS_PER_PGD,PTRS_PER_PMD和PTRS_PER_PTE分別對應PGD表,PMD表和PT表中的項數。在32位核心架構下這三個宏分別定義為1024,1,1024。

       此段代碼將從第pfn號物理頁開始的物理空間映射到從線性地址__PAGE_OFFSET開始的線性空間。頁表構造過程經過兩次迭代,兩次迭代除了填充的頁表項的屬性欄位不同,其它都相同。

       281-283行將pgd_idx初始化第pfn號物理頁對應的PGD目錄項指標。

       285行本是建立並返回pgd目錄項對應的PMD表pmd,不過在32位架構下,此函數只是簡單返回pgd,即pmd等於pgd。

       295行的迴圈在32位架構下只迭代一次。

       330行建立並返回pmd目錄項對應的PT表pte。

       332-333行將pte設定為第pfn號物理頁對應的PT表項。

       334行開始的迴圈則依次填充pte及其之後的PT表項。

       

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.