您的位置:首页 > 其它

基于扩展INT 13H的光驱引导程序设计

2014-02-15 01:53 309 查看
    最近一直在研究操作系统,好多资料的引导程序完全基于软盘设计,在软盘早已被淘汰的今天,用软盘没什么意思了就,这里我是基于最常见光盘设计的,光盘还有一好处,就是引导扇区可以大于512字节,我们要考虑的,仅在于实模式下的地址访问空间(1M),这对于一个引导程序来说,已经很够用了,所以我们可以直接用C语言嵌入汇编实现,毕竟现在GCC是很NB的。

        首先参考了基于软盘的引导程序,一般用的是INT13中断,也试了一下,发现INT13获得的所谓的第一扇区是从引导扇区开始的,这样就会产生一个问题:无法读取ISO9660的文件头(ISO9660文件头之后才是引导扇区)。这样的办法一定不行,因为如果这样,我们用的系统盘的引导就没有办法实现了,因为系统盘里面文件更是多而杂,没有ISO文件头的支持,是根本没办法获得文件的相关数据的。所以肯定会有更好的办法,只是还不了解。于是我便从windows7的安装光盘的引导程序下手,IDA反编译看出引导程序调用INT13之前在AH寄存器装入了0x42,搜了一下,还真的有,这个办法叫扩展INT13.

为了确定BIOS是否支持扩展INT13(其实有些多余,不过还是分享下吧),我们先来写程序来测试一下。

        入口:AH=0x41,DL=设备号,BX=0x55AA返回:若存在,CF=1,BX=0xAA55,AH=主版本号,若不存在,CF=0,AH=错误码。

        这里我在用BOCHS和VMWARE虚拟的时候发现即使存在CF最终也会为1,不知道为什么,在这个问题上我纠结了好久,甚至翻看了BOCHS虚拟机的源代码,也没查出个究竟来。还好有第二个办法:检测BX值。附代码:
char IsExtened()//返回1,即存在;返回0,即不存在
{
unsigned int BxRtn = 0;//因为C代码中嵌入汇编一般寄存器会用32位的,所以为了防止内存访问过界,我们也要用32位的
__asm__ __volatile__(
"movb $0x41, %%ah\n\r"
"movw $0x55aa, %%bx\n\r"
"int $0x13\n\r"
"movl %0, %%ebx\n\r"//32位赋值
:"=r"(BxRtn)//输出BX到BxRtn,这里要注意gcc中汇编嵌入的输入和输出用到的r,r所代表的是32位的寄存器,百度上很多说明都是错误的
:
);
return BxRtn == 0xaa55;
}

        有了上面代码的保证,我们就可以更踏实地实现从光驱读数据了。

        下面具体了解下扩展INT13的用法,这里我们仅以读为例。

        扩展读入口:AH=0x42,DL=设备号,DS:SI指向DAP(Disk Address Packet)结构。
struct DiskAddressPacket
{
unsigned char PacketSize;// 数据包尺寸,一般直接填0x10
unsigned char Reserved;// 必须为0
unsigned short BlockCount;// 要传输的数据块个数(以扇区为单位)
unsigned long BufferAddr;// 缓冲区地址
unsigned long long BlockNum;// 磁盘起始绝对扇区数
};

    这里要注意扩展INT13和一般INT13的区别,前者的单位扇区大小是由存储介质确定的,而后者固定为512字节。所以这里要小心了,我们的ISO9660文件系统一般单位扇区大小为0x800字节,也就是2048字节,其实ISO9660还有逻辑块的说法,这里我们不做区分,暂且把逻辑块和扇区看做同等,这样不会影响到我们编程。这里我们直接定义一个地址,让DAP结构指针直接指向该地址,这样我们可以更方便地进行调试。

