在查看了start.S代码之后,就会对GRUB Legacy启动阶段有了更清晰的认识。在传统的GRUB启动中,一般分为stage1、stage1.5和stage2三个阶段,当然,stage1.5是可以忽略的,这样就直接从stage1跳转到了stage2。stage1.5主要是为stage2构建其所需要的文件系统。
目前只考虑GRUB legacy,不考虑GRUB 2.0的情况。像redhat/centos 5/6系列的系统一般使用的都是GRUB legacy代码,redhat/centos 7系列以后就开始使用GRUB 2.0(GRUB 2.0可以看作对stage1.5和stage2阶段代码进行了重构)。
在之前编写的《GRUB引导程序之第一阶段stage1.S分析》就是GRUB引导程序第一阶段完整的代码。本文所分析的start.S文件,虽然位于stage1.5代码中,但从功能上来看是不应该划分到1.5阶段的。
承前启后的start.Sstart.S主要起到一个过渡功能,根据第一阶段分析可知,stage1.S的代码位于第一扇区,由BIOS加载到0x7c00地址处,该代码负责加载第二扇区的代码(start.S)至地址0x8000。
start.S主要从0x8000开始运行,并根据配置选项选择加载stage1.5阶段代码还是stage2.0阶段代码。如果只考虑三个阶段的情况,start.S加载的是stage1.5阶段代码,stage1.5将从第三个扇区开始,占用了若干个扇区的位置。start.S负责将stage1.5阶段的代码加载到0x2200地址处,并开启后面的引导之旅。
源码分析#define ASM_FILE #include#ifndef STAGE1_5 #include #endif #ifdef STAGE1_5 # define ABS(x) (x-_start+0x2000) #else # define ABS(x) (x-_start+0x8000) #endif //打印信息 #define MSG(x) movw $ABS(x), %si; call message .file "start.S" .text //通知GAS汇编器使用16位的指令集,因此现在工作在实模式。 .code16 //代码开始的位置,该部分代码被stage1.S加载到0x8000地址处 .globl start, _start start: _start: //在stage1.S处我们在地址0x2000处开辟了堆栈,现在继续是该堆栈 //将DX进行压入堆栈,DX寄存器中存储的是磁盘号 pushw %dx //将si压入堆栈,si在第一阶段指向的是sectors对应处的地址 pushw %si //根据需要打印不同的信息 //我所找的代码是支持stage1.5阶段的,后续的分析均在支持1.5阶段基础之上 //此处将打印"Loading stage1.5",1.5阶段主要是用来构建文件系统供2阶段使用 //如果直接支持stage2,则打印“Loading stage2” MSG(notification_string) //打印的时候用到了si寄存器,现在将si寄存器的值还原 popw %si //BOOTSEC_LISTSIZE的值为8,firstlist标号指向文件的尾部 //movw以为着将blocklist_default_start标号处的绝对地址赋值给了DI寄存器 movw $ABS(firstlist - BOOTSEC_LISTSIZE), %di //blocklist_default_start标号处指向的值为下一个阶段所在逻辑扇区的号,此处为2 //扇区标号是从0开始的,此处意味着1.5阶段是从第三个扇区开始的。 //movl将1.5阶段的扇区号赋值给了EBP栈基址寄存器。 movl (%di), %ebp //从该标号开始为一个循环,读取下个阶段(1.5阶段)的引导程序 bootloop: //4(%di)是变址寻址,既4(%di)为di所代表的地址加4,指向的是blocklist_default_len处的地址 //该地址指向的值代表了后续扇区块的长度,现在是1.5阶段,默认是0值,在grub装载时会被填充修正为真正的stage1.5的扇区数 //cmp比较指令做算数减法运算,结果为0,将ZF设置为1,正确设置后将不为0,既ZF=0,则不会跳转 cmpw $0, 4(%di) //条件转移指令,判断ZF是否为1,为1则跳转到bootit,理论上此处不会跳转 je bootit setup_sectors: //si寄存器在第一阶段被指向了sector标号处,通过变地寻址既si指向的地址减去1,指向的是mode处。 //mode=1表示进行LBA扩展模式,mode=0表示读取磁盘需要使用CHS寻址模式 //cmp比较指令做算数减法运算,LBA扩展模式,ZF=0,CHS寻址模式,ZF=1 cmpb $0, -1(%si) //如果ZF=1,则跳转到chs_mode模式 //否则进行下面的lba_mode读取 je chs_mode lba_mode: //将扇区号赋值给ebx寄存器,既ebx存储这起始扇区号2 movl (%di), %ebx //清空EAX寄存器 //将AL寄存器设置为0x7f xorl %eax, %eax movb $0x7f, %al //变址寻址4(%di)得到1.5阶段所占的扇区数 //com比较1.5阶段的扇区数是否大于AX寄存器设置的限制值0x7f //如果大于,ZF设置为0,CF设置为1,如果小于,ZF设置为0,CF设置为0 cmpw %ax, 4(%di) //jg条件转移指令,起始判断的是CF进位标志位寄存器,当CF=1时跳转到下面的1标号位 //发生跳转说明1.5阶段过大,要是不跳转,则将1.5所占的扇区数赋值给AX寄存器。 jg 1f movw 4(%di), %ax 1: //sub减法指令:sub 源操作数 目的操作数 //sub是将目的操作数减去源操作数,然后将差值放入目的操作数 //AX寄存器中存储的是1.5阶段的实际数值,当该值过大时则为0x7f,此处可认为是1.5阶段的扇区数 //将1.5阶段的实际扇区数减去AX寄存器的值,然后放到blocklist_default_len标号位置 //当1.5阶段代码不过大时,blocklist_default_len处的值为0,过大时,blocklist_default_len处的值为多出来的值 subw %ax, 4(%di) //add加法指令: add 源操作数 目的操作数 //add是将目的操作数加上源操作数,然后将和存入目的操作数 //DI指向的是开始的扇区号,此处值为2,加上实际的1.5阶段代码所占的扇区数,此时DI指向的地方的值为1.5阶段尾部的扇区数 addl %eax, (%di) //将0x0010值赋值到disk_address_packet结构的地址处(在stage1.S中有介绍),既si[0]=0x10,si[1]=0x00。 //表示要传输的dap大小为0x10, movw $0x0010, (%si) //AX寄存器存储着stage1.5阶段的实际扇区大小N,将N赋值到disk_address_packet地址,既si[2]=0x1 //表示要传输的扇区数为N个扇区 movw %ax, 2(%si) //将EBX寄存器指向的地址处的值,也就是2赋值给si[8]=0x1。 //既要读取的起始扇区号为2,其实就是从第三个扇区开始读取,一共读取N个扇区。 //该编号就是LBA的扇区编号。 movl %ebx, 8(%si) //将0x7000的值赋值给si[6]和si[7],既si[6]=0x00,si[7]=0x70 movw $BUFFERSEG, 6(%si) //将AX寄存器的值也就是要读取的扇区长度N压入堆栈 pushw %ax //将EAX寄存器清零,然后设置si[4]=0和si[5]=0 //既数据缓存地址为0x7000:0x0000 //后续通过BIOS中断读取的N个扇区的内容,就读取到0x7000:0x0000地址对应的内存中。 xorl %eax, %eax movw %ax, 4(%si) //设置si[12]~si[15] = 0x0 movl %eax, 12(%si) //AH寄存器设置位0x42,调用BIOS0x13号中断,进行扩展读操作。 movb $0x42, %ah int $0x13 //进位标志位寄存器CF=0时,表示读取成功。 //中断执行失败,将CF设置为1,表示读取失败。 //jc为有条件转移执行,当CF设置位1时,跳转到read_error打印错误信息“Read Error”,然后就死循环Game Over^_^。 jc read_error //读取成功,将0x7000赋值给BX寄存器,供copy_buffer的时候进行数据迁移 movw $BUFFERSEG, %bx jmp copy_buffer chs_mode: //根据第一阶段遗留的数据来看 //最大扇区数(最大扇区是512个,si中会存储511)在si[0],si[1],si[2]和si[3]中 //最大柱面数(最大柱面数如果为1024,si中存储的值为1023)在si[8],si[9]中 //最大磁头数(最大磁头数如果为64,si中存储的值为63)在si[4],si[5],si[6]和si[7]中 //将DI寄存器指向的数值2赋值给EAX寄存器 movl (%di), %eax //清空EDX寄存器 xorl %edx, %edx //16位被除数放在AX寄存器,8位除数为源操作数,8位的商,存储在AL中,8位余数存储在AH中 //32位被除数放在DX,AX中。其中DX为高位,16位除数为源操作数,16位的商,存储在AX中,16位余数在DX中 //64位被除数在EDX,EAX中,其中EDX为高位,32位除数为源操作数,32位的商,存储在EAX中,32位余数在EDX中 //此处的被除数是2,除数位为扇区数,用stage1.5阶段开始的扇区号除以每个磁道包含的扇区数 divl (%si) //将DL寄存器中存放的余数赋值给si[10],余数既stage1.5阶段开始的扇区号,除数为磁道号 movb %dl, 10(%si) //清空EDX寄存器 //然后用被除数AX中的值(上一步的商),除以si[4]对应地址存放的单柱面最大磁头数 //其中商为stage1.5阶段所在的柱面号,余数为stage1.5阶段开始的磁道号。 xorl %edx, %edx divl 4(%si) //将DL寄存器中的值(stage1.5阶段开始的磁道号)赋值给si[11] movb %dl, 11(%si) //将AX寄存器中的值(stage1.5阶段所在的柱面号)赋值给si[12] movw %ax, 12(%si) //比较si[8]所代表地址指向的数与AX寄存器的值 //其中si[8]指向的值为柱面数,而ax代表上面div操作的商。 //当柱面号超过了最大值时跳转到geometry_error,打印“Geom Error”并死循环,然后 Game Over ^_^ //stage1.5阶段所在柱面数合法,则继续向下执行 cmpw 8(%si), %ax jge geometry_error //将si指向的单磁道最大扇区数赋值给AX寄存器 movw (%si), %ax //AL寄存器中存储的最大扇区数减去stage1.5阶段开始的扇区号得到单磁道剩余的扇区数,然后赋值到AL寄存器 subb 10(%si), %al //比较AX寄存器与blocklist_default_len的值 //由此可以判断本磁道上剩余的扇区空间是否足够容纳所有的stage1.5阶段代码 cmpw %ax, 4(%di) //jg是条件转移指令,会判断CF进位标志位寄存器的值是否为1,当空间不够时,CF=1,会跳转到下面的2标号处 jg 2f //当空间充足时,将stage1.5的扇区数赋值给AX寄存器 movw 4(%di), %ax 2: //将4(%di)指向的stage1.5阶段代码的大小减去AX寄存器中的值,然后再放入blocklist_default_len标号处 //当空间充足时,blocklist_default_len处的值通过subw减法指令计算为0,既可以一次性处理完成 //当空间不充足时,AX寄存器存储着本次需要读取的stage1.5阶段代码的扇区数,4(%di)通过subw得到剩余还需要读取的扇区数 subw %ax, 4(%di) //本次读取完AX扇区之后,在进行第二轮读取之前,需要将第二轮读取时的起始扇区数加上本次已读取的扇区 //做addl加法操作之后,将第二轮读取时的起始扇区数赋值到blocklist_default_start标号处 addl %eax, (%di) //将si[13]指向的柱面号的高位的值赋值给DL寄存器 movb 13(%si), %dl //将DL寄存器的值左移6位,然后将si[10]指向的stage1.5阶段的扇区号赋值给CL寄存器 shlb $6, %dl movb 10(%si), %cl //CL寄存器加1,得到stage1.5阶段的真实的扇区号表示 //然后通过orb或运算命令,将CL寄存器的高两位存储着柱面号,低6位存储着扇区号 incb %cl orb %dl, %cl //将si[12]指向的值(既柱面号)赋值给CH寄存器。 movb 12(%si), %ch //将DX寄存器出栈,原栈中存储的DX寄存器的低8位为磁盘号。 //然后在将DX寄存器压入堆栈 popw %dx pushw %dx //将si[11]指向的磁道号赋值给DH寄存器中。 movb 11(%si), %dh //将AX寄存器压入堆栈,AX寄存器中存储的是本次读取的stage1.5阶段扇区数,后面会对AX寄存器做修改而带来污染 pushw %ax //将0x7000赋值给BX寄存器,将BX寄存器的值赋值给ES寄存器 movw $BUFFERSEG, %bx movw %bx, %es //清空BX寄存器,将0x2赋值给AH寄存器,既AH=0x02 xorw %bx, %bx movb $0x2, %ah int $0x13 //以上设置的参数对照功能: //AH:0x02 //AL:需要读取的扇区数 //CH:起始的柱面号的值 //CL:低6位为需要的扇区号,高2位为起始的柱面号的值 //DH:起始的磁头号的值 //DL:对应的磁盘号 //ES:BX segment:offset,读取的缓存地址 //中断执行失败,CF=1,执行成功,CF=0。当执行失败是打印“Read Error”,然后执行死循环 jc read_error //读取数据成功,将ES寄存器中的0x7000赋值给BX寄存器,然后执行后续的copy_buffer数据迁移 //最终stage1.5阶段代码会迁移至0x2200处 movw %es, %bx //数据迁移操作 copy_buffer: //将blocklist_default_seg标号指定的值0x220赋值给ES寄存器,该值是1.5阶段指定的段地址 movw 6(%di), %es //将AX寄存器出栈,之前入栈的是要读取的扇区长度N popw %ax //将AX寄存器左移5位,然后赋值给AX寄存器中。 //左移5位意味着删除长度N扩大了32倍 shlw $5, %ax //将0x220加上AX寄存器中的值,然后存储到blocklist_default_seg标号指向的地方 //当stage1.5的长度大雨0x7f时,一次性读取不完,一次最多读取0x7f个扇区。 //此处主要是用来调整下一回stage1.5读取到的内存位置 addw %ax, 6(%di) //将通用型寄存器压入堆栈,顺序一般为 DI, SI, BP, BX, DX, CX, and AX //将DS寄存器压入堆栈,后面会使用该寄存器 pusha pushw %ds //前面AX左移了5位,现在又左移了4位,一共相等于扩大了512倍,也就是计算出来了要拷贝的字节数 //将要拷贝的字节数赋值给CX计数寄存器中 shlw $4, %ax movw %ax, %cx //清空SI寄存器和DI寄存器 //使用cld将方向标志位DF复位,既设置DF=0,其相反的指令为std //DF=0表示向高地址增加,DF=1表示向低地址减少。cld复位DF之后,将向高地址增加。 xorw %di, %di xorw %si, %si movw %bx, %ds cld //rep重复执行后面的movsw,rep受ECX寄存器控制,每执行依次,ECX寄存器依次减1,当ECX寄存器为0时不再执行。 rep //movsb每次传输一个byte(单字)宽度的数据。 //movsw或者movsb用来将DS:SI指向的存储单元中的数据装入ES:DI指向的存储单元中。 //此处也就是将(0x7000:0x0000,从磁盘中读取的第三扇区的数据)装入到(0x220:0x0000)地址处,依次装入双字节 //由于CX寄存器中的值为N*512,则将拷贝N*512次,每次1个字节,一共将stage1.5代码大小全部拷贝到0x2200地址处。 movsb //将DS寄存器出栈,恢复原值 popw %ds //打印“.” MSG(notification_step) //将所有通用型寄存器出栈 popa //变址寻址4(%di)代表了blocklist_default_len处的地址指向的值 //当该处的值不为0的时候,设置ZF为0,当ZF=0,通过jne条件转移指令跳转到setup_sectors进行后续启动设置 //在之前%di进行了sub操作,如果stage1.5阶段代码已经读取完成,%di指向的位置处的值为0,既ZF=1不发生跳转,否则继续setup_sectors读取 cmpw $0, 4(%di) jne setup_sectors //stage1.5已经读取完成了 //di代表了blocklist_default_start处的地址指向的值,减去8之后重新进行bootloop //如果后面是1.5阶段,subw指令其实没有实际意义,再次进入bootloop之后会因为4(%si)处的值为0而直接跳转到bootit subw $BOOTSEC_LISTSIZE, %di jmp bootloop //1.5阶段读取完成之后 bootit: //打印一个回车符 MSG(notification_done) //将之前压栈的DX寄存器出栈,保证此时的DX中是原来的磁盘号 popw %dx //执行一个长跳转,1.5阶段直接跳转到0x0000:0x2200地址处 //0x2200地址是通过0x220*16+0x0000得到的,该地址存储着1.5阶段的代码并开启1.5阶段 Success ^_^! #ifdef STAGE1_5 ljmp $0, $0x2200 #else ljmp $0, $0x8200 #endif geometry_error: MSG(geometry_error_string) jmp general_error read_error: MSG(read_error_string) general_error: MSG(general_error_string) stop: jmp stop #ifdef STAGE1_5 notification_string: .string "Loading stage1.5" #else notification_string: .string "Loading stage2" #endif notification_step: .string "." notification_done: .string "rn" geometry_error_string: .string "Geom" read_error_string: .string "Read" general_error_string: .string " Error" //在1标号的位置,将0x0001赋值给BX寄存器 //将xe赋值给AX寄存器的高8位。 //然后执行中断,中断号为16,既屏幕显示I/O //功能OE为在Teletype模式下显示字符,AL=字符 BH=页码 BL=模型模式下的前景色 //我们可以发现这次每次取出来一个字节同时调用bios中断显示出来。 1: movw $0x0001, %bx movb $0xe, %ah int $0x10 incw %si //是通过call message来调用的 //在MSG(x)中,将x对应的物理地址赋值到si寄存器 //movb取si寄存器地址对应的一个字节byte到AX寄存器的低8位中,在第一阶段中使用的指令是lodsb message: movb (%si), %al //cmpb是比较指令,比较AL寄存器中的值是否是立即数0。 //不相等的话,零标志位ZF寄存器为0,相等的话ZF的值为1。 //当字符串到达尾部时,取出的字节才会是0值。 cmpb $0, %al //条件转移指令,jne是用来比较ZF寄存器是否为0,为0的话跳转到后面的标号处。 //此处为1b,既向后(也就是之前的代码)到标号1出,也就是上面的1标号位置。 jne 1b //si进行加1操作(上面1标号的位置),然后通过movb依次提取byte至AL中,当到字符串末尾时,执行ret返回 ret lastlist: .word 0 .word 0 . = _start + 0x200 - BOOTSEC_LISTSIZE blocklist_default_start: .long 2 blocklist_default_len: //1.5阶段的扇区数在grub装载时会计算stage1.5的大小,然后在此处填充上 #ifdef STAGE1_5 .word 0 #else .word (STAGE2_SIZE + 511) >> 9 #endif blocklist_default_seg: #ifdef STAGE1_5 .word 0x220 #else .word 0x820 #endif firstlist:



