grub源码分析---1

来源:互联网 发布:Python爬取公众号 编辑:程序博客网 时间:2024/06/04 19:12

grub源码分析—1

本章开始分析grub的源码,版本为2.02。

系统开机启动后,BIOS会将硬盘(假设从硬盘启动)的第一个扇区装载到内存0x7c00位置开始执行,该地址对应grub(假设使用grub引导)中的start函数,下面来看。

boot start第一部分
grub-core/boot/i386/pc/boot.S

start:    jmp LOCAL(after_BPB)    ...LOCAL(after_BPB):    cli    .org GRUB_BOOT_MACHINE_DRIVE_CHECKboot_drive_check:        jmp     3f        testb   $0x80, %dl        jz      2f3:    testb   $0x70, %dl    jz      1f2:          movb    $0x80, %dl1:    ljmp    $0, $real_startreal_start:    xorw    %ax, %ax    movw    %ax, %ds    movw    %ax, %ss    movw    $GRUB_BOOT_MACHINE_STACK_SEG, %sp    sti    movb   boot_drive, %al    cmpb    $0xff, %al    je  1f    movb    %al, %dl1:    pushw   %dx    MSG(notification_string)    movw    $disk_address_packet, %si    movb    $0x41, %ah    movw    $0x55aa, %bx    int $0x13    popw    %dx    pushw   %dx    jc  LOCAL(chs_mode)    cmpw    $0xaa55, %bx    jne LOCAL(chs_mode)    andw    $1, %cx    jz  LOCAL(chs_mode)

首先通过cli指令关闭中断。.org伪指令用来告诉汇编器下一条指令的地址,GRUB_BOOT_MACHINE_DRIVE_CHECK宏定义为0x66,因此表示从0x7c00到达下一条指令jmp 3f一共为102个字节。
jmp 3f一共两个字节,在grub安装到第一个扇区时有可能会被改写为两个nop指令,因此下面要对此做检查。
dl寄存器被BIOS设置为引导设备号,一般为0x80~0xff,0x80号对应第一个硬盘。如果jmp 3f被改写了,肯定是从0x80号开始的设备启动的(下面的检查也是,都是一些BIOS版本造成的,具体为什么,协议里应该有写,懒着查了),如果不满足,则直接设置为0x80。
再往下testb 0x70对引导设备号做进一步的检查,将其限制在0x0~0xf以及0x80~0x8f内,如果不在这个范围内,就将其设置为0x80。

因为进入这段代码时,有可能使用CS:IP=0x07C0:0x0000,接下来的长跳转指令将该值设置为CS:IP=0x0000:0x7C00,和下面的指令一起,将cs、ds以及ss段寄存器都置位0x0000。
接着设置当前的栈顶指针为GRUB_BOOT_MACHINE_STACK_SEG,宏定义为0x2000,再通过sti打开中断。至此完成了堆栈的建立,可以使用push、pop指令使用堆栈了。

boot_drive默认值为0xff,如果被改写,表示强制使用某设备引导,将前面计算的dl替换为该设备号。

然后保存dx寄存器到堆栈,也即设备号,因为dl可能被int 0x13中断更改,因此后面要从堆栈重新赋值。再利用MSG(notification_string)通过int 10中断向屏幕打印数据。

notification_string:    .asciz "GRUB "#define MSG(x)  movw $x, %si; call LOCAL(message)1:    movw    $0x0001, %bx    movb    $0xe, %ah    int $0x10LOCAL(message):    lodsb    cmpb    $0, %al    jne 1b    ret

lodsb将notification_string中的字符串按字节存入al寄存器,通过int10中断打印到屏幕上。

再往下保存disk_address_packet的地址到si寄存器中,该地址用于保存读取硬盘的参数,后面马上就看到。

