grub源码分析---1

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_CHECK
boot_drive_check:
        jmp     3f
        testb   $0x80, %dl
        jz      2f
3:
    testb   $0x70, %dl
    jz      1f
2:  
        movb    $0x80, %dl
1:
    ljmp    $0, $real_start

real_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, %dl
1:
    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 $0x10
LOCAL(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   0
disk_address_packet:
sectors:
    .long   0
heads:
    .long   0
cylinders:
    .word   0
sector_start:
    .byte   0
head_start:
    .byte   0
cylinder_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), %ebp

LOCAL(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), %ax

1:
    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_SIZE
LOCAL(firstlist):
blocklist_default_start:
    .long 2, 0
blocklist_default_len:
    .word 0
blocklist_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,下一章继续分析。

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值