git clone https://gitlab.freedesktop.org/gnu-grub/grub.git
目的
前回GRUBがどうやってメニュー画面を描画しているかを調べると、最終的にint 0x10を呼び出していることが分かりました。
GRUBのメニュー画面がどう描画されているか調べる
今回はCのソースからどうやってBIOSの表示機能を呼び出しているのかに焦点を当てて調べました。
int10_9関数
ch :表示したいASCII 文字コード
n :その文字を何回繰り返して表示するか
static grub_uint8_t grub_console_cur_color = 0x7;
static void
int10_9 (grub_uint8_t ch, grub_uint16_t n)
{
struct grub_bios_int_registers regs;
regs.eax = ch | 0x0900; // AH=0x09 AL=表示する文字コード
regs.ebx = grub_console_cur_color & 0xff; // 文字属性(黒背景、白文字)
regs.ecx = n; // 繰り返し回数
regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
grub_bios_interrupt (0x10, ®s);
}
#define GRUB_CPU_INT_FLAGS_INTERRUPT 0x200
#ifdef GRUB_MACHINE_PCBIOS
#define GRUB_CPU_INT_FLAGS_DEFAULT GRUB_CPU_INT_FLAGS_INTERRUPT
#else
#define GRUB_CPU_INT_FLAGS_DEFAULT 0
レジスタ群構造体(grub_bios_int_registers)
struct grub_bios_int_registers
{
grub_uint32_t eax;
grub_uint16_t es;
grub_uint16_t ds;
grub_uint16_t flags;
grub_uint16_t dummy;
grub_uint32_t ebx;
grub_uint32_t ecx;
grub_uint32_t edi;
grub_uint32_t esi;
grub_uint32_t edx;
};
アセンブリからint 0x10の呼び出し
32bitモード → 16bitモードでint実行 → 32bitモード
#ifdef GRUB_MACHINE_PCBIOS
void EXPORT_FUNC (grub_bios_interrupt) (grub_uint8_t intno,
struct grub_bios_int_registers *regs)
__attribute__ ((regparm(3)));
extern struct grub_i386_idt *EXPORT_VAR(grub_realidt);
#endif
regparm(3)がついているので0x10 は %al に、®sは%edxに入ってアセンブリに渡されます。
BIOSを呼ぶ前後のレジスタの保存処理は筆者も完全には理解できていません。
BIOSが残した値を保存したりもしているようですがint 0x10では書き込むだけでBIOSの値は使わないので無視します。
ややこしいですがやっていることは、一時的に16ビットへ移行し、C言語で設定したregsの値を各レジスタに格納してBIOS機能を呼び出したあと32ビットに戻しているだけです。
FUNCTION(grub_bios_interrupt)
pushf
cli
popf
pushl %ebp
pushl %ecx
pushl %eax
pushl %ebx
pushl %esi
pushl %edi
pushl %edx
movb %al, intno # int XのXを保存
movl (%edx), %eax # ®sから32ビットの値(regs->eax)を取り出してEAXへ格納
movl %eax, LOCAL(bios_register_eax) # bios_register_eaxへregsreg->eaxを保存
movw 4(%edx), %ax
movw %ax, LOCAL(bios_register_es) # bios_register_esへregs->esを保存
movw 6(%edx), %ax
movw %ax, LOCAL(bios_register_ds) # bios_register_dsへregs->dsを保存
movw 8(%edx), %ax
movw %ax, LOCAL(bios_register_flags)# bios_register_flagsへregs->flagsを保存
movl 12(%edx), %ebx # edx = reg->edx
movl 16(%edx), %ecx # ecx = reg->ecx
movl 20(%edx), %edi # edi = reg->edi
movl 24(%edx), %esi # esi = reg->esi
movl 28(%edx), %edx # edx = reg->edx
/*
Via C3 CPUs have cache coherence problems, so we need to call
wbinvd at these 2 points. As wbinvd slows down boot, don't do
it on non-VIA. 9090 is nop nop. */
VARIABLE(grub_bios_via_workaround1)
.byte 0x90, 0x90
PROT_TO_REAL # 16bitモードへ切り替え
.code16
pushf
cli
mov %ds, %ax
push %ax
/* movw imm16, %ax*/
.byte 0xb8
LOCAL(bios_register_es): # ES復元
.short 0
movw %ax, %es
/* movw imm16, %ax*/
.byte 0xb8
LOCAL(bios_register_ds):
.short 0
movw %ax, %ds # DS復元
/* movw imm16, %ax*/
.byte 0xb8
LOCAL(bios_register_flags): # FLAGS復元
.short 0
push %ax
popf
/* movl imm32, %eax*/
.byte 0x66, 0xb8
LOCAL(bios_register_eax): # EAX復元(作業に使うから最後に復元)
.long 0
/* int imm8. */
.byte 0xcd # ここで各レジスタにはC側で設定したregsが入っているのでint Xを呼ぶ
intno: # BIOS機能番号(int XのX)。intnoは上で書き換えている
.byte 0
movl %eax, %cs:LOCAL(bios_register_eax)
movw %ds, %ax
movw %ax, %cs:LOCAL(bios_register_ds)
pop %ax
mov %ax, %ds
pushf
pop %ax
movw %ax, LOCAL(bios_register_flags)
mov %es, %ax
movw %ax, LOCAL(bios_register_es)
popf
VARIABLE(grub_bios_via_workaround2)
.byte 0x90, 0x90
REAL_TO_PROT # 32bitモードへ戻る
.code32
popl %eax
movl %ebx, 12(%eax)
movl %ecx, 16(%eax)
movl %edi, 20(%eax)
movl %esi, 24(%eax)
movl %edx, 28(%eax)
movl %eax, %edx
movl LOCAL(bios_register_eax), %eax
movl %eax, (%edx)
movw LOCAL(bios_register_es), %ax
movw %ax, 4(%edx)
movw LOCAL(bios_register_ds), %ax
movw %ax, 6(%edx)
movw LOCAL(bios_register_flags), %ax
movw %ax, 8(%edx)
popl %edi
popl %esi
popl %ebx
popl %eax
popl %ecx
popl %ebp
ret
自分でC言語からint 0x10を呼ぶ実験
ソースコードを読むだけでは完全に理解できないので自分でも同じように実装してみます。
GRUBのレジスタ構造体をそのまま使ってC言語でレジスタ値を設定し、int 0x10を呼び出します。
nasm -f bin boot.asm -o boot.bin
nasm -f elf32 entry.asm -o entry.o
gcc -m32 -ffreestanding -c term.c -o term.o
nasm -f elf32 int10.asm -o int10.o
ld -m elf_i386 -T linker.ld -o kernel.elf entry.o term.o int10.o
objcopy -O binary kernel.elf kernel.bin
cat boot.bin kernel.bin > disk.img
qemu-system-i386 -drive format=raw,file=disk.img -monitor stdio
boot.asm
[org 0x7C00]
bits 16
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
sti
; --- INT 13h CHS 読み込み ---
mov ah, 0x02 ; BIOS: Read Sectors
mov al, 1 ; 読み込むセクタ数 (1)
mov ch, 0 ; シリンダ = 0
mov dh, 0 ; ヘッド = 0
mov cl, 2 ; セクタ = 2 (ブートローダの次)
mov dl, 0x80 ; ドライブ番号=HDD
mov bx, 0x7E00 ; ES:BX = 読み込み先
int 0x13
jc disk_error
jmp 0x0000:0x7E00 ; 読み込んだコードへjmp
disk_error:
hlt
jmp disk_error
times 510-($-$$) db 0
dw 0xAA55
entry.asm
bits 16
global start
extern kernel_main
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
mov ax, 0x0003
int 0x10
; VGA mode3
mov ax, 0xB800
mov es, ax
; GDTロード
lgdt [gdt_descriptor]
; プロテクトモード有効化
mov eax, cr0
or eax, 1
mov cr0, eax
; 32ビットコードへ移行
jmp 0x08:pm_start
; -----------------------------
gdt_start:
dq 0x0000000000000000 ; NULL
gdt_code:
dw 0xFFFF ; limit 0-15
dw 0x0000 ; base 0-15
db 0x00 ; base 16-23
db 0x9A ; code segment
db 0xCF ; flags
db 0x00 ; base 24-31
gdt_data:
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92 ; data segment
db 0xCF
db 0x00
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
; -----------------------------
[bits 32]
pm_start:
; データセグメント設定
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x9FC00
call kernel_main
.hlt_loop:
hlt
jmp .hlt_loop
linker.ld
ENTRY(start)
SECTIONS {
. = 0x7E00;
.text : {
*(.text*)
}
.rodata : {
*(.rodata*)
}
.data : {
*(.data*)
}
.bss : {
*(.bss*)
*(COMMON)
}
}
typedef unsigned char grub_uint8_t;
typedef unsigned short grub_uint16_t;
typedef unsigned int grub_uint32_t;
static grub_uint8_t grub_console_cur_color = 0x7;
struct grub_bios_int_registers
{
grub_uint32_t eax; // オフセット 0
grub_uint16_t es; // オフセット 4
grub_uint16_t ds; // オフセット 6
grub_uint16_t flags; // オフセット 8
grub_uint16_t dummy; // オフセット 10
grub_uint32_t ebx; // オフセット 12
grub_uint32_t ecx; // オフセット 16
grub_uint32_t edi; // オフセット 20
grub_uint32_t esi; // オフセット 24
grub_uint32_t edx; // オフセット 28
} __attribute__((packed));
/* Sファイル側の関数宣言 */
void int10(struct grub_bios_int_registers *regs)
__attribute__((regparm(3)));
void kernel_main(){
struct grub_bios_int_registers regs;
//Cを100回表示する
regs.eax = 'C' | 0x0900;
regs.ebx = grub_console_cur_color & 0xff;
regs.ecx = 100;
int10(®s); // 汎用的ではなく0x10に限定する為、引数は1つで良い
for(;;); // ここへ戻ってくることはない
}
global int10
bits 32
int10:
pushf
cli
; regparm(3) によりEAXに®sが入っている
mov edi, eax ; EAXは後続処理に使用する為EDIに保存する
; 構造体の順番で入っているのでオフセットをずらしながら取得
mov eax, [edi] ; regs->eax
mov [bios_register_eax], eax
mov eax, [edi + 12] ; regs->ebx
mov [bios_register_ebx], eax
mov eax, [edi + 16] ; regs->ecx
mov [bios_register_ecx], eax
; ブートローダで設定したGDTを入れ替える
lgdt [gdt_descriptor]
; 16ビットデータセグメントを設定する 0x20 = GDT[4]
; D=0なので命令を16ビットととして実行する
mov ax, 0x20
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; CSを16ビットコードセグメントに変更する 0x18 = GDT[3]
jmp 0x18:tmpcseg
bits 16
tmpcseg:
; PE = 0
mov eax, cr0
and eax, 0xFFFFFFFE
mov cr0, eax
; 16bitモードへ突入
jmp 0:real_mode
real_mode:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov fs, ax
mov gs, ax
mov sp, 0x7C00
; レジスタにregsで設定した値を設定
mov eax, [bios_register_eax]
mov ebx, [bios_register_ebx]
mov ecx, [bios_register_ecx]
int 0x10 ; BIOS 呼び出し
hang: ; スタック復元がややこしいので、今回はCには戻らずここで止める
cli
hlt
loop hang
section .data
; C側で設定したregsを退避する場所
bios_register_eax:
dd 0
bios_register_ebx:
dd 0
bios_register_ecx:
dd 0
; ==================================================
; GDT
; ==================================================
align 8
gdt_start:
dq 0
; GDT[1] 32-bit code
dw 0xFFFF
dw 0x0000
db 0x00
db 0x9A
db 0xCF
db 0x00
; GDT[2] 32-bit data
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92
db 0xCF
db 0x00
; GDT[3] 16-bit code
dw 0xFFFF
dw 0x0000
db 0x00
db 0x9E
db 0x00
db 0x00
; GDT[4] 16-bit data
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92
db 0x00
db 0x00
gdt_end:
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start