int 0x13中断是计算机在实模式下提供读写磁盘信息的接口,其实是调用了BIOS的代码。
当中断参数为ah=0x41和bx=0x55aa时,该中断用于检查磁盘拓展模式。硬盘有LBA和CHS两种模式,简单说CHS模式支持的硬盘容量较小,并且完全按照硬盘的硬件结构进行读写,LBA模式支持的硬盘容量多达TB级别,因此现在大多都使用LBA模式了。当中断返回值CF=1时表示表示硬盘不支持LBA模式,直接跳转到LOCAL(chs_mode),如果CF=0表示支持LBA,继续检查。返回值bx中存储了魔数0xaa55,如果不相等,也直接跳转到LOCAL(chs_mode)。cx中的值存储了硬盘访问的接口位图,当为偶数时,表示不支持LBA的某些api,此时也跳转到CHS。

因为现在大多硬盘都支持LBA模式,下面只看LOCAL(lba_mode)的代码。

boot start第二部分
grub-core/boot/i386/pc/boot.S

LOCAL(lba_mode):    xorw    %ax, %ax    movw    %ax, 4(%si)    incw    %ax    movb    %al, -1(%si)    movw    %ax, 2(%si)    movw    $0x0010, (%si)    movl    LOCAL(kernel_sector), %ebx    movl    %ebx, 8(%si)    movl    LOCAL(kernel_sector_high), %ebx    movl    %ebx, 12(%si)    movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, 6(%si)    movb    $0x42, %ah    int $0x13    jc  LOCAL(chs_mode)    movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, %bx    jmp LOCAL(copy_buffer)LOCAL(chs_mode):    ...LOCAL(final_init):    ...

这一部分主要是读取硬盘,首先设置读取硬盘的参数到disk_address_packet地址中,也即si寄存器指向的地址。

mode:    .byte   0disk_address_packet:sectors:    .long   0heads:    .long   0cylinders:    .word   0sector_start:    .byte   0head_start:    .byte   0cylinder_start:

首先将head的前两个字节清0,其实地址为4(%si)中。接着设置mode为1,对应的地址为-1(%si),表示LBA模式,如果为0,对应CHS模式。
再往下继续将sectors的高两字节2(%si)设置为0x0001,表示传输的扇区数,低两字节(%si)设置为$0x0010,其中高字节0x00为默认值,低字节0x10表示数据块的大小。
接下来从cylinders地址开始设置8(%si)和12(%si),两者一起决定了读取的起始扇区,该值默认为0x1,也即读取第二个扇区。
再设置GRUB_BOOT_MACHINE_BUFFER_SEG到heads的高两字节中,表示传输目的地址,默认值为0x7000。

#define GRUB_BOOT_MACHINE_BUFFER_SEG    0x7000

接着执行int 0x13中断,参数0x42表示通过LBA模式从硬盘读取数据。如果返回标志位cf=1,则不支持LBA读,此时跳转到CHS模式LOCAL(chs_mode)。

如果读取成功了就将前面的缓存地址GRUB_BOOT_MACHINE_BUFFER_SEG保存到bx中,跳转到jmp LOCAL(copy_buffer)继续执行。

boot start第三部分
grub-core/boot/i386/pc/boot.S

LOCAL(copy_buffer):    pusha    pushw   %ds    movw    $0x100, %cx    movw    %bx, %ds    xorw    %si, %si    movw    $GRUB_BOOT_MACHINE_KERNEL_ADDR, %di    movw    %si, %es    cld    rep    movsw    popw    %ds    popa    jmp *(LOCAL(kernel_address))

首先通过pusha指令压入ax、cx、dx、bx、sp、bp、si和di寄存器,pushw压入ds段寄存器,以便恢复。
然后向cx存入循环次数256。
接着设置ds段寄存器指向前面的缓存地址GRUB_BOOT_MACHINE_BUFFER_SEG,然后清空si寄存器。
接下来设置目的地址GRUB_BOOT_MACHINE_KERNEL_ADDR到di寄存器中,宏定义为

#define GRUB_BOOT_MACHINE_KERNEL_ADDR   (GRUB_BOOT_MACHINE_KERNEL_SEG << 4)

