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 にロードする操作を行う。