龙空技术网

30天自制操作系统day22:使用GDT的保护模式

老师明明可以靠颜值 292

前言:

今天我们对“win10xbox0x409”都比较关怀,姐妹们都想要剖析一些“win10xbox0x409”的相关知识。那么小编也在网摘上搜集了一些关于“win10xbox0x409””的相关文章,希望姐妹们能喜欢,同学们快快来学习一下吧!

在之前的两天里,我们在自制操作系统上开发了一些简单APP。

其中在day20,我们初步开发出了API,并用API实现了几个汇编写的APP。

30天自制操作系统day20:API

在day21,我们又用同样的方法开发出了几个API,然后用C写了几个APP,最后,还写了一个带窗口的APP。

30天自制操作系统day21:在自制操作系统上用C开发APP

其实我们可以继续写更加复杂的APP,比如一个文档浏览器,图片的浏览器,一个游戏等。

不过我们先不那么做。

在真正的开始在操作系统上大规模的开发APP之前,我们需要把操作系统本身的代码与应用程序上的代码隔离起来,也就是说把把操作系统本身的代码保护起来,对各种APP的代码有效的管理起来。

为什么有GDT和IDT?

GDT是global description table,全局描述表,表里存储了对内存段的描述。

IDT是interrupt description table,中断描述表,表示存储了中断函数的地址。

为什么要有GDT? 因为要对内存里存在的代码区,数据区进行监控,管理,权限设置。

当GDT记录一段内存时,同时会记录这段内存的权限标志,这标志着这段内存是操作系统可用,还是应用程序可用,指令指针是否能指向这个内存段,如果不能指向这个内存段,说明这个内存段只能存放数据,不能存放代码。

如何设置内存块的权限?我们写的一个函数set_segmdesc:

set_segmdesc(登记位置A, 内存大小size, 内存开始地址base, 权限ar);

这个函数在GDT表的第A行登记了一个从base开始的size大小的内存区,这个内存区的权限是ar当ar=0x4092时,标志这个内存区只能存取数据,不能存放代码,而且特权等级很高

当ar=0x409a时,标志这个内存区只能存放可读取执行的代码,不能写,而且特权等级很高

当ar=0x40F2时,标志这个内存区只能存取数据,不能存放代码,而且特权等级很低

当ar=0x40Fa时,标志这个内存区只能存可读取执行的代码,不能写,而且特权等级很低

在GDT表中登记过的内存块,cpu才会去访问它,访问它的时候,会遵守ar设定的权限。一但我们写的代码让cpu不遵守ar设定的权限,就会发生中断,发生中断时执行IDT中登记的函数。

这种保护性的逻辑的实现,不在操作系统中,而在操作系统之前的BIOS中,或者在CPU内部就已经设定好了,写操作系统的我们只用使用这种逻辑就行了。

使用GDT去登记内存区域

我们可以按这样的方式去设置所有的内存区域的权限:

#define ADR_IDT			0x0026f800#define LIMIT_IDT		0x000007ff#define ADR_GDT			0x00270000#define LIMIT_GDT		0x0000ffff#define ADR_BOTPAK		0x00280000#define LIMIT_BOTPAK	0x0007ffff#define AR_DATA32_RW	0x4092#define AR_CODE32_ER	0x409a// 把内存ADR_GDT=0x0027 0000处作为GDT的首地址struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;// GDT表中的每一项的结构struct SEGMENT_DESCRIPTOR {	short limit_low, base_low;	char base_mid, access_right;	char limit_high, base_high;};// 使用函数来设置GDT表格中的一行void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar){	if (limit > 0xfffff) {		ar |= 0x8000; /* G_bit = 1 */		limit /= 0x1000;	}	sd->limit_low    = limit & 0xffff;	sd->base_low     = base & 0xffff;	sd->base_mid     = (base >> 16) & 0xff;	sd->access_right = ar & 0xff;	sd->limit_high   = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0);	sd->base_high    = (base >> 24) & 0xff;	return;}//设置GDT表格中的第一行,这只从0x0000 0000到0xffff ffff的内存段的属性是0x4902set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);//设置GDT表格中的第二行set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);//设置GDT表格中的第1003行set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);//设置GDT表格中的第1004行set_segmdesc(gdt + 1004, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);