GRUB_BOOT_MACHINE_KERNEL_SEG的最终宏定义为

#define GRUB_BOOT_I386_PC_KERNEL_SEG    0x800

因此目的地址GRUB_BOOT_MACHINE_KERNEL_ADDR为0x8000。
这段代码就是将0x7000:0x0000地址处的代码搬运256个字(512字节)到 0x0000:0x8000地址上去。

最后跳转到LOCAL(kernel_address)处继续执行,其实就是GRUB_BOOT_MACHINE_KERNEL_ADDR,即0x8000。

第二个扇区的代码在grub源码的diskboot.S中,下面来看。

diskboot start第一部分
grub-core/boot/i386/pc/diskboot.S

_start:    pushw   %dx    pushw   %si    MSG(notification_string)    popw    %si    movw    $LOCAL(firstlist), %di    movl    (%di), %ebpLOCAL(bootloop):    cmpw    $0, 8(%di)    je  LOCAL(bootit)LOCAL(setup_sectors):    cmpb    $0, -1(%si)    je  LOCAL(chs_mode)    movl    (%di), %ebx    movl    4(%di), %ecx    xorl    %eax, %eax    movb    $0x7f, %al    cmpw    %ax, 8(%di)    jg  1f    movw    8(%di), %ax1:    subw    %ax, 8(%di)    addl    %eax, (%di)    adcl    $0, 4(%di)    movw    $0x0010, (%si)    movw    %ax, 2(%si)    movl    %ebx, 8(%si)    movl    %ecx, 12(%si)    movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, 6(%si)    pushw   %ax    movw    $0, 4(%si)    movb    $0x42, %ah    int $0x13    jc  LOCAL(read_error)    movw    $GRUB_BOOT_MACHINE_BUFFER_SEG, %bx    jmp LOCAL(copy_buffer)

首先将dx寄存器入栈,从前面可知,该寄存器此时保存了引导设备号。因为紧接着MSG打印要使用si寄存器,因此再保存si寄存器,对应前面disk_address_packet处的地址。
然后向屏幕打印notification_string字符串。

notification_string:    .asciz "loading"

LOCAL(firstlist)是即将读取硬盘的参数的起始地址,分别赋值给di和ebp寄存器。

.org 0x200 - GRUB_BOOT_MACHINE_LIST_SIZELOCAL(firstlist):blocklist_default_start:    .long 2, 0blocklist_default_len:    .word 0blocklist_default_seg:    .word (GRUB_BOOT_MACHINE_KERNEL_SEG + 0x20)

0x200即512,即第二个扇区数据的末尾。GRUB_BOOT_MACHINE_LIST_SIZE为12,表示一个first list项读取参数的字节数,最大一共15个first list项(14个有效),从上面也可以看出4+4+2+2=12。因此LOCAL(firstlist)处的地址就为0x1f4,该地址对应最后一个first list项。

接着进入外层循环LOCAL(bootloop),外层循环每次遍历一个first list项。
8(%di)对应上面的blocklist_default_len,也即即将读取的扇区数,该值0会在生成该段程序时改写成正确的数值。因为后面循环会递减该值,如果该值等于0,表示将硬盘的所有数据搬到了内存中,此时跳转到LOCAL(bootit)中。

然后进入内层循环,int 0x13中断读取硬盘一次最大只能读取0x7f个扇区,因此需要循环读取。
首先从boot.S的mode地址处(-1(%si))获得硬盘的模式,如果为0,表示为CHS模式,跳转到LOCAL(chs_mode),否则继续执行。

把blocklist_default_start的高低位各4个字节保存在ebx和ecx寄存器中。表示硬盘LBA模式下扇区的逻辑起始地址,默认值为0x2,即第3个扇区。接着将al寄存器赋值0x7f,表示每次拷贝最大的扇区数,8(%di)表示剩余多少扇区没有拷贝,如果8(%di)小于al,则表示将要进行最后一次拷贝,此时将8(%di)赋值给al。

