Linux在Windows下实现结构化的异常处理
时间:2010-10-04 来源:huangyoulia..
盲目性的习惯编写程序,毕竟是很不好的心态;显而已知在这些过程中便形成了编程就变得异常了。之所以还是安下心来认真,然而认真还是不够的,今天我便认真阅读和思考了这样一些关于Linux在Windows下实现结构化的异常处理问题;往往这就是我们所突劣的重要所在,比如我们在Windows平台下编写程序,经常会遇到代码因为种种原因而发生异常的情况;而Linux下是没有类似的机制的,异常处理例程是是静态包含在内核代码中的;而且必须精确定位出可能产生异常的代码的地址,这样便给异常处理带来很大的麻烦。通常用Linux是开放式的,既然内核自己提供类似的机制,那就让我们自己来实现它。 首先我们要先认识SEH总名称(Structure Exception Handling) 技术的出现为程序员供了一个标准解决方案,程序员只需要注册好异常处理函数(由编译器负责,通常不是一个完整的函数,而是和可能发生异常的代码在同一个函数中),如果在当前线程的代码引发异常时系统便会自动调用异常处理函数,完成善后工作。要完成SEH工作,必须能够拦截异常,拦截之后便转移到相关的代码,最后在可能产生异常的代码之前注册异常代码处理例程。 要做到异常拦截,程序员必须直接修改IDT表,用自己的GP(general-Protection)异常处理服务代码替换掉内核的异常处理代码就行啦,这个工嫩很容易实现。以下代码如: void*_Origin_GP_Entry; void_hook_generalfalut() { dtr idtr; desc *pdesc; asm(“sidt%0”:"=m"(idtr)); *pdesc=phys_to_virt(idtr->base); pdesc+=0xd;//point to GP description _Origin_GP_Entry_=(void*)((pdesc->basehigh<<16) pdesc->baselow);
pdesc->basehigh=((unsigned long)_ExceptionEntry>>16); pdesc->baselow=((unsigned long)_ExceptionEntry&0xffff); } 注意:在程序推出时必须恢复原来的GP异常入口。 要做到异常处理时,必须在自己的异常处理服务程序区检测到相应的异常处理程序是否注册,如果是,那么则恢复现场,然后跳转到注册异常处理程序中运行;如果没有注册就把控制权转交给内核的异常处理程序。 我们在异常处理时把新的异常服务程序分为两个部分,第一部分用汇编语言实现,通常用于保护现场及完成一些对于C代码无法实现的操作,代码如下: Unsigned char_ExceptionEntry[]={ 0x9c, //1 pushfd 0x60, //2 pushad 0x8d,0x44,0x24,0x24 //3 lea eax,[esp+9*4] 0x50, //4 push eax 0xe8,0,0,0,0 //5 call_c_ExceptionEntry 0x8d,0x64,0x24,4 //6 lea esp,[esp+4] 0xb,0xc0, //7 or eax,eax 0x74,0x11, //8 jz_call_system_handler 0x8b,0x44,0x24,0x30, //9 mov eax,dword ptr [esp+0x0c*4] 0x89,0x44,0x24,0x20, //10 mov dword ptr [esp+0x20],eax 0x61, //11 popad 0x9d, //12 popfd 0x8d, //13 lea esp,[esp+4] 0xca,4,0, //14 retf4 0x61, //15 _call_system_handler: popad 0x9d, //16 popfd 0xe9,0,0,0,0 //17 jmp_system_handler }; 首先,这一部分代码要使用的是机器码而不是汇编代码,是因为其中部分代码需要在初始化时动态修改,以机器码的形式写出来便便于计算偏移量。另外也方便在注释中使用了Intel汇编语言格式,毕竟是AT&T汇编的格式太麻烦。现在我们来解释一下这些代码的作用: 第1、2行,起保护现场,保存所有有可能会破坏到的寄存器。 第3行,EAX指向堆找栈中的异常错误代码。 第4、5行,以指向异常错误代码的指针为参数调第二部分异常处理程序,因为汇汇译之前无法确定第二部分异常处理程序的地址,所以初始化时需要将第二部分异常处理程序的地址填入0xe8之后的双字中。 第6 行,第二部分异常处理程序执行完毕之后恢复调用前的堆栈。 第7、8 行,确认该异常是否已经处理,如果没有处理则跳转到_call_system_handler(见第15行) 第9、10行,将异常发生时的标志寄存的值复制到第1行保存的标志寄存器的位置。 第11、12行,恢复异常发生前的现场。 第13行,清除堆栈中的错误代码,注意这里绝对不能用add esp,4因为此时标志寄存器已经恢复到发生异常的状态,add指令会改变标志寄存器的值,而用pop指令清除堆栈的话没,有地方存放取出的数据,所以只能用lea指令。 第14行,以远的调用方式返回被中断代码,这一句非常重要,因为是异常处理程序返回被中断代码,所以本来应该使用IRET来返回,但异常可能发生在某个子程序中,因此在返回时需要调整堆栈指针,所以这里带调整堆栈的RETF指令来完成这一任务,这里RETF附带的参数也需要按每次异常发生是的状态来修正。 第15、16行,如果当前异常不能处理,恢复现场,准备转到系统的异常处理程序。 第17行,跳转到系统的异常处理程序。这里是跳转的地址也是在初始化是设置。 异常处理程序的第二部分,用于搜索注册的异常处理程序链以检查当前异常是否需要由我们处理。如果是,设置返回信息,返回非0值;如果不是,返回0,这样由系统进行异常程序处理。
第二部代码如下:
asmlinkage_c_ExceptionEntry (unsigned long*pStackTop) { uint32_t uThreadID=getpid(); unit32_t uStackPointer; unsigned short*pDelta; uResumeEntry=_enum_exception_handler (uThreadID,&uStackPointer); if(uResumeEntry) {//异常处理程序已经注册 pStackTop[l]=uResumeEntry; //返回地址修正为已经注册的异常处理程序入口 pDelta=(unsigned short*)(_ExceptionEntry+35); //指向retf指令的参数——一个指明返回时从堆栈中 //清除多少个字节的Word值 *pDelta=uStackPointer-(unsigned long)pStackTop+4; //修正返回时的堆栈指针 } return uResumeEntry; } 第二部分代码的主要功能是确认发生异常的线程是否已经注册过异常处理例程(异常处理例程可以嵌套,即在某段代码注册了异常之后,它的子程序依旧可以注册异常处理例程,在发生异常时,将会执行最近的注册的那个异常处理例程),如果没注册,表示该异常由操作系统处理;如果該线程注册过异常处理程序,则恢复现场并转到注册的异常程序入口执行。 相关阅读 更多 +