如果我们能自己编写一个类似调试器的功能,这个调试器需要实现我们对于跟踪监视工具的要求,即自动记录输入输出参数,自动让目标进程继续运行。下面我们就来介绍在不知道函数原型的情况下也可以简单输出监视结果的方案——用Debug函数实现API函数的监视。
用Debug函数实现API函数的监视
大家知道,VC可以用来调试程序,除了调试Debug程序,当然也可以调试Release程 序(调试Release程序时为汇编代码)。如果知道函数的入口地址,只需在函数入口上设置断点,当程序调用了设置断点的函数时,VC就会暂停目标程序的 运行,你就可以得到目标程序内存的所有你希望得到的东西了。一般来说,只要你有足够的耐心和毅力,以及一些汇编知识,对于监视API函数的输入输出参数还 是可以完成的。
不过,由于VC的调试器会在每次断点时暂停目标程序的运行,对目标程序的过多的暂停对于监视任务而言实在不能忍受。所以,不会有太多的人真的会用VC的调试器作为一个良好的API函数监视器的。
如果VC调试器能够在你设置好断点后,在运行时自动输出断点时的堆栈值(也就是函数 的输入参数),在函数运行结束时也自动输出堆栈值(也就是函数的输出参数)和CPU寄存器的值(就是函数返回值),并且不会暂停目标程序。所有一切都是自 动的无需我们干预。你会用它来作为监视器吗?我会的。
我不知道如何让VC这样作(或许VC真的可以这样,但我不知道。有人知道的话请通 知我一声,谢谢),但我知道显然VC也是通过调用Windows API函数完成调试器的任务,而且,这些函数显然可以实现我的要求。我需要作的事情就是自己利用这些API函数,写一个简单的调试器,在目标程序断点发生 时自动输出监视结果并且自动恢复目标程序的运行。
显然,用VC调试器作为监视器的话无需知道目标函数的原型就可以得到简单的输入输 出参数和函数运行结果,而且,由于监视代码没有注入目标程序中,就不会出现监视目标函数和监视代码的冲突。VC调试器显然可以跟踪递归函数,也可以跟踪 DLL模块调用DLL本身的函数,以及EXE内部调用自身的函数。只要你知道目标函数的入口地址,就可以跟踪了(监视Exe自身的函数可以通过生成Exe 模块时选择输出Map文件,就可以参考Map文件得到Exe内部函数的地址)。没有听说VC不能调试多线程的,最多是说调试多线程比较麻烦----证明多 线程是可以调试的。显然,VC也可以调试DllMain中的代码。这些,已经可以证明通过调试函数可以实现我们的目标了。
如何编写实现我们目标的程序?需要哪些调试函数?
首先,让目标程序进入被调试状态:
对于一个已经启动的进程而言,利用DebugActiveProcess函数就可以捕获目标进程,将目标进程进入被调试状态。
<ccid_nobr></ccid_nobr>
<ccid_code>BOOL DebugActiveProcess(DWORD dwProcessId);</ccid_code>
|
参数dwProcessId是目标进程的进程ID。如何通过ToolHelp系列函 数或Psapi库函数获得一个运行程序的进程ID在很多文章中介绍过,这里就不再重复。对于服务器程序而言,由于没有权限无法捕获目标进程,可以通过提升 监视程序的权限得到调试权限进行捕获目标进程(用户必须拥有调试权限)。
对于启动一个新的程序而言,通过CreateProcess函数,设置必要的参数就可以将目标程序进入被调试状态。
<ccid_nobr></ccid_nobr>
<ccid_code>BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine, <br>LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES <br>lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID <br>lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, <br>LPPROCESS_INFORMATION lpProcessInformation );</ccid_code>
|
该函数的具体说明请参考MSDN,在这里我仅介绍我们感兴趣的参数。这里和一般的用 法不同,作为被调试程序dwCreationFlags必须设置为DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。这样启 动的目标程序就会进入被调试状态。这里说明一下DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。 DEBUG_ONLY_THIS_PROCESS就是只调试目标进程,而DEBUG_PROCESS参数则不仅调试目标进程,而且调试由目标进程启动的所 有子进程。比如:在A.exe中启动B.exe,如果用DEBUG_ONLY_THIS_PROCESS启动,监视进程只调试A.exe不会调试 B.exe,如果是DEBUG_PROCESS就会调试A.exe和B.exe。为简单起见,本文只讨论启动参数为 DEBUG_ONLY_THIS_PROCESS的情况。
使用方法:
<ccid_nobr></ccid_nobr>
<ccid_code>STARTUPINFO st = {0};<br>PROCESS_INFORMATION pro = {0};<br>st.cb = sizeof(st);<br>CreateProcess(NULL, pszCmd, NULL, NULL, FALSE, <br>DEBUG_ONLY_THIS_PROCESS,<br>NULL, szPath, &st, &pro));<br>// 关闭句柄---这些句柄在调试程序中不再使用,所以可以关闭<br>CloseHandle(pro.hThread);<br>CloseHandle(pro.hProcess);</ccid_code>
|
其次,对进入被调试状态的程序进行监视:
目标进程进入了被调试状态,调试程序(这里调试程序就是我们的监视程序,以后不再说 明)就负责对被调试的程序进行调试操作的调度。调试程序通过WaitForDebugEvent函数获得来自被调试程序的调试消息,调试程序根据得到的调 试消息进行处理,被调试进程将暂停操作,直到调试程序通过ContinueDebugEvent函数通知被调试程序继续运行。
<ccid_nobr></ccid_nobr>
<ccid_code>BOOL WaitForDebugEvent(<br> LPDEBUG_EVENT lpDebugEvent, // debug event information<br> DWORD dwMilliseconds // time-out value<br>);</ccid_code>
|
在参数lpDebugEvent中可以获得调试消息,需要注意的是该函数必须和让目 标程序进入调试状态的线程是同一线程。也就是说和通过DebugActiveProcess或CreateProcess调用的线程是一个线程。另外,我 又喜欢将dwMilliseconds设置为-1(无限等待)。所以我通常都会将CreateProcess和WaitForDebugEvent函数在 一个新的线程中使用。
<ccid_nobr></ccid_nobr>
<ccid_code>typedef struct _DEBUG_EVENT { <br> DWORD dwDebugEventCode; <br> DWORD dwProcessId; <br> DWORD dwThreadId; <br> union { <br> EXCEPTION_DEBUG_INFO Exception; <br> CREATE_THREAD_DEBUG_INFO CreateThread; <br> CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; <br> EXIT_THREAD_DEBUG_INFO ExitThread; <br> EXIT_PROCESS_DEBUG_INFO ExitProcess; <br> LOAD_DLL_DEBUG_INFO LoadDll; <br> UNLOAD_DLL_DEBUG_INFO UnloadDll; <br> OUTPUT_DEBUG_STRING_INFO DebugString; <br> RIP_INFO RipInfo; <br> } u; <br>} DEBUG_EVENT, *LPDEBUG_EVENT;</ccid_code>
|
在这个调试消息结构体中,dwDebugEventCode记录了产生调试中断的消息代码。消息代码的详细说明可以参考MSDN。其中,我们感兴趣的消息代码为:
<ccid_nobr></ccid_nobr>
<ccid_code>EXCEPTION_DEBUG_EVENT:产生调试例外<br>CRATE_THREAD_DEBUG_EVENT:新的线程产生<br>CREATE_PROCESS_DEBUG_EVENT:新的进程产生。注:在DEBUG_ONLY_THIS_PROCESS时只有一次,<br>在DEBUG_PROCESS时如果该程序启动了子进程就可能有多次。<br>EXIT_THREAD_DEBUG_EVENT:一个线程运行中止<br>EXIT_PROCESS_DEBUG_EVENT:一个进程中止。注:在DEBUG_ONLY_THIS_PROCESS时只有一次,<br>在DEBUG_PROCESS可能有多次。<br>LOAD_DLL_DEBUG_EVENT:一个DLL模块被载入。<br>UNLOAD_DLL_DEBUG_EVENT:一个DLL模块被卸载。</ccid_code>
|
在得到目标程序的调试消息后,调试程序根据这些消息代码进行不同的处理,最后通知被调试程序继续运行。
<ccid_nobr></ccid_nobr>
<ccid_code>BOOL ContinueDebugEvent(<br> DWORD dwProcessId, // process to continue<br> DWORD dwThreadId, // thread to continue<br> DWORD dwContinueStatus // continuation status<br>);</ccid_code>
|
该函数通知被调试程序继续运行。
使用例:
<ccid_nobr></ccid_nobr>
<ccid_code>DEBUG_EVENT dbe;<br>BOOL rc;<br>CreateProcess(NULL, pszCmd, NULL, NULL, FALSE, <br>DEBUG_ONLY_THIS_PROCESS,<br>NULL, szPath, &st, &pro));<br>while(WaitForDebugEvent(&dbe, INFINITE))<br>{<br>// 如果是退出消息,调试监视结束<br>if(dbe. dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)<br>break;<br>// 进入调试监视处理<br>rc = OnDebugEvent(&dbe);<br>if(rc)<br>ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId , DBG_CONTINUE );<br>else<br>ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId , <br>DBG_ DBG_EXCEPTION_NOT_HANDLED);<br>}<br>// 调试消息处理程序<br>BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)<br>{<br>// 我们还没有对目标进程进行操作,所以,先返回TRUE。<br>return TRUE;<br>}</ccid_code>
|
上面这些程序就是一个最简单的调试程序了。不过,它基本上没有什么用途。你还没有在目标进程中设置断点,你就不能完成对API函数监视的任务。
对目标进程设置断点:
我们的目标是监视API函数的输入输出,那么,首先应该知道DLL模块中提供了哪些API函以及这些API的入口地址。在前面将过,广义的API还包括未导出的内部函数。如果你有DLL模块的调试版本和调试连接文件(pdb文件),也可以根据调试信息得到内部函数的信息。
· 得到函数名及函数入口地址
通过程序得到函数的入口地址有很多种方法。对于用VC编译出来的DLL,如果是Debug版本,可以通过ImageHlp库函数得到调试信息,分析出函数的入口地址。如果没有Debug版本,也可以通过分析导出函数表得到函数的入口地址。
1.用Imagehlp库函数得到Debug版本的函数名和函数入口地址。
可以利用Imagehlp库函数分析Debug信息,关联的函数为 SymInitialize、SymEnumerateSymbols和UnDecorateSymbolName。详细可以参考MSDN中关于这些函数 的说明和用法。不过,用Imagehlp只能分析出用VC编译的程序,对C++Builder编译的程序不能用这种方法分析。
2.DLL的导出表得到函数导出函数名和函数的入口地址。
在大多数情况下,我们还是希望监视的是Release版本的输入输出参数,毕竟Debug版本不是我们最终提供给用户的产品。Debug和Release的编译条件不同导致产生的结果不同,在很多BBS中都讨论过。所以,我认为跟踪监视Release版本更加有实用价值。
通过分析DLL导出表得到导出函数名在MSDN上就有源代码。关于导出表的说明大家可以参考关于PE结构的文章。
3.通过OLE函数取得COM接口
你也可以通过OLE函数分析DLL提供的接口函数。接口函数不是通过DLL导出表导 出的。你可以通过LoadTypeLib函数来分析COM接口,得到COM记录接口的入口地址,这样,你就可以监视COM接口的调用了。这是API HOOK没法实现的。在这里我不打算分析分析COM接口的方式了。在MSDN上通过搜索LoadTypeLib sample关键词你就可以找到相关的源代码进行修改实现你的目标。
这里是通过计算机自动分析目标模块得到DLL导出函数的方案,作为我们监视的目的 而言,这些工作只是为了得到一系列的函数名和函数地址而已。函数名只是一个让我们容易识别函数的名称而已,该函数入口地址才是我们真正关心的目标。换句话 说,如果你能够确保某一个地址一定是一个函数(包括内部函数)的入口地址,你就完全可以给这个函数定义自己的名称,将它加入你的函数管理表中,同样可以实 现监视该函数的输入输出参数的功能。这也是实现Exe内部函数的监视功能的原因。如果你有Exe编译时生成的Map文件(你可以在编译时选择生成Map文 件),你就可以通过分析Map文件,得到内部函数的入口地址,将内部函数加入到你的函数管理表中。(一个函数的名称对于监视功能来讲究竟是FunA还是 FunB并没有什么意义,但名称是FunA还是FunB的名称对于监视者分析监视结果是有意义的,你完全可以将MessageBox的函数在输出监视结果 是以FunA的名称输出,所以在监视一些内部无名称的函数时,你完全可以定义你自己的名字)。
· 在函数入口地址处设置断点
设置断点非常简单,只要将0xCC(int 3)写入指定的地址就可以了。这样程序运行到指定地址时,将产生调试中断信息通知调试程序。修改指定进程的内存数据可以通过 WriteProcessMemory函数来完成。由于一般情况下作为程序代码段都被保护起来了,所以还有一个函数也会用到。 VirtualProtectEx。在实际情况下,当调试断点发生时,调试程序还应该将原来的代码写回被调试程序。
<ccid_nobr></ccid_nobr>
<ccid_code>unsigned char SetBreakPoint(DWORD pAdd, unsigned char code)<br>{<br> unsigned char b;<br> BOOL rc;<br> DWORD dwRead, dwOldFlg;<br>// 0x80000000以上的地址为系统共有区域,不可以修改<br>if( pAdd >= 0x80000000 || pAdd == 0)<br> return code;<br>// 取得原来的代码<br>rc = ReadProcessMemory(_ghDebug, pAdd, &b, sizeof(BYTE), &dwRead);<br>// 原来的代码和准备修改的代码相同,没有必要再修改<br>if(rc == 0 || b == code)<br> return code;<br>// 修改页码保护属性<br>VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), PAGE_READWRITE, <br>&dwOldFlg);<br>// 修改目标代码<br>WriteProcessMemory(_ghDebug, pAdd, &code, sizeof(unsigned char), &dwRead);<br>// 恢复页码保护属性<br>VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), dwOldFlg, &dwOldFlg);<br> return b;<br>}</ccid_code>
|
在设置断点时你必须将原来的代码保存起来,这样在恢复断点时就可以将代码还原了。一 般用法为:设置断点m_code = SetBreakPoint( pFunAdd, 0xCC); 恢复断点:SetBreakPoint( pFunAdd, m_code); 记住,每个函数入口地址的代码都可能不同,你应该为每个断点地址保存一个原来的代码,在恢复时就不会发生错误了。
好了,现在目标程序中已经设置好了断点,当目标程序调用设置了断点的函数时,将产生一个调试中断信息通知调试程序。我们就要在调试程序中编写我们的调试中断程序了。
编写调试中断处理程序
被调试程序产生中断时,将产生一个EXCEPTION_DEBUG_EVENT信息通知调试程序进行处理。同时将填充EXCEPTION_DEBUG_INFO结构。
<ccid_nobr></ccid_nobr>
<ccid_code>typedef struct _EXCEPTION_DEBUG_INFO { <br> EXCEPTION_RECORD ExceptionRecord; <br> DWORD dwFirstChance; <br>} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;<br>typedef struct _EXCEPTION_RECORD { <br> DWORD ExceptionCode; <br> DWORD ExceptionFlags; <br> struct _EXCEPTION_RECORD *ExceptionRecord; <br> PVOID ExceptionAddress; <br> DWORD NumberParameters; <br> ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];<br>} EXCEPTION_RECORD, *PEXCEPTION_RECORD;</ccid_code>
|
在该结构中,我们比较感兴趣的是产生中断的地址ExceptionAddress和产生中断的信息代码ExceptionCode。在信息代码中与我们任务相关的信息代码为:
<ccid_nobr></ccid_nobr>
<ccid_code>EXCEPTION_BREAKPOINT:断点中断信息代码<br>EXCEPTION_SINGLE_STEP:单步中断信息代码</ccid_code>
|
断点中断是由于我们在前面设置断点0xCC代码运行时产生的。由于产生中断后,我们 必须将原来的代码写回被调试程序中继续运行。但是,代码一旦被写回目标程序,这样,当目标程序再次调用该函数时将不会产生中断,我们就只能实现一次监视 了。所以,我们必须在将原代码写回被调试程序后,应该让被调试程序已单步的方式运行,再次产生一个单步中断的调试信息。在单步中断处理中,我们再次将 0xCC代码写入函数的入口地址,这样就可以保证再次调用时产生中断。
首先,在进行中断处理前我们必须作些准备工作,管理起线程ID和线程句柄。为了管理单步中断处理,我们还必须维护一个基于线程的单步地址的管理,这样就可以允许被调试程序拥有多线程的功能。--我们不能保证单步运行时不被该进程的其他线程所打断。
<ccid_nobr></ccid_nobr>
<ccid_code>// 我们利用一个map进行管理线程ID和线程句柄之间的关系<br>// 同时也用一个map管理函数地址和断点的关系<br>typedef map<DWORD, HANDLE, less<DWORD> > THREAD_MAP;<br>typedef map<DWORD, void*, less<DWORD> > THREAD_SINGLESTEP_MAP;<br>THREAD_MAP _gthreads;<br>FUN_BREAK_MAP _gFunBreaks;<br>// 并且假设设置断点时采用了如下方案进行原来代码的管理<br>BYTE code = SetBreakPoint(pFunAdd, 0xCC);<br>if(code != 0xCC)<br>_gFunBreaks[pFunAdd] = code;<br>…<br>// 调试处理程序<br>BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)<br>{<br>BOOL rc = TRUE;<br>switch(pEvent->dwDebugEventCode)<br>{<br>case CREATE_PROCESS_DEBUG_EVENT:<br>// 记录线程ID和线程句柄的关系<br>_gthreads[pEvent->dwThreadId] = pEvent->u.CreateProcessInfo.hThread;<br>…<br>break;<br>case CREATE_THREAD_DEBUG_EVENT:<br>// 记录线程ID和线程句柄的关系<br>_gthreads [pEvent->dwThreadId] = pEvent->u.CreateThread.hThread;<br>…<br>break;<br>case EXIT_THREAD_DEBUG_EVENT:<br>// 线程退出时清除线程ID<br>_gthreads.erase (pEvent->dwThreadId);<br>…<br>break;<br>case EXCEPTION_DEBUG_EVENT:<br>// 中断处理程序<br>rc = OnDebugException(pEvent);<br>break;<br>…<br>}<br>return rc;<br>}</ccid_code>
|
下面进行中断处理程序。同样,我们只考虑我们关心的中断信息代码。在发生中断时,我 们通过GetThreadContext(&context)得到中断线程的上下文信息。此时,context.esp就是函数的返回地址, context.esp+4位置的值就是函数的第一个参数,context.esp+8就是第二个参数,依次类推可以得到你想要的任何参数。需要注意的是 因为参数是在被调试进程中的内容,所以你必须通过ReadProcessMemory函数才能得到:
<ccid_nobr></ccid_nobr>
<ccid_code>DWORD buf[4]; // 取4个参数<br>ReadProcessMemory(_ghDebug, (void*)(context.esp + 4), &buf, sizeof(buf), <br>&dwRead);</ccid_code>
|
那么buf[0]就是第一个参数,buf[1]就是第二个参数。。。注意,在 FunA(int a, char* p, OPENFILENAME* pof)函数调用时,buf[0] = a, buf[1] = p这里buf[1]是p的指针而不是p的内容,如果你希望访问p的内容,必须同样通过ReadProcessMemory函数再次取得p的内容。对于结构 体指针也必须如此:
<ccid_nobr></ccid_nobr>
<ccid_code>// 取得p的内容:<br>char pBuf[256];<br>ReadProcessMemory(_ghDebug, (void*)(buf[1]), &pBuf, sizeof(pBuf), &dwRead);<br>//取得pof的内容:<br>OPENFILENAME of<br>ReadProcessMemory(_ghDebug, (void*)(buf[2]), &of, sizeof(of), &dwRead);</ccid_code>
|
如果结构体中还有指针,要取得该指针的内容,也必须和取得p的内容一样的方式读取被调试程序的内存。总的来说,你必须意识到监视目标程序的所有内容都是对目标进程的内存读取操作,这些指针都是目标进程的内存地址,而不是调试进程的地址。
很明显,当被调试进程在函数入口产生中断调试信息时,调试程序只能得到函数的输入参数,而不 能得到我们希望的输出参数及返回值!为了实现我们的目标,我们必须在函数调用结束时,再次产生中断,取得函数的输出参数和返回值。在处理函数入口中断时, 就必须设置好函数的返回地址的断点。这样,在函数返回时,就可以得到函数的输出参数和返回值了。关于这里的实现说明请参考附录的源代码。
你完全可以参照附录的源代码写出你自己的简单的调试监视程序。当然,有几个问题因 为比较复杂,我没有在这里进行说明。一个就是函数返回断点的处理,比如TRY、CATCH的处理,就必须重新设计好RETURN_FUN_STACK的结 构,考虑一些除错处理还是可以解决这个问题的。另外一个问题就是函数的入口断点和返回断点没有任何关系。这个问题更好解决,只需重新设计 RETURN_FUN,FUN_BREAK_MAP等结构体就可以将它们关联起来。由于我在这里只要是分析如何实现中断调试处理的过程,这些完善程序的工 作就由读者自行跟踪改造了。
关于Win9X系统
细心的读者在上面可以发现一个问题,那就是在SetBreakPoint函数中有一个限制, 就是函数的入口地址不能大于0x80000000。确实如此,我们知道0x80000000以上的空间是系统共有的空间,我们一般不能修改这些空间的程 序,否则将影响系统的工作。在NT环境下,所有的DLL都被加载在0x80000000下,修改0x80000000以下空间的代码不会对其它进程产生影 响。所以在NT下可以用上面的方案监视所有的DLL函数。然而,在Win9X下,kernel32.dll,user32.dll,gdi32.dll等 系统DLL都被加载到0x80000000以上的空间,修改这些空间的代码将破坏系统工作。那么,在9X下就不能监视这些DLL模块的函数吗?
的确,在Win9X平台下不能利用在函数入口处设置断点的方法实现监视。我们必须 采用另外的方法实现该功能。在前面讨论中知道,通过API HOOK修改模块导入表的方法可以实现将API的入口修改为自己监视程序的入口,也可以实现监视功能。如果采用API HOOK的方法有限制,即必须知道函数原型,对每一个函数都必须编写相应的监视代码,灵活性受到限制。而我们的目标是不管有多少个DLL,不管DLL有多 少个导出函数,在不修改我们的程序前提下都可以实现我们的监视功能。所以,API HOOK是不可以完成我们的目标,但我们可以利用修改导入表的方案实现目标。首先,修改导入表,将函数的调用地址指向我们的监视代码,在监视代码中,我们 无需对函数编程,只是简单调用jmp XXXX就可以了。然后,设置断点时,不是设置在函数的入口点,而是设置在我们的监视代码上。这样,当我们的模块调用系统API函数时,就可以实现监视功 能了。修改原理如图:
如图所示,假设我们的监视代码在目标进程的的0x20000000空间,我们在分析DLL导 出表的同时,将导出表函数的地址经过计算,在监视代码中设置为jmp xxxx的代码。这样我们在修改EXE模块的导入表时写入的地址为监视代码的地址。当目标程序调用MessageBox函数是,程序将首先跳转到监视代码 中执行jmp指令到user32.dll的MessageBox入口地址中。经过这样处理后,我们希望监视MessageBox函数的调用时,只需在监视 代码的0x20000000处设置断点,就达到了监视的目的。限于篇幅原因,这里不再讨论。
扩展应用
你可以很轻松的在此基础上进行扩展你的监视跟踪功能。只需要修改一下记录输入输出函数结果的程序,就得到一个新的功能:
1.在记录输入输出参数的地方加入取得当前时刻的功能,就实现了监视函数调用性能的功能。(相当于Numega的TrueTime功能)由于采用了Debug技术,得到的时间将包括调试函数导致产生进程的切换时间。等到的时间只是一个参考价值,但对分析性能而言一般足够。
2.在记录输入输出参数的地方加入函数调用的计数器,就实现了Numega的TrueCoverage功能。
3.监视malloc, free, realloc函数的输入输出值,并进行统计,就实现了简单的内存泄漏检查功能。关键的是你可以通过Map文件得到Release版本的malloc等函数的地址,实现对Release版的跟踪。
4.在记录输入参数处理中加入StackWalk函数可以实现call stack功能,分析是由哪个函数调用了自己。在jmp方案中也可以实现这个功能,但是你必须确保StackWalk关联的函数没有调用被你监视的函数。 在Hook API(IAT)的方案中到是不用保证,但得出的调用列表中有可能包含你的监视代码。
有一点需要注意的是,我们的目标是监视程序的运行路径,并不是改变参数和修改结果,所以,在jmp和Hook Api(IAT)中可以实现的修改参数和运行路径的做法在这里不能实现。
其他:
本文附录的代码TestDebug.zip就是实现了一个简单的调试监视器,自动输出监视函数的4个输入参数的地址内容和函数调用返回值。该代码只是表明通过监视函数可以实现对API的跟踪,所以没有实现9X下对系统DLL的监视。
DebugApi.zip是一个利用这个方案编写的应用程序DebugApiSpy.exe,它实现了这个方案中的最基本的跟踪监视函数的输入输出参数功能,也实现了9X下对系统DLL的监视支持。该程序支持Win9X/NT/W2K/XP上的运用。
源代码下载:TestDebug.zip,DebugApi.zip
参考资料:
1.《Windows核心编程》, Jeffrey Richter,机械工业出版社
2.微软的MSDN
3.detours 可以在http://research.microsoft.com/sn/detours/ 上得到源代码。detours功能在WinNT和W2K下有效,对9X不支持。
分享到:
相关推荐
### 使用Debug函数实现API函数跟踪 #### 概述 在软件开发过程中,有时我们需要对特定的应用程序接口(API)函数进行跟踪,以便更好地理解和调试程序的行为。本文将介绍一种利用Debug函数来实现API函数跟踪的方法。...
Debug API主要包括DebugActiveProcess、DebugBreak、ContinueDebugEvent、WaitForDebugEvent等函数,它们使得开发者可以在目标进程中设置断点、检查内存、跟踪执行流程等。在进行网络截取时,我们需要对与网络通信...
《精通Windows API函数接口编程实例源代码》是一个深入学习Windows API编程的重要资源,它涵盖了大量实践案例,旨在帮助开发者熟练掌握API接口的使用技巧。Windows API是微软为开发者提供的一个平台,通过它,程序员...
使用 API 函数 `SetWindowPos` 可以实现让窗体总是在最前面的效果。首先,我们需要在模块中声明 `SetWindowPos` 函数: `Declare Function SetWindowPos Lib "user32" (ByVal hwnd As Long, ByVal hwndInsertAfter ...
《Windows API函数大全》中文版是一本针对Windows操作系统编程的核心参考资源,对于深入学习和理解高级编程至关重要。Windows API(应用程序接口)是微软提供的一系列函数、常量、数据类型和结构,允许开发者直接与...
API函数是软件开发中的一个重要概念,它是一组预先定义好的函数,允许开发者调用这些函数来实现特定功能,而无需理解底层实现细节。在汇川机器人的上下文中,API函数使得程序员能够更方便地编写控制逻辑,控制...
- `debugfs_create_x8`、`debugfs_create_x16`、`debugfs_create_x32`、`debugfs_create_x64`:与上述函数类似,但使用`x`前缀表示数据以十六进制格式显示。 - `debugfs_create_size_t`:用于创建表示`size_t`类型...
在"基于STM32_C8T6的Debug函数项目"中,我们关注的是如何在STM32开发过程中实现有效的调试功能。Debug功能对于任何软件开发都至关重要,它帮助开发者追踪代码执行、定位错误、理解程序运行状态,从而优化代码性能。...
同时,还需要熟悉C或C++语言,因为大多数API函数都是用这两种语言的语法进行调用的。对于初学者来说,理解参数的意义、函数的返回值以及错误处理机制是关键。 在实践中,开发者往往需要借助诸如调试器(如DebugView...
在嵌入式系统开发中,uC/...总之,理解和熟练掌握uCOS的API函数以及LWIP网络库的使用,是开发高效、可靠的嵌入式系统的关键。通过深入学习和实践,开发者可以充分利用这些工具实现复杂的功能,满足各种应用场景的需求。
这个单元可能包含了`DebugPrint`函数的声明和实现,以及如何在项目中使用它的示例。通过查看此文件,我们可以学习如何自定义调试工具,以提高我们的开发效率和调试体验。 总的来说,`DebugPrint`是为了解决`...
在分析VistaLKD的驱动程序时,我们发现了一个名为GetProcessNameOffset()的函数,它通过IoGetCurrentProcess和_strnicmp等API来识别当前进程,这在内核编程中常见于识别关键系统进程的场景。GetProcessNameOffset()...
除了使用`EnableDebugPrivilege` API外,还可以借助其他Windows API函数来尝试结束进程,例如通过`CreateToolhelp32Snapshot`和`Process32First/Next`遍历进程,然后使用`OpenProcess`和`TerminateProcess`。...
### 在VB中使用API函数(之四):深入探讨回调函数与窗口过程 #### 标题解析 本篇文章主要讨论了如何在Visual Basic (VB) 编程环境中使用API函数,特别是聚焦于第四部分的内容——回调函数的应用以及窗口过程的...
WebGL是一种基于OpenGL标准的JavaScript API,用于在任何兼容的Web浏览器中实现2D和3D图形渲染。在WebGL编程中,为了提高代码的可重用性和简化复杂任务,通常会使用各种公用函数库。这里提到的"cuon-matrix.js"、...
本文档提供了关于Oracle数据库API接口函数的详细设计说明,包括如何设置项目环境以及如何使用提供的接口函数进行数据库操作。通过学习本文档,开发者能够有效地利用这些API接口函数完成与Oracle数据库的交互任务。...
WebGL是一种基于OpenGL标准的JavaScript API,用于在任何兼容的Web浏览器中实现硬件加速的2D和3D图形渲染。这个“WebGL编程指南函数库”包含了一系列辅助脚本,帮助开发者更方便地进行WebGL编程。以下是这些脚本的...
使用钩子函数时,开发者需要通过`SetWindowsHookEx`函数注册钩子,并指定钩子类型、处理函数、所属线程以及模块句柄。当钩子被触发时,系统会调用提供的处理函数。需要注意的是,为了确保钩子能够正确工作,必须确保...
Debugviewer是一款专为C语言API开发设计的DLL(动态链接库)查看工具,它提供了对公有函数的详细查看功能,对于深入理解底层开发和Windows API编程的开发者来说,是不可或缺的辅助工具。通过Debugviewer,程序员能够...