「30天自制操作系统」Day20 - API

显示单个字符的 API

我们要做一个显示单个字符的 API,只要让应用程序能够调用 cons_putchar 就可以了。

调用函数时,我们使用 CALL 指令。由于 cons_putchar 是用 C 语言写的函数,即使我们将字符编码存入寄存器,函数也无法接收。因此,我们用汇编语言写一个将寄存器的值推入栈的函数 _asm_cons_putchar,代码如下:

1
2
3
4
5
6
7
8
_asm_cons_putchar:
PUSH 1
AND EAX,0xff
PUSH EAX
PUSH DWORD [0x0fec]
CALL _cons_putchar
ADD ESP,12
RET

这里 0x0fec 地址中保存了 cons 的地址,通过这段代码,应用程序在调用 _asm_cons_putchar 时,通过它调用了 cons_putchar。

我们 make 一下,可以在 bootpack.map 文件中发现如下一行代码:

1
0x00000BE3 : _asm_cons_putchar

这就是 _asm_cons_putchar 的地址了,有了这个地址我们就可以在测试程序中调用该函数了。测试程序如下:

1
2
3
4
5
6
[BITS 32]
MOV AL,'A'
CALL 0xbe3
fin:
HLT
JMP fin

我们 make run 一下,结果我们的模拟器出错关闭了。这是因为我们在执行 CALL 时,没有加上段号,这里我们加上操作系统所在的段 2 * 8,使用 far-CALL 与 far-RET,就可以正常运行了。运行效果如下:

结束应用程序

现在我们的应用程序结束后会执行 HLT,然后我们就无法输入命令了。为了改进这一点,我们可以把 HLT 改成 RET,这样就可以在应用程序结束后返回操作系统了。

同时我们要用 CALL 来代替 JMP,由于要调用的程序位于不同的段,实际上我们应该使用 far-CALL,我们来创建一个 farcall 函数:

1
2
3
_farcall:		; void farcall(int eip, int cs);
CALL FAR [ESP+4] ; eip, cs
RET

然后把 console.c 里 hlt 命令的处理改为调用 farcall,最后改写一下我们的应用程序 hlt.nas,代码如下:

1
2
3
[BITS 32]
MOV AL,'A'
CALL 2*8:0xbe8

这里我们把 0xbe3 改成了 0xbe8,是因为 _asm_cons_putchar 的地址发生了变化,因此需要修改。

这样我们的 hlt 指令就可以连续执行了,我们尝试通过多次调用来输出一个字符串:

不随操作系统版本而改变的 API

刚才我们看到了,当修改了操作系统的代码时,_asm_cons_putchar 的地址发生了变化,这给我们带来了极大的不方便,下面来解决这个问题。

我们可以在 IDT 中对函数进行注册,这里将 _asm_cons_putchar 注册到 0x40 号,在 IDT 初始化的代码中加如下一行即可:

1
set_gatedesc(idt + 0x40, (int) asm_cons_putchar, 2 * 8, AR_INTGATE32);

这样我们只要用 INT 0x40 来代替原来的 CALL 2*8:0xbd1 就可以调用 asm_cons_putchar 了。

由于使用 INT 指令来调用会被视作中断来处理,我们需要使用 IRETD 指令来返回。此外,被当作中断来处理还会导致 CPU 自动执行 CLI 指令来禁止中断,这对我们来说是不必要的,因此需要在 _asm_cons_putchar 开头加一条 STI 指令来允许中断。

为应用程序自由命名

之前我们调用应用程序 hlt 是通过判断命令行的输入内容是否为字符串 “hlt”,如果是的话,我们就去执行 cmd_hlt。这样当我们的应用程序改名了,就无法执行了,或者说要再来修改 cons_runcmd 中的判断语句,这显然是不可以的。

现在我们想办法让程序自己寻找应该执行的应用程序,也就是说根据我们在命令行中输入的字符串,如果找到名称为该字符串的文件,就执行该文件。我们通过 cmd_app 来实现该功能:

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
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
struct FILEINFO *finfo;
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
char name[18], *p;
int i;

