1.某大佬精心总结 链接
2.为何要从16位实模式 进入到 保护模式
①实模式下 你可以随心所欲 使用任何一块地址 ,写入任何你想写入的数据
这个地址 甚至可能是OS的代码你都可以随意修改
②进入到保护模式,是对内存进行了等级划分,防止你有问题的程序去破坏其他的程序 或者 OS的代码
③GDT就是一个保护的例子,他限制了你的最大偏移长度,权限等
3.段描述符如何实现保护模式
在段描述符中,主要将有三类字段来协同参与实现保护功能:
①.段限长—LIMIT。该字段规定了每个定义段的长度,比如你要跳转的程序长度超过了的CS定义的段长度或者要访问的数据内存地址超过了DS的定义的段长度,就会报错。
②.段类型—TYPE。我们知道,每个段都可以定义了可执行、可读写等类型,如果你要在只读的内存段写数据,无疑会报错。
③.描述符特权级—DPL。这个字段除了存在于普通的段描述符中外,还存在于各类门描述符中。该字段用来表示本段的特权级别,显然操作系统段的特权级最高,应用程序段的特权级最低,如果你要在应用程序中对操作系统的段做写入操作,就会被阻止。
4.无保护模式下
破坏OS的代码
void HariMain(void)
{
*((char *) 0x00102600) = 0;
return;
}
①直接往内存地址0x00102600写成0,0x00102600操作实际就是软盘第一个文件名被修改成了0。操作系统看到文件名是0x00开头的,认为是异常(其内核程序是这么设计的),因此再执行任何dir,type或运行任何应用程序都不会有反应,这样就相当于操作系统被坏了。
5.给应用程序分配专门的段
上面应用程序搞瘫操作系统,最的问题就是操作系统和应用程序的数据段没有做区分所致,操作系统和应用程序的数据段、堆栈段都混为一团,其实就和实模式的管理方法没有本质区别。所以,为应用程序分配独立的段,是最基本的要求。
应用程序的内存空间都是临时申请的,并把这个临时内存空间注册到GDT中就形成了应用程序的代码段和数据段。需要注意的是:代码段的长度是应用程序的实际大小,而数据段的长度由于不好估计,因此这里固定申请为64KB。可以看到,无论是操作系统还是应用程序都只有代码段和数据段,并没有再设置堆栈段。这是因为我们的堆栈寄存器SS一直是和数据类寄存器DS\ES\FS\GS保持的同一个值,可以理解为堆栈段就是用的是数据段,那么在同一个程序中,就只需要重新定义ESP栈顶的指针值就可以了,由于这次我们给应用程序申请的数据区长度是64KB,那么我们把ESP栈顶指针设置在这64KB的最顶端最适合了
p = (char *)MemoryManagerMalloc4K(MemMan, pFileInfo->u32FileSize);
pAppDataBuf = (char *)MemoryManagerMalloc4K(MemMan, 64*1024);
*((int *) 0xfe8) = (int) p;//把段的首地址放到0xfe8
FileLoadFile(pFileInfo->u16WriteClustno, pFileInfo->u32FileSize,
p, pFat, (char *)(ADR_DISKIMG + 0x003e00));
//应用程序代码段 数据段
SetSegmDesc(pGdt + 1003, pFileInfo->u32FileSize - 1,
(int)p, AR_CODE32_ER);
SetSegmDesc(pGdt + 1004, 64*1024-1, (int)pAppDataBuf,
AR_DATA32_RW);
start_app(0, 1003*8, 64*1024, 1004*8);
//app和操作系统的代码不在一个段内
//我们需要传递过去 让OS能切换到应用所在的段
MemoryManagerFree4K(MemMan, (int)p, pFileInfo->u32FileSize);
MemoryManagerFree4K(MemMan, (int)pAppDataBuf, 64*1024-1);
这里用start_app()来启动应用程序,对应参数分别为应用程序启动之后新的EIP值和新的CS,ESP和DS(SS)段地址之值。这样,我们再执行应用程序:*((char *) 0x00102600) = 0的时候,它操作的内存区就不再是操作系统数据段了,而是应用程序的数据段,所以不会对操作系统造成破坏,我们达到了初级保护的目的
6.OS调用应用程序,应用程序又需要调用OS的函数,所以二者需要来回切换,而且在应用程序运行的过程当中,OS的一些中断服务仍在运行,也需要被切回
① 应用程序的启停
我们需要在操作系统里写一个启动应用程序的内核程序start_app(),策略是在应用程序启动之前,通过操作系统的堆栈先保存好操作系统的数据段寄存器(DS和SS等)和操作系统的堆栈指针ESP,再把应用程序的数据段和应用程序的堆栈指针ESP写入相应寄存器,然后开始调用应用程序。应用程序执行完成之后返回之后,操作系统还原之前的各个寄存器值即可。这里为什么不需要将操作系统的代码段CS和EIP入栈保护和恢复呢?这是因为这两个值是通过call和ret等配合调用自动切换的,CPU会自动管理,不需要我们操心。
保存和恢复操作系统普通的段寄存器DS和SS等非常容易,用传统的PUSH和POP即可。但是要保存和恢复操作系统堆栈指针ESP,是不能简单的使用PUSH和POP就行的,:POP ESP 指令不但没有意义,反而它会使程序异常,严重的话计算机会崩溃,应绝对禁用!
;由于OS系统的代码 和 APP的代码不在一个段中 需要进行切换
_start_app: ; void start_app(int EIP, int CS, int ESP, int DS);
PUSHAD ; 将OS 32位寄存器的值全部保存起来
MOV EAX,[ESP+36] ; 应用程序的EIP 0
MOV ECX,[ESP+40] ; 应用程序的CS 1003*8
MOV EDX,[ESP+44] ; 应用程序的ESP 64*1024
MOV EBX,[ESP+48] ; 应用程序的DS/SS 1004*8
MOV [0xfe4],ESP ; OS的ESP 保存在内存0xfe4 这个位置
CLI ; 进行切换的时候 禁止所有的中断
MOV ES,BX
MOV SS,BX
MOV DS,BX
MOV FS,BX
MOV GS,BX
MOV ESP,EDX
STI ; 切换完成后 恢复中断请求
PUSH ECX ; far-CALL push(cs)
PUSH EAX ; far-CALL push(eip)
CALL FAR [ESP] ; 调用应用程序
; 应用程序结束后 返回此处
;然后我们返回到操作系统的段中
MOV EAX,1*8 ; OS的DS/SS
CLI ; 切换中禁止所有的中断
MOV ES,AX
MOV SS,AX
MOV DS,AX
MOV FS,AX
MOV GS,AX
MOV ESP,[0xfe4]
STI ; 切换完成后 恢复中断请求
POPAD ; 恢复所有的寄存器
RET
② 应用程序调用操作系统API
我们一个应用程序要调用OS的字符串显示API,他两又不在一个段中,如何解决呢?
操作系统的字符串显示API
//字符串显示API
void HRB_API(int EDI, int ESI, int EBP, int ESP, int EBX, int EDX, int ECX, int EAX)
{
struct CONSOLE* pConsoleInfo = (struct CONSOLE*)*( (int *) 0x0fec);
int CS_Base = *((int *) 0xfe8);
if(1 == EDX)
{
ConsolePutChar(pConsoleInfo, EAX&0xff, 1);
}
else if(2 == EDX)
{
ConsolePutString0(pConsoleInfo, (char *)EBX + CS_Base);
}
else if(3 == EDX)
{
ConsolePutStringLen(pConsoleInfo, (char *)EBX + CS_Base, ECX);
}
return;
}
**如何从应用程序 调用API **
;_HRB_API这个函数是OS系统代码 而 _asm_hrb_api 是由应用程序调用的
_asm_hrb_api:
; 从开始 就禁止中断请求
PUSH DS ;此时我们保存应用程序的DS和ES
PUSH ES
PUSHAD ; 我们把这 八个参数全部压栈
;void HRB_API(int EDI, int ESI, int EBP, int ESP,
;int EBX, int EDX, int ECX, int EAX)
MOV EAX,1*8
MOV DS,AX ; 我们仅将DS改成操作系统的数据段
MOV ECX,[0xfe4] ; OS的ESP
ADD ECX,-40
MOV [ECX+32],ESP ; 保存应用程序的ESP
MOV [ECX+36],SS ; 保存应用程序的SS
; 上面的PUSHAD把八个参数都保存在了应用程序的栈中 ,
;我们现在把他写到 OS的栈中
MOV EDX,[ESP ]
MOV EBX,[ESP+ 4]
MOV [ECX ],EDX ; hrb_api的第一个参数
MOV [ECX+ 4],EBX ; hrb_api的第二个参数
MOV EDX,[ESP+ 8]
MOV EBX,[ESP+12]
MOV [ECX+ 8],EDX ; hrb_api的第三个参数
MOV [ECX+12],EBX ; hrb_api的第四个参数
MOV EDX,[ESP+16]
MOV EBX,[ESP+20]
MOV [ECX+16],EDX ; hrb_api的第五个参数
MOV [ECX+20],EBX ; hrb_api的第六个参数
MOV EDX,[ESP+24]
MOV EBX,[ESP+28]
MOV [ECX+24],EDX ; hrb_api的第七个参数
MOV [ECX+28],EBX ; hrb_api的第八个参数
MOV ES,AX ; 把剩余寄存器也设为操作系统使用
MOV SS,AX
MOV ESP,ECX
STI ; 恢复中断请求
CALL _HRB_API
MOV ECX,[ESP+32] ; 恢复应用程序的ESP
MOV EAX,[ESP+36] ; 恢复应用程序的SS
CLI
MOV SS,AX
MOV ESP,ECX
POPAD
POP ES
POP DS
IRETD ; 这个命令会自动执行STI
HRB_API是操作系统的字符串显示函数,他需要八个参数,这八个参数存在于应用程序的栈中,我们要做的就是要把这八个参数 拷贝到OS的栈中,并且切换到操作系统
这个程序的步骤分为以下几步:
(1) 保存应用程序的所有段。偶先保存数据段DS等,这个和我们之前说的思路是一致的。但是这里需要注意一个问题,就是刚开始进入程序的时候,所有的段和寄存器内容都是应用程序的,所有这些内容也只能暂时存在应用程序的堆栈区。
(2) 切换到操作系统所有段。这个过程里面最复杂的就是操作系统和应用程序之间的栈切换,需要具体来仔细分析。我们最终的目的是要调用操作系统C程序函数hrbapi,由于这个函数hrbapi的8个入口参数是在应用程序里的8个32位寄存器,我们首先需要把8个32位寄存器压入操作系统的栈才行。但现在SS和ESP指向的是应用程序,故应将SS和ESP先切换到操作系统。SS不能直接赋值,只能通过AX等中转,因此我们需要用mov ss, ax指令;要切换到操作系统ESP,方法是从上一节start_app()启动应用程序中暂存在内存地址[0xfe4]中取,所以得采用mov esp [0xfe4]的形式,但是要访问到操作系统的[0xfe4],又要先把DS切换到操作系统:mov ds, 18,但是这样的指令是不运行的,必须借助寄存器:mov ds, ax。所以要实现将SS和ESP先切换到操作系统,必须要用以下条指令:mov ax, 18, mov ss, ax,mov ds, ax。这样,我们还没来得及将应用程序的8个32位寄存器入到操作系统的栈区,就已经修改了EAX了,所以这个方法不行。我们只有换一种方法:先把应用程序的8个32位寄存器存入应用程序的堆栈区,然后将SS和ESP切换到操作系统,再将这个8个32位寄存器从应用程序的堆栈区复制到操作系统的堆栈区
(3) 恢复应用程序所有段。当操作系统所有的事情都完成之后,就要恢复所有的应用程序所有段了,注意顺序是先恢复栈类数据SS和ESP,因为8位32的寄存器还全部放在应用程序的栈内呢,程序的最后才恢复数据段。