主页 > 人工智能  > 

30天开发操作系统第22天--用C语言编写应用程序

30天开发操作系统第22天--用C语言编写应用程序
前言

在昨天的最后我们成功干掉了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已经没有用了, 所以把它删掉了哦。

总结

明天见哦。 运行得很顺利,心里相当满意呀。那么今天就到这里吧, 大家晚安!

标签:

30天开发操作系统第22天--用C语言编写应用程序由讯客互联人工智能栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“30天开发操作系统第22天--用C语言编写应用程序