全部博文(685)
分类: LINUX
2015-03-12 15:44:41
阅读本段代码,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模块。