当前位置: 首页 > news >正文

30天开发操作系统 第27天 -- LDT与库

前言

大家早上好,我们今天的第一个任务就是修复昨天晚上的那个bug。是个什么bug来着?就是用nsct命令运行的应用程序,无论是按Shift+F1还是点击窗口的“x”按钮都没有反应的那个bug啦。
我们得先来找到出问题的原因,然后才能采取对策。从昨天晚上到今天早上一直在思考这个问题,想来想去暂时能得到的结论是,昨天编写的内容貌似没有什么问题,因此这个bug可能之前就已经存在了,只是我们没有发现而已。

一、先来修复bug

今天早上我们不能局限在昨天修改过的范围中,而是要扩大一下思路。哦,终于找到了!其实只要修改两行代码就可以了。

void HariMain(void)
{...for (;;) {...if (fifo32_status(&fifo) == 0) {...}else{...if (256 <= i && i <= 511) {...if (i == 256 + 0x3b && key_shift != 0 && key_win != 0) {	/* Shift+F1 */task = key_win->task;if (task != 0 && task->tss.ss0 != 0) {cons_putstr0(task->cons, "\nBreak(key) :\n");io_cli();	/* 强制结束处理时禁止切换任务 */task->tss.eax = (int) &(task->tss.esp0);task->tss.eip = (int) asm_end_app;io_sti();task_run(task, -1, 0);	/* 为了确实执行结束处理,如果处于休眠状态则唤醒 */ /*这里!*/}}...} else if (512 <= i && i <= 767) { /* 鼠标 */if (mouse_decode(&mdec, i - 512) != 0) {if ((mdec.btn & 0x01) != 0) {if (mmx < 0) {for (j = shtctl->top - 1; j > 0; j--) {...if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) {if (sht->buf[y * sht->bxsize + x] != sht->col_inv) {...if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {/* 点击 x 按钮 */if ((sht->flags & 0x10) != 0) {		/* 是否是应用程序窗口 */task = sht->task;cons_putstr0(task->cons, "\nBreak(mouse) :\n");io_cli();	/* 强制结束处理时禁止切换任务 */task->tss.eax = (int) &(task->tss.esp0);task->tss.eip = (int) asm_end_app;io_sti();task_run(task, -1, 0);	/*这里!*/} else {...}}

我们添加的两行语句都是task_run(task,-1,0);,它的功能当然是将休眠的任务唤醒,不过为什么加上这两句问题就解决了呢?下面我们来一起探讨一下。
到底为什么需要唤醒任务呢?尽管我们特地在TSS中改写了EIP和EAX以便执行结束任务的处理,可如果任务一直处于休眠状态的话结束任务的处理就永远不会开始执行,因此我们需要唤醒它,使得结束处理确实能够被执行。
可是之前一直没有这个语句,强制结束功能也没出过问题,这是怎么回事呢?因为命令行窗口会触发用来控制光标闪烁的定时器中断(在命令行窗口中,不显示光标时也会每0.5秒触发一次定时器中断),当产生定时器中断时,定时器超时时会向FIFO写人数据,于是任务就被自动唤醒了。
在之前没有这个语句的情况下,(不使用ncst的时候)即便看上去可以正常执行强制结束,但其实距离应用程序真正结束还是会产生最大0.5秒的延迟。因此通过这次的修改,Shif+F1和“x”按钮的反应速度应该也会有所改善。
好,我们来“make run”,试试看用ncst运行的应用程序是否也可以通过点击“x”按钮关闭。
哦哦,成功了!嗯嗯,感觉从点击按钮到程序关闭所经过的时间也确实比之前要短了。
在这里插入图片描述
在这里插入图片描述

二、应用程序运行时关闭命令行窗口

终于除掉了bug,神清气爽。命令行窗口的功能已经实现得差不多了,不过还有一个地方不太满意,就是在应用程序运行的时候无法关闭所对应的命令行窗口。
我们先不考虑ncst命令,用普通的方法运行应用程序的时候,在应用程序退出之前,我们是无法关闭用来启动这个程序的命令行窗口的。直到程序运行之后才觉得命令行窗口太碍事了,但事已至此也不想再重新启动应用程序(比如说,热水已经倒好了,总不能这个时候重新启动noodle.hrb吧!),于是就只好将就了。
因此还是想办法解决这个问题比较好。
首先我们来修改bootpack.c。

void HariMain(void)
{...for (;;) {...if (fifo32_status(&fifo) == 0) {...}else{...if (256 <= i && i <= 511) {...if (i == 256 + 0x3b && key_shift != 0 && key_win != 0) {	/* Shift+F1 */task = key_win->task;if (task != 0 && task->tss.ss0 != 0) {cons_putstr0(task->cons, "\nBreak(key) :\n");io_cli();	/* 强制结束处理时禁止切换任务 */task->tss.eax = (int) &(task->tss.esp0);task->tss.eip = (int) asm_end_app;io_sti();task_run(task, -1, 0);	/* 为了确实执行结束处理,如果处于休眠状态则唤醒 */ /*这里!*/}}...} else if (512 <= i && i <= 767) { /* 鼠标 */if (mouse_decode(&mdec, i - 512) != 0) {if ((mdec.btn & 0x01) != 0) {if (mmx < 0) {for (j = shtctl->top - 1; j > 0; j--) {...if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) {if (sht->buf[y * sht->bxsize + x] != sht->col_inv) {...if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {/* 点击 x 按钮 */if ((sht->flags & 0x10) != 0) {		/* 是否是应用程序窗口 */task = sht->task;cons_putstr0(task->cons, "\nBreak(mouse) :\n");io_cli();	/* 强制结束处理时禁止切换任务 */task->tss.eax = (int) &(task->tss.esp0);task->tss.eip = (int) asm_end_app;io_sti();task_run(task, -1, 0);	/*这里!*/} else {	/* 命令行窗口 */task = sht->task;sheet_updown(sht, -1); /* 暂且隐藏该图层 */	/*从此开始*/keywin_off(key_win);key_win = shtctl->sheets[shtctl->top - 1];keywin_on(key_win);	/*到此结束*/io_cli();fifo32_put(&task->fifo, 4);io_sti();}}break;}...} else if (768 <= i && i <= 1023) {	/* 命令行结束处理 */close_console(shtctl->sheets0 + (i - 768));} else if (1024 <= i && i <= 2023) {close_constask(taskctl->tasks0 + (i - 1024));} else if (2024 <= i && i <= 2279) {	/* 只关闭命令行窗口 */		/*从此开始*/sht2 = shtctl->sheets0 + (i - 2024);memman_free_4k(memman, (int) sht2->buf, 256 * 165);sheet_free(sht2);	/*到此结束*/}}}
}

我们修改了bootpack.c中的两个地方。前面一个地方的修改是让系统在按下“x”按钮时暂且将命令行窗口从画面上隐藏起来。为什么要要这样一个小聪明呢?这是因为关闭有的应用程序的命令行窗口时需要消耗一定的时间,如果点了按钮还不关闭用户会觉得很烦躁,先隐藏窗口就可以避免这样的问题。总之这只是一个小技巧而已,并不是本次修改的重点。
后面一处修改是当FIFO接收到从console.c发送的“关闭窗口”请求数据时所进行的处理,主要是释放指定的图层。关于这一处修改的内容,看了对console.c进行修改的部分之后会更容易理解。
下面就是对console.c的修改:

void console_task(struct SHEET *sheet, int memtotal)
{...if (cons.sht != 0) {	/*这里!*/cons.timer = timer_alloc();timer_init(cons.timer, &task->fifo, 1);timer_settime(cons.timer, 50);}...for (;;) {io_cli();if (fifo32_status(&task->fifo) == 0) {task_sleep(task);io_sti();} else {i = fifo32_get(&task->fifo);io_sti();if (i <= 1 && cons.sht != 0) { /* 光标用定时器 */	/*这里!*/...if (i == 3) {	/* 光标OFF */if (cons.sht != 0) {	/*这里!*/boxfill8(cons.sht->buf, cons.sht->bxsize, COL8_000000,	/*这里!*/cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15);}cons.cur_c = -1;}...if (256 <= i && i <= 511) { /* 键盘数据(任务A) */if (i == 8 + 256) {...} else if (i == 10 + 256) {...if (cons.sht == 0) {	/*这里!*/cmd_exit(&cons, fat);}...} else {...}}/* 重新显示光标 */if (cons.sht != 0) {	/*这里!*/if (cons.cur_c >= 0) {boxfill8(cons.sht->buf, cons.sht->bxsize, cons.cur_c, 	/*这里!*/cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15);}
/*这里!*/				sheet_refresh(cons.sht, cons.cur_x, cons.cur_y, cons.cur_x + 8, cons.cur_y + 16);}}}
}

修改的要点是将变量sheet的部分改用变量cons.sht代替。虽然两个变量的值基本上是一致的,但cons.sht在命令行窗口关闭后会被置为0,而sheet则不变,因此在这里我们需要使用前者。
接下来我们来修改API中键盘输人的部分:

int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{...struct FIFO32 *sys_fifo = (struct FIFO32 *) *((int *) 0x0fec);...} else if (edx == 15) {for (;;) {...if (i == 4) {	/* 只关闭命令行窗口 */	/*从此开始*/timer_cancel(cons->timer);io_cli();fifo32_put(sys_fifo, cons->sht - shtctl->sheets0 + 2024);	/* 2024 ~ 2279 */cons->sht = 0;io_sti();}	/*到此结束*/

在等待键盘输人期间,如果FIFO中接收到4这个数据,则表示收到了关闭命令行窗口的信号,此时取消定时器,并发出清理图层的消息,然后将cons->sht置为0。

好,大功告成了,我们来“make run”吧。
我们故意不使用ncst命令,而是用一般的方法运行笔者最喜欢的color2.hrb,程序启动后尝试关闭命令行窗口……耶!
在这里插入图片描述

三、保护应用程序运行

1.0

嘿嘿嘿,没想到吧,我胡汉三又回来了!最近进步很快嘛,命令行窗口也增加了呢,这样一来我搞起破坏来也更有成就感啦,嘿嘿。
这次我可是想出了新的攻击方法哦,一定要给大家露一手。现在有了异常保护功能,大家已经对安全很放心了吧?我就是要彻底粉碎你们的心理防线,嘿嘿嘿。啊,当然,我是不会用篡改操作系统那种低级手段的,只需要用一个应用程序就可以搞破坏了哦。详细的原理我们后面再说,先来看下面这个应用程序吧,嘿嘿。

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "crack7.nas"]GLOBAL	_HariMain[SECTION .text]_HariMain:MOV		AX,1005*8MOV		DS,AXCMP		DWORD [DS:0x0004],'Hari'JNE		fin					; 不是应用程序,因此不执行任何操作MOV		ECX,[DS:0x0000]		; 读取该应用程序数据段的大小MOV		AX,2005*8MOV		DS,AXcrackloop:							; 整个用123填充ADD		ECX,-1MOV		BYTE [DS:ECX],123CMP		ECX,0JNE		crackloopfin:								; 结束MOV		EDX,4INT		0x40

这个要怎么用呢?首先“makerun”并启动lines.hrb(不使用ncst),然后打开一个新的命令行窗口,在新窗口中运行crack7.hrb。这样一来,会发生很了不得的事情哟!哈哈哈!
在这里插入图片描述
唉?你说什么都没发生?你太天真了哦,不信你把鼠标移动到lines的窗口上试试看?怎么样,这次我赢了吧!
这次我们只攻击了lines.hrb,但对于其他的应用程序,用同样的手段也可以破坏它们哦。当然,对于不同的应用程序,其出现运行混乱的现象也不一样罢了。嘿嘿嘿。
好啦,我先闪了,你们努力研究研究找找原因吧!
哎呀,那个坏人又来了,而且还成功地攻击了我们的系统,真不甘心!
这次破坏行动的特征是,由于无法破坏操作系统本身,转而破坏运行中的应用程序,也就是找软柿子捏嘛。运行中的应用程序存在被破坏的风险,如果我们不拿出对策的话,用户可能就不敢同时运行多个应用程序了——如果因为一个程序的bug,而导致别的程序也受到牵连,甚至出错退出的话,那就不好了。
这个捣乱的程序到底做了什么坏事呢?首先它从1005号段的第4字节读取数据,判断其是否为“Hari”。这个1005其实是代表第一个打开的命令行窗口所运行的应用程序的代码段编号。
1003:task_a用(没有应用程序,不使用)
1004:idle用(没有应用程序,不使用)
1005:第一个命令行窗口的应用程序代码段
1006:第二个命令行窗口的应用程序代码段
如果从那个段读出“Hari”这个字符串,说明应用程序正在运行的可能性很高,接下来就读取段开头的4个字节,即应用程序用数据段的大小。
随后我们切换到2005号段,并将其中的内容全部用123这个数值填充。当然,123这个值并没有什么特别的意义,用234、255或者其他什么的都可以,目的只是覆盖应用程序数据段原有的内容,使其无法正常运行。这招好狠啊!
对于CPU来说,应用程序访问应用程序用的段是理所当然的事情,所以不会产生异常。我们当然不能就这么败下阵来,得好好想想办法才行。要防御这样的攻击,我们只要禁止应用程序随意访问其他任务所拥有的内存段就可以了。这样一来,捣乱的程序就只能攻击自己了,结果只能是自取灭亡啦。

2.0

到底该怎样阻止应用程序攻击别的应用程序呢?我们倒是有一个办法,就是通过改写GDT的设置,只将正在运行的那个程序的段设置为应用程序用,其他的应用程序段都暂时设置为操作系统用。不过,用这个方法的话,需要在每次任务切换时都改写GDT的设置,话说我们现在每个任务也就只有两个应用程序段,这样看来这个方法也并不是不可行。
不过其实CPU已经为我们准备好了这个问题的解决方案,那就是LDT。难得有这么好的功能,我们当然要充分利用啦。
GDT是“global(segment)descriptor table”的缩写,LDT则是“local(segment)descriptor table”的缩写。相对于global代表“全局”,local则代表“局部”的意思,即GDT中的段设置是供所有任务通用的,而LDT中的段设置则只对某个应用程序有效。
如果将应用程序段设置在LDT中,其他的任务由于无法使用该LDT,也就不用担心它们来搞破坏了。
和GDT一样,LDT的容量也是64KB(可容纳设置8,192个段),不过在我们现在只需要设置两个段,所以只使用了其中的16个字节,我们把这16个字节的信息放在struct TASK中。
我们可以通过GDTR这个寄存器将GDT的内存地址告知CPU,而LDT的内存地址则是通过在GDT中创建LDT段来告知CPU的。也就是说,在GDT中我们可以设置多个LDT(当然,不能同时使用两个以上的LDT),这和TSS非常相似。下面我们在bootpack.h中添加用于设置LDT的段属性编号。

#define ADR_IDT			0x0026f800
#define LIMIT_IDT		0x000007ff
#define ADR_GDT			0x00270000
#define LIMIT_GDT		0x0000ffff
#define ADR_BOTPAK		0x00280000
#define LIMIT_BOTPAK	0x0007ffff
#define AR_DATA32_RW	0x4092
#define AR_CODE32_ER	0x409a
#define AR_LDT			0x0082	/*这里!*/
#define AR_TSS32		0x0089
#define AR_INTGATE32	0x008estruct TASK {int sel, flags; int level, priority;struct FIFO32 fifo;struct TSS32 tss;struct SEGMENT_DESCRIPTOR ldt[2];	/*这里!*/struct CONSOLE *cons;int ds_base, cons_stack;
};

接下来我们修改mtask.c以便设置LDT。我们可以将LDT编号写人tss.ldtr,这样在创建TSS时就顺便在GDT中设置了LDT,CPU也就知道这个任务应该使用哪个LDT了。

struct TASK *task_init(struct MEMMAN *memman)
{...for (i = 0; i < MAX_TASKS; i++) {taskctl->tasks0[i].flags = 0;taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;taskctl->tasks0[i].tss.ldtr = (TASK_GDT0 + MAX_TASKS + i) * 8;	/*这里!*/set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss, AR_TSS32);set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15, (int) taskctl->tasks0[i].ldt, AR_LDT);	/*这里!*/}...
}struct TASK *task_alloc(void)
{...for (i = 0; i < MAX_TASKS; i++) {if (taskctl->tasks0[i].flags == 0) {.../*删掉原来的task->tss.ldtr=0;*/task->tss.fs = 0;task->tss.gs = 0;task->tss.iomap = 0x40000000;task->tss.ss0 = 0;return task;}}return 0; /* 已经全部正在使用 */
}

最后我们来修改console.c,使得应用程序段创建在LDT中:

int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{...if (finfo != 0) {...if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {...set_segmdesc(task->ldt + 0, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);	/*这里!*/set_segmdesc(task->ldt + 1, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);	/*这里!*/for (i = 0; i < datsiz; i++) {q[esp + i] = p[dathrb + i];}start_app(0x1b, 0 * 8 + 4, esp, 1 * 8 + 4, &(task->tss.esp0));	/*这里!*/...} else {cons_putstr0(cons, ".hrb file format error.\n");}...
}

在start_app的地方,我们指定的段号是4(=0x8+4)和12(=1x8+4),这里乘以8的部分和GDT是一样的,但不一样的是还加上了4,这是代表“该段号不是GDT中的段号,而是LDT内的段号”的意思。
不过如果用这样的写法,在多个应用程序同时运行时,应用程序用的代码段号就都为4,数据段号都为12,这不就跟我们之前遇到的一个bug差不多了嘛(第25天)。其实不然,由于这里我们使用的是LDT的段号,而每个任务都有自己专用的LDT,因此这样写完全没有问题。耶!
于是我们总共只修改了8行代码就完成了对LDT的支持,赶紧来测试一下吧。
我们来“make run”,当然,之前能实现的功能现在也完全没问题。
然后我们运行lines.hrb,再运行crack7.hrb。哦哦,crack7产生异常了,这是因为1005和2005号段现在并不是应用程序用的代码段和数据段了。
在这里插入图片描述

那么如果我们将crack7.nas中的段号从10058和20058改为4和12会怎么样(上面指定的)?

_HariMain:MOV		AX,4				;这里!MOV		DS,AXCMP		DWORD [DS:0x0004],'Hari'JNE		fin					; 不是应用程序,因此不执行任何操作MOV		ECX,[DS:0x0000]		; 读取该应用程序数据段的大小MOV		AX,12				;这里!MOV		DS,AX

这样的话就变成自己攻击自己了,对lines.hrb没有任何影响,所以坏人被我们打败了。
不过对于坏人来说还有一个漏洞可以利用,那就是CPU中的LLDT指令,用这个指令可以改变LDTR寄存器(用于保存当前正在运行的应用程序所使用的LDT的寄存器)的值,这样的话就可以切换到其他应用程序的LDT,从而引发问题。但是大家别担心,因为这个指令是系统专用指令,位于应用程序段内的程序是无法执行的,即时要强行执行这个指令,也会像执行CLI指令那样产生异常(第22天)捣乱的程序就会被强制结束。

四、优化应用程序的大小

话说,我们的操作系统开发计划已经差不多接近尾声了,第29天和第30天我们主要是来编写一些应用程序的,对于操作系统本身的开发也就只剩下今明两天的时间了。正琢磨着这1天半里还能做点什么的时候,忽然想起操作系统的大小来了。截止到harib24d时的haribote.sys大小为33331字节,也就是32.5KB。麻雀虽小,五脏俱全,这么小的文件也已经具备了操作系统的基本功能了。
而另一方面,应用程序的大小又如何呢?不看不知道,一看吓一跳,hello3.hrb居然有520字节那么大了,在21天的时候明明才只有100字节来着!后来我们也只是将结束应用程序的方式改为了API方式而已,居然会增大到520字节,这也太不可思议了。
究其原因,主要是因为创建hello3.hrb时所引用的a_nask.nas变大了。也就是说,在hello3.hrb中,除了包含像_api_putchar和_api_end这样真正需要用到的函数之外,还包含了像_api_openwin和_api_linewin这些在这个应用程序中根本用不到的函数。
这实在是对空间的浪费,我们得想个办法才行。如果能够只将需要用到的部分包含在可执行文件中,就可以解决这个问题了。
那么我们该怎么办呢?我们可以将这些函数做成不同的.obj文件,将_api_putchar等需要用到的函数和_api_openwin等不需要用到的函数分离开。连接器(Linker,即obj2bim)的功能只是决定是否将.obj文件连接上去,而不会在一个包含多个函数的.obj文件中挑出需要使用的部分,并舍弃不需要使用的部分(这并不是因为obj2bim的功能不够强大,一般的连接器都是这样设计的)。
因此,我们将函数都拆开吧。
api001.nas

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api001.nas"]GLOBAL	_api_putchar[SECTION .text]_api_putchar:	; void api_putchar(int c);MOV		EDX,1MOV		AL,[ESP+4]		; cINT		0x40RET

api002.nas

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api002.nas"]GLOBAL	_api_putstr0[SECTION .text]_api_putstr0:	; void api_putstr0(char *s);PUSH	EBXMOV		EDX,2MOV		EBX,[ESP+8]		; sINT		0x40POP		EBXRET

api003.nas

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "api003.nas"]GLOBAL	_api_putstr1[SECTION .text]_api_putstr1:	; void api_putstr1(char *s, int l);PUSH	EBXMOV		EDX,3MOV		EBX,[ESP+ 8]	; sMOV		ECX,[ESP+12]	; lINT		0x40POP		EBXRET

照这样全都写出来的话太浪费纸张了,后面的就省略了哦(全部是从a_nask.nas中原封不动拆出来的)。依此类推,我们将这些函数拆分成api001.nas~api020.nas。
由于hello3.hrb所需要的.obj文件只有api001.obj和api004.obj,因此我们来修改一下Makefile。

hello3.bim : hello3.obj api001.obj api004.obj Makefile$(OBJ2BIM) @$(RULEFILE) out:hello3.bim map:hello3.map hello3.objapi001.obj api004.obj

这样一make,hello3.hrb就只有112字节了,减少了408字节!
我们索性将其他的应用程序也全部重新修改一下吧。首先是a.hrb,它所需的.obj文件也是api001和api004,所以像上面一样修改就可以了。

a.bim : a.obj api001.obj api004.obj Makefile$(OBJ2BIM) @$ (RULEFILE) out:a.bim map:a.map a.obj api001.obj api004.obj

然后是beepdown.hrb,它用到了api004、api015、api016、api017、api018和api020。

beepdown.bim : beepdown.obj api004.obj api015.obj api016.obj api017.obj¥
api018.obj api020.obj Makefile$(OBJ2BIM) @$ (RULEFILE) out:beepdown.bim stack:1k map:beepdown.map ¥
beepdown.obj api004.obj api015.obj api016.obj api017.obj api018.obj ¥
api020.obj

剩下也像这样一个一个修改好就可以了,不过笔者还是觉得好麻烦啊。在此之前我们什么都不用想,只要将a_nask.obj连接上去就好了,而现在还要根据程序来确认所使用的API。于是,想了一个偷懒的办法。
其实obj2bim这个连接器有一个功能,如果所指定的.obj文件中的函数并没有被程序所使用,那么这个.obj文件是不会被连接的,所以我们把用不到的.obj文件写进去也没有问题。其实市面上大多数连接器都没有这个功能,只要指定好的.obj文件就都会连接进去,而我们用的的这个obj2bim则会先判断一下。
利用这一特性我们就可以偷懒了,也就是说,我们可以不管三七二十一,把api001到api020全都写上去。比如说,a.hrb的话可以这样写:

a.bim: a.obj api001.obj api002.obj (中略) api019.obj api020.obj Makefile$(OBJ2BIM) @$(RULEFILE) out:a.bim map:a.map a.obj api001.obj api002.obj ¥
(中略) api019.objapi020.obj

这样一来,今后如果追加了api021.obj,我们也只要在Makefile里修改一行代码就可以了。
我们将Makefile中的a_nask.obj全部替换为$(OBJS_API)之后,应用程序果然变小了很多,而且运行起来毫无问题,很棒!

OBJS_API = api001.obJapi002.obj (中略) api019.obj api020.obj
a.bim : a.obj $(OBJS_API) Makefile$(OBJ2BIM) @$ (RULEFILE) ouT:a.bIM mAp:A.Map a.obj $(OBJS_API)

有几个应用程序是无法正常工作的。比如hello.hrb和hello2.hrb并不是用bim2hrb生成的,因此运行时会报hrb文件格式错误。此外,我们现在已经支持了LDT,crack7.hrb
也就没什么用了。因此,上面3个文件我们会在harib24f中删除。

五、库

如果像上一节那样,把函数拆分开来,并用连接器来进行连接的话,我们需要创建很多很多个.obj文件。当然,如果不拆分函数,而是做成一个大的.obj文件也可以(如同a_nask.obj)但这样的话应用程序没有引用的函数也会被包含进去,生成的应用程序文件就会像之前那样无端增大很多。
作为一个操作系统来说,现在我们的规模还不算大,但如果我们要实现Windows和Linux这样的操作系统中的全部API函数,最终需要多少个.obj文件呢?大概得有几千
个吧,只是想想头就大了。
要解决这个问题,我们可以使用“库”。库的英文名称是“library”,原本是图书馆的意思,在这里它的用途是将很多个.obj文件打包成一个文件(这种管理方式的确有点像图书馆吧),这样一来文件的数量就变少了,整个系统的结构也精简了。
要创建一个库,我们首先需要.obj文件作为原材料,除此之外,我们还需要一个叫做库管理器的程序。库管理器英文是“librarian”,原本是图书馆管理员的意思。其实,tolset中已经包含编写的库管理器了,大家不用担心。
好了,我们马上来创建一个库吧。在Makefile中加上下面的代码:

GOLIB = $ (TOOLPATH) golibO0.exe
apilib.lib : Makefile $(OBJS_API)$(GOLIB) $(OBJS_API) out:apilib.lib

完工了,通过短短几行代码我们就得到了apilib.lib这样一个文件。
我们可以在obj2bim中指定刚刚生成的这个apilib.lib来代替那一串.obj文件。仅从Makefile来看的话好像也没有太大的好处,不过只用一个.lib文件就能代替那么多.obj文件,怎么说都是很酷的哦。

a.bim : a.obj apilib.lib Makefile$(OBJ2BIM) @$(RULEFILE) out:a.bim map:a.map a.obj apilib.lib

借此机会,我们顺便写一个apilib.h:

void api_putchar(int c);
void api_putstr0(char *s);
void api_putstr1(char *s, int l);
void api_end(void);
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_initmalloc(void);
char *api_malloc(int size);
void api_free(char *addr, int size);
void api_point(int win, int x, int y, int col);
void api_refreshwin(int win, int x0, int y0, int x1, int y1);
void api_linewin(int win, int x0, int y0, int x1, int y1, int col);
void api_closewin(int win);
int api_getkey(int mode);
int api_alloctimer(void);
void api_inittimer(int timer, int data);
void api_settimer(int timer, int time);
void api_freetimer(int timer);
void api_beep(int tone);

有了它,我们用一句话就可以搞定应用程序开头的API函数声明了 – #include"apilib.h"。
例如,beepdown.c就可以简化成下面这样了:

#include"apilib.h"	/*这里!*/
void HariMain(void)
{...
}

这样的代码非常易读,因此我们把其他应用程序也改成这样了,看起来多清爽,耶!
下面我们来详细讲解一下库的知识。
在很久很久以前,程序都是一个整块,也就是说,编程的时候必须完整地从头写到尾,这种编程的方法自然是十分麻烦的。
后来,有人提出了一种新的编程方法:将程序的功能拆分为小的部件(函数)然后再组合起来构成一个完整的程序。这样,编写好的部件还可以保存起来,下次可以用在其他程序上面。这种编程方法被称为“结构化编程”。“纸娃娃系统”的核心以及各个应用程序就是由无数个函数组合而成的,这便是结构化编程理念的体现。
果依据结构化编程的思想,把将来可以用于其他程序的部件组织起来就构成了库。结构化编程中库是一个很宽泛的概念,除了.lib文件,.obj文件本身也可以被称为库(总之凡是能作为部件使用的都是库)。
我们不仅在a.c中使用过用来调用API的函数,在其他程序的开发中也使用过。之前我们也没有特别提过,大家可能认为前面编写过的部分后面可以直接拿过来用是理所当然的,但其实这就是结构化编程的技术之一。
不断扩充的库就像自己所拥有的财产一样,手上拥有的部件种类越多,后面的各种开发工作就会越轻松、迅速。而且库不一定要自己来编写,我们也可以使用别人编写的库。例如,在我们的操作系统中,我们所用到的sprintf和rand等函数就属于这一类。我们并没有在本书中特地编写这些函数,但依然能够正常地使用它们。因此,库也可以说是人类的公共财产(就像公共图书馆一样)。
在结构化编程的思想中,库越容易使用越好。如果文件数量太多,一不小心丢掉了其中一个又没有发现就非常麻烦了。此外,在将库分享给别人时,自然也是文件数量越少越好,与人方便,自己方便,因此像这种.lib形式的库是十分常用的。
对了,sprintf和rand其实包含在tolset的z_tool/haribote目录下的golibc.lib中。
顺便说一句,随着结构化编程的普及,人类的程序开发能力大幅提升,众多的开发者编写出了无数的库。但随着时代进步,函数的数量实在太多了,无法对每个函数的使用方法进行有效的管理。为了解决这个问题,“面向对象编程”的新思想应运而生(当然,这其实是结构化编程的扩展版,是在结构化编程思想的基础上发展而来的)关于面向对象编程的具体内容,在这里就不详细讲解了(因为本书中并没有用面向对象编程的方法所开发的程序)。


总结

`好啦,今天我们已经很努力了,就到这里吧。明天继续,晚安喽!

相关文章:

  • 工业主义与民主的兴衰:历史逻辑与未来危机
  • uniswap v4 合约解析1 pool初始化
  • VTK 数据结构和算法类介绍
  • pyqt写一个单片机配置界面
  • 基于YOLOv的目标检测训练数据构建方法研究—图像采集、标注、划分与增强一体化流程设计
  • java单元测试代码
  • Python中的JSON库,详细介绍与代码示例
  • 《RESTful API版本控制的哲学思辨:稳定性与创新性的终极平衡》
  • Node.js 是什么?
  • 深入理解 TensorFlow 的模型保存与加载机制(SavedModel vs H5)
  • 蓝桥杯单片机国赛模板——基于柳离风模板
  • 列日-巴斯通-列日:与VELO Senso TT+见证精彩时刻
  • java类=null的回收
  • PostgreSQL 的 pg_ls_waldir 函数
  • Scala day6(Class,field,Single Object)
  • 【Flask】ORM模型以及数据库迁移的两种方法(flask-migrate、Alembic)
  • 学习路线(嵌入式软件)
  • 【C/C++】无锁编程——compare_exchange_strong
  • LeetCode 热题 100 46. 全排列
  • 为React组件库引入自动化测试:从零到完善的实践之路
  • 江西省文化和旅游厅厅长梅亦已任省委宣传部副部长
  • 杨德龙:取得长期投资胜利法宝,是像巴菲特一样践行价值投资
  • 各地各部门贯彻落实习近平总书记重要指示精神坚决防范遏制重特大事故发生
  • 中小企业数字化转型的破局之道何在?
  • 贵州省黔西市发生载人游船侧翻事故
  • 两千万粉丝网红“大LOGO”带货茶叶被指虚假宣传,涉事茶企被立案调查