「30天自制操作系统」Day27 - LDT 与库

先来修复 bug

昨天最后我们留下一个 bug,用 nsct 运行的应用程序,无论是按 Shift + F1 还是点击窗口的 × 按钮 都没有反应。

我们在这两部分处理的最后加了一句 task_run(task, -1, 0),其功能是将休眠的任务唤醒。因为如果任务一直处于休眠状态的话,结束任务的处理就永远不会开始执行。

现在我们就可以点击 × 或者按下 shift + F1 来关闭应用程序了。

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

我们对 bootpack.c 做几处修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {
/* 点击 × 按钮 */
if ((sht->flags & 0x10) != 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();
}
}

这部分的作用是在按下 × 时将命令行窗口隐藏起来,这是为了防止关闭命令行窗口的时间过长对用户造成不好的体验。

1
2
3
4
5
if (2024 <= i && i <= 2279) {	/* 只关闭命令行窗口 */
sht2 = shtctl->sheets0 + (i - 2024);
memman_free_4k(memman, (int) sht2->buf, 256 * 165);
sheet_free(sht2);
}

这部分对应我们对 console.c 中的一处修改:

1
2
3
4
5
6
7
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。

运行一下,现在可以关闭命令行窗口了。

保护应用程序

现在再来写一个破坏程序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[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

如下所示,成功对我们的程序进行了破坏。

这个程序所做的是从 1005 号段(第一个命令行窗口的应用程序代码段)的第 4 字节读取数据,判断如果为 “Hari”,则切换到 2005 号段,并将其中的内容全部用 123 填充,因此产生了异常。

要防御这样的攻击,我们只要禁止应用程序随意访问其他任务所拥有的内存段就可以了。

我们可以通过 LDT 来实现,将应用程序段设置在 LDT 中,其他任务由于无法使用该 LDT,就不担心它们来恶意修改了。

可以通过 GDTR 寄存器找到 GDT 的内存地址,然后通过 GDT 中创建 LDT 段来找到 LDT 的内存地址(与课上讲的类似)。

我们添加用于设置 LDT 的段属性编号,并修改 mtask.c 以便设置 LDT,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define AR_LDT 0x0082

struct TASK {
int sel, flags; /* sel代表GDT编号*/
int level, priority;
struct FIFO32 fifo;
struct TSS32 tss;
struct SEGMENT_DESCRIPTOR ldt[2];
struct CONSOLE *cons;
int ds_base, cons_stack;
};

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.fs = 0;
task->tss.gs = 0;
task->tss.iomap = 0x40000000;
task->tss.ss0 = 0;
return task;
}
}
return 0; /* 已经全部正在使用 */
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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");
}
}
return 0;
}

我们指定的段号是 0 8 + 4 = 4 和 1 8 + 4 = 12,这样我们每个任务都有自己专用的 LDT 了。现在我们再运行 crack7 就会产生异常了。如下所示:

优化应用程序的大小

现在我们的 hello3.hrb 变得很大,这是因为我们把 a_nask.nas 全都引用了进来,包括一些根本用不到的函数。因此,我们考虑把这些函数做成不同的 .obj 文件,于是把 a_nask.nas 拆成了 api001.mas ~ api020.nas。这样我们的程序就大大减小了。

我们可以把一些 .obj 文件打包成一个库文件,这样我们写 Makefile 的时候就不用写一大串 .obj,只要写一个 .lib 就可以了。

整理 make 环境

我们把文件分分类,将源代码以及 Makefile 放到 haribote 目录下。把库相关的源代码以及 Makefile 放到 apilib 目录下。应用程序放在外面。这样由于操作系统核心和应用程序的 Makefile 是分开的,我们每次新增应用程序不需要重新生成一遍,make 的速度会有所提高。