grub----Stage1.s源代码分析

Stage1.s源文件是用古老的 at&t汇编编写而成,是大名鼎鼎的unix家族操作系统引导程序GRUB中的第一个文件。它编译后产生的二进制代码正好是512字节(故意的, 也是必须的),刚好填充满硬盘初始的一个扇区,也即0柱面、0磁道、1扇区。人们称之为MBR——主引导记录。它的作用是载入stage2文件。

    阅读本段代码,gemfield建议你首先具备以下能力:cpu寄存器、BIOS中断、PC架构、at&t汇编、GRUB背景知识。幸运的,青岛 之光论坛(bbs.civilnet.cn)的嵌入系统版块里都或多或少包含了这些介绍。并且可以从青岛之光论坛上查找stage1.s的源代码,此处不 一一罗列了。

    程序刚开始处的宏定义使用了和gcc相同的规范,定义的3个宏变量在后面用到的地方再由gemfield详细阐述。在定义了一个全局变量_start后, 程序的真正入口就到了。事实上,在二进制代码中,开始部分的代码是eb48,其中eb就是jmp的机器码,在标号_start后,紧跟着的就是这个jmp 指令,跳转到after_BPB处。Jmp后的nop指令,恐怕永远也不会执行了。注意,刚开机时cpu会调用int19h将第一个扇区的内容调到内存地 址为0x0000:0x7coo处,你要问为什么是这个地址或者为什么会发生这样的调用,原因大抵和usb为什么是2根数据线是一样的。

    .=_start + 4是一个让人困惑的语句,其实这个dot是一个特殊的标号,在as汇编规范中,就代表当前的地址。从开始处的_start处填充空间至_start+4处,相当于4个字节的空间。但是,从_start开始后的jmp nop 和jmp的参数已经占用了3个字节的空间,相当于在它们的后面再用0填充1个字节的空间即可。

    后面紧跟的是一系列称之为汇编directive的“伪指令”。这一部分是对磁盘等一些参数进行设置。像起始的扇区、磁道和柱面以及它们的起始地址、还有 stage1的版本号、boot_drive变量、force_lba变量、stage2的地址、扇区、段等参数,这在后面的代码中涉及的时候再由 gemfield阐述,到时候gemfield会称这部分为初始化参数部分,切记。但在这一系列的参数设置中,还有个相似的语句,就 是.=_start+STAGE1_BPBEND,照样是从上一条指令处填充0直至到达_start+0x3e处。

    在jmp之后,清中断允许位,然后陈列80ca这个二进制代码。80ca就相当于orb $ox80,%dl,意思是给dl寄存器赋值80,要知道,在开机初始, BIOS加载完启动代码会把%dl寄存器设置成启动盘号(boot drive number):
DL = 00h 1st floppy disk ( "drive A:" )
DL = 01h 2nd floppy disk ( "drive B:" )
DL = 80h 1st hard disk
DL = 81h 2nd hard disk

    硬盘的代号是80,所以上面代表的是stage1装到硬盘上的情形,如果是软盘的话,就是orb $0x00,%dl,很显然,软驱代号是0x00。

    关于boot_drive_mask这一部分,包含的ljmp $0,ABS(real_start)指令的意思是,跳转到cs:ip = 0x0000:$ABS(real_start)这个地方执行指令。程序的开头部分定义了ABS这个宏,在此处就相当于real_start- _start+0x7c00。如果是“正常的”int19h中断,这句就是废话。因为物理地址是(Segment value * 16) + Offset value,正常情况下MBR被加载到cs:ip = 0x0000:0x7c00上,而有些糟糕的BIOS会将其加载到07c0:0000上,其实这两个代表的物理地址是完全一样的(你可以用上上行的公式计 算)。有些人从来就不考虑这种事实,那就是大多数人常常把segment值设为0,这样引导代码就可以假定任何段寄存器都是0从而只对付ip里的偏移量。 所以,在grub里,加上这么一个长转移,就防止了这类糟糕的BIOS带来的大麻烦。

    接着进入real_start了,ax清零,ds赋值0,ss赋值0,将STAGE1_STACKSEG(0x2000)赋值给sp,这样就设置了实模式 下的堆栈段地址(栈顶位置)ss:sp = 0x0000:0x2000。接着置中断允许位,然后检查是否设置了启动的磁盘。先用MOV_MEM_TO_AL宏将boot_drive量存到al中, 然后与0xff进行比较,用的是cmpb $0xff,%al ;je 1f。cmpb指令是将两个操作数进行相减,对标志位的影响同sub指令,但是不保存结果。其中,此处用到的是zf标志位(因为是je指令),这样,当操 作数相等(即相减为零时)zf被置1。所以,cmpb和je一起使用时,就是指,当操作数相等时,跳转至je制定的标号。所以,在这里,若 boot_drive等于0xff,则使用BIOS传递过来的默认的驱动器进行启动;如果不是,movb %al,%dl,将boot_drive的值保存至dl中,表示由boot_drive的值确定启动设备。不管怎么样,现在开始正式启动了……

    驱动器号信息压栈、输出信息“GRUB”,注意,在屏幕上输出信息时调用了MSG宏。下面分析一下这个宏,#define MSG(X)  movw $ABS(x),%si ;call message