以上代码的36行,AR_DATA32_RW=0x4902代表着段的属性信息:是否操作系统专用,是否可以存放程序,是否可读,是否可写等。

AR_CODE32_ER中的AR,Access Right,即访问权限。

要研究0x4092代表着怎样的访问权限,需要把0x4902展开成二进制

0x4092 = 0b 0100 0000 1001 0010

可以看到它有64位二进制,这个32位对应着结构体中的

// GDT表中的每一项的结构,一个8个字节,64位struct SEGMENT_DESCRIPTOR {	short limit_low, base_low;   // 16位,16位	char base_mid, access_right; // 8位,8位	char limit_high, base_high;  // 8位,8位};

这个结构体又对应着这个图

其中代码中:

//设置GDT表格中的第一行,这只从0x0000 0000到0xffff ffff的内存段的属性是0x4902set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);

AR_DATA32_RW=0x4092的低8位0x92会给到access_right,表示在这段内存是系统专用的,放在这段内存里的内容是可读,可写,但是不能执行,所以只能放数据,如果放了代码,也不能执行,放了也白放。

其中0x9=0b1001对应着GDT结构中的P=1,DPL=00,S=1;

0x2=0b0010对应着TYPE=0010,

对于0x9的详细解释

p = 1 代表当前一行GDT结构是有效的

p = 0 代表当前一行GDT结构是无效的

DPL是特权级的意思,DPL=00,表示最高特权,DPL=11表示最低特权

s = 1 表示可以放代码段或者数据

s = 0 表示系统段

所以,0x9=0b1001表示,p=1,DPL=00,s=1,就指最高特权的可以放代码或者数据的内存段

0xf=0b1111,表示:p=1,DPL=11,s=1,表示最低特权,可以放数据或内存

对于0x2对应type时的详细解释

0x2=0b0010,是对type的设置,type的四位分别表示D,E,W,A 或则C,C,R,A,

type这4位,如果最高位第3位是0,结合s=1,表示这个内存块是存放数据的,是可读的

此时,

第2位记录了自己是否被cpu访问过,通常用于 debug或者虚拟内存技术使用,用A表示

第1位表示是否可写,用W 表示

第0位表示这段内存是否可扩展,用E表示。

所以这里0x2=0b0010,最高位第3位=0,表示存放数据,可读,第1位=1,表示可写。

综合起来,就把这个内存块记录为可读可写的存放数据用的。

那么0xa表示什么意思呢?

如果最高位第3位是1,结合s=1,表示这块内存是放代码用的,是可执行的。

第2位记录了自己是否被cpu访问过,通常用于 debug或者虚拟内存技术使用,用A表示

第1位表示是否可读,用R 表示

第0位表示这段内存是否能被访问,为0表示不能被低特权的代码访问,通常是一种权限保护。

综合起来,

0xa=0b1010,表示这块内存是专门存放代码用的,可执行的,可读的。

那么综合起来,0x92,表示系统专用的存放数据的可读可写的内存块。

0xf2,表示应用程序使用的存放数据的可读可写的内存块。

0x9a表示系统专用的存放代码的可读可执行的内存块。

0xfa表示应用程序使用的存放代码的可读可执行的内存块。

0x4092的高四位0x4会给到结构体limit_high的高位,对应着GDT结构图中的 20--23位,分别表示G,D/B,L,AVL,这四位份别表示是否对应4G内存,16/32位,64位,是否有系统权限。0x4对应的二进制是0x0100,对应着D/B为1,其他都为零,表示32位。

再注意到,代码里,有对ar与0x8000进行了或这个操作:

ar |= 0x8000; 

这相当于把G设置为1,表示段值每改变1,对应内存地址改变4K,4K就是1页,1Page.

到此,我们就把函数的第四个参数AR_DATA32_RW的意义说清楚了。

那么函数的第3个参数0x0000 0000是什么意思?表示此内存块的开始地址

函数的第2个参数0xffff ffff是什么意思?这里只取低5位0xf ffff表示此内存块的结束地址或者结束地址对应的page地址。

那么从0x0000 0000 到 0xf ffff一共多少个内存地址呢?

16x16x16x16x16= 16x16x4x4x16x16=1M

表示1M的内存地址?

如果它是结束地址,那么此内存块的大小是1M

如果ar的最高位设置为1,则它表示Page地址,每个Page里含有4K个内存地址,所以4Kx1M=4G,那么此内存块的大小是4G

所以,综合第2个参数0xffff ffff, 第3个参数0x0000 0000,以及第4个参数0x4092,如下代码:

//设置GDT表格中的第一行,这只从0x0000 0000到0xffff ffff的内存段的属性是0x4092set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);

我们在GDT表中记录了一个这样的内存块:开始地址是0x0000 0000, 结束地址是0xf ffff的内存区域,这个内存区域的大小是1M,它是系统专用的内存,可读,可写,不可执行,所以只能存放数据。

这条记录放在了GDT表的gdt + 1的位置,表示放在了GDT的第1行。

由于gdt是结构体SEGMENT_DESCRIPTOR,而这个结构体一共8个字节,所以gdt+1其实表示内存地址+8。

依据上面的解读,如下几句代码设置的内存块是怎样的?

//设置GDT表格中的第二行,专门存放操作系统代码的内存set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);//设置GDT表格中的第1003行,专门存放APP代码的内存set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);//设置GDT表格中的第1004行,专门存放APP数据的内存set_segmdesc(gdt + 1004, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);

在GDT的第2行,设置一个以ADR_BOTPAK=0x0028 0000开头,大小为LIMIT_BOTPAK=0x0007ffff的内存地址,这段内存地址大小是512K,它的属性是AR_CODE32_ER=0x409a,表示系统专用,可读,不可写,可执行,可执行就是可以存放代码的地方。总的来说,这块512K的内存区域,是专门用来存放操作系统代码的。

这里,我们注意到, 操作系统放在内存中的什么位置,要占用多大区域的内存,都是我们写操作系统的人,自己可以指定的。比如上面的代码就指定了操作系统在内存中存放情况。

以上代码还设置了GDT 的第1003行,它在GDT的1003行登记了一个其实地址为p,结束地址为finfo0->size-1的一个内存区域,这块内存的权限是AR_CODE32_ER + 0x60 = 0x409a+0x60=0x40fa,表示不是操作系统专用,而是应用程序专用,可读,不可写,可执行,综合来说,表示一块抓门存放应用程序APP的可执行代码的内存块。

注意到,我们正式利用GDT这个表,登记了哪些内存块是否可读,可写,是给操作系统用,还是给应用程序用,是存放代码的,还是存放数据的。我们只用登记,登记完后,CPU上的硬件会根据登记的结果,对内存做相应设置。

设置完后,如果应用程序的代码,不小心访问了不该访问的内存区域,cpu就会产生中断,

我们如果想对应用程序的代码进行这方面的监控,就可以就收这个中断,并打印出中断产生中断时的各种寄存器的值,从而知道是GDT中的那个内存区域的代码访问了不该访问的区域。

同理,以上代码第6行,设置了GDT的第1004行,这行表示从q开始的一块大小为segsize-1的内存,这块内存的权限是AR_DATA32_RW+0x60=0x40f2,表示应用程序专用的,可读,可写,不可执行,所以它表示一块专用于存放数据的内存。

了解了GDT表的用途,我们就可以把操作系统的代码和应用程序的代码所放的内存区域都在GDT表中进行登记,只要对内存区域做一定的权限设置,就可以限制操作系统与应用程序代码的自由访问,从而把操作系统和应用程序隔离起来。

使用GDT表的权限设置把操作系统和应用程序隔离起来

根据以上的解析,我们自制的操作系统的代码的内存区域和数据的内存区域可以这样登记:

