本文主要是介绍GRUB引导程序之承前启后的start.S—源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
启动阶段
在查看了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.S
start.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 <shared.h>#ifndef STAGE1_5
#include <stage2_size.h>
#endif#ifdef STAGE1_5
# define ABS(x) (x-_start+0x2000)
#else
# define ABS(x) (x-_start+0x8000)
#endif /* STAGE1_5 *///打印信息
#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 bootitsetup_sectors:
//si寄存器在第一阶段被指向了sector标号处,通过变地寻址既si指向的地址减去1,指向的是mode处。
//mode=1表示进行LBA扩展模式,mode=0表示读取磁盘需要使用CHS寻址模式
//cmp比较指令做算数减法运算,LBA扩展模式,ZF=0,CHS寻址模式,ZF=1cmpb $0, -1(%si)//如果ZF=1,则跳转到chs_mode模式
//否则进行下面的lba_mode读取je chs_modelba_mode:
//将扇区号赋值给ebx寄存器,既ebx存储这起始扇区号2movl (%di), %ebx//清空EAX寄存器
//将AL寄存器设置为0x7fxorl %eax, %eaxmovb $0x7f, %al//变址寻址4(%di)得到1.5阶段所占的扇区数
//com比较1.5阶段的扇区数是否大于AX寄存器设置的限制值0x7f
//如果大于,ZF设置为0,CF设置为1,如果小于,ZF设置为0,CF设置为0cmpw %ax, 4(%di)//jg条件转移指令,起始判断的是CF进位标志位寄存器,当CF=1时跳转到下面的1标号位
//发生跳转说明1.5阶段过大,要是不跳转,则将1.5所占的扇区数赋值给AX寄存器。jg 1fmovw 4(%di), %ax1:
//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)/*
disk_address_packet地址处的数据与磁盘参数的对应关系:struct dap {u8 len; 一般长度取值为0x10,表示dap结构长度为16字节u8 zero; 默认必须为0u16 nsector: 实际上是8位有效,表示读取的扇区数,一般取值从1~127u16 addr: 内存地址addru16 segment: 段选择子的值u32 sectorLo: 表示LBA扇区号的低4字节u32 sectorHi:表示LBA扇区号的高4字节
}
*/
//将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]=0x70movw $BUFFERSEG, 6(%si)
//将AX寄存器的值也就是要读取的扇区长度N压入堆栈pushw %ax
//将EAX寄存器清零,然后设置si[4]=0和si[5]=0
//既数据缓存地址为0x7000:0x0000
//后续通过BIOS中断读取的N个扇区的内容,就读取到0x7000:0x0000地址对应的内存中。xorl %eax, %eaxmovw %ax, 4(%si)
//设置si[12]~si[15] = 0x0movl %eax, 12(%si)//AH寄存器设置位0x42,调用BIOS0x13号中断,进行扩展读操作。movb $0x42, %ahint $0x13
//进位标志位寄存器CF=0时,表示读取成功。
//中断执行失败,将CF设置为1,表示读取失败。
//jc为有条件转移执行,当CF设置位1时,跳转到read_error打印错误信息“Read Error”,然后就死循环Game Over^_^。jc read_error
//读取成功,将0x7000赋值给BX寄存器,供copy_buffer的时候进行数据迁移movw $BUFFERSEG, %bxjmp copy_bufferchs_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, %edxdivl 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), %axjge 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), %ax2:
//将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 %clorb %dl, %cl
//将si[12]指向的值(既柱面号)赋值给CH寄存器。movb 12(%si), %ch
//将DX寄存器出栈,原栈中存储的DX寄存器的低8位为磁盘号。
//然后在将DX寄存器压入堆栈popw %dxpushw %dx
//将si[11]指向的磁道号赋值给DH寄存器中。movb 11(%si), %dh
//将AX寄存器压入堆栈,AX寄存器中存储的是本次读取的stage1.5阶段扇区数,后面会对AX寄存器做修改而带来污染pushw %ax//将0x7000赋值给BX寄存器,将BX寄存器的值赋值给ES寄存器movw $BUFFERSEG, %bxmovw %bx, %es
//清空BX寄存器,将0x2赋值给AH寄存器,既AH=0x02xorw %bx, %bxmovb $0x2, %ahint $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寄存器出栈,之前入栈的是要读取的扇区长度Npopw %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寄存器压入堆栈,后面会使用该寄存器pushapushw %ds//前面AX左移了5位,现在又左移了4位,一共相等于扩大了512倍,也就是计算出来了要拷贝的字节数
//将要拷贝的字节数赋值给CX计数寄存器中shlw $4, %axmovw %ax, %cx//清空SI寄存器和DI寄存器
//使用cld将方向标志位DF复位,既设置DF=0,其相反的指令为std
//DF=0表示向高地址增加,DF=1表示向低地址减少。cld复位DF之后,将向高地址增加。xorw %di, %dixorw %si, %simovw %bx, %dscld//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而直接跳转到bootitsubw $BOOTSEC_LISTSIZE, %dijmp bootloop//1.5阶段读取完成之后
bootit:
//打印一个回车符MSG(notification_done)
//将之前压栈的DX寄存器出栈,保证此时的DX中是原来的磁盘号popw %dx//执行一个长跳转,1.5阶段直接跳转到0x0000:0x2200地址处
//此时CS寄存器的值为0x0,EIP寄存器的值就是0x2200
//0x2200地址是通过0x220*16+0x0000得到的,该地址存储着1.5阶段的代码并开启1.5阶段 Success ^_^!
#ifdef STAGE1_5ljmp $0, $0x2200
#else /* ! STAGE1_5 */ljmp $0, $0x8200
#endif /* ! STAGE1_5 *//** BIOS Geometry translation error (past the end of the disk geometry!).*/
geometry_error:MSG(geometry_error_string)jmp general_error/** Read error on the disk.*/
read_error:MSG(read_error_string)general_error:MSG(general_error_string)/* go here when you need to stop the machine hard after an error condition */
stop: jmp stop#ifdef STAGE1_5
notification_string: .string "Loading stage1.5"
#else
notification_string: .string "Loading stage2"
#endifnotification_step: .string "."
notification_done: .string "\r\n"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, %bxmovb $0xe, %ahint $0x10incw %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_LISTSIZEblocklist_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 /* this is the segment of the starting addressto load the data into */
#endiffirstlist: /* this label has to be after the list data!!! */
这篇关于GRUB引导程序之承前启后的start.S—源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!