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	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)

/*
disk_address_packet地址处的数据与磁盘参数的对应关系:
	struct dap {
	u8 len; 一般长度取值为0x10,表示dap结构长度为16字节
	u8 zero; 默认必须为0
	u16 nsector: 实际上是8位有效,表示读取的扇区数,一般取值从1~127
	u16 addr: 内存地址addr
	u16 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]=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地址处
//此时CS寄存器的值为0x0,EIP寄存器的值就是0x2200
//0x2200地址是通过0x220*16+0x0000得到的,该地址存储着1.5阶段的代码并开启1.5阶段 Success ^_^!
#ifdef STAGE1_5
	ljmp	$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"
#endif

notification_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, %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	/* this is the segment of the starting address
			   to load the data into */
#endif
	
firstlist:	/* this label has to be after the list data!!! */

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值