【操作系统】30天自制操作系统--(26)LDT与库

        本章主要描述了一种基于LDT(本地段描述符表)的程序保护机制。另外引入了库,并基于此整理了一下当前的操作系统源码以及应用程序的目录。

一 解决BUG

        上一章最后留有一个“使用 ncst 启动的程序无论是用 shift + F1 还是鼠标点 x 都无法结束”的BUG。

        问题在于在shift + F1 还是鼠标点 x 处需要唤醒任务,以便结束处理能够得到执行:

task_run(task, -1, 0);  //功能是将休眠的任务唤醒

        之前也没有这个语句,但是退出没有问题,又是什么原因呢?原因在于,命令行窗口会触发用来控制光标闪烁的定时器中断,当产生定时器中断时,定时器超时会向FIFO写数据,于是任务就被自动唤醒了(没有这个语句的情况下,最大会产生大约0.5s的延迟)。

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

        用普通的方法运行应用程序的时候,在应用程序退出之前,我们是无法关闭用来启动这个程序的命令行窗口的。
        首先在 bootpack.c 中加入点击 x 后的隐藏窗口和接收到 console.c 发送的关闭窗口请求数据时所进行的处理:

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();
}
else if (2024 <= i && i <= 2279) { /*只关闭命令行窗口*/
    sht2 = shtctl->sheets0 + (i - 2024);
    memman_free_4k(memman, (int) sht2->buf, 256 * 165);
    sheet_free(sht2);
}

        在 console.c 中将变量 sheet 改用变量 cons.sht 来代替,cons.sht 在命令行窗口关闭之后有会被置为 0,而 sheet 则不会变。再来修改 API 中键盘输入的过程。
        如果FIFO中接收到4这个数据,则表示收到了关闭命令行窗口的信号,此时取消定时器,并发出清理图层的消息,然后 将cons—>sht置为0。

三 基于LDT的程序保护

        前面介绍过几种针对操作系统的攻击行为,并分别作了保护措施(【操作系统】30天自制操作系统--(20)保护操作系统),这边补充一种攻击方式。这种攻击不是针对操作系统,而是针对操作系统中运行的其他应用程序。恶意程序会覆盖应用程序段原有的内容:

_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

        要防御这样的攻击,我们只要禁止应用程序随意访问其他任务所拥有的内存段就可以了。这样一来,捣乱的程序就只能攻击自己,自取灭亡了。

        所幸,CPU已经提供了这样的机制,即LDT(local (segment) descriptor table)。区别于之前用的GDT(global (segment) descriptor table),GDT中的段设置时供所有任务使用的,而LDT的段设置只是针对某个应用程序有效。将应用程序段设置在LDT就不用担心上述的这种攻击了。

        和GDT一样,LDT的容量也是64KB(1024 * 64 / 8 段),但是在此处我们只需要设置两个段(一段 == 8字节)。我们将这 16字节的的信息放在 struct TASK 中:
        (1)我们可以通过GDTR这个寄存器将GDT的内存地址告知CPU;
        (2)LDT的内存地址则是通过在GDT中创建LDT段来告知CPU;
        (3)在 GDT 中我们设置多个 LDT,但是不能同时使用超过 2 个以上的 LDT;
        我们在bootpack.h中添加用于设置LDT的段属性编号:

【1】首先在 bootpack.h 中添加用于设置LDT的段属性编号:

/* dsctbl.c */
// ...
#define AR_LDT 0x0082 /*这里!*/
// ...

/* mtask.c */
// ...
struct TASK {
    // ...
    struct SEGMENT_DESCRIPTOR ldt[2]; /*这里!*/
	// ...
};

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

/* mtask.c */

struct TASK *task_init(struct MEMMAN *memman) {
    //...
    taskctl->tasks0[i].tss.ldtr = (TASK_GDT0 + MAX_TASKS + i) * 8;
	// ...
	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; /*已经全部正在使用*/
}

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

int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
    // ...
    if (finfo !=s 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(=0×8+4)和12(=1×8+ 4),这里乘以8的部分和GDT是一样的,但不一样的是还加上了4,这 是代表该段号不是GDT中的段号,而是LDT内的段号的意思。由于这里我们使用的是LDT的段号,而每个任务都有自己专用的LDT,因此这样写完全没有问题。

四 基于库的程序目录整理

        基于库的程序目录整理,可以总结为:先分(拆分a_nask.nas),再合(整合obj为lib),最后整理

【1】拆分 a_nask.nas

        现在,每写一个应用程序,大小都很大,原因在于,创建应用程序.hrb时所引用的a_nask.nas变大了(持续添加API),这实在是对空间的浪费,我们只需要将实际用到的部分包含在可执行文件中即可。

        解决办法是将这些API做成不同的 .obj 文件。连接器的功能只是决定是否将 .obj 文件连接上去,而不会在一个包含多个函数的 .obj 文件中挑出需要使用的部分,并舍去不需要使用的部分。

        作者将 a_nask.nas 拆成了 api001.nas----api020.nas 。对于应用程序 a.hrb,我们只需要用到api001.nas 和 api004.nas ,所以Makefile这么改即可,以此类推,其他的应用程序也根据需要作出改动:

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

        这样每个程序改还是很麻烦,所以基于 obj2bim 连接器的一个特性(如果所指定的.obj文件中的函数并没有被程序所使用,那么这个.obj文件是不会被连接的,所以我们把用不到的.obj文件写进去也没有问题),作者提出了一个更简单的改法:

OBJS_API =	api001.obj api002.obj api003.obj api004.obj api005.obj api006.obj \
			api007.obj api008.obj api009.obj api010.obj api011.obj api012.obj \
			api013.obj api014.obj api015.obj api016.obj api017.obj api018.obj \
			api019.obj api020.obj

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

        其他的应用程序直接用 $(OBJS_API) 即可方便扩展。

【2】整合 obj 为库文件 lib

        引入库来管理所有的 obj 文件,达到精简系统结构的作用。要创建一个库,需要 .obj 文件作为原材料,也需要一个叫做 库管理器 的东西,这个东西作者写好了包含在 tolset 中:

GOLIB    = $(TOOLPATH)golib00.exe 

apilib.lib : Makefile $(OBJS_API)
	$(GOLIB) $(OBJS_API) out:apilib.lib

        所以说在下面使用到 $(OBJS_API) 的地方都可以使用 apilib.lib 来替换。借此机会,顺便写一个 apilib.h:

/* 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函数,可以包含这个头文件,然后直接引用指定函数即可。例如beepdown.c 就可以简化成下面这样:

#include  "apilib.h"    /* 这里! */

void HariMain(void)
{
    /* 中略 */
}

       库是一种结构化编程的概念。,便于移植和扩展。我们可以自己编写库,也可以直接使用别人编写的库。例如之前用到的 sprintf 和 rand 等函数,就是包含在 golibc.lib 库中的函数。

【3】整理 make 环境

        现在操作系统、应用程序和库的源文件都堆在一起,没有层次,这边各就各位:

         (1)haribote : 只包含操作系统核心部分,用于生成操作系统相关的 .hrb 文件,包含 make | make clean | make src_only。包含的重要的文件有 : 引导文件ipl10.nas、做 C语言做不了的事情的 naskfunc.c、使用汇编语言经行一系列初始化的 asmhead.nas。
        (2)apilib:只是make | make clean | make src_only用来生成.lib文件。
        (3)应用程序:所有的应用程序的 Makefile 都比较短,将其中的公共部分提取出来放在 app_make.txt 中:

APP      = color
STACK    = 1k
MALLOC   = 56k

include ../app_make.txt

        app_make.txt 篇幅比较长,就不详细列出了,内容就是文件生成规则和编译命令的定义等。

        (4)总的Makefile:篇幅也比较长,不详细列出了,内容就是对haribote.img的编译、文件生成规则和编译命令的定义,可以支持很多命令:

         综上所述,文件整理结束。


版权声明:本文为sinat_33408502原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。