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

February 18, 2018

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 の残りをコピーする処理は完了。


 © 2023, Dealing with Ambiguity