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
		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一下——

30天自制操作系统——第二十天保护操作系统_第1张图片

用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);
		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 一下——

30天自制操作系统——第二十天保护操作系统_第2张图片

保护操作系统

平时使用操作系统时,也会遇到一些恶意应用程序。比如某些病毒吧,当操作系统中招了之后,就没法使用了。

比如下面的应用程序:

crack1.c文件:

void HariMain(void)
{
     
	*((char *) 0x00102600) = 0;
	return;
}

这个应用程序的功能是向内存的0x00102600地址写一个0,看似简单但对操作系统的破坏却是巨大的。

我们运行一下看看:

30天自制操作系统——第二十天保护操作系统_第3张图片

执行了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,看一下效果——

30天自制操作系统——第二十天保护操作系统_第4张图片

crack1和crack2都被强制结束了,并报了一般保护异常的错误信息。

注:本文参照《30天自制操作系统》制作,感谢各位的持续关注。跟着作者思路制作起来,实在非常有趣,在此仅做学习分享。

最近看到有不少小伙伴也在尝试制作做操作系统,实感喜悦。大家可以一起动手尝试看看,乐趣满满的把知识学到了才是最重要不是么。

另对原著感兴趣的想要电子版的评论区留邮箱或私信我,原著源码在上一篇有分享。

你可能感兴趣的:(操作系统,操作系统,c语言,汇编)