#define  DAPAddr 0xa000//DAP结构首地址
int MapIsoBlockToMemory(unsigned long addr,unsigned long long Block,unsigned short count)//内存地址,第几个块,读取的个数
{
int status = 0;
struct DiskAddressPacket *DAP;
DAP = (struct DiskAddressPacket*)DAPAddr;
DAP->Reserved = 0;
DAP->PacketSize = 0x10;
DAP->BlockCount = count;
DAP->BlockNum = Block;
DAP->BufferAddr = addr;
__asm__ __volatile__(
"movl %1, %%edx\n\r"
"movl %2, %%esi\n\r"
"movb $0x42, %%ah\n\r"
"int $0x13\n\r"
"jne Success\n\r"
"movl $0, %0\n\r"
"Success:\n\r"
"movl $1, %0\n\r"
:"=r"(status)//为0失败,为1成功
:"r"(GetCdromNumber()),"p"(DAP)//GetCdromNumber()返回驱动号
);
//DispInt16(status);
return status;
}
    到这里,我们就解决的光驱文件映射至内存的问题,下面就是了解ISO9660文件系统并把内核文件映射至内存了,这里我们需要参考文档ECMA-119里面有非常详细的介绍,因为这里我们仅是以映射内核文件至内存为目的,所以并没有完全实现它,够用就好。

 
  首先我们必须要接触的结构是Primary Volume Descriptor,里面详细记载了整个ISO文件的数据,其实也可以是其他的结构来记录,但因为最常见的是这个结构,所以就以它为例了,想知道更多东西的朋友可以看ECMA文档。代码选自linux源代码的Iso_fs.h,这里提到的711是指ECMA-119中7.1.1章节说明的数据记录方式,下同。这里我们仅注意我们需要的成员:
#define ISODCL(from, to) (to - from + 1)
struct iso_primary_descriptor {
char type           [ISODCL (  1,   1)]; /* 711 */
char id             [ISODCL (  2,   6)];
char version            [ISODCL (  7,   7)]; /* 711 */
char unused1            [ISODCL (  8,   8)];
char system_id          [ISODCL (  9,  40)]; /* achars */
char volume_id          [ISODCL ( 41,  72)]; /* dchars */
char unused2            [ISODCL ( 73,  80)];
char volume_space_size      [ISODCL ( 81,  88)]; /* 733 */
char unused3            [ISODCL ( 89, 120)];
char volume_set_size        [ISODCL (121, 124)]; /* 723 */
char volume_sequence_number [ISODCL (125, 128)]; /* 723 */
char logical_block_size     [ISODCL (129, 132)]; /* 723 */ //我们需要的,逻辑块大小,其实也可以硬性规定为0x800,这里是为了保证通用
char path_table_size        [ISODCL (133, 140)]; /* 733 */
char type_l_path_table      [ISODCL (141, 144)]; /* 731 */
char opt_type_l_path_table  [ISODCL (145, 148)]; /* 731 */
char type_m_path_table      [ISODCL (149, 152)]; /* 732 */
char opt_type_m_path_table  [ISODCL (153, 156)]; /* 732 */
char root_directory_record  [ISODCL (157, 190)]; /* 9.1 */  //我们需要的,记载了记录根文件相关数据的数据结构
char volume_set_id      [ISODCL (191, 318)]; /* dchars */
char publisher_id       [ISODCL (319, 446)]; /* achars */
char preparer_id        [ISODCL (447, 574)]; /* achars */
char application_id     [ISODCL (575, 702)]; /* achars */
char copyright_file_id      [ISODCL (703, 739)]; /* 7.5 dchars */
char abstract_file_id       [ISODCL (740, 776)]; /* 7.5 dchars */
char bibliographic_file_id  [ISODCL (777, 813)]; /* 7.5 dchars */
char creation_date      [ISODCL (814, 830)]; /* 8.4.26.1 */
char modification_date      [ISODCL (831, 847)]; /* 8.4.26.1 */
char expiration_date        [ISODCL (848, 864)]; /* 8.4.26.1 */
char effective_date     [ISODCL (865, 881)]; /* 8.4.26.1 */
char file_structure_version [ISODCL (882, 882)]; /* 711 */
char unused4            [ISODCL (883, 883)];
char application_data       [ISODCL (884, 1395)];
char unused5            [ISODCL (1396, 2048)];
};
    这样的结构其实不不复杂,我们仅需要用前面的MapIsoBlockToMemory函数将ISO文件的第一扇区(其实是第16扇区)映射到一块缓冲区,缓冲区首地址即是该结构的首地址。

 
  下一个我们需要研究的结构是Directory Record,这里面记录了一层文件夹的相关数据,因为ISO文件是可以分很多层的,会有各种子目录,所以会有很多这样的结构,这里我们仅研究根目录的数据,毕竟内核文件一般是直接放在根目录的,参考了linux代码,这里我们仿照着也写一个。