/* 根据命令行生成文件名 */
for (i = 0; i < 13; i++) {
if (cmdline[i] <= ' ') {
break;
}
name[i] = cmdline[i];
}
name[i] = 0;

/* 寻找文件 */
finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
if (finfo == 0 && name[i - 1] != '.') {
/* 找不到文件就加上扩展名 .hrb 后重新寻找 */
name[i ] = '.';
name[i + 1] = 'H';
name[i + 2] = 'R';
name[i + 3] = 'B';
name[i + 4] = 0;
finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
}

if (finfo != 0) {
/* 找到文件的情况 */
p = (char *) memman_alloc_4k(memman, finfo->size);
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
farcall(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
/* 没有找到文件的情况 */
return 0;
}

我们再修改 cons_runcmd,在输入的命令不是 mem、cls、dir、type 其中之一时,则调用 cmd_app。

现在我们将 hlt.nas 改名为 hello.nas,然后汇编生成 hello.hrb,我们运行一下看看:

可以发现我们输入 hellohello.hrb 时都可以正确执行,但输入 hlt 时会提示 Bad Command 了。是我们想要达到的效果。

当心寄存器

将应用程序代码改为循环后,发现只能显示一个字母。这是由于 INT 0x40 之后 ECX 寄存器的值发生了变化,_cons_putchar 改动了 ECX 的值。我们加上 PUSHAD 和 POPAD 确保可以将全部寄存器的值还原,这样程序就能正常运行了。修改后的 asm_cons_putchar 如下:

1
2
3
4
5
6
7
8
9
10
11
_asm_cons_putchar:
STI
PUSHAD
PUSH 1
AND EAX,0xff
PUSH EAX
PUSH DWORD [0x0fec]
CALL _cons_putchar
ADD ESP,12
POPAD
IRETD

用 API 来显示字符串

对于显示字符串,我们实现以下两种方式:第一种是显示一串字符直到遇到字符编码 0 结束,第二种是先指定好显示的字符串长度再显示。两种方式的实现分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void cons_putstr0(struct CONSOLE *cons, char *s)
{
for (; *s != 0; s++) {
cons_putchar(cons, *s, 1);
}
return;
}

void cons_putstr1(struct CONSOLE *cons, char *s, int l)
{
int i;
for (i = 0; i < l; i++) {
cons_putchar(cons, s[i], 1);
}
return;
}

现在我们想要把这两个个函数变成 API,如果再使用 IDT,那么 256 个位置很快就用完了。这里我们借鉴 BIOS 的调用方式,在寄存器中存入功能号,这里我们使用 EDX 寄存器。

我们创建如下函数:

1
2
3
4
5
6
7
8
_asm_hrb_api:
STI
PUSHAD ; 用于保存寄存器值的 PUSH
PUSHAD ; 用于向 hrb_api 传值的 PUSH
CALL _hrb_api
ADD ESP,32
POPAD
IRETD

我们用 C 语言编写 API 处理程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx, ecx);
}
return;
}

我们在 hello.nas 中加一条 MOV EDX,1,看一下能否正常运行:

成功了,我们再来写一个 hello2.nas,令 EDX = 2。

1
2
3
4
5
6
7
8
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,2
MOV EBX,msg
INT 0x40
RETF
msg:
DB "hello",0

运行一下,输入 hello2 却没有反应,这是由于内存段出了问题。我们在显示单个字符时,用 [CS:ECX] 的方式特意指定了代码段寄存器。但在显示字符串时无法指定段地址,程序从错误的内存地址中读取了内容,因此我们什么也没有输出出来。

因此我们需要在 API 中将应用程序传递的地址解释为代码段内的地址,我们当初在 cmd_app 中设置了代码段的起始地址,因此只要将其传到 hrb_api 中就可以了,这里还是通过指针传递的,代码如下:

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
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
if (finfo != 0) {
p = (char *) memman_alloc_4k(memman, finfo->size);
*((int *) 0xfe8) = (int) p;
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER);
farcall(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
return 0;
}

void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int cs_base = *((int *) 0xfe8);
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + cs_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + cs_base, ecx);
}
return;
}

现在我们再试一下输入 hello2 能不能成功显示出字符串:

成功了!