读了2004年《程序员》杂志中的《程序员田园》板块中相关的文章之后,对此颇有感触。因此也想对此作一番小小的研究。
其实说穿了,系统引导很简单:
1、 启动电源之后,硬件完成它所应完成的工作;
2、 BIOS按照CMOS中保存的设置,按照启动顺序读取启动盘中的0柱面0磁道1扇区,将512字节装载到内存的0:7C00处并运行,并把硬件的控制权交给软件;判断一个扇区是否是引导扇区的依据是它是否以055AA结束;
3、 这512字节引导记录的任务就是完成必需的初始化工作,然后装载、运行操作系统的内核。
分析完原理之后,让我们来动动手,作一番尝试。
该过程所需要的工具如下:
1、 NASM或MASM汇编语言编译器,我选择NASM;自从接触到这个Open Source的编译器以后,我就把MASM扔在一旁了。其主要原因,是因为它可以编写跨平台的程序。下载连接:http://sourceforge.net/projects/nasm
2、 一个虚拟机软件,VMWare或者VirtualPC均可。用虚拟机做实验,可免去一次又一次重启的麻烦,也可以保护自己的爱机;
3、 Ultra Edit + ASM语法高亮显示包。用汇编语言写程序,没有什么好的IDE,这个我认为是最完美的组合了。也有人推荐用Edit Plus的,但由于个人喜好的原因,我一直使用这个。http://www.ultraedit.com,词库文件可以免费下载。
先来做一个最简单的测试,它完成的任务仅仅是引导系统后在屏幕上显示红色的’A’:
打开Ultra Edit,输入如下代码:
ORG 7C00H ;设置程序装载时的偏移量
MOV AH,0 ;调用BIOS中断,设置显示模式
MOV AL,1
INT 10H
MOV AH,09H ;调用BIOS中断,显示字符
MOV AL,'A'
MOV BH,0
MOV BL,4
MOV CX,1
INT 10H
JMP $ ;停止程序运行
DW 0AA55H ;扇区结束标志
然后编译这段程序,nasm boot.asm –f bin –o boot.bin。
-f开关符是指定输出方式为flat,-o是指定输出文件。Flat方式不会为源程序添加任何其它信息,只是照翻源程序,将其翻译成机器码。不过NASM默认的输出方式就是flat,所以这个开关符也可以省略。
因为其不足512字节,所以我用UltraEdit的Hex编辑模式将其打开,插入489个字节,将其凑满512字节。
接下来的任务就是准备一张软盘,用绝对扇区读写工具将其写入磁盘;幸好,手头的MINIX源码光盘中有这么一个工具,可以将其编译一下拿来用,源代码如下:
/* fdvol.c - A stripped down version of the MINIX vol program. It takes
* a file and breaks into into floppy-sized chunks, writing each one raw
* to a floppy.
*
* Usage: fdvol file drive KB [slow]
*
* Examples: fdvol 1440 a: foo.taz # 1.4 MB floppy a:
* fdvol 1200 b: foo.taz # 1.2 MB floppy b:
* fdvol slow 360 a: foo.taz # old machine
* fdvol 1440 a: foo bar # concatenate
*
* The optional 'slow' parameter forces the program to write in units of 3
* sectors. Folk tradition has it that this works around some buggy BIOSes.
*
* This code borrows heavily from Mark Becker's RaWrite program.
*/
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define TEST 0
#if !TEST
#include <alloc.h>
#include <bios.h>
#include <dir.h>
#include <dos.h>
#include <io.h>
#endif
#define FALSE 0
#define TRUE (!FALSE)
#define SECTORSIZE 512
#define RESET 0
#define LAST 1
#define READ 2
#define WRITE 3
#define VERIFY 4
#define FORMAT 5
int done;
char buffer[18*SECTORSIZE]; /* do I/O in units of up to 18 sectors */
char testbuf[SECTORSIZE]; /* to do a test read of the first sector */
int handler(void)
{
/* Catch CTRL-C and CTRL-Break. */
done = 1;
return(0);
}
void msg(char (*s))
{
/* Print an error message and quit. */
fprintf(stderr, "%s\n", s);
_exit(1);
}
void Error(int status, int cyl, int head, int sector)
{
/* Identify the error code with a real error message. */
fprintf(stderr, "\nError occured while writing cyl %d, head=%d, sector=%d\n", cyl,head,sector+1);
switch (status) {
case 0x00: msg("Operation Successful");
break;
case 0x01: msg("Bad command");
break;
case 0x02: msg("Address mark not found");
break;
case 0x03: msg("Attempt to write on write-protected disk");
break;
case 0x04: msg("Sector not found");
break;
case 0x05: msg("Reset failed (hard disk)");
break;
case 0x06: msg("Disk changed since last operation");
break;
case 0x07: msg("Drive parameter activity failed");
break;
case 0x08: msg("DMA overrun");
break;
case 0x09: msg("Attempt to DMA across 64K boundary");
break;
case 0x0A: msg("Bad sector detected");
break;
case 0x0B: msg("Bad track detected");
break;
case 0x0C: msg("Unsupported track");
break;
case 0x10: msg("Bad CRC/ECC on disk read");
break;
case 0x11: msg("CRC/ECC corrected data error");
break;
case 0x20: msg("Controller has failed");
break;
case 0x40: msg("Seek operation failed");
break;
case 0x80: msg("Attachment failed to respond");
break;
case 0xAA: msg("Drive not ready (hard disk only");
break;
case 0xBB: msg("Undefined error occurred (hard disk only)");
break;
case 0xCC: msg("Write fault occurred");
break;
case 0xE0: msg("Status error");
break;
case 0xFF: msg("Sense operation failed");
break;
}
exit(1);
}
void main(int argc, char *argv[])
{
int disknr = 1, fdin, count, drive, head, cyl, status, sector;
int max_cyl, chunk, nsectors, ct;
long offset, drive_size, r, cyl_size;
char *p, c;
int slow;
int kbsize;
char **files;
int nfiles, i;
#if !TEST
/* Catch breaks. */
ctrlbrk(handler);
#endif
#if 0 /* Do we have to clear the screen? */
fprintf(stderr, "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
#endif
if (argc > 1 && strcmp(argv[1], "slow") == 0) { /* Lousy BIOS? */
slow = 1;
argc--;
argv++;
} else {
slow = 0;
}
/* Check the arguments for validity. */
if (argc < 4)
msg("Usage: fdvol [slow] #kilobytes drive-letter file1 [file2 ...]");
kbsize = atoi(argv[1]);
p = argv[2];
c = *p;
if (c == 'a' || c == 'A')
drive = 0;
else if (c == 'b' || c == 'B')
drive = 1;
else
msg("fdvol: Second parameter must be drive, either a: or b:");
files = argv + 3;
nfiles = argc - 3;
switch(kbsize) {
case 360:
cyl_size = 9*2*SECTORSIZE; /* bytes/cylinder */
max_cyl = 39; /* zero-based counting */
drive_size = cyl_size * (max_cyl+1);
chunk = (!slow ? 9 * SECTORSIZE : 3 * SECTORSIZE);
nsectors = chunk/SECTORSIZE;
break;
case 720:
cyl_size = 9*2*SECTORSIZE; /* bytes/cylinder */
max_cyl = 79; /* zero-based counting */
drive_size = cyl_size * (max_cyl+1);
chunk = (!slow ? 9 * SECTORSIZE : 3 * SECTORSIZE);
nsectors = chunk/SECTORSIZE;
break;
case 1200:
cyl_size = 15*2*SECTORSIZE; /* bytes/cylinder */
max_cyl = 79; /* zero-based counting */
drive_size = cyl_size * (max_cyl+1);
chunk = (!slow ? 15 * SECTORSIZE : 3 * SECTORSIZE);
nsectors = chunk/SECTORSIZE;
break;
case 1440:
cyl_size = 18*2*SECTORSIZE; /* bytes/cylinder */
max_cyl = 79; /* zero-based counting */
drive_size = cyl_size * (max_cyl+1);
chunk = (!slow ? 18 * SECTORSIZE : 3 * SECTORSIZE);
nsectors = chunk/SECTORSIZE;
break;
default:
msg("fdvol: First parameter must be one of: 360, 720, 1200, or 1440");
}
#if !TEST
biosdisk(RESET, drive, 0, 0, 0, 0, testbuf);
#endif
/*
* Start writing data to diskette until there is no more data to write.
* Optionally read and write in units of 3 sectors. Folk tradition says
* that this makes fewer buggy BIOSes unhappy than doing a whole track at a
* time.
*/
offset = 0;
i = 0;
fdin = -1;
while(1) {
if (done > 0) {
if (done == 1) msg("User abort");
#if !TEST
biosdisk(READ, drive, 0, 0, 1, 1, testbuf); /* Retract head */
#endif
fprintf(stderr, "Done. \n");
exit(done == 1 ? 1 : 0);
}
/* Until a chunk is read. */
count = 0;
while (count < chunk) {
if (fdin == -1) { /* open next file */
#if !TEST
_fmode = O_BINARY;
#endif
fdin = open(files[i], O_RDONLY);
if (fdin < 0) {
perror(files[i]);
exit(1);
}
}
/* read from file */
ct = read(fdin, buffer + count, chunk - count);
if (ct < 0) {
perror(files[i]);
exit(1);
}
if (ct == 0) { /* end of file */
close(fdin);
fdin = -1;
/* choose next file */
if (++i >= nfiles) break; /* no more files */
}
count += ct;
}
if (count == 0) { /* absolute EOF */
done = 2;
continue;
}
if (count < chunk) { /* pad last track */
/* Pad out buffer with zeroes. */
p = &buffer[count];
while (p < &buffer[chunk]) *p++ = 0;
done = 2;
}
r = offset % drive_size;
if (r == 0) {
/* An integral number of diskettes have been filled. Prompt. */
fprintf(stderr, "Please insert formatted diskette #%d in drive %c, then hit Enter%c\n", disknr, c, 7);
disknr++;
#if !TEST
while(bioskey(1) == 0) ; /* wait for input */
if ((bioskey(0) & 0x7F) == 3) exit(1); /* CTRL-C */
biosdisk(READ, drive, 0, 0, 1, 1, testbuf); /* get it going */
#endif
}
/* Compute cyl, head, sector. */
cyl = r/cyl_size;
r -= cyl * cyl_size;
head = (r < cyl_size/2 ? 0 : 1);
r -= head * cyl_size/2;
sector = r/SECTORSIZE;
fprintf(stderr, "Track: %2d Head: %d Sector: %2d File offset: %ld\r",
cyl, head, sector+1,offset);
#if !TEST
status = biosdisk(WRITE, drive, head, cyl, sector+1, nsectors, buffer);
if (status != 0) Error(status, cyl, head, sector);
#else
write(1, buffer, chunk);
#endif
offset += chunk;
}
}
然后,用这个工具将刚才编译成功的bin文件写入软盘就成功了。J
如果要对其进行更深入的研究,可以参阅相关书籍。
在http://sourceforge.net/上面搜索NASM的时候,我偶然看到了一个FDOS的项目,可以免费获得。它是“在一张磁盘上的DOS系统”,支持常用DOS命令、FAT12、TSR等。它的文档中有很详细的引导记录说明,我将在下一篇中对其源代码作一些剖析。
另外,网上也能找到MSDOS系统的汇编源代码,有兴趣的同志也可以拿来研究一下。当然,拿Linux的引导代码来研究也未尝不可,但刚开始的时候,不应好高骛远,打击自己的信心,还是从简单的开始吧。J