前言
在昨天的最后我们成功干掉了crack2.hrb, 今天我们要尝试一下更厉害的攻击手段。 所以说, 从现在开始又要打开坏人模式了哟,嘿嘿嘿
虽然把操作系统的段地址存入DS这一招现在已经不能用了,不过我可不会善罢甘休的。我要想个更厉害的招数,把使用的人推进恐怖的深渊,哈哈哈哈!在操作系统管理的内存空间里搞破坏是行不通了,这次算你厉害,不过我还可以在定时器上动动手脚。一定很不爽吧。这样一来,光标闪烁就会变得异常缓慢,任务切换的速度也会变慢。嗯,光想想就觉得很有趣啊,啊哈哈。
一、保护操作系统 4.0
好,完成了!赶紧 – make run 然后输入 crack3,口中念念有词道: 吃我这招!然后按下回车键。
[INSTRSET "i486p"]
[BITS 32]
MOV AL,0x34
OUT 0x43,AL
MOV AL,0xff
OUT 0x40,AL
MOV AL,0xff
OUT 0x40,AL
; 上述代码的功能与下面代码相当
; io_out8(PIT_CTRL, 0x34);
; io_out8(PIT_CNT0, 0xff);
; io_out8(PIT_CNT0, 0xff);
MOV EDX,4
INT 0x40
哎呀,竟然有闪,有两下子嘛!可恶!
当以应用程序模式运行时,执行IN指令和OUT指令都会产生一般保护异常。当然,通过修改CPU设置,可以允许应用程序使用IN指令和OUT指令,不过这样大家会担心留下bug而遭到恶意攻击。
我还没输呢,这点挫折我可不会善罢甘休!既然如此,我就给你执行CLI然后再HLT,这样 一来电脑就死机了。由于不再产生定时器中断,任务切换也会停止,键盘和鼠标中断也停止响应, 除了按下机箱上的Reset按钮以外没有别的办法了。我真是个天才,哈哈哈哈!
[INSTRSET "i486p"]
[BITS 32]
CLI
fin:
HLT
JMP fin
这次一定要成功, make run!
又产生了异常, 为什么啊!
当以应用程序模式运行时,执行CLI、STI和HLT这些指令都会产生异常。因为中断应该 是由操作系统来管理的,应用程序不可以随便进行控制。不能执行HLT的话,应用程序就没 办法省电了,不过一般情况下,这应该通过调用任务休眠API来实现,而不能由应用程序自 己来执行HLT。此外,在多任务下,调用休眠API还可以让系统将CPU时间分配给其他任务。
连CLI也不让我执行吗?怎么会有这种事!这样的话不就干不成坏事了吗?难道只能缴械投降了?
哦哦,想起来了!操作系统里面不是有一个用来CLI的函数嘛,far-CALL这个函数不就行了吗?这样一来应该就会死机了。应该CALL哪个地址呢?只要有map文件就可以轻松找到了。
要嗯, map文件中有这样一行:
0x00000AC1 : _io_cli
我就来far-CALL这个地址吧, 哈哈!
[INSTRSET "i486p"]
[BITS 32]
CALL 2*8:0xac1
MOV EDX,4
INT 0x40
嘿嘿, 准备接招吧 make run!
又产生异常了!到底为啥呀!能不能让我赢一次啊!可恶哦!
如果应用程序可以CALL任意地址的话,像这样的恶作剧就可以成功了,因此CPU规定除了设置好的地址以外,禁止应用程序CALL其他的地址。因此,应用程序要调用操作系统只能采用INT0x40的方法。
于是坏人只好失望地洗洗睡了(笑)。3天后·····
有了!这次应该能行,我怎么早没想到这个办法呢?哈哈, 这次绝对可以成功! 既然应用程序只能调用API,那么把API修改一下不就行了吗?
嘿嘿嘿,改好了, 然后只要写这样一个应用程序就行了。
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int cs_base = *((int *) 0xfe8);
struct TASK *task = task_now();
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);
} else if (edx == 4) {
return &(task->tss.esp0);
} else if (edx == 123456789) {
*((char *) 0x00102600) = 0;
}
return 0;
}
好啦!准备接招吧, make run
没有产生异常!不过到底成功了没有呢? dir一下看看…成功了!这次我赢了, 哈哈!
如果操作系统内部存在这种笨到作茧自缚的API,那么再优秀的CPU也对此无能为力, 操作系统只能束手就擒。即使操作系统原本没有这样的API,如果像这次一样被篡改的话, 也有可能被植入后门。 “不安装不可靠的操作系统”了。如果大家都能遵 要防止这种问题的发生,我们只能 守这条原则,就不会因为随意下载应用程序而弄坏电脑了——当然,如果操作系统本身就破 绽百出的话就另当别论了。
这次的crack6.hrb其实只能在使用咱们操作系统的人身上发挥效果。如果对方不安装我们的操作系统的话,即便运行了这个应用程序也不会发生任何问题。因此,就目前而言,这个应用程序的受害者就只有这个坏人自己而已,从这个角度来说,他“赢”得还真是空虚啊。
现在坏人已经走了,接下来我们继续做系统吧。
二、帮助发现bug
CPU的异常处理功能,除了可以保护操作系统免遭应用程序的破坏,还可以帮助我们在编写应用程序时及早发现bug。 我们来举个例子:
void api_putchar(int c);
void api_end(void);
void HariMain(void)
{
char a[100];
a[10] = 'A'; /* 这句当然没有问题 */
api_putchar(a[10]);
a[102] = 'B'; /* 这句就有问题了 */
api_putchar(a[102]);
a[123] = 'C'; /* 这句就有问题了 */
api_putchar(a[123]);
api_end();
}
这明显是个有bug的程序,因为a是一个100字节的数组,“A” 的赋值显然没有问题,肯定会显示出“A”这个字符,但“B"的赋值就不行, 因为它已经超出数组范围了;“C”的赋值当然也是不行的。
把这个程序 make run 一下, 结果如下
本来我们以为会产生异常, 结果却没有出现。我们在真机环境下试试看。 在真机环境下运行了一下,结果电脑自动重启了。嗯,这可不妙啊,电脑自动重启应该是产生了没有设置过的异常所导致的。
哦对了, 坏人刚刚擅自加上去的API已经删掉了哦,crack应用程序也已经玩腻了, 所以一起都删除了。
由于a这个数组是保存在栈中的,因此这次可能产生了栈异常。 我们需要一个函数来处理栈异常, 栈异常的中断号为 – 0x0c
_asm_inthandler0c:
STI
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler0c
CMP EAX,0
JNE end_app
POP EAX
POPAD
POP DS
POP ES
ADD ESP,4 ; 在INT 0x0c中也需要这句
IRETD
栈异常的中断号为0x0c:可能大家会问,除此之外还有什么异常呢?我们在这里补充讲解一下吧。根据CPU说明书,从0X00到0x1f都是异常所使用的中断,因此,IRQ的中断号都是从0x20之后开始的。其他一些比较有用的异常有0x00号除零异常(当试图除以0时产生)和0x06号非法指令异常(当试图执行CPU无法理解的机器语言指令, 例如当试图执行一段数据时,有可能会产生)等。
然后,我们编写inthandler0c函数,只是将inthandler0d中的出错信息改了一下而已。
int *inthandler0c(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
return &(task->tss.esp0); /* 强制结束程序 */
}
在IDT中也需要登记一下:
set_gatedesc(idt + 0x0c, (int) asm_inthandler0c, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32 + 0x60);
试试看。啊,果然QEMU对异常的模拟有问题, 因此程序还是可以 顺利运行的,看来只能在真机环境下测试了。真机环境下成功产生了异常。
在真机环境下, “AB”之后才产生异常, 也就是说,写入的“C”被判定为异常,而显示出 “B”却被放过去了。从这个例子可以看出,异常并不能发现所有的bug。不过,比起一个bug都发现不了来说,哪怕能发现一个bug也是非常有帮助的,请大家一定要好好利用哦。
可能有人会问,为什么“C” 会被判定为异常而“B”就可以被放过去呢?下面我们就来简单讲一讲。
a[102]虽然超出了数组的边界,但却没有超出为应用程序分配的数据段的边界,因此虽 然这是个bug, CPU也不会产生异常。另一方面,a[123]所在的地址已经超出了数据段的边界, 因此CPU马上就发现并产生了异常。
其实,CPU产生异常的目的并不是去发现bug,而是为了保护操作系统,它的思路是: “这个程序试图访问自身所在数据段以外的内存地址,一定是想擅自改写操作系统或者其他 应用程序所管理的内存空间,这种行为岂能放任不管?”因此,即便CPU不能帮我们发现所有的bug,也不可以责怪它哦。
要想让它帮忙发现bug的话,最好是能知道引发异常的指令的地址。这个功能很简单, 我们来加上去。
int *inthandler0c(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
char s[30];
cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
sprintf(s, "EIP = %08X\n", esp[11]); /*这里!*/
cons_putstr0(cons, s); /*这里!*/
return &(task->tss.esp0); /* 强制结束程序 */
}
int *inthandler0d(int *esp)
{
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct TASK *task = task_now();
char s[30];
cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
sprintf(s, "EIP = %08X\n", esp[11]); /*这里!*/
cons_putstr0(cons, s); /*这里!*/
return &(task->tss.esp0); /* 强制结束程序 */
}
上面代码的功能是,将esp (即栈)的11号元素(即EIP)显示出来。
如果想要得到产生异常时其他寄存器的值,只要按照下表显示相应的元素即可。
esp[0~7]为_asm_inthandler中POSHAD的结果
esp[ 0]: EDI
esp[ 1] : ESI
esp[ 2]: EBP
esp[ 4]: EBX
esp[ 5]: EDX
esp[ 6] : ECX
esp[ 7] : EAX
epp[8~9]为_as_inthandler中PUSH的结果
esp[ 8] : DS
esp[ 9] : ES
esp[10] : 错误编号(基本上是0, 显示出来也没什么意思)
esp[11] : EIP
esp[10~15]为异市产生时CPU自动PUBH的结采
esp[12]: CS
esp[13]: EFLAGS
epp[14]: ESP (应用程序用ESP)
esp[15]: SS (应用程序用SS)
三、强制结束应用程序
现在我们的系统已经可以对付大部分恶意破坏和bug,变得越来越优秀了,不过,我们还需要一些别的功能,比如强制结束应用程序。
void HariMain(void)
{
for (;;) { }
}
如果运行这样一个程序,将永远循环下去而无法结束。中断并没有被禁用,因此其他的任务还可以照常工作,不过这个任务总归要消耗一定的CPU运行时间,系统整体的速度就会变慢,还会白白浪费电。如果操作系统没有强制结束应用程序的功能,那么bug2.hrb也可以算是一个不错 的恶意破坏程序了。
怎样实现强制结束功能呢?将某一个按键设定为强制结束键,按一下就可以结束程序,这样看起来不错。本来想在console.c的console_task中编写当按下强制结束键时结束应用程序的处理,但是命令行窗口任务在应用程序运行的时候不会去读取FIFO缓冲区,强制结束键也就不管用 了,因此我们还是换个方式吧。
于是,我们只好把强制结束处理写在其他的任务中,而bootpack.c看起来很适合。强制结束键我们就定义为 “Shif+F1"吧, 当然,用其他的组合键也完全没问题,大家请按照自己的喜好修改吧。
void HariMain(void)
{
...
for (;;) {
...
}else{
...
if (256 <= i && i <= 511) {
...
if (i == 256 + 0x3b && key_shift != 0 && task_cons->tss.ss0 != 0) { /* Shift+F1 */
cons = (struct CONSOLE *) *((int *) 0x0fec);
cons_putstr0(cons, "\nBreak(key) :\n");
io_cli(); /* 不能在改变寄存器值时切换到其他任务 */
task_cons->tss.eax = (int) &(task_cons->tss.esp0);
task_cons->tss.eip = (int) asm_end_app;
io_sti();
}
...
}
}
}
asm_ app_end是将naskfunc.nas中的end_ app改名之后得来的函数。
上述程序的工作原理是,当按下强制结束键时,改写命令行窗口任务的的寄存器值,并goto到asm_end_app,仅此而已。
这样一来程序会被强制结束,但也有个问题,那就是当应用程序没有在运行的时候,按下强制结束键会发生误操作。这样可不行,必须要确认task_cons->tss.ss0不为0时才能继续进行处理。
为此,我们还得进行一些修改,使得当应用程序运行时, 该值一定不为0;而当应用程序没有运行时,该值一定为0。
harib19c
// naskfunc.nas
_asm_end_app:
; EAX为tss.esp0的地址
MOV ESP,[EAX]
MOV DWORD [EAX+4],0 ;这里!
POPAD
RET ; 返回cmd_app
// mtask.c
struct TASK *task_alloc(void)
{
int i;
struct TASK *task;
for (i = 0; i < MAX_TASKS; i++) {
if (taskctl->tasks0[i].flags == 0) {
task = &taskctl->tasks0[i];
task->flags = 1; /* 正在使用的标志 */
task->tss.eflags = 0x00000202; /* IF = 1; */
task->tss.eax = 0; /* 将其置为0*/
...
task->tss.ss0 = 0;
return task;
}
}
return 0; /* 已经全部正在使用 */
}
我们来 make run,按下“Shift+F1” 就可以轻松结束应用程序了。
我们再来创建个bug3.hrb, 该程序负责不断显示字符 a
void api_putchar(int c);
void api_end(void);
void HariMain(void)
{
for (;;) {
api_putchar('a');
}
}
make run 按下强制结束键就可以顺利停止了。
也许在这个阶段就准备强制结束和异常处理还有点为时过早,因为我们还有很多功能想尽快实现。不过早点做好这些基础工作,在后面制作示例程序时就会轻松很多(更容易发现bug), 所以我们就把这部分内容放在今天做了。
四、用C语言显示字符串
1.0
我们已经做好了用来显示字符串的API,却没做可供C语言调用该API的函数。不过这个很容易,我们现在就来做做看。
_api_putstr0: ; void api_putstr0(char *s);
PUSH EBX
MOV EDX,2
MOV EBX,[ESP+8] ; s
INT 0x40
POP EBX
RET
利用上面的函数我们来写-个hello4.hrb:
void api_putstr0(char *s);
void api_end(void);
void HariMain(void)
{
api_putstr0("hello, world\n");
api_end();
}
make run – 什么都没显示出来,太奇怪了。
运行没成功感觉很不爽,不过在读程序排查原因思考对策的时候, 想到了一件与此无关的事:那时候对开头6个字节的改写,既然已经不能用RETF指令来结束程序了,那么“Hari" 不到了吧。
去掉6个字节的改写之后,程序就不再JMP到0x1b了,因此start_app的地址也需要修改一下。
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
...
if (finfo != 0) {
...
if (finfo->size >= 8 && strncmp(p + 4, "Hari", 4) == 0) {
start_app(0x1b, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));
} else {
start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));
}
}
}
这样改过以后,hello3.hrb还能不能正常运行呢?我们来试验一下。哦哦, 不错不错,运行正常,太完美了…不过hello4.hrb还是不行。
2.0
为什么字符申显示API会失败呢?怎么想都不应该是a nask,nas的问题,难道这次又是内存段 的问题吗?于是我们对操作系统进行一点修改,使其在字符申显示API被调用的时候,显示EBX 寄存器的值。
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
char s[12];
int ds_base = *((int *) 0xfe8);
struct TASK *task = task_now();
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); /*从此开始*/
cons_putstr0(cons, (char *) ebx + ds_base);
cons_putstr0(cons,s); /*到此结束*/
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + ds_base, ecx);
} else if (edx == 4) {
return &(task->tss.esp0);
}
return 0;
}
make run一下,然后运行hello4.hrb,屏幕上显示出00000400。这到底是怎么回事呢?hello4.hrb的文件大小只有114个字节,这样根本不可能显示出“hello, world"。
为什么EBX里面会被写入这样一个匪夷所思的值呢?其实是因为连接了.obj文件的bim2hrb认为“hello, world”这个字符申就应该存放在0x400这个地址中。
由bim2hrb生成的.hrb文件其实是由两个部分构成的:
代码部分与数据部分
虽然有两个部分,不过之前我们一直都是不考虑数据部分的。当程序中没有使用字符申和外 部变量(即在函数外面所定义的变量)时, 就会生成不包含数据部分的.hrb文件,因此之前的程序都没有任何问题。
由bim2hrb生成的.hrb文件,开头的36个字节不是程序,而是存放了下列这些信息:
0x0000 (DWORD) ------ 请求操作系统为应用程序准备的数据段的大小
0x0004 (DWORD) ------ “Hari" (.hrb文件的标记)
0x0008 (DWORD) ------ 数据段内预备空间的大小
0x000c (DWORD) ------ ESP初始值与数据部分传送目的地址
0x0010 (DWORD) ------ hrb文件内数据部分的大小
0x0014 (DWORD) ------ hb文件内数据部分从哪里开始
0x0018 (DWORD) ------ 0xe9000000
0x001c (DWORD) ------ 应用程序运行入口地址-0x20
0x0020 (DWORD) ------ malloc空间的起始地址
0x0000中存放的是数据段的大小。现在在“纸娃娃系统”中,应用程序用的数据段大小固定 为64KB,但根据应用程序的内容,可能会需要更 更多的内存空间。那么把数据段都改成1MB不就 好了吗?但这样一来,明明不需要那么多内存就可以运行的程序,也会被分配很大的内存空间, 内存很快就会不够用了。因此,我们就在应用程序中先写好需要多大的内存空间。
只是操作系统用来判断 0x0004中存放的是“Hari”这4个字节。这几个字符本来没什么用, 这是不是一个应用程序文件的标记,在文件中写入这样的标记,说不定在某些情况下就会派上用场。也许在这个世界上,除了我们的系统以外,还会有其他的软件也使用.hrb这个扩展名,那样的话,光凭扩展名来判断文件的格式就有点危险了。因此,我们在文件中加上一个标记,并在操作系统中添加相应的判断功能,如果没有找到这个标记,则停止运行该文件。
如果我们不去确认“Hari” 这个标记,而错误地运行了一个数据文件的话,这就和去运行一个JPEG文件差不多,会造成很严重的后果。不过现在我们使用了异常处理功能来保护操作系统,像磁盘数据被清除以及损坏电脑这种情况,已经完全可以避免了,而且操作系统也不会发生宕机。 能做到这些,都是异常处理的功劳。
0x0008中存放的内容为 “数据段内预备空间的大小”,不过这个值目前还没什么用(说不定以后也不会有什么用),大家不用管它就是了。 在hello4.hrb中,这个值并没有被设置,所以为0。
0x000c中存放的是应用程序启动时ESP寄存器的初始值,也就是说在这个地址之前的部分会 被作为栈来使用,而这个地址将被用于存放字符串等数据。 在hello4.hrb中,这个值为0x400·· 也就是说ESP寄存器的初始值为0x400,并且分配了1KB的栈空间。1KB这个数是从哪里来的呢? 其实是在生成hello4.bim的时候,在Makefile中设置的(注意看“stack:1k”这里!)。
hello4.bim : hello4.obj a_nask.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:hello4.bim stack:1k map:hello4.map \
hello4.obj a_nask.obj
0x0010中存放的是需要向数据段传送的部分的字节数。
0x0014中存放的是需要向数据段传 送的部分在.hrb文件中的起始地址。
0x0018中存放的是Oxe9000000这个数值,这个 数在内存中存放的时候形式为“000000E9”。 前面几个00的部分没什么用,后面的E9才是关键。其实E9是JMP指令的机器语言编码, 和后面4个字节合起来的话,就表示JMP到应用程序运行的入口地址。
0x001c中存放的是应用程序运行入口地址减去0x20后的值。为什么不直接写上入口地址而是 要减掉一个数呢?因为我们在0x0018(其实是0x :001b)写了一个JMP指令,这样可以通过JMP指 令跳转到应用程序的运行入口地址。通过这样的处理,只要先JMP到0x001b这个地址,程序就可以开始运行了。
0x0020中存放的是将来编写应用程序用malloc函数时要使用的地址,因此现在先不用管它。malloc这个函数和memman_ alloc函数十分相似。
根据上面的讲解,我们来修改console.c:
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
int i, segsiz, datsiz, esp, dathrb;
...
if (finfo != 0) {
/* 找到文件的情况 */
p = (char *) memman_alloc_4k(memman, finfo->size);
file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
segsiz = *((int *) (p + 0x0000));
esp = *((int *) (p + 0x000c));
datsiz = *((int *) (p + 0x0010));
dathrb = *((int *) (p + 0x0014));
q = (char *) memman_alloc_4k(memman, segsiz);
*((int *) 0xfe8) = (int) q;
set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
set_segmdesc(gdt + 1004, segsiz - 1, (int) q, AR_DATA32_RW + 0x60);
for (i = 0; i < datsiz; i++) {
q[esp + i] = p[dathrb + i];
}
start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0));
memman_free_4k(memman, (int) q, segsiz);
} else {
cons_putstr0(cons, ".hrb file format error.\n");
}
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
/* 没有找到文件的情况 */
return 0;
}
本次修改的要点如下:
1.文件中找不到“Hari”标志则报错。
2.数据段的大小根据.hrb文件中指定的值进行分配。
3.将.hrb文件中的数据部分先复制到数据段后再启动程序。
hello4.hrb运行成功了,但不是由bim2hrb生成的hello.hrb等程序就会出错。在以后的内容中, 即便使用汇编语言编写应用程序,我们也需要先生成.obj文件,然后再生成.bim并转换成.hrb。这样一来即便将文件扩展名误写为.hrb,也不会发生运行不该运行的文件的风险了。
下面我们用一个子来看看只用汇编语言编写应用程序的情形,我们写一段和hello4.c功能相同的程序。
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "hello5.nas"]
GLOBAL _HariMain
[SECTION .text]
_HariMain:
MOV EDX,2
MOV EBX,msg
INT 0x40
MOV EDX,4
INT 0x40
[SECTION .data]
msg:
DB "hello, world", 0x0a, 0
将上面的程序make一下,得到78个字节的hello5.hrb, 而同样内容的hello4.hrb却需要114个字节,果然还是汇编语言比较节省呢(哈哈)。
在WCOFF模式下的nask中必须要使用SECTION命令, 这个命令是用来下达“将程序的这个部分放在代码段,将那个部分放在数据段”之类的指示(不过在.obj文件中不用“段”[segment] 这个词,而是用“区”[section],比如代码段在这里要被称为文本区[text section]。为什么呢?我也不知道,从一开始就是这样叫的,如果大家有意见的话…不知道该去找谁投诉了)。
如果大家明白了,.hrb文件中所包含的信息,那么对于asmhead.nas启动bootpack.hrb的部分,应该也会理解得更透彻了。
5、显示窗口
应用程序显示字符已经玩腻了,这次我们来挑战让应用程序显示窗口吧。这要如何实现呢?我们只要编写一个用来显示窗口的API就可以了,听起来很简单吧。
这个API应该写成什么样呢?考虑了一番之后,我们决定这样设计。
EDX=5
EBX=窗口缓冲区
ESI=窗口在x轴方向上的大小(即窗口宽度)
EDI=窗口在y轴方向上的大小(即窗口高度)
EAX=透明色
ECX=窗口名称
调用后,返回值如下:
EAX=用于操作窗口的句柄(用于刷新窗口等操作)
确定思路之后,新的问题又来了:我们没有考虑如何在调用API之后将值存入寄存器并返回给应用程序。
不过说起来,在asm_hrb_api中我们执行了两次PUSHAD,第一次是为了保存寄存器的值,第二次是为了向hrb_api传递值。因此如果我们查出被传递的变量的地址,在那个地址的后面应该正好存放着相同的寄存器的值。然后只要修改那个值,就可以由POPAD获取修改后的值,实现将 值返回给应用程序的功能。
我们来按这种思路编写程序。
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int ds_base = *((int *) 0xfe8);
struct TASK *task = task_now();
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct SHTCTL *shtctl = (struct SHTCTL *) *((int *) 0x0fe4);
struct SHEET *sht;
int *reg = &eax + 1; /* eax后面的地址 */
/* 强行改写通过PUSHAD保存的值 */
/* reg[0] : EDI, reg[1] : ESI, reg[2] : EBP, reg[3] : ESP */
/* reg[4] : EBX, reg[5] : EDX, reg[6] : ECX, reg[7] : EAX */
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + ds_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + ds_base, ecx);
} else if (edx == 4) {
return &(task->tss.esp0);
} else if (edx == 5) { /* 从此开始 */
sht = sheet_alloc(shtctl);
sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);
make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);
sheet_slide(sht, 100, 50);
sheet_updown(sht, 3); /* 背景层高度3位于task_a之上 */
reg[7] = (int) sht;
} /* 到此结束 */
return 0;
}
shtctl的值是bootpack.c的HariMain中的变量, 因此我们可以从0x0fe4地址获得。reg就是我们为了向应用程序返回值所动的手脚。
窗口我们就暂且显示在(100,50)这个位置上,背景层高度3。
bootpack.c中也添加了1行。
void HariMain(void)
{
...
*((int *) 0x0fe4) = (int) shtctl;
...
}
我们编写这样一个应用程序来测试:
// a_nask.nas
_api_openwin: ; int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
PUSH EDI
PUSH ESI
PUSH EBX
MOV EDX,5
MOV EBX,[ESP+16] ; buf
MOV ESI,[ESP+20] ; xsiz
MOV EDI,[ESP+24] ; ysiz
MOV EAX,[ESP+28] ; col_inv
MOV ECX,[ESP+32] ; title
INT 0x40
POP EBX
POP ESI
POP EDI
RET
// winhelo.c
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_end(void);
char buf[150 * 50];
void HariMain(void)
{
int win;
win = api_openwin(buf, 150, 50, -1, "hello");
api_end();
}
大家应该能理解吧?
make run” 快出现吧, …出来了!
6、在窗口中描绘字符和方块
虽然时间已经很晚了,大家也很困了,不过看到成功显示出窗口,我们的精神又振奋了起来,所以我们再来试一下在窗口上显示字符和方块吧。这两个功能都是现成的,只要加在API上面就可以了。
在窗口上显示字符的API如下:
EDX=6
EBX=窗口句柄
ESI=显示位置的x坐标
EDI=显示位置的)坐标
EAX=色号
ECX=字符串长度
EBP=字符串
描绘方块的API如下:
EDX=7
EBX=窗口句柄
EAX=x0
ECX=y0
ESI=x1
EDI =y1
EBP=色号
如果再多一个参数寄存器就要不够用了。 哎哟, 真悬!
接下来就是写程序了,这个简单。
// console.c
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
...
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + ds_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + ds_base, ecx);
} else if (edx == 4) {
return &(task->tss.esp0);
} else if (edx == 5) {
...
} else if (edx == 6) { /*从此开始*/
sht = (struct SHEET *) ebx;
putfonts8_asc(sht->buf, sht->bxsize, esi, edi, eax, (char *) ebp + ds_base);
sheet_refresh(sht, esi, edi, esi + ecx * 8, edi + 16);
} else if (edx == 7) {
sht = (struct SHEET *) ebx;
boxfill8(sht->buf, sht->bxsize, ebp, eax, ecx, esi, edi);
sheet_refresh(sht, eax, ecx, esi + 1, edi + 1); /*到此结束*/
}
return 0;
}
```c
// a_nask.nas
_api_putstrwin: ; void api_putstrwin(int win, int x, int y, int col, int len, char *str);
PUSH EDI
PUSH ESI
PUSH EBP
PUSH EBX
MOV EDX,6
MOV EBX,[ESP+20] ; win
MOV ESI,[ESP+24] ; x
MOV EDI,[ESP+28] ; y
MOV EAX,[ESP+32] ; col
MOV ECX,[ESP+36] ; len
MOV EBP,[ESP+40] ; str
INT 0x40
POP EBX
POP EBP
POP ESI
POP EDI
RET
_api_boxfilwin: ; void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
PUSH EDI
PUSH ESI
PUSH EBP
PUSH EBX
MOV EDX,7
MOV EBX,[ESP+20] ; win
MOV EAX,[ESP+24] ; x0
MOV ECX,[ESP+28] ; y0
MOV ESI,[ESP+32] ; x1
MOV EDI,[ESP+36] ; y1
MOV EBP,[ESP+40] ; col
INT 0x40
POP EBX
POP EBP
POP ESI
POP EDI
RET
// winhelo2.c
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_putstrwin(int win, int x, int y, int col, int len, char *str);
void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
void api_end(void);
char buf[150 * 50];
void HariMain(void)
{
int win;
win = api_openwin(buf, 150, 50, -1, "hello");
api_boxfilwin(win, 8, 36, 141, 43, 3 /* 黄色 */);
api_putstrwin(win, 28, 28, 0 /* 黑色 */, 12, "hello, world");
api_end();
}
大功告成, 之后结果如下。对了, 刚刚忘记说了, bug1.hrb已经没有用了, 所以把它删掉了哦。
总结
明天见哦。 运行得很顺利,心里相当满意呀。那么今天就到这里吧, 大家晚安!