struct RootDirectoryRecord
{
char lengh_of_Directory_Record[ISODCL (1, 1)];/*711*/  //需要注意的,循环的时候会用到
char extended_attribute_record_lengh[ISODCL (2,2)];/*711*/
char location_of_extent[ISODCL (3, 10)];/*733*/  //需要注意的,根文件数据入口,以扇区为单位
char data_lengh[ISODCL (11, 18)];/*733*/  //需要注意的,文件大小
char record_of_data_and_time[ISODCL (19, 25)];
char file_flags[ISODCL (26, 26)];
char file_unit_size[ISODCL (27, 27)];/*711*/
char interleave_gap_size[ISODCL (28, 28)];/*711*/
char volumm_sequence_number[ISODCL (29, 32)];/*711*/
char lengh_of_file_identifier[ISODCL (33, 33)];//需要注意的,文件名长度
char file_identifier;//需要注意的,文件名,注意这里我们用到的是char类型,其实没什么意义,读取的时候只需要取此成员的首地址读取就行了
};
    这里要注意一点,ISO文件头所记录的root_directory_record实质是描述了根文件目录,里面的location of extent正是指向了存放根文件目录的Directory Record结构的扇区,这个扇区里面会存放很多的Directory Record结构,这些结构会对根目录的每个文件进行描述。这里如果大家不太理解,可以用WinHex研究下,很快就能理解了。有了这两个结构,搜索内核文件就容易多了。
const char KernelName[]="KERNEL.BIN";
#define BufferBaseAddr 0x9000//这里便于调试,临时buffer和kernel文件的映射内存区域分开了
#define KernelBaseAddr 0x2000//内核文件映射首地址
#define FirstBlock 16//第一扇区
int MapKernelToMemory()
{
struct PrimaryVolumeDescriptor *PVD;
struct RootDirectoryRecord *RDR;
if(!IsExtened())
{
DispString16("System Error!",13);
return -1;
}
if(!MapIsoBlockToMemory(BufferBaseAddr,FirstBlock,1))//映射ISO9660文件头
{
DispString16("map   Error!",13);
return -1;
}
PVD = (struct PrimaryVolumeDescriptor*)BufferBaseAddr;
unsigned int BlockSize = 0;
BlockSize = (PVD->logical_block_size[2] << 8) + PVD->logical_block_size[3];//获取块大小
//DispInt16(PVD);
//DispString16("NextBlock0",10);
//DispInt16(BlockSize);
RDR = (struct RootDirectoryRecord*)PVD->root_directory_record;//获取描述根目录的数据结构
unsigned int NextBlock = 0;
unsigned long FileSize = 0;
//DispString16("NextBlock1",10);
//DispInt(RDR->location_of_extent[9]);
NextBlock = (RDR->location_of_extent[4] << 24) +(RDR->location_of_extent[5] << 16) + (RDR->location_of_extent[6] << 8)+ RDR->location_of_extent[7];//取得记录Directory Record的扇区
//NextBlock = (BlockSize * NextBlock)/512;//千万千万要注意,我就上过当
//DispInt16(NextBlock);
if(!MapIsoBlockToMemory(BufferBaseAddr,NextBlock,1))
{
DispString16("Map Error...",12);
return -1;
}
RDR = (struct RootDirectoryRecord*)BufferBaseAddr;
for(;(*RDR->lengh_of_Directory_Record) != 0;RDR = (struct RootDirectoryRecord*)(((char*)RDR) + (*RDR->lengh_of_Directory_Record)))//激动人心的搜索
{
if(*RDR->lengh_of_file_identifier == 0)//可能会有空描述,不鸟他
continue;
if(!strcmp(&RDR->file_identifier,KernelName))//这个strcmp要自己实现了,我就不贴了,网上有很多的
{//搜索到了
FileSize = (((RDR->data_lengh[7] + (RDR->data_lengh[6] << 8)) + (RDR->data_lengh[5] << 16)) + (RDR->data_lengh[4] << 24));//文件大小
NextBlock = (RDR->location_of_extent[6] << 8) + RDR->location_of_extent[7];//取2字节
MapIsoBlockToMemory(KernelBaseAddr,NextBlock,(FileSize / BlockSize) + 1);//映射
break;//退出搜索
}
}
return 0;
}
 
  这样,基于光驱的引导程序的关键部分我们就完成了,总的来说还是比较容易实现的,毕竟是用C,对于刚学汇编不久的我算是很兴奋的一件事了。

    最后建议,千万不要用百度,虽然多数时候他会帮你,但是有时候真的会很坑。。。有点英文基础的,直接上google吧。

    有的博客用一句话来结束文章,诸如你不能休息什么的,我觉得没必要说得那么励志了,好像马上要上刀山下火海。我只想说:理想,总是要顺其自然的,跟着心走,听听内心在说什么。千万不要价值观绑架,用别人的眼睛看自己,那样真的会很累,而且还会被时代的大山压得死死的。

参考文献:

Wikipedia
-- INT 13H    

ECMA-119
   
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息