让内核从错误中恢复
1.简介
微软早期的DOS系统 存在一个严重的问题是
如果应用程序运行出现问题 它会导致整个系统完全奔溃掉
我们当前的系统内核也存在这一的问题
例如打开api_call.asm,其内容如下
[SECTION .s32] BITS 32 call main retf api_putchar: mov edx, 1 mov al, [esp + 4] int 02Dh ret %include "app.asm"
call main 时CPU控制权会提交给应用程序
执行应用程序的代码 应用程序执行完毕后
返回到call main语句的下一条指令继续执行
call main 的下一条语句是retf
它的作用是从堆栈上得到内核代码的全局描述符
把该描述符在描述符表中的下标赋值给寄存器cs
同时把内核要执行语句的地址从堆栈上取得 并赋值给寄存器ip
这样CPU的控制器会重新交还给内核
假设我们对上面代码做一个修改如下
[SECTION .s32] BITS 32 call main pop eax ;故意让返回地址出错造成异常保护中断 retf ....
我们在执行retf语句时 把存储在堆栈上的内核要执行的语句地址弹出
这样要返回的内核地址就会遭遇破坏
当执行retf语句后 ip指针会执行内存的任意一个无法确定的地方
于是CPU在接下来的执行中就会遇到错误
上面代码修改后运行起来 就会出现报错
点击确定之后 系统就被关闭了
出现这种现象的原因
由于ip指针指向了无效地址 致使CPU执行了无效指令进而导致系统的整体奔溃
接下来我们看看如何处理让系统从这种严重错误中恢复回来
这样就可以防止应用程序执行恶意代码而对系统造成伤害
当CPU在执行指令出现错误时
例如遇到了无效指令 那么CPU会出发0Dh号中断
让该中断来处理当前所出现的异常局面
如果该中断无法处理 那么CPU就会停止运行
于是就出现了上面的情况
为了让系统能从错误中恢复 我们需要实现0Dh号中断
在中断中 我们直接结束掉当前正在运行的应用程序 直接把CPU的控制全交还给内核
2.代码
kernel.asm中注册0Dh号中断
代码修改如下
LABEL_IDT: %rep 13 Gate SelectorCode32, SpuriousHandler,0, DA_386IGate %endrep .0Dh: Gate SelectorCode32, exceptionHandler,0, DA_386IGate %rep 18 Gate SelectorCode32, SpuriousHandler,0, DA_386IGate %endrep
上面代码在中断描述符表中注册了一个0Dh号中断
当中断发送时 CPU会调用exceptionHandler函数
看看该函数的实现 同样也是在kernel.asm中
_exceptionHandler: exceptionHandler equ _exceptionHandler - $$ cli ;把内存段切换到内核 mov ax, SelectorVram mov ds, ax mov es, ax mov ax, SelectorStack ;切换到内核堆栈段 mov ss, ax mov ecx, [0xfe4];获取内核堆栈指针 add ecx, -8 mov [ecx+4], ss ;保存中断时的堆栈段 mov [ecx], esp ;保存中断时堆栈指针 mov esp, ecx ;切换内核指针 call intHandlerForException .kill: ;通过把CPU交给内核的方式直接杀掉应用程序 mov esp, [0xfe4] sti popad ret
中断运行时
先把内核专有内存段的描述符赋值给寄存器ds,es
这样代码运行时可以直接读写内核的数据 同时把内核的堆栈描述符赋值给寄存器ss
这样代码运行时使用的堆栈就是内核的专有堆栈
然后把错误发生时的应用程序使用的堆栈段描述符和堆栈指针存入内核堆栈
接着调用函数intHandlerForException进行错误处理
该函数实现在内核的C语言部分 也就是在write_vga_desktop.c中
void intHandlerForException(int *esp) { g_Console.cur_x = 8; cons_putstr("INT 0D "); g_Console.cur_x = 8; g_Console.cur_y += 16; cons_putstr("General Protected Exception"); g_Console.cur_y += 16; g_Console.cur_x = 8; return 1; }
它的做法很简单 只是在控制台上打印出两行字符串
分别是"INT OD" 和 “General Protected Exception”
然后就返回了
回到中断实现部分 也就是.kill对应的代码
在内核把控制权交给应用程序时 会把它当时的堆栈指针存储到内存[0xfe4]处
内核时通过start_app把控制权交给应用程序的 我们再看看start_app的代码
start_app: ;void start_app(int eip, int cs,int esp, int ds) cli pushad ... mov [0xfe4], esp ...
start_app在执行时 通过指令把当时所有通用寄存器的数据存储到堆栈上
同时把esp指针的值 也就是当时的内核堆栈指针存储到内存0xfe4这个地方
因此.kill 的指令 mov esp [0xfe4]实际上就是对上面指令mov [0xfe4], esp 的逆操作
同时popad也是对上面指令pushad的逆操作
我们再看看start_app是如何被调用的 在write_vga_desktop.c中
void cmd_hlt() { .... start_app(0, 11*8,64*1024, 12*8); char *pApp = (char*)(q + 0x100); showString(shtctl, sht_back, 0, 179, COL8_FFFFFF, pApp); memman_free_4k(memman, buffer.pBuffer, buffer.length); memman_free_4k(memman, q, 64 * 1024); }
start_app是在cmd_hlt中被调用的
我们知道C语言在调用子函数时
会把调用子函数后要执行的下一条指令的地址压入堆栈
因此当上面代码在调用start_app函数时
下一条指令 也就是char pApp=(char)这条语句的地址会被压入到堆栈上
当.kill处的代码执行完语句popad后
此时堆栈上存储的恰好就是语句char pApp=(char)的地址
于是当我们执行指令ret的时候 该语句的地址会赋值给ip寄存器
这样CPU就会直接从char pApp=(char)这条语句开始执行
这就类似与应用程序正常执行完毕后 CPU控制权正常返还给内核的情况是一样的
这样 当应用程序执行出现严重错误时 CPU触发0Dh号异常处理中断
在该中断中 代码会把CPU控制权直接交还给内核
就相当于把出现异常的应用程序毁尸灭迹
内核就好像什么事都没发生过一样 继续按照老样子运行
3.运行