问题
如何捕获应用程序的异常行为,并进一步处理?
内存保护设计一
当应用程序写入被保护的内存时,将产生 Page Fault (0x0E)
- Page Fault 可以看作一种特殊的中断类型 (异常)
通过中断服务程序结束当前任务,并调度下一任务执行
- 即:中断服务程序的主要工作是调用 KillTask()
原理剖析
应用程序向内核的内存空间写入数据时会触发 Page Fault 异常,在此异常对应的中断服务程序中将触发此异常的任务消灭。
中断与异常的细微差异
中断由外部设备触发或指令主动触发 (不可预见)
异常是指令执行的过程中遇见的错误 (可预见)
中断处理结束后,返回到被中断的指令处执行 (不可预见)
异常处理结束后,返回到触发异常的指令处重新执行 (可预见)
中断不带错误参数,异常可能带有错误参数 (位于栈顶)
异常发生时的压栈
异常发生时,和中断一样,需要特权级的转移,并且将相关寄存器的值入栈;除此之外,最后还会将错误异常号入栈。
通过 Page Fault 结束异常任务
kentry.asm
%macro BeginFSR 0
cli
pushad
push ds
push es
push fs
push gs
mov dx, ss
mov ds, dx
mov es, dx
mov esp, BaseOfLoader
%endmacro
;
;
PageFaultHandlerEntry:
BeginFSR
call PageFaultHandler
EndISR
ihandler.c
void PageFaultHandler()
{
SetPrintPos(0, 6);
PrintString("PageFault kill: ");
PrintString(gCTaskAddr->name);
KillTask();
}
app.c
void AppMain()
{
RegApp("TaskA", TaskA, 255);
RegApp("TaskB", TaskB, 230);
//RegApp("TaskC", TaskC, 230);
//RegApp("TaskD", TaskD, 255);
}
void TaskA()
{
int i = 0;
int* p = (void*)0xE000;
SetPrintPos(0, 12);
*p = 1000;
PrintString(__FUNCTION__);
PrintIntDec(*p);
while(i < 5)
{
SetPrintPos(8, 12);
PrintChar('A' + i);
i = (i + 1) % 26;
Delay(1);
}
SetPrintPos(8, 12);
}
首先我们注册了 PageFault 中断服务程序,它的中断向量为 0x0E;这个中断服务程序用于打印当前发生 PageFault 异常的任务名,并将此任务消灭。
因为异常发生时还需要将 error_code 入栈,所以我们将保护任务上下文的宏替换成 BeginFSR,这个宏会将 error_code 保存到 RegValue 任务结构体里的 error_code 里,而不是直接跳过它。
TaskA 执行过程中会向内核内存区写数据,当向内核内存区写数据时,会触发 PageFault 异常,从而调用对应的中断服务程序,将当前出现 PageFault 异常的任务消灭。其他任务会继续正常运行。
问题
如果任务强行修改了页表所在内存区域,会发生什么?
我们可以将页表所在的页的属性设置为应用程序只读,但这样需要修改 ConfigPageTable 函数,我们不这样做。
内存保护设计二
缩小应用程序可使用的内存范围 (如:0xF000 - 4FFFF)
利用段越界所产生的异常保护高地址内存 (如:0x50000)
当发生段越界时 (0x0D)
- 通过中断服务程序结束当前任务
- 调度下一个任务执行
具体实现
编写段越界异常所对应的处理函数 ( SegmentFaultHandler() )
编写段越界异常处理入口 ( SegmentFaultHandlerEntry() )
注册 SegmentFaultHandlerEntry 为 0x0D 中断服务处程序
通过 Segment Fault 结束异常任务
ihandler.c
void SegmentFaultHandler()
{
SetPrintPos(0, 7);
PrintString("SegmentFault kill: ");
PrintString(gCTaskAddr->name);
KillTask();
}
kentry.asm
;
;
SegmentFaultHandlerEntry:
BeginFSR
call SegmentFaultHandler
EndISR
task.c
static void InitTask(Task* pt, uint id, const char* name, void(*entry)(), ushort pri)
{
pt->rv.cs = LDT_CODE32_SELECTOR;
pt->rv.gs = LDT_VIDEO_SELECTOR;
pt->rv.ds = LDT_DATA32_SELECTOR;
pt->rv.es = LDT_DATA32_SELECTOR;
pt->rv.fs = LDT_DATA32_SELECTOR;
pt->rv.ss = LDT_DATA32_SELECTOR;
pt->rv.esp = (uint)pt->stack + sizeof(pt->stack);
pt->rv.eip = (uint)TaskEntry;
pt->rv.eflags = 0x3202;
pt->id = id;
StrCpy(pt->name, name, sizeof(pt->name) - 1);
pt->tmain = entry;
pt->current = 0;
pt->total = 256 - pri;
gTSS.ss0 = GDT_DATA32_FLAT_SELECTOR;
gTSS.esp0 = (uint)&pt->rv + sizeof(pt->rv);
gTSS.iomb = sizeof(TSS);
SetDescValue(AddrOff(pt->ldt, LDT_VIDEO_INDEX), 0xB8000, 0x07FFF, DA_DRWA + DA_32 + DA_DPL3);
SetDescValue(AddrOff(pt->ldt, LDT_CODE32_INDEX), 0x00, 0x4FFFF, DA_C + DA_32 + DA_DPL3);
SetDescValue(AddrOff(pt->ldt, LDT_DATA32_INDEX), 0x00, 0x4FFFF, DA_DRW + DA_32 + DA_DPL3);
pt->ldtSelector = GDT_TASK_LDT_SELECTOR;
pt->tssSelector = GDT_TASK_TSS_SELECTOR;
}
app.c
void TaskA()
{
int i = 0;
int* p = (void*)0x50000;
SetPrintPos(0, 12);
// *p = 1000;
PrintString(__FUNCTION__);
PrintIntDec(*p);
while(i < 5)
{
SetPrintPos(8, 12);
PrintChar('A' + i);
i = (i + 1) % 26;
Delay(1);
}
SetPrintPos(8, 12);
}
task.c 中,我们将应用程序的可访问的界限设置为 0x00 - 0x4FFFF,当要访问 0x4FFFF 地址处以上的内存空间时,会触发 SegmentFault 异常,它的中断向量为 0x0D,调用对应的中断服务程序,该中断服务程序将打印触发 SegmentFault 异常的任务名,并将这个任务消灭。
当应用程序想要读 0x50000 地址处的内存时,会触发 SegmentFault 异常,该异常对应的中断服务程序会将 TaskA 消灭,其他任务继续正常运行。