#define ADR_BOTPAK		0x00280000#define LIMIT_BOTPAK	0x0007ffff#define AR_DATA32_RW	0x4092#define AR_CODE32_ER	0x409a// 登记操作系统可以读写的内存区域为0x0000 0000 至 0xf ffff * 1K ,一共4G的内存区域set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);// 登记操作系统可以存放代码的区域为0x0028 0000 至 0x0028+0x7 ffff,一共512K的内存区域set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);

对我们写的C语言APP的代码和数据所要使用的内存区域可以遮掩登记:

//设置GDT表格中的第1003行,专门存放APP代码的内存set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);//设置GDT表格中的第1004行,专门存放APP数据的内存set_segmdesc(gdt + 1004, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);

这样,我们再运行C语言写的APP的代码时,就需要把APP的代码都存放在以p开头,大小为finfo-size的内存区域里,把APP的数据都存放在以q开头,大小为segsize的内存区域里。

这样登记记录好后,再把GDT表的1003行里所设置的内存地址,赋值CPU代码段寄存器ECS,CPU就会去执行p里的代码了。

把GDT表的1004行里所设置的内存地址,赋值CPU数据段寄存器EDS,CPU再执行时,凡是需要访问数据,就会去q里查找数据了。

不过要实现分开读取到内存的p和q位置,就需要我们在编译c源码得到hrb可执行文件的时候,就把源码中的代码和数据分开存放。只有分开存放了,才能分开读入到内存p和q中。

对C源码的编译

编译过程还是去day21的教程:

30天自制操作系统day21:在自制操作系统上用C开发APP

这里编译了一个a.c的c源码文件

//将a.c编译成a.gasa.gas : a.c bootpack.h Makefile	cc1.exe -I$(INCPATH) -Os -Wall -quiet -o a.gas a.c//将a.gas编译成a.nasa.nas : a.gas Makefile	gas2nask.exe -a a.gas a.nas//将a.nas编译成a.obja.obj : a.nas Makefile	nask.exe a.nas a.obj a.lst//将a_nask.nas编译成a_nask.obja_nask.obj : a_nask.nas Makefile	nask.exe a_nask.nas a_nask.obj a_nask.lst//将a.obj,a_nask.obj编译为a.bina.bim : a.obj a_nask.obj Makefile	obj2bim.exe @$(RULEFILE) out:a.bim map:a.map a.obj a_nask.obj//将a.bim编译为a.hrba.hrb : a.bim Makefile	bim2hrb.exe a.bim a.hrb 0

最终编译完,得到了 可执行文件a.hrb。

这个文件的36个字节存放的是文件信息,这36字节就相当于文件结构的说明,我们把这36字节搞清楚了,整个文件的结构就搞清楚了。

注意到,文件为什么这么存放,我们自己是可以决定的。不用拘泥于此形式。

其中0x0000-0x0003,4个字节,表示文件希望操作系统给自己分配用于存放数据的内存长度

0x0004-0x0007,4个字节,是4个字符"Hari",是操作系统的可执行文件的标志。如果没有这个标志,我们操作系统就不把这个文件看成可执行文件。

0x0008-0x000b , 4个字节,表示给数据预备的内存地址个数

0x000c-0x000f , 4个字节,表示栈地址

0x0010-0x0013, 4个字节,表示hrb内部数据长度

0x0014-0x0017, 4 个字节,表示hrb内部数据的开始地址

0x0018-0x001b, 4个字节内的值是:0xe9 00 00 00,那么对应0x001b位置就是0xe9,0xe9正好的 JMP 指令的机器码。 之前我们运行C程序的时候,要JMP 0x1b才能运行,就是因为跳转到0x1b后,0x1b位置还是JMP,于是继续跳转,跳转到什么位置呢?跳转到0x001c内所存储的地址处。

0x001c-0x001f,存放C程序的入口相对于0x20的地址。有了这个相对地址,0x1b位置的JMP指令就可以跳转到C程序的入口地址了。

明白了C源码编译好的可执行文件hrb的文件结构后,就可以把可执行文件中的数据和代码分开读取到p和q中,然后再注册到GDT中。

把可执行文件的数据和代码分别放在不同性质的内存段中

