本篇内容不多,主要是一些优化的工作。首先优化了应用程序,然后引入对应用程序的保护功能,最后引入库的概念。
1. 应用程序优化
首先来解决上一篇中遗留的一个bug:使用ncst命令运行的应用程序,按下Shift+F1或者点击x按钮都无法关闭。
分析上一篇新增的代码,没有发现问题,因此这个问题应该是之前就已经存在了。解决的方法也很简单,只需要增加以下的代码:
按下Shift+F1按钮:
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); // 增加此行代码
}
}
鼠标点击:
if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {
/* 点击「×」按钮 */
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;
io_cli();
fifo32_put(&task->fifo, 4);
io_sti();
}
}
增加的语句是用来将休眠的任务唤醒。
虽然我们已经增加了结束任务的处理,但如果任务一直处于休眠状态,还是无法执行这些处理,导致无法结束任务。因此我们需要将任务唤醒。
为什么之前没有发现这个问题呢?因为在有命令行窗口的情况下,命令行窗口会触发控制光标闪烁的定时器中断。中断超时,会向FIFO写入数据,这样任务被自动唤醒了。因此之前虽然看起来可以强制结束,实际上还是有一定的延时的,最大是0.5s。
这样修改之后,就可以通过按键或者鼠标点击来关闭窗口了。
现在还有一个问题,在应用程序处于运行过程中时,无法关闭对应的命令行窗口。这里我们需要做一些优化。
首先是主程序:
……
if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19)
{
/* 点击「×」按钮 */
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);//修改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();
}
}
……
else if (2024 <= i && i <= 2279)
{ // 修改2:只关闭命令行窗口
sht2 = shtctl->sheets0 + (i - 2024);
memman_free_4k(memman, (int) sht2->buf, 256 * 165);
sheet_free(sht2);
}
这里主要是两点优化。首先是点击x按钮时将命令行窗口图层隐藏的操作。因为有时关闭命令行窗口需要消耗一定的时间,这时先将图层隐藏能让用户觉得响应及时;然后就是在从FIFO中接收到console.c发送的关闭窗口请求数据后所进行的处理,这里主要是释放命令行窗口的图层。
API中键盘输入的部分需要做如下修改:
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。
2. 应用程序保护
接下来是增加一些对应用程序的保护措施。首先看下面的代码:
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "crack7.nas"]
GLOBAL _HariMain
[SECTION .text]
_HariMain:
MOV AX,1005*8
MOV DS,AX
CMP DWORD [DS:0x0004],'Hari'
JNE fin ; 不是应用程序,不执行任何操作
MOV ECX,[DS:0x0000] ; 读取应用程序数据段的大小
MOV AX,2005*8
MOV DS,AX
crackloop: ; 全部用123填充
ADD ECX,-1
MOV BYTE [DS:ECX],123
CMP ECX,0
JNE crackloop
fin: ; 结束
MOV EDX,4
INT 0x40
打开命令行窗口,运行lines程序;然后打开一个新的命令行窗口,在新窗口中运行crack7,lines应用程序的运行就会产生异常,这是crack7程序攻击了lines程序。
crack7程序首先从1005号段中的第四个字节开始读取数据,判断是否为“Hari”。这里1005其实代表第一个打开的命令行窗口所对应的应用程序代码段编号。如果从中读出“Hari”,该应用程序正在运行的可能性很高,接下来就读取段开头的4个字节,即应用程序数据段的大小。随后切换到2005号段,将其中的内容全部用123覆盖,这样就造成了程序的异常。
那么如何阻止应用程序攻击其他的应用程序呢?CPU已经具备了解决方案,这就是LDT(Local Descriptor Table)。与GDT相比,LDT同样用于设置段,但LDT中段的设置只对某个应用程序有效。这样其他程序无法使用,也就无法搞破坏了。
LDT的内存地址是通过在GDT中创建LDT段来告知CPU的。在GDT中可以设置多个LDT,和TSS非常相似。
我们在头文件中添加用于设置LDT的段属性编号。
#define AR_LDT 0x0082
……
struct TASK {
int sel, flags;
int level, priority;
struct FIFO32 fifo;
struct TSS32 tss;
struct SEGMENT_DESCRIPTOR ldt[2];// 增加LDT
struct CONSOLE *cons;
int ds_base, cons_stack;
};
将LDT的编号写入tss.ldtr,则创建TSS时就在GDT中设置了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;
// 设置ldtr
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);
// 设置GDT时设置LDT
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.fs = 0;
task->tss.gs = 0;
// 删除原来的task->tss.ldtr = 0;
task->tss.iomap = 0x40000000;
task->tss.ss0 = 0;
return task;
}
}
return 0;
}
最后再修改应用程序,使应用程序创建在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) {
……
// 在LDT中设置程序的代码段与数据段
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");
}
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
return 1;
}
return 0;
}
在start_app中使用的段号为4(=0x8 + 4)和12(1x8 + 4),乘以8的部分和GDT一致,而加4的部分表示该段号不是GDT中的段号,而是LDT中的段号。
这样再重复上面的操作,会产生异常,应用程序得到了保护。
3. 库
本篇的最后,为了简化已经开发完成的程序,引入了库的概念。
将一个庞大的程序拆解成小的部分(函数),将各个部分再组成完整的程序,这种编程方式被称为“结构化编程”。这样编写好的程序还可以保存下来,应用于其他程序。
依据这种思想,将可以用于其他程序的部件组织起来,就构成了库。库的扩展名为.lib,可以将多个.obj文件打包成库。如下面的Makefile内容:
GOLIB = $(TOOLPATH)golib00.exe
apilib.lib : Makefile $(OBJS_API)
$(GOLIB) $(OBJS_API) out:apilib.lib
最终可以得到apilib.lib这样一个文件。
我们不仅可以自己编写库,也可以使用别人开发好的库。把库分享给别人时,文件也是越少越好,因此.lib这种格式的库很常用。通过引入库,我们可以减少生成的目标文件。
下一篇内容是文件操作与文字显示,敬请期待。