在实现保护操作系统之前,我们先来实现两个功能。一个是使用API显示字符串,另一个是用C语言编写应用程序。
显示字符串有两种方法,一种是显示一串字符,当遇到字符编码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
ADD ESP,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一下——
我们想要用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);
MOV EDX,1
MOV AL,[ESP+4] ; c
INT 0x31
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 一下——
平时使用操作系统时,也会遇到一些恶意应用程序。比如某些病毒吧,当操作系统中招了之后,就没法使用了。
比如下面的应用程序:
crack1.c文件:
void HariMain(void)
{
*((char *) 0x00102600) = 0;
return;
}
这个应用程序的功能是向内存的0x00102600地址写一个0,看似简单但对操作系统的破坏却是巨大的。
我们运行一下看看:
执行了crack1命令后,操作系统已经坏掉了,我们再输入dir命令就毫无反应。
那么怎么避免这样的应用程序破坏操作系统呢?
我们要为应用程序分配自己的内存空间,就在自己的一亩三分田上好好工作,别跑到隔壁家瞎捣乱。创建应用程序专用的数据段,将DS和SS指向该段地址。不过如果有应用程序用汇编语言直接将操作系统的段地址存入DS,又会对操作系统产生破坏了。比如下面的应用程序:
crack2.nas:
[INSTRSET "i486p"]
[BITS 32]
MOV EAX,1*8 ; OS用的段号
MOV DS,AX ; 将其存入DS
MOV BYTE [0x102600],0
MOV EDX,4
INT 0x31
解决的这个问题可以在段定义的地方加上访问权限,将段设置为应用程序可用,这里访问权限设置为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位寄存器的值全部保存下来
MOV EAX,[ESP+36] ; 应用程序用EIP
MOV ECX,[ESP+40] ; 应用程序用CS
MOV EDX,[ESP+44] ; 应用程序用ESP
MOV EBX,[ESP+48] ; 应用程序用DS/SS
MOV EBP,[ESP+52] ; tss.esp0的地址
MOV [EBP ],ESP ; 保存操作系统用ESP
MOV [EBP+4],SS ; 保存操作系统用SS
MOV ES,BX
MOV DS,BX
MOV FS,BX
MOV GS,BX
; 下面调整栈,以免用RETF跳转到应用程序
OR ECX,3 ; 将应用程序用段号和3进行OR运算
OR EBX,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,看一下效果——
crack1和crack2都被强制结束了,并报了一般保护异常的错误信息。
注:本文参照《30天自制操作系统》制作,感谢各位的持续关注。跟着作者思路制作起来,实在非常有趣,在此仅做学习分享。
最近看到有不少小伙伴也在尝试制作做操作系统,实感喜悦。大家可以一起动手尝试看看,乐趣满满的把知识学到了才是最重要不是么。
另对原著感兴趣的想要电子版的评论区留邮箱或私信我,原著源码在上一篇有分享。