显示单个字符的 API
我们要做一个显示单个字符的 API,只要让应用程序能够调用 cons_putchar 就可以了。
调用函数时,我们使用 CALL 指令。由于 cons_putchar 是用 C 语言写的函数,即使我们将字符编码存入寄存器,函数也无法接收。因此,我们用汇编语言写一个将寄存器的值推入栈的函数 _asm_cons_putchar,代码如下:
1 | _asm_cons_putchar: |
这里 0x0fec 地址中保存了 cons 的地址,通过这段代码,应用程序在调用 _asm_cons_putchar 时,通过它调用了 cons_putchar。
我们 make 一下,可以在 bootpack.map
文件中发现如下一行代码:
1 | 0x00000BE3 : _asm_cons_putchar |
这就是 _asm_cons_putchar 的地址了,有了这个地址我们就可以在测试程序中调用该函数了。测试程序如下:
1 | [BITS 32] |
我们 make run 一下,结果我们的模拟器出错关闭了。这是因为我们在执行 CALL 时,没有加上段号,这里我们加上操作系统所在的段 2 * 8,使用 far-CALL 与 far-RET,就可以正常运行了。运行效果如下:
结束应用程序
现在我们的应用程序结束后会执行 HLT,然后我们就无法输入命令了。为了改进这一点,我们可以把 HLT 改成 RET,这样就可以在应用程序结束后返回操作系统了。
同时我们要用 CALL 来代替 JMP,由于要调用的程序位于不同的段,实际上我们应该使用 far-CALL,我们来创建一个 farcall 函数:
1 | _farcall: ; void farcall(int eip, int cs); |
然后把 console.c
里 hlt 命令的处理改为调用 farcall,最后改写一下我们的应用程序 hlt.nas,代码如下:
1 | [BITS 32] |
这里我们把 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 | int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline) |
我们再修改 cons_runcmd,在输入的命令不是 mem、cls、dir、type 其中之一时,则调用 cmd_app。
现在我们将 hlt.nas
改名为 hello.nas
,然后汇编生成 hello.hrb
,我们运行一下看看:
可以发现我们输入 hello
和 hello.hrb
时都可以正确执行,但输入 hlt
时会提示 Bad Command 了。是我们想要达到的效果。
当心寄存器
将应用程序代码改为循环后,发现只能显示一个字母。这是由于 INT 0x40 之后 ECX 寄存器的值发生了变化,_cons_putchar 改动了 ECX 的值。我们加上 PUSHAD 和 POPAD 确保可以将全部寄存器的值还原,这样程序就能正常运行了。修改后的 asm_cons_putchar 如下:
1 | _asm_cons_putchar: |
用 API 来显示字符串
对于显示字符串,我们实现以下两种方式:第一种是显示一串字符直到遇到字符编码 0 结束,第二种是先指定好显示的字符串长度再显示。两种方式的实现分别如下:
1 | void cons_putstr0(struct CONSOLE *cons, char *s) |
现在我们想要把这两个个函数变成 API,如果再使用 IDT,那么 256 个位置很快就用完了。这里我们借鉴 BIOS 的调用方式,在寄存器中存入功能号,这里我们使用 EDX 寄存器。
我们创建如下函数:
1 | _asm_hrb_api: |
我们用 C 语言编写 API 处理程序如下:
1 | void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax) |
我们在 hello.nas 中加一条 MOV EDX,1
,看一下能否正常运行:
成功了,我们再来写一个 hello2.nas,令 EDX = 2。
1 | [INSTRSET "i486p"] |
运行一下,输入 hello2 却没有反应,这是由于内存段出了问题。我们在显示单个字符时,用 [CS:ECX]
的方式特意指定了代码段寄存器。但在显示字符串时无法指定段地址,程序从错误的内存地址中读取了内容,因此我们什么也没有输出出来。
因此我们需要在 API 中将应用程序传递的地址解释为代码段内的地址,我们当初在 cmd_app 中设置了代码段的起始地址,因此只要将其传到 hrb_api 中就可以了,这里还是通过指针传递的,代码如下:
1 | int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline) |
现在我们再试一下输入 hello2 能不能成功显示出字符串:
成功了!