好久之前写的Offline Judge,当时RE评测结果功能的实现使用的是debug api,结果有一个bug,此功能就暂时被删掉了。昨天和学长讨论,说debug api可能会影响运行时间的评测结果,当时我还觉不使用debug api怎么能捕捉到子进程的异常呢。今天突然想到不用debug api的方法:注入dll,挂钩UnhandledExceptionFilter。然而写好之后令我没想到的是,一个7KB的DLL文件注入之后居然多占用了1.5M的内存,这样内存使用的信息也就只能“仅作参考”了。目前我还没有想到解决办法。对于一些对内存使用量不敏感的应用来说,这种方法也可以考虑,当然前提是程序内部没有对异常做出处理。
最开始想到的方法是注入之后调用SetUnhandledExceptionFilter,不过发现居然没用(也许是在我的dll注入进去之后UnhandledExceptionFilter又被改掉了?还是什么?)。然后想到挂钩SetUnhandledExceptionFilter让所有人都靠边站,不过既然这样为何不直接挂钩UnhandledExceptionFilter呢,因为每次异常发生(测试用的是一个人为除零的小程序)都是windows的“xxxx已停止工作……”,这说明最后是UnhandledExceptionFilter得到控制权的(这也是一定的),而且并不是所有未处理异常都会被SetUnhandledExceptionFilter所设置的函数拦截到的。于是,动工。
void HookUnhandledExceptionFilter(){ BYTE fakeEntry[5]; fakeEntry[0] = 0xE9; LPVOID pFunc = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "UnhandledExceptionFilter"); *((PDWORD)(&fakeEntry[1])) = (DWORD)NewExceptionFilter - (DWORD)pFunc - 5; DWORD dwProtect, dwWrite; VirtualProtect(pFunc, 5, PAGE_READWRITE, &dwProtect); WriteProcessMemory(GetCurrentProcess(), pFunc, fakeEntry, 5, &dwWrite); VirtualProtect(pFunc, 5, dwProtect, NULL); return 0; } int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved) { switch(reason) { case DLL_PROCESS_ATTACH: HookUnhandledExceptionFilter(); break; } return 1; }
HookUnhandledExceptionFilter函数把原来的UnhandledExceptionFilter函数的前5个字节改成了跳转。其中的NewExceptionFilter是我们新的UnhandledExceptionFilter函数。
NewExceptionFilter的作用就很简单了,得到ExceptionCode,根据异常类型向主程序传递信息。传递信息这里我是直接通过StdErr实现的,主程序通过匿名管道实现StdErr的重定向,只要检测是否有错误输出就可以知道是否有Runtime Error了。如果是其他应用,使用命名管道之类的手段即可。
这是一种不用debug api来获得子进程异常的方法,也算作是一种思路吧。