「30天自制操作系统」Day28 - 文件操作与文字显示

alloca

我们写一个求质数的程序,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void HariMain(void)
{
char flag[MAX], s[8];
int i, j;
for (i = 0; i < MAX; i++) {
flag[i] = 0;
}
for (i = 2; i < MAX; i++) {
if (flag[i] == 0) {
sprintf(s, "%d ", i);
api_putstr0(s);
for (j = i * 2; j < MAX; j += i) {
flag[j] = 1;
}
}
}
api_end();
}

当 MAX 为 10000 时,会产生一条警告:“Warning: can’t link alloca”。也就是说我们缺少一个叫 alloca 的函数。

这是因为我们的 C 语言编译器规定,如果栈中的变量超过 4KB,则需要调用 alloca 函数,其功能是根据操作系统的规格来获取栈的空间。因此我们需要编写一个 alloca 函数,对 ESP 进行减法运算。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "alloca.nas"]

GLOBAL __alloca

[SECTION .text]

__alloca:
ADD EAX,-4
SUB ESP,EAX
JMP DWORD [ESP+EAX] ; 代替 RET

现在我们来运行一下,效果如下:

这样减小了我们的程序大小,同理,之前的 winhelo、winhelo2 也可以改为从栈中分配空间。

文件操作 API

现在我们来实现文件操作的 API,总共要实现五个功能,如下所示。

打开文件

寄存器
EDX 21
EBX 文件名
EAX 文件句柄(为 0 时表示打开失败)(由操作系统返回)
1
2
3
4
5
6
7
_api_fopen:			; int api_fopen(char *fname);
PUSH EBX
MOV EDX,21
MOV EBX,[ESP+8] ; fname
INT 0x40
POP EBX
RET

关闭文件

寄存器
EDX 22
EAX 文件句柄
1
2
3
4
5
_api_fclose:		; void api_fclose(int fhandle);
MOV EDX,22
MOV EAX,[ESP+4] ; fhandle
INT 0x40
RET

文件定位

寄存器
EDX 23
EAX 文件句柄
ECX 定位模式
EBX 定位偏移量
  • 若 ECX = 0,定位的起点为文件开头
  • 若 ECX = 1,定位的起点为当前的访问位置
  • 若 ECX = 2,定位的起点为文件末尾
1
2
3
4
5
6
7
8
9
_api_fseek:			; void api_fseek(int fhandle, int offset, int mode);
PUSH EBX
MOV EDX,23
MOV EAX,[ESP+8] ; fhandle
MOV ECX,[ESP+16] ; mode
MOV EBX,[ESP+12] ; offset
INT 0x40
POP EBX
RET

获取文件大小

寄存器
EDX 24
EAX 文件句柄
ECX 文件大小获取模式
EAX 文件大小(由操作系统返回)
  • 若 ECX = 0,普通文件大小
  • 若 ECX = 1,当前读取位置从文件开头起算的偏移量
  • 若 ECX = 2,当前读取位置从文件末尾起算的偏移量
1
2
3
4
5
6
_api_fsize:			; int api_fsize(int fhandle, int mode);
MOV EDX,24
MOV EAX,[ESP+4] ; fhandle
MOV ECX,[ESP+8] ; mode
INT 0x40
RET

文件读取

寄存器
EDX 25
EAX 文件句柄
EBX 缓冲区地址
ECX 最大读取字节数
EAX 本次读取到的字节数(由操作系统返回)
1
2
3
4
5
6
7
8
9
_api_fread:			; int api_fread(char *buf, int maxsize, int fhandle);
PUSH EBX
MOV EDX,25
MOV EAX,[ESP+16] ; fhandle
MOV ECX,[ESP+12] ; maxsize
MOV EBX,[ESP+8] ; buf
INT 0x40
POP EBX
RET

我们在 TASK 中添加了 fhandle 和 fat 两个元素,fhandle 用来存放应用程序所打开文件的信息。

最后编写一个测试程序,功能是将 ipl10.nas 的内容显示出来,程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "apilib.h"

void HariMain(void)
{
int fh;
char c;
fh = api_fopen("ipl10.nas");
if (fh != 0) {
for (;;) {
if (api_fread(&c, 1, fh) == 0) {
break;
}
api_putchar(c);
}
}
api_end();
}

运行效果如下:

嗯,没有什么问题(作者是怎么那么快按下 shift + F1 的??)

命令行 API

我们编写一个获取命令行的 API。

寄存器
EDX 26
EBX 存放命令行内容的地址
ECX 最多可存放多少字节
EAX 实际存放了多少字节(由操作系统返回)
1
2
3
4
5
6
7
8
_api_cmdline:		; int api_cmdline(char *buf, int maxsize);
PUSH EBX
MOV EDX,26
MOV ECX,[ESP+12] ; maxsize
MOV EBX,[ESP+8] ; buf
INT 0x40
POP EBX
RET

用这个 API 我们就可以编写 type 程序了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void HariMain(void)
{
int fh;
char c, cmdline[30], *p;

api_cmdline(cmdline, 30);
for (p = cmdline; *p > ' '; p++) { } /* 跳过之前的内容,直到遇到空格 */
for (; *p == ' '; p++) { } /* 跳过空格 */
fh = api_fopen(p);
if (fh != 0) {
for (;;) {
if (api_fread(&c, 1, fh) == 0) {
break;
}
api_putchar(c);
}
} else {
api_putstr0("File not found.\n");
}
api_end();
}

现在我们就可以使用 type + 文件名的方式来打印一个文件了,这就取代了我们之前的命令。相比之下,这次我们是用一个应用程序来实现的,可以中途强制结束。打印 ipl10.nas 的效果与刚才相同。

文字显示

要想显示文字,我们就要准备好相应的字库。在系统启动之前先检查是否存在字库文件,如果存在则自动将其读入内存。

我们要在 TASK 中加一个 langmode,代表当前的语言模式,这样我们在不同的应用程序中可以用不同的语言模式,然后在 putfont8_asc 函数中根据 langmode 的取值进行对应的输出。

此外,为了防止字库文件太大,用 ipl10.nas 无法全部载入,我们可以用 ipl20.nas 来替换,只要将最开头的地方修改一下就好了:

1
CYLS	EQU		20				; 要载入多少数据