3 对FAT的支持(harib16c)
笔者在这里得先跟大家道个歉,其实这个type命令是有问题的,有时候会无法正确显示文件内容。现在的type命令,肯定可以正确显示文件开头的512个字节的内容,但是如果遇到大于512个字节的文件,中间可能就会突然显示出其他文件的内容。
因此,我们需要解决这个问题。
按照Windows管理磁盘的方法,保存大于512字节的文件时,有时候并不是存入连续的扇区中,虽然这种情况并不算多。怎么样,吓一跳吧?如果中了这一招的话,harib16b可就应付不了了。
其实,对于文件的下一段存放在哪里,在磁盘中是有记录的,我们只要分析这个记录,就可以正确读取文件内容了。这个记录在哪里呢?它位于从0柱面、0磁头、2扇区开始的9个扇区中,在磁盘映像中相当于0x000200~0x0013ff。这个记录被称为FAT,是“file allocation table”的缩写,翻译过来叫作“文件分配表”(即记录文件在磁盘中存放位置的表)。
补充一下,文件在磁盘中没有存放在连续的扇区,这种情况被称为“磁盘碎片”。Windows中的磁盘整理工具,就是用来将扇区内容重新排列,以减少磁盘碎片的程序。
■■■■■
再往后光靠文字已经很难讲明白了,我们还是先看看FAT的样子吧。
这就是FAT
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0123456789ABCDEF
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0123456789ABCDEF
000200 F0 FF FF 03 40 00 05 60 00 70 80 00 09 A0 00 0B ….@..
........
000210 C0 00 0D E0 00 0F 00 01 11 20 01 13 40 01 15 60 ......... ..@..
000220 01 17 80 01 19 A0 01 1B C0 01 1D E0 01 1F 00 02 …………….
000230 21 20 02 23 40 02 25 60 02 27 80 02 29 A0 02 2B …….&.29…..
000240 C0 02 2D E0 02 2F 00 03 31 20 03 33 40 03 35 60 ..-../..1 .3@5.`
000250 03 37 80 03 39 F0 FF 3B C0 03 3D E0 03 3F F0 FF .7..9..;..=..?..
000260 FF 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
000270 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
这就是harib16b磁盘映像中的FAT,不过这个FAT使用了微软公司设计的算法进行压缩,因此我们无法直接看懂里面的内容,需要先用下面的方法进行解压缩(看了下面的讲解,可能有人会想,这哪里算是压缩啊?关于这一点,我们会在后面的专栏中详细讨论)。
首先将数据以3个字节分为一组,并进行换位,我们用开头的“F0 FF FF”来举例。
- F0 FF FF → FF0 FFF
- ab cd ef dab efc
为了让大家看清楚具体是如何换位的,我们在下面标记了英文字母。换位之后,原来的3个字节变成了2个数字。同样地,“03 40 00”换位后结果如下。
- 03 40 00 → 003 004
- ab cd ef dab efc
以此类推,我们对整个FAT解码之后的样子如下。
FAT数据
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9
0 FF0 FFF 003 004 005 006 007 008 009 00A
10 00B 00C 00D 00E 00F 010 011 012 013 014
20 015 016 017 018 019 01A 01B 01C 01D 01E
30 01F 020 021 022 023 024 025 026 027 028
40 029 02A 02B 02C 02D 02E 02F 030 031 032
50 033 034 035 036 037 038 039 FFF 03B 03C
60 03D 03E 03F FFF FFF 000 000 000 000 000
■■■■■
虽然我们已经对数据进行了解压缩,不过这样还是看不懂啊。我们先来看看文件信息的部分。
harib16b的磁盘映像节选
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0123456789ABCDEF
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0123456789ABCDEF
002600 48 41 52 49 42 4F 54 45 53 59 53 20 00 00 00 00 HARIBOTESYS ….
002610 00 00 00 00 00 00 18 74 FF 32 02 00 C8 6E 00 00 …….t.2…n..
002620 49 50 4C 31 30 20 20 20 4E 41 53 20 00 00 00 00 IPL10 NAS ….
002630 00 00 00 00 00 00 B9 34 63 34 3A 00 B0 0B 00 00 …….4c4:…..
002640 4D 41 4B 45 20 20 20 20 42 41 54 20 00 00 00 00 MAKE BAT ….
002650 00 00 00 00 00 00 F6 10 81 30 40 00 2E 00 00 00 ………0@…..
002660 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
从上面的信息我们可以得到:
- HARIBOTE.SYS 02 00 → clustno = 0x0002
- IPL10.NAS 3A 00 → clustno = 0x003a
- MAKE.BAT 40 00 → clustno = 0x0040
我们以haribote.sys为例来分析一下。已知clustno = 2,因此我们读取0x004200~0x0043ff这512个字节。那么接下来应该读取哪里的数据呢?我们来看FAT的第2号记录,其值为003,也就是说下面的部分存放在clustno = 3这个位置,即读取0x004400~0x0045ff这512个字节。再接下来参照FAT的第3号记录,即可得到下一个地址clustno = 4。
以此类推,我们一直读取到clustno = 57(0x39)。57号扇区后面应该读哪里了呢?参照对应的FAT记录,居然是FFF。也就是说,57号之后已经没有数据了,即这里就是文件的末尾。一般来说,如果遇到FF8~FFF的值,就代表文件数据到此结束。
正如上面所讲的,文件读取就是一个接力的过程,大家明白了吗?不明白也没关系,看了程序代码相信大家一定会理解的。
不过,这种接力的方式下,只要其中一个地方损坏,之后的部分就会全部混乱。因此,微软公司将FAT看作是最重要的磁盘信息,为此在磁盘中存放了2份FAT。第1份FAT位于0x000200~0x0013ff,第2份位于0x001400~0x0025ff。其中第2份是备份FAT,内容和第1份完全相同。
■■■■■
好了,我们来写程序吧,代码如下。
本次的bootpack.c节选
void console_task(struct SHEET *sheet, unsigned int memtotal)
{
(中略)
int *fat = (int *) memman_alloc_4k(memman, 4 * 2880);
(中略)
file_readfat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); /*这里!*/
(中略)
for (;;) {
io_cli();
if (fifo32_status(&task->fifo) == 0) {
(中略)
} else {
(中略)
if (256 <= i && i <= 511) { /*键盘数据(通过任务A)*/
if (i == 8 + 256) {
(中略)
} else if (i == 10 + 256) {
(中略)
if (strcmp(cmdline, "mem") == 0) {
(中略)
} else if (strcmp(cmdline, "cls") == 0) {
(中略)
} else if (strcmp(cmdline, "dir") == 0) {
(中略)
} else if (strncmp(cmdline, "type ", 5) == 0) {
/* type命令*/
(中略)
if (x < 224 && finfo[x].name[0] != 0x00) {
/*找到文件的情况*/
/*这里!*/ p = (char *) memman_alloc_4k(memman, finfo[x].size);
/*这里!*/ file_loadfile(finfo[x].clustno, finfo[x].size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
cursor_x = 8;
/*这里!*/ for (y = 0; y < finfo[x].size; y++) {
/*逐字输出*/
/*这里!*/ s[0] = p[y];
s[1] = 0;
(中略)
}
/*这里!*/ memman_free_4k(memman, (int) p, finfo[x].size);
} else {
/*没有找到文件的情况 */
(中略)
}
cursor_y = cons_newline(cursor_y, sheet);
} else if (cmdline[0] != 0) {
(中略)
}
(中略)
} else {
/*一般字符*/
(中略)
}
}
(中略)
}
}
}
void file_readfat(int *fat, unsigned char *img)
/*将磁盘映像中的FAT解压缩 */
{
int i, j = 0;
for (i = 0; i < 2880; i += 2) {
fat[i + 0] = (img[j + 0] | img[j + 1] << 8) & 0xfff;
fat[i + 1] = (img[j + 1] >> 4 | img[j + 2] << 4) & 0xfff;
j += 3;
}
return;
}
void file_loadfile(int clustno, int size, char *buf, int *fat, char *img)
{
int i;
for (;;) {
if (size <= 512) {
for (i = 0; i < size; i++) {
buf[i] = img[clustno * 512 + i];
}
break;
}
for (i = 0; i < 512; i++) {
buf[i] = img[clustno * 512 + i];
}
size -= 512;
buf += 512;
clustno = fat[clustno];
}
return;
}
程序的结构很简单。首先,由于压缩状态的FAT很难使用,因此我们先用file_readfat将其展开到fat[]。
然后,在type命令中,我们先分配一块和文件大小相同的内存空间,用file_loadfile将文件的内容读入内存。这样一来内存中的文件内容已经排列为正确的顺序,使用之前的程序来显示文件内容即可。显示完成后,释放用于临时存放文件内容的内存空间。
■■■■■
好,我们来“make run”。嗯嗯,貌似运行成功了。其实我们最好是特意准备一个不连续的文件,以便确认程序是否能够正常显示它,不过准备不连续的文件实在比较麻烦,我们就权且“相信”它可以正常显示吧(笑)。
COLUMN-11 FAT的压缩
软盘中的FAT是经过压缩的,将2个扇区的信息挤到了3个字节中。正是因此,我们的程序必须先从FAT的解压缩开始。
如果不进行压缩,到底会浪费多少空间呢?我们来简单计算一下。磁盘中一共有2880个扇区,正常使用WORD保存扇区号的话,FAT总共需要2880×2=5760个字节,也就是说,一个FAT要占用12个扇区。
经过压缩后,3个字节可以保存2个扇区号,也就是说,存放1个扇区号所需要的空间变为了1.5个字节。因此,FAT总共需要2880×1.5=4320个字节,这样的话,一个FAT只占用9个扇区。
一张软盘中一共有2份FAT,因此经过压缩后,总共可以节省6个扇区的空间,但对于磁盘的总容量来说,也只相当于0.2%(6 / 2880×100)。唔,仅仅为了节省这0.2%的空间,有必要搞这么麻烦的压缩吗?从笔者的感觉来说,这实在是够纠结的。