输出GRUB字样时,变量是 notification_string,相当于将notification_string地址上的16位内容送入si寄存器,然后调用message函 数,而message函数使用了int10中断来在屏幕上显示字符。涉及到串操作指令。message函数:lodsb,从%si指向的源地址中逐一读取 一个字符,送入al中,然后检查al是否为零,如果为零,表示字符已经传输完成了(.string伪指令会在指定的字符串后加入一个字节的0),此时调用 ret返回。而若不为零,表明字符还未传输完,此时跳转到int 10h“中断前夕”,用int 10h 的oeh子功能在屏幕上以telemode模式写字符,其中,ah是子功能号,al是字符,bh是页,bl是前背景色(在图形模式下)。所以这里movw $0x0001,%bx ; movb $0x0e,%ah ;int $0x10(显示一个字符)就ok了。

    在屏幕上显示完GRUB后,要来决定是进入chs模式还是lba模式(也就是看硬盘是否支持LBA模式,因为两种模式对硬盘的读写等操作有很不一样的地 方),但在这之前,你得首先判断这里是硬盘而不是软盘或者根本就没有盘(言下之意就是,如果不是硬盘,判断LBA或者CHS模式就没有意义了),所以,在 判断硬盘是否支持LBA时,先判断是不是硬盘。这里用testb $STAGE1_BIOS_HD_FLAG,%dl来判断,dl寄存器里装载的是磁盘号,有三大类情况:硬盘(0x80、0x81)、软盘(0x00、 0x01)、无效的盘(0xff)。而前面的宏就是0x80,所以通过testb和jz指令判断,如果dl中不是80或81(也就是不是硬盘),就跳转到 chs_mode函数下面。另外,如果此处判断出是硬盘的话,再接着判断是否支持LBA,使用的工具就是BIOS的int 13h中断。通过 BIOS 调用 INT 0x13 来确定是否支持扩展,
LBA 扩展功能分两个子集 , 如下 :
第一个子集提供了访问大硬盘所必须的功能 , 包括
1.检查扩展是否存在 : ah = 41h , bx = 0x55aa , dl = drive( 0x80 ~ 0xff )
2.扩展读  : ah = 42h
3.扩展写  : ah = 43h
4.校验扇区  : ah = 44h
5.扩展定位  : ah = 47h
6.取得驱动器参数 : ah = 48h
第二个子集提供了对软件控制驱动器锁定和弹出的支持 ,包括
1.检查扩展  : ah = 41h
2.锁定/解锁驱动器 : ah = 45h
3.弹出驱动器  : ah = 46h
4.取得驱动器参数 : ah = 48h
5.取得扩展驱动器改变状态: ah = 49h

    下面开始具体检测 , 首先检测扩展是否存在。此时寄存器的值和 BIOS 调用分别是:AH = 0x41,BX = 0x55AA,DL = driver( 0x80 ~ 0xFF ),然后INT  13H,看返回结果:如果支持CF= 0;否则 CF = 1;CF = 0 (支持LBA) 时的寄存器值代表含义:
ah:扩展功能的主版本号( major version of extensions )
al:内部使用( internal use )
bx :AA55h ( magic number )
cx:Bits  Description
0  extended disk access functions
1  removable drive controller functions supported
2  enhanced disk drive (EDD) functions (AH=48h,AH=4Eh) supported.
Extended drive parameter table is valid
3~15  reserved (0)
CF = 1 (不支持LBA) 时的寄存器值 :
ah = 0x01 ( invalid function )

    现在stage1.s使用movb $0x41, %ah;movw $0x55aa, %bx;int $0x13; jc chs_mode来进行上述判断。如果不支持LBA,则cf就是1,跳转到chs_mode函数运行。有的bios的int 13h中断会影响到dl,所以此处用pop和push指令将其保护起来。然而cf不等于1也不表示就支持LBA了,还得再判断bx是不是aa55h,使用 cmpb $0xaa55,%bx ;jne chs_mode再判断一次,如果bx里存的不是预期的返回值,同样不支持lba,也要进入chs_mode函数。这里有个强制LBA模式要注意下,就是 说,当cf是1,bx也是aa55,那么可以不用在判断就进入强制LBA模式,代码是这样写的,使用MOV_MEM_TO_AL宏将force_lba变 量值传递到al,判断是否为0。不为零强行进入lba_mode函数。然后判断cx,如果cx为0的话表明不支持扩展第一子集,这时也进入 chs_mode函数。所以总结进入chs_mode的情况,如下:

第一、   磁盘号非80h或81h,进入chs_mode

第二、   int13h,41h子功能,返回cf为0,进入进入chs_mode

第三、   int13h,41h子功能,返回bx不为aa55,进入chs_mode

第四、   如果没有设置强制LBA,而且也不支持扩展第一子集,进入chs_mode

第五、   其它情况,进入lba模式

    那我们就先来分析进入chs模式的代码,你看,我们是以以上种种情况的发生而进入chs模式的,所以进入chs模式时,再来进行一些检测,来确定具体的情况。首先就是int13h的08功能号的使用。使用08功能可以检测chs模式中硬盘的参数,保存在各寄存器里:

DL:本机软盘驱动器的数目
DH:最大磁头号(或说磁面数目)。0表示有1个磁面,1表示有2个磁面
CH:存放10位磁道柱面数的低8位(高2位在CL的D7、D6中)。1表示有1个柱面,2表示有2个柱面,依次类推。
CL:0~5位存放每磁道的扇区数目。6和7位表示10位磁道柱面数的高2位。
AX=0
BH=0
BL表示驱动器类型:
1=360K 5.25
2=1.2M 5.25
3=720K 3.5
4=1.44M 3.5
ES:SI 指向软盘参数表

    如果成功返回参数,则进入final_init函数;但是如果调用失败,进位标志CF=1,AH存放错误信息码。表明不支持硬盘的chs模式(前面也判断 了不支持lba),那就要考虑是不是软盘了。再使用testb和jz指令,若dl是00或01,则认为是软盘,就跳转到floppy_probe函数执行 (后文讨论此函数)。但是若连软盘也不是,只好准备报错了。跳转到hd_probe_error函数,这个函数调用MSG函数连同 general_error函数一道输出“hard disk error”的字符。

    好了,现在我们回来。刚开始经过一些列的判断,我们进入了LBA模式。然后,代码做了以下工作,movl 0x10(%si),%ecx,这个代码就是个废话,ecx寄存器被置入了一个无意义的值;然后将标号disk_address_packet处的地址赋 给si,再接着将[si-1]内存处置1(也就是mode被置1,表示LBA扩展读;如果是0,就是CHS寻址读)、将stage2的扇区数赋予ebx、 在[si]和[si+1]处存放10和00(movw $0x0010,(si))、在[si+2]和[si+3]处存放01和00、在[si+4]和[si+5]处存放00和00、在[si+6]和 [si+7]处存放0x00和0x70(这是stage1_bufferseg的值)、在[si+8][si+9][si+A][si+B]处存放 0x01/0x00/0x00/0x00、在[si+c][si+d][si+e][si+f]处存放0x00/0x00/0x00/0x00。设置完毕 后,开始调用int 13h的42功能中断。如果出错,就跳转到chs_mode处。那么中断执行成功呢?

    由si及其偏移量指向的内存保存着磁盘参数块,如下:

偏移量     大小       位数       描述

00h       BYTE        8        数据块的大小 (10h or 18h)
01h       BYTE        8        保留,必须为0

02h       WORD      16       传输数据块数,传输完成后保存传输的块数

04h       DWORD     32       传输时的数据缓存地址

08h       QWORD     64       起始绝对扇区号(即起始扇区的LBA号码)

    所以,通过int13h(42)中断的作用,硬盘上第二个扇区上的内容就被读到由si偏移量为4h、5h、6h、7h确定的内存区域上了,此处是0x7000:0x0000。执行成功,将bx赋值0x7000,然后跳至copy_buffer子函数处。

    LBA已完,gemfield在阅读copy_buffer前再回头看当初程序跳至chs_mode后是怎么运行的。上文中已经指出了,到达 chs_mode后经过条件判断,一共产生了三种情况,第一是进入硬盘的chs子函数(final_init);第二是进入软盘子程序 (floppy_probe);第三种情况是进入报错子函数,在屏幕上输出一系列错误。那就由gemfield从第一种情况开始吧。程序运行到 final_init后,将扇区数保存到si、设置mode为0、eax清零为存放磁头数做准备、将dh中存放的磁头数保存到al中、使用incw %ax指令(因为磁头数是以0~n-1方式排列的,所以增1后才是真正的磁头数)、将磁头数保存至[si+4][si+5][si+6][si+7]内存 地址上、清dx为存放扇区数做准备、cl中的0~5位存放的是扇区数,所以dx逻辑左移2位后在dh中出现的两位就是柱面数的高2位,并且把这2位移到 ah中,而ch存放的柱面数低8为移至al中,这样ax里就是柱面数了,这里因为同样的道理要进行incw %ax操作,并且把真正的柱面数放到地址为[si+8][si+9]的内存上、然后用同样的移动方法产生真正的扇区数并保存在地址为[si][si+1] [si+2][si+3]的内存上。

    然后在使用int 13h(0x02)功能前要进行必备的参数设置:eax存放stage2的扇区编号(stage2_sector,默认为1)、清edx寄存器、然后通过 (stage2扇区数)/(扇区数)获得引导扇区数。注意对于div指令来说,eax恒定存放被除数,div后面的寄存器存放的是除数。余数在edx中存 放,第一个余数(扇区数)放到地址为[si+10]的内存上并将edx清零、再用(上一步除法的商) /(磁头数)得到的余数为磁头数,存放在[si+11]内存地址上。商为柱面数并存放在eax中并同时保存至[si+12][si+13]内存地址上。然 后将之前中断获得的柱面数与此处stage2所占柱面数相比较,如果stage2柱面数大,那么明显错误,程序将跳至geometry_error处。

    现在,将[si+13]的内容赋值给dl(柱面数的高2位)并且左移6位、将扇区数放到cl中再增1、然后通过orb %dl,%cl和movb 12(%si),%dh指令达到这么一种情况,即:cl中存放的是扇区数和柱面数的高2位,ch中存放的是柱面数的低8位、然后恢复驱动器号(popw %dx)、然后将磁头数放置到dh中,然后将0x7000赋值给es并将bx清零,赋值0x0201给ax(获得中断功能号),参数现在设置完毕,开始调用int 13h中断:

%al = number of sectors(需要读的扇区数)

%ah= 0x02(功能号)
                %ch = cylinder(起始柱面数)
                %cl = sector (bits 6-7 are high bits of "cylinder")
                %dh = head
                %dl = drive (0x80 for hard disk, 0x0 for floppy disk)
                %es:%bx = segment:offset of buffer

    调用中断后,将0柱面、0磁道、2扇区的内容读到0x7000:0x0000内存处。然后程序跳转至copy_buffer处,和LBA殊途同归呀。

    我们看看copy_buffer做了什么。将0x8000赋值给es、给cx赋值0x100、给ds赋值0x7000、si和di清零、方向标志DF置 零,然后使用rep和movsw指令将ds:si处连续的512字节内容传输到es:di指定的内存地址(0x8000:0x0000)。其中,rep指 令的含义就是重复执行后一句指令,没执行一次。cx减1,直至cx为0。这也是前面cx赋值0x100(256)的原因。movsw每次传输一个 字,256次就是512字节。然后popw %ds; popa还原寄存器。

    接着,程序跳转到0x8000处继续执行,到此就开始执行新的模块了,stage1的任务也已经结束了。代码中*(stage2_address)的星号是at&t汇编的规范:绝对跳转/调用(相对于与程序计数器有关的跳转/调用)操作数前面要加星号"*"。

    然而,前面所述的chs模式中的第二种情况——软驱情况将会带领gemfield进入floppy_probe子函数,此处要使用int 13h(0x00功能号)来进行软驱的复位。成功的话cf=0; 然后准备调用int 13h(功能号是0x02),这和chs中的int 13h,ah=0x02是一样的。所以,先来为中断准备必须的参数:软驱复位后,将[si]处的值赋给cl(cl是起始扇区数),我们知道,由于循环,我们给了cl 4次机会,因为循环中有incw %si指令,所以si中的值是递增的,从probe_values开始,在每一次的机会中依次给cl赋予了0x24、0x12、0x0f、0x09这几种值,当然,试完后还不对的话就要执行报错函数了。

    像以前那样,依次准备好bx、ah、al、ch、cl、dh的值后,就要int 13h了。成功后,dh赋值1、ch赋值0x4f,dh 设置为 79 , 表示柱面最大值为 79(80柱:0~79),dh 设置为 1 , 表示磁头数最大值为 1(2头:0~1),然后跳转至 final_init,在上文中关于final_init的分析 , 我们知道保存时会把柱面和磁头分别加 1 , 扇区不变,因此 , 在软盘加载时 , 将设置 Cylinder : Head : Sector = 80 : 2 : start_sector。最终就跳转至final_init函数处执行了。

    gemfield的本文中,依然要注意的还有为了兼容性而设置的windows nt魔术头标识的偏移、part_start作为标识的分区表起始地址的标记的偏移、以及引导扇区结束标志0xaa55。

    总的说来,在gemfield这篇稍显凌乱的文章里,主要介绍了stage1.s的使命,简介来说,就是开机时首先被BIOS INT19H装载到内存0x7c00处,然后判断chs和lba模式,然后使用int13h中断将磁盘上第二扇区的内容读到0x7000处,然后通过子函 数copy_buffer再将其调到0x8000的位置上,这个第二扇区的内容就是以后gemfield的嵌入系统版块中将要介绍的start.s模块。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值