GRUB引导程序之承前启后的start.S—源码分析

2024-09-02 01:08

本文主要是介绍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—源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128563

相关文章

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

Spring中Bean有关NullPointerException异常的原因分析

《Spring中Bean有关NullPointerException异常的原因分析》在Spring中使用@Autowired注解注入的bean不能在静态上下文中访问,否则会导致NullPointerE... 目录Spring中Bean有关NullPointerException异常的原因问题描述解决方案总结

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

python-nmap实现python利用nmap进行扫描分析

《python-nmap实现python利用nmap进行扫描分析》Nmap是一个非常用的网络/端口扫描工具,如果想将nmap集成进你的工具里,可以使用python-nmap这个python库,它提供了... 目录前言python-nmap的基本使用PortScanner扫描PortScannerAsync异

Oracle数据库执行计划的查看与分析技巧

《Oracle数据库执行计划的查看与分析技巧》在Oracle数据库中,执行计划能够帮助我们深入了解SQL语句在数据库内部的执行细节,进而优化查询性能、提升系统效率,执行计划是Oracle数据库优化器为... 目录一、什么是执行计划二、查看执行计划的方法(一)使用 EXPLAIN PLAN 命令(二)通过 S