Linux Boot Sequence を読む ( 3 ) ~ GRUB Core 2 ~

February 18, 2018

core.img (続き)

diskboot モジュールで “残りの自分自身” を読み込む処理は終わったので、ここからは real mode -> protect mode の移行のあたりを読む。
まだまだ旅は長い…。

startup_raw.S

0x08200 にロードされるプログラムは startup_raw.S であり、ソースファイルは grub-core/boot/i386/pc/startup_raw.S となる。

#define ABS(x)  ((x) - LOCAL (base) + GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200)  
  
        .file   "startup_raw.S"  
  
        .text  
  
        /* Tell GAS to generate 16-bit instructions so that this code works                                                                                                                                                                     
           in real mode. */  
        .code16  
  
        .globl  start, _start  
start:  
_start:  
LOCAL (base):  
        /*                                                                                                                                                                                                                                      
         *  Guarantee that "main" is loaded at 0x0:0x8200.                                                                                                                                                                                      
         */  
#ifdef __APPLE__  
        ljmp $0, $(ABS(LOCAL (codestart)) - 0x10000)  
#else  
        ljmp $0, $ABS(LOCAL (codestart))  
#endif  

いきなり codestart にジャンプする。
ABS はアセンブリ時に codestart のアドレスに 0x08200 を加えるマクロ。codestart の手前まではデータ構造体の領域があり、ここを飛び越えるためにジャンプしている。

/* the real mode code continues... */  
LOCAL (codestart):  
        cli             /* we're not safe here! */  
  
        /* set up %ds, %ss, and %es */  
        xorw    %ax, %ax  
        movw    %ax, %ds  
        movw    %ax, %ss  
        movw    %ax, %es  
  
        /* set up the real mode/BIOS stack */  
        movl    $GRUB_MEMORY_MACHINE_REAL_STACK, %ebp  
        movl    %ebp, %esp  
  
        sti             /* we're safe again */  
  
        /* save the boot drive */  
        movb    %dl, LOCAL(boot_drive)  
  
        /* reset disk system (%ah = 0) */  
        int     $0x13  
  
        /* transition to protected mode */  
	calll   real_to_prot  
  
        /* The ".code32" directive takes GAS out of 16-bit mode. */  
        .code32  
  
       ...(中略)...  
  
#include "../../../kern/i386/realmode.S"  
  
LOCAL(boot_drive):  
        .byte   0x00   

まず、DS SS ES、3 つのセグメントレジスタを初期化している。
DL レジスタには core.img の先頭部分を読み込んだドライブ番号が格納されており、これを boot_drive に保存している。
その後、int 命令で Reset Disk Drive を実行 (参照) し、real_to_prot で CPU をリアルモードからプロテクトモードに移行している。
real_to_prot のコードは realmode.S に書かれている。

        /* The ".code32" directive takes GAS out of 16-bit mode. */  
        .code32  
  
        cld  
        call    grub_gate_a20  

realtoprot から戻ってきた CPU は 32 bit で動作している。
grubgatea20 は 1 MB 以降のメモリ領域にアクセスできるようにするために A20 を有効にする。

post_reed_solomon:  
  
#ifdef ENABLE_LZMA  
        movl    $GRUB_MEMORY_MACHINE_DECOMPRESSION_ADDR, %edi  
#ifdef __APPLE__  
        movl    $decompressor_end, %esi  
#else  
        movl    $LOCAL(decompressor_end), %esi  
#endif  
        pushl   %edi  
        movl    LOCAL (uncompressed_size), %ecx  
        leal    (%edi, %ecx), %ebx  
        /* Don't remove this push: it's an argument.  */  
        push    %ecx  
        call    _LzmaDecodeA  
        pop     %ecx  
        /* _LzmaDecodeA clears DF, so no need to run cld */  
        popl    %esi  
#endif  
  
        movl    LOCAL(boot_dev), %edx  
        movl    $prot_to_real, %edi  
        movl    $real_to_prot, %ecx  
        movl    $LOCAL(realidt), %eax  
        jmp     *%esi  
  
#ifdef ENABLE_LZMA  
#include "lzma_decode.S"  
#endif  
  
        .p2align 4  
  
#ifdef __APPLE__  
        .zerofill __DATA, __aa_before_bss, decompressor_end, 10, 0  
#else  
        .bss  
LOCAL(decompressor_end):  

“LOCAL(decompressor_end)” は startup_raw.S の最終行を示すラベルで、この後ろに圧縮された GRUB カーネルのデータが続いている。
その前の処理 (popl %esi まで) は、GRUB_MEMORY_MACHINE_DECOMPRESSION_ADDR = 0x100000 以降に圧縮されたデータを解凍する処理を行っている。

realmode.S

ここでは startup_raw.S で呼ばれた real_to_prot を中心に読む。

gdtdesc:  
        .word   0x27                    /* limit */  
        .long   gdt                     /* addr */  
  
... (中略) ...  
  
real_to_prot:  
        .code16  
        cli  
  
        /* load the GDT register */  
        xorw    %ax, %ax  
        movw    %ax, %ds  
#ifdef GRUB_MACHINE_QEMU  
        /*                                                                                                                                                                                                                                      
        qemu is special: gdtdesc is in ROM.                                                                                                                                                                                                     
        %cs = 0xf000                                                                                                                                                                                                                            
        _start + GRUB_BOOT_MACHINE_SIZE = 0x100000                                                                                                                                                                                              
        So                                                                                                                                                                                                                                      
        _start + GRUB_BOOT_MACHINE_SIZE - 0x10000 points to the same point                                                                                                                                                                      
        as %cs.                                                                                                                                                                                                                                 
        gdtdesc - (_start + GRUB_BOOT_MACHINE_SIZE - 0x10000)                                                                                                                                                                                   
        = gdtdesc - _start - GRUB_BOOT_MACHINE_SIZE + 0x10000                                                                                                                                                                                   
        but the later can be computed by assembly.                                                                                                                                                                                              
        */  
        lgdtl   %cs:(gdtdesc - _start - GRUB_BOOT_MACHINE_SIZE + 0x10000)  