if (finfo != 0) {		p = (char *) memman_alloc_4k(memman, finfo->size);		file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));		// 如果文件的第4个字节至第8个字节是“Hari",说明是和执行文件    if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {			// 取文件内准备分配的内存的总长度      segsiz = *((int *) (p + 0x0000));      // 取堆栈地址			esp    = *((int *) (p + 0x000c));			// 文件内数据的实际总长度      datsiz = *((int *) (p + 0x0010));      // 可执行文件内的数据的开始地址			dathrb = *((int *) (p + 0x0014));      // 用memman找一个segsiz大小的空闲内存空间,用来存放应用程序的数据			q = (char *) memman_alloc_4k(memman, segsiz);			*((int *) 0xfe8) = (int) q;// 把内存空间的地址存放到0xfe8处,传递给中断函数			// 将文件所在的内存空间p登记一下,让cpu按照一个应用程序的可执行可读的,用于存放代码内存块来管理      set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);			// 将文件内数据要存放的内存空间q登记一下,让cpu按照一个应用程序的可读写的,用于存放数据的内存块来管理      set_segmdesc(gdt + 1004, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);			// 把文件中的数据复制到q中      for (i = 0; i < datsiz; i++) {				q[esp + i] = p[dathrb + i];			}      //跳转到0x1b处开始执行C程序,      // C程序的代码是存放在1003*8的,C程序的栈是从esp开始的,C程序的数据是存放在1004*8的      // C程序执行时,把操作系统的栈备份到task->tss.esp0      // 当C程序执行完,返回操作系统程序时,从&(task->tss.eps0)处恢复处栈的地址			start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0));			memman_free_4k(memman, (int) q, segsiz);		} else {			cons_putstr0(cons, ".hrb file format error.\n");		}		memman_free_4k(memman, (int) p, finfo->size);		cons_newline(cons);		return 1;	}

以上代码,把C编译好的可执行文件的内存地址p放在了GDT的1003*8处,把C程序编译好的可执行文件中的数据的内存地址q放在了1004*8处。

只要把代码和数据放到不同的内存块里,然后把内存块以应用程序的权限记录到GDT中,CPU就会自动限制内存块中的代码的访问权限,如果这些代码访问了那些登记为操作系统用的内存区域,CPU就会产生某个号码的中断,我们通过编写相应的中断函数,就会知道是在什么位置的代码在越界访问。

这样,我们就把应用程序的代码和操作系统的代码做了隔离,就相当于对操作系统做了保护。

不过由于应用程度代码以及数据分别放在了GDT表中登记的不同的内存块中,所以在执行代码的时候,就需要设置一下cpu的代码段寄存器ECS,数据段寄存器EDS,以及栈的寄存器EPS,

为此,我们新制作了执行应用程序函数start_app, 因为要在这个函数里设置寄存器的值,所以这个函数最好是用汇编写,这样设置寄存器方便一些:

代码如下:

_start_app:		; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);		PUSHAD		; 预先备份所有的寄存器       		MOV		EAX,[ESP+36]	; EAX = dip		MOV		ECX,[ESP+40]	; ECX = cs		MOV		EDX,[ESP+44]	; EDX = esp		MOV		EBX,[ESP+48]	; EBX = ds		MOV		EBP,[ESP+52]	; EBP = tss.esp0		MOV		[EBP  ],ESP		; ESP的值备份到tss.esp0中 		MOV		[EBP+4],SS		; SS的值备份到tss.esp0+4中		MOV		ES,BX         ; 把应用程序的数据地址ds/ss 赋值给段寄存器ES,DS,FS,GS   		MOV		DS,BX		MOV		FS,BX		MOV		GS,BX;			OR		ECX,3			; ECX=1003*8 | 3,因为要访问特权级为0x11的应用程序段,所以这里将特权级设置为3,即为0x11		OR		EBX,3			; EBX=1004*8 | 3  		PUSH	EBX				; 将ds入栈		PUSH	EDX				; 将esp入栈		PUSH	ECX				; 将cs入栈		PUSH	EAX				; 将eip入栈		RETF            ; 返回栈顶所代表的代码位置处执行,即eip位置处执行,这就成功了执行了C程序;这里,利用RETF返回到了应用程序APP处执行,并没有使用CALL或者JMP,因为当利用GDT将内存区域隔离起来之后;从当前所处的GDT的2*8位置所登记的内存块中是无法直接跳转到GDT的1003*8位置所登记的内存块中去执行程序的;只能通过RETF指令去返回。那如何使RETF指令返回到APP的代码处呢?先将APP代码的地址push 入栈然后再RETF就可以跳转到1003*8位置了