进入标号1,首先将剩余的扇区数8(%di)减去当前即将读出的扇区数al,起始扇区数(%di)加上%eax,如果有进位,则需要将进位加到4(%di)中,这三条指令都是为下一次拷贝做准备。

接下来就要设置本次硬盘读取的各个参数了,类似前面对boot.S代码的分析,(%si)指向disk_address_packet,将sectors设置为0x007f0010(默认),0x10是数据块的大小,0x00是默认值,0x007f是本次传输的扇区数;8(%si)和12(%si)保存了本次传输的起始扇区数,起始值为0x2;
6(%si)保存了缓存地址,默认值为GRUB_BOOT_MACHINE_BUFFER_SEG,也即0x7000,和boot.S中的缓存地址一样;最后将本次读取的扇区数ax入栈,将4(%si)清0。然后执行int 0x13中断,参数ah为0x42,执行读取。

如果CF置1,则发生错误,跳转到LOCAL(read_error)。
如果成功读取了数据,则将缓冲地址GRUB_BOOT_MACHINE_BUFFER_SEG存入bx寄存器,跳转到jmp LOCAL(copy_buffer)。

diskboot start第二部分
grub-core/boot/i386/pc/diskboot.S

LOCAL(copy_buffer):    movw    10(%di), %es    popw    %ax    shlw    $5, %ax    addw    %ax, 10(%di)    pusha    pushw   %ds    shlw    $3, %ax    movw    %ax, %cx    xorw    %di, %di    xorw    %si, %si    movw    %bx, %ds    cld    rep    movsw    popw    %ds    MSG(notification_step)    popa    cmpw    $0, 8(%di)    jne LOCAL(setup_sectors)    subw    $GRUB_BOOT_MACHINE_LIST_SIZE, %di    jmp LOCAL(bootloop)

10(%di)对应blocklist_default_seg,存储了GRUB_BOOT_MACHINE_KERNEL_SEG + 0x20目的地址,也即目的地址对应的段寄存器值为0x820,存入es寄存器。
然后出栈恢复ax寄存器,保存了本次读取的扇区数。shlw指令将ax寄存器左移5位加到段地址上,相当于将ax左移9位加到最终的目的地址上,也即将本次拷贝的扇区数转化为总字节数(2的9次方为512字节,对应一个扇区),累加到10(%di)中,表示下一次拷贝的目的地址。
接下来将对应寄存器入栈,用于恢复。
然后再将ax左移3位,前面已经左移5位,加在一起一共左移8位,表示本次拷贝的字数(1个字等于2个字节),存入cx寄存器中。

接着将di、si寄存器清0。bx存储了源地址GRUB_BOOT_MACHINE_BUFFER_SEG,也即0x7000,存入ds段寄存器,cld指令设置拷贝方向,然后循环直到拷贝完成。

拷贝完成后,恢复寄存器并向屏幕打印notification_step。

notification_step:  .asciz "."

然后检查8(%di)中,也即本次读取的first list项中是否还有数据未拷贝,如果有剩余数据,则跳转回LOCAL(setup_sectors)内循环再次读取0x7f个扇区。

如果8(%di)中为0,表示该first list项中没有可读取的数据了,此时将di寄存器减去GRUB_BOOT_MACHINE_LIST_SIZE,得到下一个first list项的起始地址,然后跳转到LOCAL(bootloop)外循环继续执行。

diskboot start第三部分
grub-core/boot/i386/pc/diskboot.S

LOCAL(bootit):    MSG(notification_done)    popw    %dx    ljmp    $0, $(GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200)

到达这里,表示所有的数据已经拷贝完成,首先打印notification_done。

notification_done:  .asciz "\r\n"

再从堆栈恢复dx寄存器,存储了引导设备号。
最后长跳转到GRUB_BOOT_MACHINE_KERNEL_ADDR + 0x200执行,也即从0x0820:0x0000开始执行。
0x8200起始地址处保存了startup_raw.S,下一章继续分析。