在本系列文章的前两篇文章中,简要地介绍了 Win32 调试接口中用户态调试器结构和调试事件的相关知识
Win32 调试接口设计与实现浅析 [1] 用户态调试器结构初探
Win32 调试接口设计与实现浅析 [2] 调试事件
在这一小节中,将进一步展开分析 NT 系统核心中对上述功能提供支持的调试子系统的创建过程。
从前面两个小节我们可以了解到,Win32 调试接口对用户态调试器来说,实际上绝大多数工作都是通过一个调试界面端口"DbgUiApiPort"完成的。用户态调试器通过此端口完成对调试子系统的挂接,进而接收和处理调试事件。因此,对调试子系统的分析也将从此端口开始。
首先来看看调试子系统以及调试界面和调试服务端口的创建过程。
在《Windows 2000 内部揭密》的第四章中,Solomon详细地介绍了 NT 系统启动的整个过程。其中SMSS (Session Manager) 是启动程序 NTLDR 载入运行的第一个本机(Native)应用程序(不使用 Win32 子系统的 API),其被作为操作系统一部分受到信任,完成系统初始化工作。Win32 子系统 CSRSS(Client-Server Runtime SubSystem)和系统登陆进程 WinLogon 在 SMSS 初始化工作完成后被载入执行,以实际完成接受用户登陆运行的工作。其中 SMSS 系统初始化的工作就包括对调试子系统的初始化。一个启动后的进程树实例如下:
以下为引用:
System(4)
smss.exe(388)
csrss.exe(436)
winlogon.exe(460)
services.exe(504)
lsass.exe(516)
smss.exe的入口函数(smserversmss.c:28)首先检查从 NTLDR 通过命令行传入的参数中是否有调试参数,如果有则将之分析后放入 SmpDebug 全局变量(smserversmsrvp.h:82)中;然后调用 SmpInit 函数(smserversminit.c:683)初始化Session Manager。
SmpInit 函数在完成初始化工作、构造 SMSS 服务端口 "SmApiPort" 和两个用于处理向 SMSS 发送服务请求的线程后,会调用 SmpLoadDataFromRegistry 函数(smserversminit.c:934)从注册表 HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession Manager 键中载入 Session Manager 的相关参数。
Session Manager配置注册表键下SubSystems子键的Required、Optional和Kmode三个键,定义了系统支持的子系统类型。通常情况下,Required包括Debug和Windows子系统;Optional包括可选的Posix子系统;Kmode定义核心子系统Windows在核心态的实现win32k.sys。而子系统名字又进一步指向实现子系统的可执行文件映象。一个典型的设置如下:
以下为引用:
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerSubSystemsRequired = "Debug Windows"
Optional = "Posix"Kmode = "%SystemRoot%system32win32k.sys"
Debug = ""
Windows = "%SystemRoot%system32csrss.exe ObjectDirectory=Windows SharedSection=1024,3072,512 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserServerDllInitialization,3 ServerDll=winsrv:ConServerDllInitialization,2 ProfileControl=Off MaxRequestThreads=16"
Posix = "%SystemRoot%system32psxss.exe"
其中Required中包括的子系统,SMSS将自动载入并初始化之。值得注意的是SubSystems子键中的Debug与Windows、Posix等其他子系统不同,并没有指向实际的可执行文件。因为调试子系统是由 SmpLoadDataFromRegistry 函数根据子系统名字是否为 debug,判断在调用执行载入子系统命令的 SmpExecuteCommand 函数(smserversminit.c:3235)时,是否带 SMP_DEBUG_FLAG 标志,表示当前需要载入的是调试子系统。而 SmpExecuteCommand 函数入口处一旦发现标志参数包含 SMP_DEBUG_FLAG 标志,就立刻调用 SmpLoadDbgSs 函数(smserversmdbg.c:108)实际载入调试子系统并直接返回,不再进一步解析和执行命令。
完整的初始化调试子系统函数调用流程如下:
以下为引用:
main (smserversmss.c:28)
SmpInit (smserversminit.c:683)
SmpLoadDataFromRegistry (smserversminit.c:934)
SmpExecuteCommand (smserversminit.c:3235)
SmpLoadDbgSs (smserversmdbg.c:108)
Win2003下处理子系统载入的部分代码被放入一个独立的 SmpLoadSubSystemsForMuSession 函数中,而 debug 子系统则改为在每个Session中载入。也就是说传入 SmpExecuteCommand 函数的 SMP_DEBUG_FLAG 标志会导致此函数直接退出。<TBD>
NT 下 SmpLoadDbgSs 函数中分别对调试子系统中用户态和核心态调试时间响应端口和处理线程做了初始化。伪代码如下:
以下为引用:
NTSTATUS SmpLoadDbgSs(IN PUNICODE_STRING DbgSsName)
{
NTSTATUS st = DbgpInit(); // 初始化用户态调试器环境if(!NT_SUCCESS(st)) return st;
st = DbgSsInitialize(...); // 初始化核心态调试器环境
SmpDbgSsLoaded = TRUE; // 调试子系统已经成功载入
return STATUS_SUCCESS;
}
DbgInit 函数(smserverdbginit.c:26)中首先完成对应用程序线程Hash表的初始化;然后构造一个具有所有访问权限的安全描述符;使用此安全描述符创建两个LPC端口对象"DbgSsApiPort"和"DbgUiApiPort",分别被用户态调试器用于连接调试服务和调试界面;最后创建两个线程分别处理这两个端口上的调试事件,线程由 DbgpSsApiLoop 函数(smserverdbgloop.c:123)和 DbgpUiApiLoop 函数(smserverdbgloop.c:288)完成实际事件处理工作。
DbgSsInitialize 函数(ntosdlldllssstb.c:429)中则先建立与用户态调试器的调试服务端口的链接;然后初始化核心态调试器调试服务的相关全局变量;最后创建用户线程使用DbgSspSrvApiLoop函数(ntosdlldllssstb.c:737)处理核心调试事件。
其中核心态调试器的相关实现本节暂且不涉及,等后面具体讨论核心态调试器的原理时再详细分析。
下一节中将详细分析调试子系统中调试服务端口和调试界面端口的事件处理线程工作流程,以及如何与用户态调试器配合完成调试工作。