2 用C语言编写应用程序(harib18b)
在此之前,我们的应用程序都是用汇编语言编写的,因为用汇编语言没有使用不了的指令,而且还可以在寄存器层面上实现精确的操作。不过一直用汇编语言写应用程序实在太累,要是能用C语言就省事多了,比如像下面这样:
本次的a.c
void api_putchar(int c);
void HariMain(void)
{
api_putchar('A');
return;
}
(注:这里的函数名HariMain可能会让大家联想到bootpack.c,但其实两者并没有任何关系。用C语言编写程序时,开始执行的入口函数就叫HariMain。这里的a.c和bootpack.c是完全独立编译的应用程序。)
■■■■■
好,让我们开始吧。要实现C语言编写应用程序,需要在应用程序方面创建一个api_putchar函数。注意,这个函数不是创建在操作系统中。api_putchar函数需要用C语言来调用,功能是向EDX和AL赋值,并调用INT 0x40。也许这样说大家还不太明白,看看下面的程序应该马上就能理解了。
本次的a_nask.nas
[FORMAT "WCOFF"] ; 生成对象文件的模式
[INSTRSET "i486p"] ; 表示使用486兼容指令集
[BITS 32] ; 生成32位模式机器语言
[FILE "a_nask.nas"] ; 源文件名信息
GLOBAL _api_putchar
[SECTION .text]
_api_putchar: ; void api_putchar(int c);
MOV EDX,1
MOV AL,[ESP+4] ; c
INT 0x40
RET
这里的api_putchar需要与a.c的编译结果进行连接,因此我们使用对象文件模式。
然后,Makefile也需要修改一下。
本次的Makefile节选
a.bim : a.obj a_nask.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:a.bim map:a.map a.obj a_nask.obj
a.hrb : a.bim Makefile
$(BIM2HRB) a.bim a.hrb 0
这里我们借鉴了生成bootpack.hrb时的方法。话说回来,当时我们并没有详细讲解为什么要用这样的方式生成bootpack.hrb,关于这一点稍后我们会讲到。
■■■■■
我们来“make run”,生成了一个72个字节的a.hrb。明明只显示1个字符,却比显示5个字符的hello.hrb还要大,真郁闷,不过因为是用C语言编写的,这也很正常。接着我们在命令行中输入“a”来运行一下,结果QEMU没反应了,貌似程序有bug。看来用C语言编写应用程序还是困难重重啊。
我们当然不能就这样放弃,在这里我们来变个神奇的小戏法。这个小戏法可是很有内涵的哟,不过暂时先卖个关子。
修改前的a.hrb
+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
000000 00 00 01 00 48 61 72 69 00 00 00 00 00 00 01 00 ….Hari……..
000010 00 00 00 00 48 00 00 00 00 00 00 E9 1C 00 00 00 ….H………..
000020 00 00 00 00 55 89 E5 6A 41 E8 02 00 00 00 C9 C3 ….U..jA…….
000030 BA 01 00 00 00 8A 44 24 04 CD 40 C3 55 89 E5 5D ……D$..@.U..]
000040 E9 DF FF FF FF 00 00 00 ……..
我们将开头的6个字节替换成“E8 16 00 00 00 CB”,替换后就变成了这样:
修改后的a.hrb
+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
000000 E8 16 00 00 00 CB 72 69 00 00 00 00 00 00 01 00 …… ri……..
000010 00 00 00 00 48 00 00 00 00 00 00 E9 1C 00 00 00 ….H………..
000020 00 00 00 00 55 89 E5 6A 41 E8 02 00 00 00 C9 C3 ….U..jA…….
000030 BA 01 00 00 00 8A 44 24 04 CD 40 C3 55 89 E5 5D ……D$..@.U..]
000040 E9 DF FF FF FF 00 00 00 ……..
大家还记得16进制编辑器的使用方法吗?如果用只读模式无法修改内容,隔的时间太久已经全忘了的同学,请好好回忆一下。
我们再来“make run”一下,a.hrb居然可以正常运行了。仅仅6个字节就解决了问题,二进制编辑器太厉害了!
看,运行成功了
■■■■■
现在a.hrb已经可以正常运行了,那么我们就来讲讲这6个字节小戏法的原理吧。其实说起来也简单,这6个字节其实就相当于下面3行代码用nask汇编之后的结果。
[BITS 32]
CALL 0x1b
RETF
也就是先调用0x1b这个地址的函数,从函数返回后再执行far-RET,仅此而已。
这里的0x1b,其实就是.hrb文件中HariMain的地址(其实还是有点差别的,不过这里我们先照这样来讲)。如果我们回想一下很久之前的内容,在asmhead.nas中,最后调用bootpack.hrb的时候有这样一句:
- JMP DWORD 2*8:0x0000001b
你看,这里也是0x1b,一样的。
至于为什么还需要一个far-RET,大家知道吗?因为如果没有它的话,程序结束之后就无法返回到命令行了。
■■■■■
现在我们已经可以用C语言编写应用程序了,为了纪念这一进步,我们再来写一个。
本次的hello3.c
void api_putchar(int c);
void HariMain(void)
{
api_putchar('h');
api_putchar('e');
api_putchar('l');
api_putchar('l');
api_putchar('o');
return;
}
这个程序也使用了api_putchar,也需要a_nask.obj,因此Makefile要这样写。
hello3.bim : hello3.obj a_nask.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:hello3.bim map:hello3.map hello3.obj a_nask.obj
hello3.hrb : hello3.bim Makefile
$(BIM2HRB) hello3.bim hello3.hrb 0
“make”一下,生成了一个正好100个字节的hello3.hrb。但光这样还不行,我们得把那6个字节的小戏法写进去才能正常运行。
■■■■■
不过每次都替换那6个字节实在是太麻烦了,笔者还是希望“make run”一下就能一步到位,所以我们在操作系统上面做点手脚。
本次的console.c节选
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);
if (finfo->size >= 8 && strncmp(p + 4, "Hari", 4) == 0) { /*从此开始*/
p[0] = 0xe8;
p[1] = 0x16;
p[2] = 0x00;
p[3] = 0x00;
p[4] = 0x00;
p[5] = 0xcb;
} /*到此结束*/
farcall(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
/*没有找到文件的情况*/
return 0;
}
凡是通过bim2hrb生成的hrb文件,其第4~7字节一定为“Hari”,因此程序通过判断第4~7字节的内容,将读取的数据先进行修改之后再运行。这样一来,不需要用二进制编辑器手工修改,程序应该也可以正常运行了。
我们来试试看,将之前修改过的hello3.hrb删除,然后重新“make run”一下。哇,成功啦!
不用二进制编辑器也能运行