写个自己的调试器
对于写了一段时间的程序员来说,了解一些debugger的实质无疑对于技术的提高是有很大帮助的。而debugging自身也是一门非常细节化,比较复杂的技术。好的Debug工具如SoftICE,也是技术稍深一些的程序员必备的技术之一。这篇随笔并不会去讨论Debug技术的实质,而只是利用Platform SDK和最新的DbgHelp.dll提供的API作为引擎写一个自己的debugger,也即是写一个实用的debugger的Host端。呵呵,本人不擅于写文章,所以下面就指一个例子来说,即如何给一个进程设下断点并捕获。其实看过IA32的人都知道,CPU为Debug提供了强力的支持,比如设单步跟踪的标志;再比如BIOS代码的调试,对于ROM的BIOS代码是无法进行插CC(int 3)指令进行断点设置的,这时CPU提供了地址中断,执行至指定地址时,CPU自行中断。在这里并不会提及这些内容,当然也许有时间,或大家感兴趣,我可以写点或讨论一二。下面单就如何给进行设断点来说说,即然是随笔,所以可能会有错别字,欢迎大家来找别字 ^_^,
调试目标程序时,你得有相应的权限。用下面这个函数得到它:
//--------------------------------------------------------------------------
// EnableDebugAccessCtl
// Description: Enable or disable debug access control
// Return value: 0 means succeed
DWORD EnableDebugAccessCtl(BOOL bEnable)
{
HANDLE hTokenHandle = NULL;
if( !OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES,
&hTokenHandle ) )
{
return GetLastError();
}
// Lookup the privilege value
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
if( !LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid ) )
{
CloseHandle(hTokenHandle);
return -1;
}
// Enable/disable the privilege
tp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
if( !AdjustTokenPrivileges( hTokenHandle, FALSE, &tp, sizeof(tp), NULL, NULL ) )
{
CloseHandle(hTokenHandle);
return -1;
}
CloseHandle(hTokenHandle);
return 0;
}
我们可以attach一个正在运行的进行或重新启动一个进程进行调试。DebugActiveProcess可以实现第一种情况;第二种情况可以用CreateProcess,下面是代码,注意创建标志。
// Create Process
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
if( !CreateProcess(NULL, csCmdParms.GetBuffer(),
NULL, NULL, FALSE,
CREATE_NEW_CONSOLE | DEBUG_ONLY_THIS_PROCESS,
NULL, NULL,
&si, &pi ) )
{
return GetLastError();
}
这下初调试的程序就跑起来了,接下来一般来用一个循环来获取目标进行的Debug事件。主循环看起来应该是这样的,嘿,MSDN给了你一个模板,那就用吧:
DEBUG_EVENT DebugEv; // debugging event information
DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation
for(;;)
{
// Wait for a debugging event to occur. The second parameter indicates
// that the function does not return until a debugging event occurs.
WaitForDebugEvent(&DebugEv, INFINITE);
// Process the debugging event code.
switch (DebugEv.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
// Process the exception code. When handling
// exceptions, remember to set the continuation
// status parameter (dwContinueStatus). This value
// is used by the ContinueDebugEvent function.
switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION:
// First chance: Pass this on to the system.
// Last chance: Display an appropriate error.
break;
case EXCEPTION_BREAKPOINT:
// First chance: Display the current
// instruction and register values.
break;
case EXCEPTION_DATATYPE_MISALIGNMENT:
// First chance: Pass this on to the system.
// Last chance: Display an appropriate error.
break;
case EXCEPTION_SINGLE_STEP:
// First chance: Update the display of the
// current instruction and register values.
break;
case DBG_CONTROL_C:
// First chance: Pass this on to the system.
// Last chance: Display an appropriate error.
break;
default;
// Handle other exceptions.
break;
}
case CREATE_THREAD_DEBUG_EVENT:
// As needed, examine or change the thread's registers
// with the GetThreadContext and SetThreadContext functions;
// and suspend and resume thread execution with the
// SuspendThread and ResumeThread functions.
break;
case CREATE_PROCESS_DEBUG_EVENT:
// As needed, examine or change the registers of the
// process's initial thread with the GetThreadContext and
// SetThreadContext functions; read from and write to the
// process's virtual memory with the ReadProcessMemory and
// WriteProcessMemory functions; and suspend and resume
// thread execution with the SuspendThread and ResumeThread
// functions. Be sure to close the handle to the process image
// file with CloseHandle.
break;
case EXIT_THREAD_DEBUG_EVENT:
// Display the thread's exit code.
break;
case EXIT_PROCESS_DEBUG_EVENT:
// Display the process's exit code.
break;
case LOAD_DLL_DEBUG_EVENT:
// Read the debugging information included in the newly
// loaded DLL. Be sure to close the handle to the loaded DLL
// with CloseHandle.
break;
case UNLOAD_DLL_DEBUG_EVENT:
// Display a message that the DLL has been unloaded.
break;
case OUTPUT_DEBUG_STRING_EVENT:
// Display the output debugging string.
break;
}
// Resume executing the thread that reported the debugging event.
ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
}
可以从CASE语句中看到可以获取什么样的事件。注意CREATE_PROCESS_DEBUG_EVENT事件,我们设置断点是在这里,也就是进行被创建的时候。
说到这里就要讲讲断点是怎么设置的了。通常我们设置的断点称为软件断点。通俗点说这个断点其实并不是在原代码中写的,它的基本工作是:调试器跟据用户的设置对代码进行了修改,也即是在指地址处更换一个字节的值为CC,当程序运行到用户指定地址时,CC被处理器装入执行,因为CC对应用int 3指令,这是处理器的一个断点异常,于是处理暂停当前程序,并发出相应异常,通过异常处理程序和debug驱动程序,调试器Host端得到这个事件,此时用户就可以对被debugging的程序进行一些访问了,比如寄存器,局部变量等各种信息,可以对目标程序的指定代码块进行反汇编或进行源码级的查看。
注意:这里指出的是当程序被断点所中断时,调试器获得此断点事件后第一件是要将有被改写为CC的内存单元重新改会为原来程序的代码,以保证程序的完整性。另一点提示,被捕获的第一个断点异常应该被丢掉,呵呵,那不关你的事,它是规定的一个步骤,当然,你是可以用它做点什么事的。
当然继续调试的时候,目标程序会根据EIP来执行下一条指令,而这时EIP实际指向的是CC后的位置,因此必须令EIP指针值减1,这样程序就对用户实现了透明,用户是不知道调试器作了什么手脚的,当然别忘,程序继续跑时,其实的断点还要改成CC的。
写目标进程可以用WriteProcessMemory进行,写完后别忘了用FlushInstructionCache指令flush一下目标进行的指令的cache。如果页为只读时,可以使用VirtualProtectEx改其为读写属性然后再写。
修改目标线程EIP的值可以用SetThreadContext,当然EIP的值请先用GetThreadContext来获得。啊,这样就可以了。好热,写了40多分钟......要吃冰棒去了。