阅读本文你将获知:
- 调用溯源的过程
- 一堆琐碎但必要了解的知识
- 如果你想了解调试器的详细知识,请阅读:<软件调试>-张银奎
前奏
MiniDebugger是我在学习调试原理的时候制作的一个调试器,提供了基本的调试功能.我在该项目中做过一些有趣的探究.详情看这里:
开源小小调试器-MiniDebugger
今天看书之余顺手用MiniDebugger附加了一下平时用的程序流程图绘制软件.g回车.
不将东西搞砸就好像若有所失,点了一下流程图绘制程序中File->打开:
这……
环顾四周,发觉附加唯一的妹纸还在发呆看着什么剧表示大舒了一口气.
我们是有源码的怕什么怕?(~ o ~)~
刨根问底
打开源码在Debug模式下启动我们的MiniDebugger,重复上面BUG产生的过程.
可重现,我要给这个Bug+1:
访问违例.
中断下来,我们在Output.c中.这里可没什么好修理的,这是系统的模块.我们得追根溯源,打开”调用堆栈”选项卡.
如果你并不了解了什么是调用堆栈,那么你得先学一些关于程序栈结构的知识.
大概的意思是:它能让我们找到以前干过什么.(~ o ~)~
从上到下越来越接近之前调用的代码.很容易找到最近的我们的模块:
OutputDebug是我们MiniDebugger中的一个模块,用来打印调试信息.该模块来自意大利黑客组织Hacking Team泄密程序”战士”项目中调试模块.难道这个模块出的问题?难下定论.
继续让我们看一下产生”访问违例”异常地址的内容,看一下在访问什么的时候出现的这种情况:
0x7ef8e028处内存栏显示:问号????
????表示该地址指向无效内存即未向操作系统申请的内存.OK,让我们看看这个错误的地址从何处而来.继续溯源.
我们来到调用OutputDebug的模块Deal_LDDE().
这个模块用来处理加载模块事件.右键监视661行传入的参数值:
原来那个无效内存地址是通过第二个参数DbgEvt.u.LoadDll.lpImageName传入的.
DbgEvt的类型是DEBUG_EVENT,结构如下:
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
这个结构在发生事件后由操作系统填充给我们.发生异常异常时该结构的其它部分读取正常,难道是操作系统疯特了?
其实原因出在这里:
OutputDebug(L"模块加载: 基址:Ox%p %s",
DbgEvt.u.LoadDll.lpBaseOfDll,DbgEvt.u.LoadDll.lpImageName);
wsprintfW(swPrintBuffer,L"模块加载: 基址:Ox%p %s",
DbgEvt.u.LoadDll.lpBaseOfDll,(wchar_t*)DbgEvt.u.LoadDll.lpImageName);
系统给我们的是名字的指针,而这个指针指向的被我们程序调试的程序-画程序流程图的它.
同样上面的wsprintfW一句的参数也有同样的问题:
(wchar_t*)DbgEvt.u.LoadDll.lpImageName
指向的也是被调试程序.
怎么解决呢?
方法是将这个换做读取目标进程的代码地址即可.
想起之前调试这段代码的时候遇到的各种模块名字获取不全乱码的问题,原来症结在此.
为什么之前没崩溃?
因为如果没开随机基址的情况下很多系统模块在各种程序的地址相同.但是由于我的调试器没有加“文件->打开”相关功能.MiniDebugger在调试其他程序的时候相同的模块位置导致错误一直隐秘在程序中只是以乱码名字来展示.
知得
- 遇到幽灵事件,除了记录,更应该主动去探究防止隐藏BUG.
- 崩溃确实是个好现象.没有错误迹象的BUG更危险是定时炸弹.