标 题: 调试器设计3
作 者: Tweek
时 间: 2010-3-1


最后一篇知识性介绍是关于调试API的内部实现的简单分析。

这样可以帮助我们进一步深入认识被调试程序。

调试API的内部实现原理分析:

我们先看看DebugActiveProcess内部结构
BOOL 
WINAPI 
DebugActiveProcess(IN DWORD dwProcessId) 

NTSTATUS Status; 
HANDLE Handle; 
/* Connect to the debugger */ 
Status = DbgUiConnectToDbg(); 
if (!NT_SUCCESS(Status)) 

SetLastErrorByStatus(Status); 
return FALSE; 

/* Get the process handle */ 
Handle = ProcessIdToHandle(dwProcessId); 
if (!Handle) return FALSE; 
/* Now debug the process */ 
Status = DbgUiDebugActiveProcess(Handle); 
NtClose(Handle); 
/* Check if debugging worked */ 
if (!NT_SUCCESS(Status)) 

/* Fail */ 
SetLastErrorByStatus(Status); 
return FALSE; 

/* Success */ 
return TRUE; 
}

简单的初始化,然后调用DbgUiDebugActiveProcess。

关于DbgUiDebugActiveProcess的功能嘛。内核需要和进程之间交流调试相关的信息。Xp后,调试部不再利用LPC,而是直接为调试建立Debug object。所以它至少建立了一个debug object。我们可以用NtQueryInformationXXX获取debug object的信息。

有了一些初步的认识,让我们再看看WaitForDebugEvent。

BOOL 
WINAPI 
WaitForDebugEvent(IN LPDEBUG_EVENT lpDebugEvent, 
IN DWORD dwMilliseconds) 

LARGE_INTEGER WaitTime; 
PLARGE_INTEGER Timeout; 
DBGUI_WAIT_STATE_CHANGE WaitStateChange; 
NTSTATUS Status; 
/* Check if this is an infinite wait */ 
if (dwMilliseconds == INFINITE) 

/* Under NT, this means no timer argument */ 
Timeout = NULL; 

else 

/* Otherwise, convert the time to NT Format */ 
WaitTime.QuadPart = UInt32x32To64(-10000, dwMilliseconds); 
Timeout = &WaitTime; 

/* Loop while we keep getting interrupted */ 
do 

/* Call the native API */ 
Status = DbgUiWaitStateChange(&WaitStateChange, Timeout); 
} while ((Status == STATUS_ALERTED) || (Status == STATUS_USER_APC)); 
/* Check if the wait failed */ 
if (!(NT_SUCCESS(Status)) || (Status != DBG_UNABLE_TO_PROVIDE_HANDLE)) 

/* Set the error code and quit */ 
SetLastErrorByStatus(Status); 
return FALSE; 

/* Check if we timed out */ 
if (Status == STATUS_TIMEOUT) 

/* Fail with a timeout error */ 
SetLastError(ERROR_SEM_TIMEOUT); 
return FALSE; 

/* Convert the structure */ 
Status = DbgUiConvertStateChangeStructure(&WaitStateChange, lpDebugEvent); 
if (!NT_SUCCESS(Status)) 

/* Set the error code and quit */ 
SetLastErrorByStatus(Status); 
return FALSE; 

/* Check what kind of event this was */ 
switch (lpDebugEvent->dwDebugEventCode) 

/* New thread was created */ 
case CREATE_THREAD_DEBUG_EVENT: 
/* Setup the thread data */ 
SaveThreadHandle(lpDebugEvent->dwProcessId, 
lpDebugEvent->dwThreadId, 
lpDebugEvent->u.CreateThread.hThread); 
break; 
/* New process was created */ 
case CREATE_PROCESS_DEBUG_EVENT: 
/* Setup the process data */ 
SaveProcessHandle(lpDebugEvent->dwProcessId, 
lpDebugEvent->u.CreateProcessInfo.hProcess); 
/* Setup the thread data */ 
SaveThreadHandle(lpDebugEvent->dwProcessId, 
lpDebugEvent->dwThreadId, 
lpDebugEvent->u.CreateThread.hThread); 
break; 
/* Process was exited */ 
case EXIT_PROCESS_DEBUG_EVENT: 
/* Mark the thread data as such */ 
MarkProcessHandle(lpDebugEvent->dwProcessId); 
break; 
/* Thread was exited */ 
case EXIT_THREAD_DEBUG_EVENT: 
/* Mark the thread data */ 
MarkThreadHandle(lpDebugEvent->dwThreadId); 
break; 
/* Nothing to do for anything else */ 
default: 
break; 

/* Return success */ 
return TRUE; 
}


DbgUiWaitStateChange 检测debug object,看看是否有调试事件返回。

DbgUiConvertStateChange作用是为了版本的兼容,将以前系统的结构转换成现在可以识别的结构。

到了下面的switch了,里面的东西我们似乎可以通过名字看懂。但是,我们看看它的细节不是更好

VOID 
WINAPI 
SaveProcessHandle(IN DWORD dwProcessId, 
IN HANDLE hProcess) 

PDBGSS_THREAD_DATA ThreadData; 
/* Allocate a thread structure */ 
ThreadData = RtlAllocateHeap(RtlGetProcessHeap(), 
0, 
sizeof(DBGSS_THREAD_DATA)); 
if (!ThreadData) return; 
/* Fill it out */ 
ThreadData->ProcessHandle = hProcess; 
ThreadData->ProcessId = dwProcessId; 
ThreadData->ThreadId = 0; 
ThreadData->ThreadHandle = NULL; 
ThreadData->HandleMarked = FALSE; 
/* Link it */ 
ThreadData->Next = DbgSsGetThreadData(); 
DbgSsSetThreadData(ThreadData); 
}

出现了DBGSS_THREAD_DATA这个结构体。将当前的DBGSS_THREAD_DATA添加到原来的链表(暂且让我说他是链表)里面。然后将新的设置成链表表头。

再看看DBGSS_THREAD_DATA的定义

typedef struct _DBGSS_THREAD_DATA 

struct _DBGSS_THREAD_DATA *Next; 
HANDLE ThreadHandle; 
HANDLE ProcessHandle; 
DWORD ProcessId; 
DWORD ThreadId; 
BOOLEAN HandleMarked; 
} DBGSS_THREAD_DATA, *PDBGSS_THREAD_DATA;

嗯,里面有进程和线程的句柄和ID。还有一个句柄的标记。应该是MarkProcess/ThreadHandle返回的。判断是否已经退出。

saveProcessHandle里面还有个这个
DbgSsSet/GetThreadData

让我们来看看他的实现

#define DbgSsSetThreadData(d) \ 
NtCurrentTeb()->DbgSsReserved[0] = d 
#define DbgSsGetThreadData() \ 
((PDBGSS_THREAD_DATA)NtCurrentTeb()->DbgSsReserved[0])

原来是将TEB中的DbgSsReserved的第一个元素被赋予了ThreadData.

关于SaveThreadHandle的实现:

VOID 
WINAPI 
SaveThreadHandle(IN DWORD dwProcessId, 
IN DWORD dwThreadId, 
IN HANDLE hThread) 

PDBGSS_THREAD_DATA ThreadData; 
/* Allocate a thread structure */ 
ThreadData = RtlAllocateHeap(RtlGetProcessHeap(), 
0, 
sizeof(DBGSS_THREAD_DATA)); 
if (!ThreadData) return; 
/* Fill it out */ 
ThreadData->ThreadHandle = hThread; 
ThreadData->ProcessId = dwProcessId; 
ThreadData->ThreadId = dwThreadId; 
ThreadData->ProcessHandle = NULL; 
ThreadData->HandleMarked = FALSE; 
/* Link it */ 
ThreadData->Next = DbgSsGetThreadData(); 
DbgSsSetThreadData(ThreadData); 
}

正如你所想。没有什么大的变化。

最后我们看看ContinueDebugEvent

BOOL 
WINAPI 
ContinueDebugEvent(IN DWORD dwProcessId, 
IN DWORD dwThreadId, 
IN DWORD dwContinueStatus) 

CLIENT_ID ClientId; 
NTSTATUS Status; 
/* Set the Client ID */ 
ClientId.UniqueProcess = (HANDLE)dwProcessId; 
ClientId.UniqueThread = (HANDLE)dwThreadId; 
/* Continue debugging */ 
Status = DbgUiContinue(&ClientId, dwContinueStatus); 
if (!NT_SUCCESS(Status)) 

/* Fail */ 
SetLastErrorByStatus(Status); 
return FALSE; 

/* Remove the process/thread handles */ 
RemoveHandles(dwProcessId, dwThreadId); 
/* Success */ 
return TRUE; 
}

噢,DbgUiContinue已经做了所有的事了。


最后,我们还是看看MS都为我们干了些什么:

首先,调试器访问在ntdll里面的接口DbgUi,然后,DbgUi通过dbgk建立了一个debug object(当然我们可以用NtQueryInformationXXX或者NtSetInformationXXX查看和修改它,修改只能针对部分位置。)然后,dbgk通过DBGSS_THREAD_DATA 将调试事件依次保存在一个”链表”里面。然后,teb的DbgSsReserved的第一个元素被赋予了那个链表。而DbgSsReserved的第二个元素则是指向调试事件。所以,我们可以IsDebugergPresent通过检测TEB,判断是否被调试,同时,我们解释了第二篇的那个例子程序里面为什么会在 不断的弹出大量框。而利用NtQueryInformationXXX和DebugPort则必须利用驱动程序内核完成。因为他们是查询的内核的object。

大家一定意识到debug object在调试中处于一个多么重要的地位。

那么,我们就来看看他到底是什么?

 typedef struct _DEBUG_OBJECT 

KEVENT EventsPresent; 
FAST_MUTEX Mutex; 
LIST_ENTRY EventList; 
union 

ULONG Flags; 
struct 

UCHAR DebuggerInactive:1; 
UCHAR KillProcessOnExit:1; 
}; 
}; 
} DEBUG_OBJECT, *PDEBUG_OBJECT;
没有想象的那么复杂吧。
LIST_ENTRY一个调试事件的列表。
Flag和调试会话相联系
其他的看名字就知道什么意思了吧!


到此,我们设计调试器需要的知识储备已经差不多了。一般性的问题,我们都可以给出解释了。
对调试API也有了比较深入的认识。

上面三篇介绍了一些关于调试器紧密相关的信息,都是介绍比较基本的。
如果希望更深入的了解相关信息。
可以:……
进程更加深入的信息 请参考windows核心编程
调试API更深入的细节。大家可以参考dbgk的介绍。

同时,xp 增加了一些调试API,虽然他们内部实现没有本质变化。
但是,却给调试器的设计给与了更多选择:
 DebugActiveProcessStop,
 DebugSetProcessKillOnExit,
 DebugBreakProcess,
 CheckRemoteDebuggerPresent.

具体使用方法请参考msdn

待续……