「30天自制操作系统」Day19 - 应用程序

type 命令

我们再来实现一个命令,type 文件名 能够显示文件的内容(类似 Linux 下的 cat 命令)

回想上节中我们提到的 clustno,我们有如下公式:磁盘映像中的地址 = clustno * 512 + 0x003e00。

type 命令实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
if (cmdline[0] == 't' && cmdline[1] == 'y' && cmdline[2] == 'p' && cmdline[3] == 'e' && cmdline[4] == ' ') {
/* 准备文件名 */
for (y = 0; y < 11; y++) {
s[y] = ' ';
}
y = 0;
for (x = 5; y < 11 && cmdline[x] != 0; x++) {
if (cmdline[x] == '.' && y <= 8) {
y = 8;
} else {
s[y] = cmdline[x];
if ('a' <= s[y] && s[y] <= 'z') {
/* 将小写字母转换成大写字母 */
s[y] -= 0x20;
}
y++;
}
}
/* 寻找文件 */
for (x = 0; x < 224; ) {
if (finfo[x].name[0] == 0x00) {
break;
}
if ((finfo[x].type & 0x18) == 0) {
for (y = 0; y < 11; y++) {
if (finfo[x].name[y] != s[y]) {
goto type_next_file;
}
}
break; /* 找到文件 */
}
type_next_file:
x++;
}
if (x < 224 && finfo[x].name[0] != 0x00) {
/* 找到文件的情况 */
y = finfo[x].size;
p = (char *) (finfo[x].clustno * 512 + 0x003e00 + ADR_DISKIMG);
cursor_x = 8;
for (x = 0; x < y; x++) {
/* 逐字输出 */
s[0] = p[x];
s[1] = 0;
putfonts8_asc_sht(sheet, cursor_x, cursor_y, COL8_FFFFFF, COL8_000000, s, 1);
cursor_x += 8;
if (cursor_x == 8 + 240) {
cursor_x = 8;
cursor_y = cons_newline(cursor_y, sheet);
}
}
} else {
/* 没有找到文件的情况 */
putfonts8_asc_sht(sheet, 8, cursor_y, COL8_FFFFFF, COL8_000000, "File not found.", 15);
cursor_y = cons_newline(cursor_y, sheet);
}
cursor_y = cons_newline(cursor_y, sheet);
}

这个过程大概分为三步:首先从我们的 cmdline 中提取出我们在 type 后输入的文件名,然后我们在磁盘中寻找与输入文件名相符的文件。最后如果我们找到了文件,就将其输出出来。看一下效果:

只到现在还不够,接下来我们实现换行和制表符的处理。首先说明一下这几个字符。

制表符的编码为 0x09,功能是在当前位置到下一个制表位之间填充空格。

换行符在 Windows 的编码为 0x0d 0x0a 两个字节,0x0a 代表拆行,即移动到下一行;0x0d 代表回车,即回到行首。在 Linux 下编码为 0x0a 一个字节,在我们的操作系统中,将 0x0a 作为换行+回车,与 Linux 下一致。

我们在代码中加入对换行符和制表符的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (s[0] == 0x09) {	/* 制表符 */
for (;;) {
putfonts8_asc_sht(sheet, cursor_x, cursor_y, COL8_FFFFFF, COL8_000000, " ", 1);
cursor_x += 8;
if (cursor_x == 8 + 240) {
cursor_x = 8;
cursor_y = cons_newline(cursor_y, sheet);
}
if (((cursor_x - 8) & 0x1f) == 0) {
break; /* 被 32 整除则 break */

}
} else if (s[0] == 0x0a) { /* 换行符s */
cursor_x = 8;
cursor_y = cons_newline(cursor_y, sheet);
}

这里将 cursor_x 减 8 是因为命令行窗口的边框有 8 个像素。然后一个字符的宽度是 8 个像素,每个制表位相隔 4 个字符,故 (cursor_x - 8) 能够被 32 整除的位置即为制表位,我们填充空格到制表位便 break 跳出循环。

现在我们输出一下 ipl10.nas 的内容看看:

现在唯一的问题是日文还为乱码,其他的字符已经能够正常显示了~

对 FAT 的支持

按照 Windows 管理磁盘的方法,保存大于 512 字节的文件时,有时候并不是存入连续的扇区中,对于文件的下一段存放在哪里,记录在于从 0 柱面、0 磁头、2 扇区开始的 9 个扇区中,在磁盘映像里相当于 0x000200 ~ 0x0013ff,这个记录被称为 FAT。首先看看我们的 FAT:

这个 FAT 是经过压缩的,想要解压也很简单,我们将数据以 3 个字节分为一组,假设用 16 进制表示为 ab cd ef,解压后我们得到的就是 dab efc 2 个数字。

在读取文件时,假设我们知道文件的 clustno = x,这代表文件的第一部分位于 x 号扇区,读完之后我们看 FAT 的第 x 号记录,假设为 y,这代表文件的下一部分位于 y 号扇区。我们「走一步,看一步」,直到从 FAT 中读出 FF8 ~ FFF 中的值,代表文件数据到此结束。

可以看出只要我们有一个环节出错,就会走上一条不归路,读出完全错误的数据。在磁盘中我们存放了两份 FAT,第一份位于 0x000200 ~ 0x0013ff,第二份位于 0x001400 ~ 0x0025ff,作为备份。

我们实现了如下两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void file_readfat(int *fat, unsigned char *img)
{
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;
}

其中 file_readfat 用来对 FAT 进行解压缩,将其展开到 fat。然后我们使用 file_loadfile 函数将文件的内容读到内存。在 HariMain 中,我们先分配一块和文件大小相同的内存空间,然后调用 file_loadfile,最后在释放这部分内存。

第一个应用程序

现在我们来运行如下程序:

1
2
3
4
[BITS 32]
fin:
HLT
JMP fin

我们将其保存为 hlt.nas,然后编译生成 hlt.hrb。要运行该文件的内容,我们用 file_loadfile 将文件的内容读到内存中,为了应用程序能够顺利运行,我们需要为其创建一个内存段。我们只需要通过 farjmp 指令跳转到该段就可以开始执行了。部分代码如下:

1
2
3
4
5
6
7
8
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));
set_segmdesc(gdt + 1003, finfo[x].size - 1, (int) p, AR_CODE32_ER); /* 创建内存段 */
farjmp(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo[x].size);
}

注意到这里我们把 hlt.hrb 注册为 GDT 的 1003 号,原因是 1 ~ 1002 号分别被 dsctbl.c 和 mtask.c 使用了。现在运行一下看看效果吧: