操作系统|30天自制操作系统——第二十天保护操作系统

在实现保护操作系统之前,我们先来实现两个功能。一个是使用API显示字符串,另一个是用C语言编写应用程序。
使用API显示字符串 显示字符串有两种方法,一种是显示一串字符,当遇到字符编码0时结束。另一种是先指定要显示的字符串的长度,然后进行显示。
实现方式如下:
console.c节选:

void cons_putstr0(struct CONSOLE *cons, char *s) { for (; *s != 0; s++) {cons_putchar(cons, *s, 1); } return; }void cons_putstr1(struct CONSOLE *cons, char *s, int l) { int i; for (i = 0; i < l; i++) {cons_putchar(cons, s[i], 1); } return; }

我们把上面两个函数变成API,用之前的方法就是给每个函数分配一个INT。可是如果每次都给新的函数分配一个INT,IDT(只能设置256个)很快就会用完。因此我们学习BIOS中的方法,用EDX来存放功能号,这样就能够设置上亿个API函数了。
naskfunc.nas节选
_asm_hrb_api: STI PUSHAD ; 用于保存寄存器值的PUSH PUSHAD ; 用于向hrb_api的PUSH CALL _hrb_api ADDESP,32 POPAD IRETD

需要注意的是,hrb_api并不知道需要显示的代码段起始位置位于内存的什么地址,因此我们在内存中存放一下这个地址,这个地址暂时就放在0xfe8。
console.c节选:
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline) { (略) if (finfo != 0) {/* 找到文件的情况 */ p = (char *) memman_alloc_4k(memman, finfo->size); *((int *) 0xfe8) = (int) p; file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00)); set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER); farcall(0, 1003 * 8); memman_free_4k(memman, (int) p, finfo->size); cons_newline(cons); return 1; } /* 没有找到文件的情况 */ return 0; }void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax) { int cs_base = *((int *) 0xfe8); /* 地址在这 */ 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); } return; }

make run一下——
操作系统|30天自制操作系统——第二十天保护操作系统
文章图片

用C语言来编写应用程序 我们想要用C语言来编写应用程序,这里需要应用程序创建一个api_putchar函数。C语言来api_putchar函数,api_putchar函数里可以调用INT 0x31。
a_nask.nas:
[FORMAT "WCOFF"]; 生成对象文件的模式 [INSTRSET "i486p"]; 表示使用486兼容指令集 [BITS 32]; 生成32位模式机器语言 [FILE "a_nask.nas"]; 源文件名信息GLOBAL _api_putchar[SECTION .text]_api_putchar: ; void api_putchar(int c); MOVEDX,1 MOVAL,[ESP+4]; c INT0x31 RET

在程序结束后,能够返回命令行,需要在代码中添加返回HariMain地址的程序。
console.c节选:
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline) { (略) if (finfo != 0) {/* 找到文件的情况 */ p = (char *) memman_alloc_4k(memman, finfo->size); *((int *) 0xfe8) = (int) p; file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00)); set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER); if (finfo->size >= 8 && strncmp(p + 4, "Hari", 4) == 0) {p[0] = 0xe8; p[1] = 0x16; p[2] = 0x00; p[3] = 0x00; p[4] = 0x00; p[5] = 0xcb; } farcall(0, 1003 * 8); memman_free_4k(memman, (int) p, finfo->size); cons_newline(cons); return 1; } /* 没有找到文件的情况 */ return 0; }

make run 一下——
操作系统|30天自制操作系统——第二十天保护操作系统
文章图片

保护操作系统 平时使用操作系统时,也会遇到一些恶意应用程序。比如某些病毒吧,当操作系统中招了之后,就没法使用了。
比如下面的应用程序:
crack1.c文件:
void HariMain(void) { *((char *) 0x00102600) = 0; return; }

这个应用程序的功能是向内存的0x00102600地址写一个0,看似简单但对操作系统的破坏却是巨大的。
我们运行一下看看:
操作系统|30天自制操作系统——第二十天保护操作系统
文章图片

执行了crack1命令后,操作系统已经坏掉了,我们再输入dir命令就毫无反应。
那么怎么避免这样的应用程序破坏操作系统呢?
我们要为应用程序分配自己的内存空间,就在自己的一亩三分田上好好工作,别跑到隔壁家瞎捣乱。创建应用程序专用的数据段,将DS和SS指向该段地址。不过如果有应用程序用汇编语言直接将操作系统的段地址存入DS,又会对操作系统产生破坏了。比如下面的应用程序:
crack2.nas:
[INSTRSET "i486p"] [BITS 32] MOVEAX,1*8; OS用的段号 MOVDS,AX; 将其存入DS MOVBYTE [0x102600],0 MOVEDX,4 INT0x31

解决的这个问题可以在段定义的地方加上访问权限,将段设置为应用程序可用,这里访问权限设置为0x60。当CS中的段地址为应用程序的段地址时,若存入操作系统的段地址则会产生异常。使用这种方法,在启动应用程序的时候,需要操作系统CALL应用程序。而根据x86规则,这样操作会产生异常。这里我们可以使用RETF,RETF的本质就是从栈中将地址POP出来,然后JMP到这个地址。
naskfunc.nas节选:
_start_app:; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0); PUSHAD; 将32位寄存器的值全部保存下来 MOVEAX,[ESP+36] ; 应用程序用EIP MOVECX,[ESP+40] ; 应用程序用CS MOVEDX,[ESP+44] ; 应用程序用ESP MOVEBX,[ESP+48] ; 应用程序用DS/SS MOVEBP,[ESP+52] ; tss.esp0的地址 MOV[EBP],ESP; 保存操作系统用ESP MOV[EBP+4],SS; 保存操作系统用SS MOVES,BX MOVDS,BX MOVFS,BX MOVGS,BX ; 下面调整栈,以免用RETF跳转到应用程序 ORECX,3; 将应用程序用段号和3进行OR运算 OREBX,3; 将应用程序用段号和3进行OR运算 PUSH EBX; 应用程序的SS PUSH EDX; 应用程序的ESP PUSH ECX; 应用程序的CS PUSH EAX; 应用程序的EIP RETF ; 应用程序结束后不会回到这里

最后加入对异常的支持,我们对操作系统产生破坏、捣乱的应用程序强制结束。在x86架构规范中,当应用程序破坏操作系统时,会产生0x0d中断,这个中断也称为异常。在中断号0x0d中注册一个_asm_inthandler0d函数,将函数注册到IDT中。
dsctbl.c节选:
void init_gdtidt(void) { (略) /* IDT的设置 */ set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32); /* 将函数注册到IDT中 */ 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 + 0x31, (int) asm_hrb_api,2 * 8, AR_INTGATE32); return; }

执行make run,看一下效果——
操作系统|30天自制操作系统——第二十天保护操作系统
文章图片

crack1和crack2都被强制结束了,并报了一般保护异常的错误信息。
注:本文参照《30天自制操作系统》制作,感谢各位的持续关注。跟着作者思路制作起来,实在非常有趣,在此仅做学习分享。
最近看到有不少小伙伴也在尝试制作做操作系统,实感喜悦。大家可以一起动手尝试看看,乐趣满满的把知识学到了才是最重要不是么。
【操作系统|30天自制操作系统——第二十天保护操作系统】另对原著感兴趣的想要电子版的评论区留邮箱或私信我,原著源码在上一篇有分享。

    推荐阅读