这里为什么要将 1003*8 | 3 ?

因为我们要使RETF能够跳转到1003*8所登记的内存地址处。

为什么一定要OR 一个3才能跳转到1003*8所登记的内存地址处呢?

因为CPU要跳转到GDT的1003*8处时,中间是有步骤的.

1003*8的计算有点复杂,我们假设访问的是3*8,步骤是:

1. 把3*8,翻译成16进制0x0000 3000

2. 此时用它的高5位0x00003, 表示GDT中的第0x00003行

3. 然后看3*8的第2位的值,第2位是0,表示CPU要访问GDT。如果第2位是1,表示CPU要访问GDT中的子表LDT。[这里还没有用到LDT,GDT与LDT的结构一样,但是目的不同。GDT是为了保护操作系统的内存段不被破环,而LDT主要是为了保护应用程序的内存度不被破环]

4. 然后看3*8的第1,0位的值,这两位表示CPU要跳转的内存段的特权级。如果这两位的值与GDT表0x00003行登记的特权级相同,或者高,那么CPU就会可以跳转到0x00003所登记的内存块去。如果这两位的值与GDT表0x00003行登记的特权级低,那么就无法访问,并且CPU还会产生访问异常的中断。

为什么只要在GDT中登记一下内存的权限,这段内存就被保护起来了?

就是因为CPU在按照地址去访问内存块的时候,地址中是包含着特权级的。只有地址中包含的特权级高于GDT表中登记的特权级时,CPU才能去访问。否则,就会收到异常访问的中断。

经过上述start_app函数里对寄存器的设置,对GDT地址中特权值的设置,通过RETF,就可以成功跳转到1003*8所指向的APP所在的内存地址了,就可以跳转过去执行APP的代码了。

也就是说,APP可以运行了。

然后就会按照APP中的代码一行行运行了。

不过这里,我们只是跳转过去执行APP代码了,并没有等这个代码执行完后,再返回到运行start_app函数的程序处。

也就是说,我们使用RET的方法跳转到APP的代码处,后,这个程序是无法再返回到调用start_app程序中继续运行的。

解决start_app的返回问题

办法有一个,就是不再使用ret返回,转而在0x40中断函数中返回。

要想从中断函数中返回,就必须把调用start_app时的地址放入ESP,然后RET。

假设我们已经得到了start_app时的地址,这个地址就在EAX中,那么就可以用如下代码返回到调用start_app的命令行程序中。

_asm_end_app:		MOV		ESP,[EAX] ;把地址给到ESP,此时如果RET的时候,就会回到ESP+1处所保存的地址处,ESP所保存的地址处,就是调用start_app的命令行程序处。		MOV		DWORD [EAX+4],0		POPAD            		RET					; 回到ESP所指向的地址

好既然能够返回了,那么如何得到这个栈顶ESP呢?

考虑到中断函数其实是属于操作系统内部函数的,所以我们可以在中断函数中,直接获取到栈的地址。其实当前操作系统任务task正在运行,不如在操作系统task中的tss中记录一下esp,tss中本来就有esp这一项。记录一下也方便。

如果在task中完成了栈顶的记录,那么就可以用task_now()取到task了,只要取到了task就可以取到栈顶了,按照这个思路,改写一下中断函数har_api

