core.img
今回は GRUB Core を読む。前回 MBR 直後の 1 セクタ分を 0x8000 にコピーしており、MBR の最後で 0x8000 にジャンプしている。
つまり、0x8000 に core.img の最初の 512 バイトのみを読み込んだ状態であり、それは diskboot モジュールである。
また、core.img には圧縮された GRUB カーネルが含まれており、こいつを展開する必要もある。
よって、以下のタスクを実行することになる。
1. まだ読み込まれていない “残りの自分自身” を読み込む
2. 圧縮された GRUB カーネルを展開する
今回は 1 の部分について dive deep したいと思います。
diskboot モジュールを読む
grub-core/boot/i386/pc/diskboot.S がソースファイル。
.code16
.globl start, _start
start:
_start:
/*
* _start is loaded at 0x2000 and is jumped to with
* CS:IP 0:0x2000 in kernel.
*/
/*
* we continue to use the stack for boot.img and assume that
* some registers are set to correct values. See boot.S
* for more information.
*/
/* save drive reference first thing! */
pushw %dx
/* print a notification message on the screen */
pushw %si
MSG(notification_string)
popw %si
MBR からジャンプしてきたが、レジスタの内容は引き継いでいることに注意。
notification_string: .asciz “loading” という定義があり、MSG(notification_string) は MBR で “GRUB” と表示したあとに、“loading” と表示するためのもの。
/* this sets up for the first run through "bootloop" */
movw $LOCAL(firstlist), %di
/* save the sector number of the second sector in %ebp */
movl (%di), %ebp
LOCAL(firstlist): /* this label has to be before the first list entry!!! */
/* fill the first data listing with the default */
blocklist_default_start:
/* this is the sector start parameter, in logical sectors from
the start of the disk, sector 0 */
.long 2, 0
blocklist_default_len:
/* this is the number of sectors to read. grub-mkimage
will fill this up */
.word 0
blocklist_default_seg:
/* this is the segment of the starting address to load the data into */
.word (GRUB_BOOT_MACHINE_KERNEL_SEG + 0x20)
firstlist ラベルが指すアドレスは残りの core.img を読み込むセクタの開始位置と残りセクタ数を保持するためのデータ領域。
blocklist_default_len のデフォルト値は 0 だが、grub-mkimage が作成した core.img のサイズから計算された値が書き込まれる。
GRUB_BOOT`MACHINE\KERNEL_SEG は 0x0800 であり、blocklist_default_seg は 0x0820 となる。これは MBR により、core.img の先頭の 512 バイトが 0x08000 から 0x081ff までロードされているため、続きとなるアドレスが 0x08200 (0x0820:0x0000) となるため。
LOCAL(bootloop):
/* check the number of sectors to read */
cmpw $0, 8(%di)
/* if zero, go to the start function */
je LOCAL(bootit)
現在 DI レジスタは firstlist を指しており、この 8 バイト目は「残りセクタ数」となる。
上記の処理は残りセクタ数が 0 になったら bootit に飛んでループから抜ける処理。
LOCAL(setup_sectors):
/* check if we use LBA or CHS */
cmpb $0, -1(%si)
/* use CHS if zero, LBA otherwise */
je LOCAL(chs_mode)
/* load logical sector start */
movl (%di), %ebx
movl 4(%di), %ecx
/* the maximum is limited to 0x7f because of Phoenix EDD */
xorl %eax, %eax
movb $0x7f, %al
/* how many do we really want to read? */
cmpw %ax, 8(%di) /* compare against total number of sectors */
/* which is greater? */
jg 1f
/* if less than, set to total */
movw 8(%di), %ax
1:
/* subtract from total */
subw %ax, 8(%di)
/* add into logical sector start */
addl %eax, (%di)
adcl $0, 4(%di)
cmpb $0, -1(%si) で、LBA/CHS どちらのモードで読み込まれたかを確認。(今回は LBA モードを想定)
movl (%di), %ebx 及び movl 4(%di), %ecx は後の処理で DAP を作成するために、読み込み開始セクタをレジスタにロードしている。
Phoenix 製 BIOS には一回の読み込みセクタ数の最大値が 128 回のものがあり、cmpw %ax, 8(%di) で 0x7f と残りセクタ数が比較され、小さい方が AX レジスタにロードされる。
その後、subw 命令及び addl 命令で残りセクタ数を引き、開始セクタ番号を増やしている。
/* set up disk address packet */
/* the size and the reserved byte */
movw $0x0010, (%si)
/* the number of sectors */
movw %ax, 2(%si)
/* the absolute address */
movl %ebx, 8(%si)
movl %ecx, 12(%si)
/* the segment of buffer address */
movw $GRUB_BOOT_MACHINE_BUFFER_SEG, 6(%si)
/* save %ax from destruction! */
pushw %ax
/* the offset of buffer address */
movw $0, 4(%si)
上記で DAP を作成している。現在 AX レジスタに残りセクタ数が格納されているので、これが読み込みセクタ数となる。
$GRUB_BOOT_MACHINE_BUFFER_SEG = 0x70000 以降のメモリを読み込み開始位置として指定している。
/*
* BIOS call "INT 0x13 Function 0x42" to read sectors from disk into memory
* Call with %ah = 0x42
* %dl = drive number
* %ds:%si = segment:offset of disk address packet
* Return:
* %al = 0x0 on success; err code on failure
*/
movb $0x42, %ah
int $0x13
jc LOCAL(read_error)
movw $GRUB_BOOT_MACHINE_BUFFER_SEG, %bx
jmp LOCAL(copy_buffer)
int 0x13、AH=0x42 でデータを実際に読み込み、0x7000 を BX レジスタに格納。
その後、copy_buffer にジャンプする。
LOCAL(copy_buffer):
/* load addresses for copy from disk buffer to destination */
movw 10(%di), %es /* load destination segment */
/* restore %ax */
popw %ax
/* determine the next possible destination address (presuming
512 byte sectors!) */
shlw $5, %ax /* shift %ax five bits to the left */
addw %ax, 10(%di) /* add the corrected value to the destination
address for next time */
copy_buffer ではディスクから読み込んだデータを適切なアドレスにコピーする。
movw 10(%di), %es でコピー先セグメントを ES レジスタにロードしている。
shlw 及び addw では、次のループで回ってきたときのために、読み込んだセクタ数をセグメント数に変換した値を blocklist_default_seg に加算する。
セグメントを一つ増やすと 16 バイトずれるため、セグメントを 32 増やすと 512 バイト、つまり 1 セクタとなる。
よって、セクタ数を 32 倍するとセグメント数になり、shlw では掛け算の代わりに AX レジスタを 5 ビット左にシフトしている。
/* save addressing regs */
pusha
pushw %ds
/* get the copy length */
shlw $3, %ax
movw %ax, %cx
xorw %di, %di /* zero offset of destination addresses */
xorw %si, %si /* zero offset of source addresses */
movw %bx, %ds /* restore the source segment */
cld /* sets the copy direction to forward */
/* perform copy */
rep /* sets a repeat */
movsw /* this runs the actual copy */
/* restore addressing regs and print a dot with correct DS
(MSG modifies SI, which is saved, and unused AX and BX) */
popw %ds
MSG(notification_step)
popa
shlw $3, %ax では AX レジスタをさらに左に 3 ビットシフトしている。
CX レジスタにこの AX レジスタの値が格納され、DI レジスタ = 0x0000、SI レジスタ = 0x0000 となる。
BX レジスタ = 0x7000 のため、DS レジスタ = 0x7000。ES レジスタにはコピー先セグメント番号がすでに格納されている。
movsw では DS:SI -> ES:DI に 2 バイトデータを格納し、それが CX レジスタの回数 (セクタ数の 256 倍) だけ rep 命令により繰り返される。
結局、読み込んだデータ全体 (セクタ数 x 512 バイト) をコピーすることになる。
MSG(notification_step) は “GRUB loading .” という文字列の ”.” を一つ表示するためのもの。
cmpw $0, 8(%di)
jne LOCAL(setup_sectors)
/* update position to load from */
subw $GRUB_BOOT_MACHINE_LIST_SIZE, %di
/* jump to bootloop */
jmp LOCAL(bootloop)
まだ読み込むセクタが残っている場合は setup_sectors にジャンプする。
もし読み込みが完了した場合は bootloop にジャンプし、直後に bootit にジャンプする。
LOCAL(bootit):
/* print a newline */
MSG(notification_done)
popw %dx /* this makes sure %dl is our "boot" drive */
ljmp $0, $(GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200)
MSG(notification_done) で改行文字を表示する。
で、core.img の先頭の処理が終わったので、続きである 0x8200 に移動する。
これで core.img の残りをコピーする処理は完了。