#else  
        lgdtl   gdtdesc  
#endif  

CPU をプロテクトモードで動作させるためには、GDT (Global Descriptor Table) と IDT (Interrupt Descriptor Table) が必須となる。
これらのテーブルは CPU がプロテクトモードに移行してからではなく、リアルモードで動作している間に作成する必要があるが、cli 命令で割り込みを禁止しているため、IDT の設定は後回しとなっている。
ちなみに GDT とは全てのプログラムから共通にアクセスするセグメントを定義するテーブルのこと。

lgdtl gdtdesc で CPU に GDT をロードしているが、gdt は以下で定義されている。

gdt:  
        .word   0, 0  
        .byte   0, 0, 0, 0  
  
        /* -- code segment --                                                                                                                                                                                                                   
         * base = 0x00000000, limit = 0xFFFFF (4 KiB Granularity), present                                                                                                                                                                      
         * type = 32bit code execute/read, DPL = 0                                                                                                                                                                                              
         */  
        .word   0xFFFF, 0  
        .byte   0, 0x9A, 0xCF, 0  
  
        /* -- data segment --                                                                                                                                                                                                                   
         * base = 0x00000000, limit 0xFFFFF (4 KiB Granularity), present                                                                                                                                                                        
         * type = 32 bit data read/write, DPL = 0                                                                                                                                                                                               
         */  
        .word   0xFFFF, 0  
        .byte   0, 0x92, 0xCF, 0  
  
        /* -- 16 bit real mode CS --                                                                                                                                                                                                            
         * base = 0x00000000, limit 0x0FFFF (1 B Granularity), present                                                                                                                                                                          
         * type = 16 bit code execute/read only/conforming, DPL = 0                                                                                                                                                                             
         */  
        .word   0xFFFF, 0  
        .byte   0, 0x9E, 0, 0  
  
        /* -- 16 bit real mode DS --                                                                                                                                                                                                            
         * base = 0x00000000, limit 0x0FFFF (1 B Granularity), present                                                                                                                                                                          
         * type = 16 bit data read/write, DPL = 0                                                                                                                                                                                               
         */  
        .word   0xFFFF, 0  
        .byte   0, 0x92, 0, 0  

上記では 5 つのセグメントが定義されている。

        /* turn on protected mode */  
        movl    %cr0, %eax  
        orl     $GRUB_MEMORY_CPU_CR0_PE_ON, %eax  
        movl    %eax, %cr0  
  
        /* jump to relocation, flush prefetch queue, and reload %cs */  
        ljmpl   $GRUB_MEMORY_MACHINE_PROT_MODE_CSEG, $protcseg  
  
        .code32  
protcseg:  
        /* reload other segment registers */  
        movw    $GRUB_MEMORY_MACHINE_PROT_MODE_DSEG, %ax  
        movw    %ax, %ds  
        movw    %ax, %es  
        movw    %ax, %fs  
        movw    %ax, %gs  
        movw    %ax, %ss  

リアルモードからプロテクトモードに移行するには CR0 レジスタの最下位ビットを 1 に設定する必要があるが、まずはこれを実行している。
Intel CPU のお作法として、プロテクトモードに移行した直後の命令は必ず JMP 命令か CALL 命令を実行することになっている。
その後、各セグメントレジスタにデータ用セグメントをロードしている。

        /* put the return address in a known safe location */  
        movl    (%esp), %eax  
        movl    %eax, GRUB_MEMORY_MACHINE_REAL_STACK  
  
        /* get protected mode stack */  
        movl    protstack, %eax  
        movl    %eax, %esp  
        movl    %eax, %ebp  
  
        /* get return address onto the right stack */  
        movl    GRUB_MEMORY_MACHINE_REAL_STACK, %eax  
        movl    %eax, (%esp)  
  
        /* zero %eax */  
        xorl    %eax, %eax  

上記はプロテクトモード用のスタックに切り替える処理を行っている。
protstack にはプロテクトモード用のスタックアドレスが書かれている。
現時点でのスタックの先頭には、リアルモードだったときに real_to_plot を呼び出したサブルーチンへの戻りアドレスが積まれている状態となっている。
このデータをなくすと呼び出し元に戻れなくなるため、一度 GRUB_MEMORY_MACHINE_REAL_STACK = 0x2000 - 0x10 = 0x1ff0 にコピーしておく。
スタックポインタ (%esp) とベースポインタ (%ebp) を書き換えたあと、スタックポインタの先頭にコピーしておいた値を書き込むことで、ret 命令で呼び出し元に戻ることができる。

LOCAL(realidt):  
        .word 0x400  
        .long 0  
protidt:  
        .word 0  
        .long 0  
  
... (中略) ...  
  
        sidt LOCAL(realidt)  
        lidt protidt  
  
        /* return on the old (or initialized) stack! */  
        ret  

sdit 命令は現在の IDT レジスタの内容をオペランドで指定する場所に書き出す命令。プロテクトモードからリアルモードに戻る時のために現在の値を保存している。
lidt 命令は IDT を CPU にロードする操作を行う。


 © 2023, Dealing with Ambiguity