int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax){	int ds_base = *((int *) 0xfe8);	struct TASK *task = task_now();// 取得task	struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);	struct SHTCTL *shtctl = (struct SHTCTL *) *((int *) 0x0fe4);	struct SHEET *sht;	int *reg = &eax + 1;	if (edx == 1) {		cons_putchar(cons, eax & 0xff, 1);	} else if (edx == 2) {		cons_putstr0(cons, (char *) ebx + ds_base);	} else if (edx == 3) {		cons_putstr1(cons, (char *) ebx + ds_base, ecx);	} else if (edx == 4) {// edx=4时,返回命令行窗口的栈地址		return &(task->tss.esp0);// 从task中取得栈顶地址	} else if (edx == 5) {		sht = sheet_alloc(shtctl);		sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);		make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);		sheet_slide(sht, 100, 50);		sheet_updown(sht, 3);	/* 3‚Æ‚¢‚¤‚‚³‚Ítask_a‚̏ã */		reg[7] = (int) sht;  }	return 0;}

好解决了栈顶传递的问题,我们还需要把中断函数相关的几个函数_asm_hrb_api, hrb_api,和_asm_end_app连一下:

_asm_hrb_api:		STI		PUSH	DS		PUSH	ES		PUSHAD		; 		PUSHAD		; 		MOV		AX,SS		MOV		DS,AX		;		MOV		ES,AX		CALL	_hrb_api		CMP		EAX,0		; 将保存在EAX中的_hrb_api中的返回值与0比较		JNE		_asm_end_app 如果返回值不为0,说明调用的是edx=4时,此次返回的是栈顶的地址,那么此时应该跳转到_asm_end_app中去执行		ADD		ESP,32  ; 如果返回值为0,就丢弃掉第二次PUSHAD的内容		POPAD         ; 恢复寄存器的值		POP		ES			; 		POP		DS		IRETD         ; 中断函数正常返回,谁调用中断函数就返回到哪里_asm_end_app:		MOV		ESP,[EAX]		MOV		DWORD [EAX+4],0		POPAD		RET					; 

通过比较hrb_api的返回值,如果返回值不为0,就说明是调用了edx=4的中断函数,此时我们利用asm_end_app返回命令行程序。

既然我们用中断函数来返回到命令行程序了,我们就把使用edx=4的函数写一下,让C语言程序调用一下,返回到命令行程序中。

用新的返回方法编写C程序

void api_putchar(int c);void api_end(void);void HariMain(void){	api_putchar('h');	api_putchar('e');	api_putchar('l');	api_putchar('l');	api_putchar('o');	api_end();}

这个程序中的api_end,还是要用汇编来写,写在a_nask.nas中:

_api_end:	; void api_end(void);		MOV		EDX,4  // 令edx=4		INT		0x40   // 调用0x40号中断

写完后,编译,就可以看到程序正常地运行了。

总结

今天,我们主要是使用GDT把操作系统和应用程序所使用的内存空间隔离起来。

我们先把C程序的数据单独编译到了文件的某一部分。

然后又把数据单独存在了一块内存中,和代码存的内存分开。

然后又把这两块内存分别登记在GDT表中,并且设置好权限。利用CPU访问GDT时,对特权级的验证,来把操作系统与应用程序的内存地址隔离开来,形成对操作系统的保护。

GDT可以保护操作系统的内存不被无权限的访问,但应用程序的内存该如何保护呢?

假设现在有2个APP应用程序,这两个应用程序在GDT中都登记了存放数据内存和存放代码内存。

那么登记的特权级都是0x11.

我们说特权级相同,就可以相互访问。

所以,这两个应用程序的内存空间是可以相互访问的。

这就坏了,如果有人写了一个程序,可以访问微信的数据,那不就会现数据泄漏了么?

所以,我们还要想办法让APP不能够相互访问内存段。也就是说,要把APP的数据内存区和代码内存区保护起来,不让其他APP访问。

其实,只要设置一个LDT表,把应用程序的数据内存,程序内存,栈内存登记到LDT表中,并且设置好权限,那么应用程序的数据内存、程序内存、栈内存就会被保护起来了。

保护机制和GDT表一样。

LDT其实想GDT表结构非常像。

只不过GDT是主要把操作系统和应用程序隔离开。

而LDT是把当前运行的应用程序和其他应用程序隔离开。

他们的保护定向不同,一个保护操作系统,一个保护应用程序APP。

标签: #win10xbox0x409