DbgPrint 函数流程分析 by 小喂 1 DbgPrint 函数流程分析 前言 Windows 下编写内核驱动时经常用到 DbgPrint 函数输出一些调试信息, 用来辅助调试 当正在用 WinDbg 内核调 试时, 调试信息会输出到 WinDbg 中 或者利用一些辅助工具也能看到输出的调试信息, 比如 Sysinternals 公司的 DebugView 工具 本文分析了 Vista 系统上 DbgPrint 系列函数的执行流程, 并揭示了 DebugView 工具的实现原理 DbgPrint 函数流程 先看一下 WDK 中 DbgPrint 函数的原型 : ULONG DbgPrint ( IN PCHAR Format,... ); 和 printf 的参数一样, 可以格式化字符串.text:0049E123 ; ULONG DbgPrint(PCH Format,...).text:0049E123 public _DbgPrint.text:0049E123 _DbgPrint proc near ; CODE XREF: sub_4046b2+11 p.text:0049e123.text:0049e123 Format = dword ptr 8.text:0049E123 arglist = byte ptr 0Ch.text:0049E123.text:0049E123 mov edi, edi.text:0049e125 push ebp.text:0049e126 mov ebp, esp.text:0049e128 push TRUE.text:0049E12A lea eax, [ebp+arglist].text:0049e12d push eax.text:0049e12e push [ebp+format].text:0049e131 mov ecx, offset??_c@_00cnpnbahc@?$aa@fnodobfm@.text:0049e136 push 3 ; DPFLTR_INFO_LEVEL = 3.text:0049E138 push 65h ; DPFLTR_DEFAULT_ID = 101 = 0x65.text:0049E13A call vdbgprintexwithprefixinternal(x,x,x,x,x,x).text:0049e13f pop ebp.text:0049e140 retn.text:0049e140 _DbgPrint endp 从反汇编代码来看,DbgPrint 函数很简单, 传递参数直接调用 vdbgprintexwithprefixinternal 函数 传递的 ComponentId 为 DPFLTR_DEFAULT_ID,Level 为 DPFLTR_INFO_LEVEL 查看 DbgPrintEx 函数的文档可以知道这两个参数的 意义
DbgPrint 函数流程分析 by 小喂 2.text:0046EBF4 stdcall vdbgprintexwithprefixinternal(x, x, x, x, x, x) proc near.text:0046ebf4 ; CODE XREF: _DbgPrintEx+19 p.text:0046ec11.text:0046ec17 push [ebp+ullevel].text:0046ec1a push [ebp+ulcomponentid].text:0046ec1d call NtQueryDebugFilterState(x,x).text:0046EC22 test eax, eax.text:0046ec24 jnz short loc_46ec2d.text:0046ec26.text:0046ec26 loc_46ec26:.text:0046ec26 xor eax, eax.text:0046ec28 jmp _exit.text:0046eba8 stdcall NtQueryDebugFilterState(x, x) proc near.text:0046eba8.text:0046eba8 ulcomponentid = dword ptr 8.text:0046EBA8 ullevel = dword ptr 0Ch.text:0046EBA8.text:0046EBA8 mov edi, edi.text:0046ebaa push ebp.text:0046ebab mov ebp, esp.text:0046ebc0 mov ecx, [ebp+ullevel].text:0046ebcc xor eax, eax.text:0046ebce inc eax.text:0046ebcf shl eax, cl.text:0046ebd1 test _Kd_WIN2000_Mask, eax.text:0046ebd7 jnz short loc_46ebe8.text:0046ebd9 mov ecx, _KdComponentTable[edx*4].text:0046EBE0 test [ecx], eax.text:0046ebe2 jnz short loc_46ebe8.text:0046ebe4 xor eax, eax.text:0046ebe6 jmp short loc_46ebeb vdbgprintexwithprefixinternal 函数首先调用 NtQueryDebugFilterState 函数检查 ComponentId 和 Level 值判断当前输 出是否需要屏蔽 DbgPrint 传入的值分别是 65h 和 3, 65h 定为的 nt!kd_default_mask 的值和 3 被移位后的 8 比较, 从而确定此次输出是否需要屏蔽 所以 Vista 系统上用 WinDbg 内核调试时缺省看不到 DbgPrint 输出的调试字符串, 可 以用 ed nt!kd_default_mask 0x8 命令或者修改注册表打开 DbgPrint 调试输出
DbgPrint 函数流程分析 by 小喂 3.text:0046EC5D push [ebp+ntstatus2] ; cbdest.text:0046ec63 push [ebp+pszdest] ; pszdest.text:0046ec69 mov ecx, 512.text:0046EC6E sub ecx, esi.text:0046ec70 lea edi, [ebp+esi+szbuffer].text:0046ec77 call RtlStringCbVPrintfA(x,x,x,x).text:0046ECB2 lea ecx, [ebp+szbuffer].text:0046ecb8 mov [ebp+asbuffer.buffer], ecx.text:0046ecbe mov [ebp+asbuffer.length], ax.text:0046ecc5 cmp _KeBugCheckActive, 0.text:0046ECCC jnz short loc_46ed09.text:0046eccc.text:0046ecce mov esi, _RtlpDebugPrintCallback.text:0046ECD4 test esi, esi.text:0046ecd6 jz short loc_46ed09.text:0046ecd6.text:0046ecd8 call ds:kegetcurrentirql().text:0046ecde mov bl, al.text:0046ece0 cmp bl, PROFILE_LEVEL.text:0046ECE3 jnb short loc_46eced.text:0046ece3.text:0046ece5 mov cl, PROFILE_LEVEL.text:0046ECE7 call ds:kfraiseirql(x).text:0046eced.text:0046eced loc_46eced:.text:0046eced push [ebp+ullevel].text:0046ecf0 push [ebp+ulcomponentid].text:0046ecf3 lea eax, [ebp+asbuffer].text:0046ecf9 push eax.text:0046ecfa call esi ; DbgPrintCallback(PANSI_STRING pasbuffer, ULONG ulcomponentid, ULONG ullevel);.text:0046ecfc cmp bl, PROFILE_LEVEL.text:0046ECFF jnb short loc_46ed09.text:0046ecff.text:0046ed01 mov cl, bl.text:0046ed03 call ds:kflowerirql(x) 如果此次输出不需要屏蔽,vDbgPrintExWithPrefixInternal 继续执行 调用 RtlStringCbVPrintfA 函数格式化字符串, 再判断是否正在蓝屏过程中, 然后提高 IRQL 调用输出回调函数 调试输出回调是 Vista 的新增功能,XP 中没有见到 NTSTATUS DbgSetDebugPrintCallback(PDBGPRINT_CALLBACE pdbgcallback, BOOLEAN bset) 函数用来设置和取消回调函数, 只能设置一个函数, 函数地址保存在 RtlpDebugPrintCallback 内部变量中 回调函数返回后降低 IRQL
DbgPrint 函数流程分析 by 小喂 4.text:0046ED09 movzx eax, [ebp+asbuffer.length].text:0046ed10 mov [ebp+pszdest], eax.text:0046ed16 mov eax, [ebp+asbuffer.buffer].text:0046ed1c mov [ebp+var_234], eax.text:0046ed22 push edi.text:0046ed23 push ebx.text:0046ed24 mov eax, 1 ; eax = BREAKPOINT_PRINT.text:0046ED29 mov ecx, [ebp+var_234] ; ecx = pszbuffer.text:0046ed2f mov edx, [ebp+pszdest] ; edx = ulbuflength.text:0046ed35 mov ebx, [ebp+ulcomponentid].text:0046ed38 mov edi, [ebp+ullevel].text:0046ed3b int 2Dh ; Internal routine for MSDOS (IRET).text:0046ED3D int 3 ; Trap to Debugger vdbgprintexwithprefixinternal 函数接着执行, 通过 int2d 调用调试服务把字符串输出到调试器 int2d 的服务例程为 KiDebugService 函数.text:0044737C _KiDebugService: ; DATA XREF: INIT:0070A35C o.text:004473ea inc dword ptr [ebp+68h].text:004473ea inc dword ptr [ebp+68h] ; [_KTRAP_FRAME].Eip.text:004473ED mov eax, [ebp+44h] ; [_KTRAP_FRAME].Eax.text:004473F0 mov ecx, [ebp+40h] ; [_KTRAP_FRAME].Ecx.text:004473F3 mov edx, [ebp+3ch] ; [_KTRAP_FRAME].Edx.text:004473F6 jmp loc_447527.text:00447527.text:00447527 loc_447527: ; CODE XREF:.text:004473F6 j.text:00447527.text:00447543 mov esi, ecx.text:00447545 mov edi, edx.text:00447547 mov edx, eax.text:00447549 mov ebx, [ebp+68h] ; [_KTRAP_FRAME].Eip.text:0044754C dec ebx.text:0044754d mov ecx, 3.text:00447552 mov eax, STATUS_BREAKPOINT.text:00447557 call CommonDispatchException.text:00446D70 CommonDispatchException proc near.text:00446d70.text:00446d70 stexceptionrecord= EXCEPTION_RECORD ptr -50h.text:00446D70.text:00446D70 sub esp, 50h.text:00446D73 mov [esp+50h+stexceptionrecord.exceptioncode], eax.text:00446d76 xor eax, eax.text:00446d78 mov [esp+50h+stexceptionrecord.exceptionflags], eax.text:00446d7c mov [esp+50h+stexceptionrecord.exceptionrecord], eax.text:00446d80 mov [esp+50h+stexceptionrecord.exceptionaddress], ebx.text:00446d84 mov [esp+50h+stexceptionrecord.numberparameters], ecx.text:00446d88 cmp ecx, 0
DbgPrint 函数流程分析 by 小喂 5.text:00446D8B jz short loc_446d99.text:00446d8d lea ebx, [esp+50h+stexceptionrecord.exceptioninformation].text:00446d91 mov [ebx], edx.text:00446d93 mov [ebx+4], esi.text:00446d96 mov [ebx+8], edi.text:00446d99.text:00446d99.text:00446da8 mov eax, [ebp+6ch] ; [_KTRAP_FRAME].SegCs.text:00446DAB.text:00446DAB loc_446dab: ; CODE XREF: CommonDispatchException+36 j.text:00446dab and eax, 1.text:00446DAE push 1 ; char.text:00446db0 push eax ; int.text:00446db1 push ebp ; BugCheckParameter3.text:00446DB2 push 0 ; int.text:00446db4 push ecx ; void *.text:00446db5 call KiDispatchException(x,x,x,x,x) KiDebugService 先构建一个陷阱帧 (KTRAP_FRAME ), 然后设置参数调用 CommonDispatchException, CommonDispatchException 会构建一个异常纪录 (EXCEPTION_RECORD), 然后调用 KiDispatchException 函数走异常处理 流程, 异常代码为 STATUS_BREAKPOINT 从 int2d >KiDebugService >CommonDispatchException >KiDispatchException 这个 流程一路看下来, 会发现 int2d 时提供的三个参数存放在异常纪录的 ExceptionInformation 数组里面, 分别是 : ExceptionInformation[0] 表示 BREAKPOINT_PRINT 功能号 ExceptionInformation[1] 表示调试信息字符串地址 ExceptionInformation[2] 表示调试信息字符串长度 进入 KiDispatchException 后的代码比较复杂, 当前只是分析 DbgPrint 的流程, 其他代码暂时不管, 只需要知道 KiDispatchException 会调用 KiDebugRoutine 把异常提交给内核调试引擎处理 当处于内核调试时,KiDebugRoutine 指向 KdpTrap 函数, 没有调试时,KiDebugRoutine 指向 KdpStub 函数 先来看看 KdpStub 函数.text:0042A2DC stdcall KdpStub(x, x, x, x, x, x) proc near.text:0042a2dc.text:0042a2dc TrapFrame = dword ptr 8.text:0042A2DC ExceptionFrame = dword ptr 0Ch.text:0042A2DC ExceptionRecord = dword ptr 10h.text:0042A2DC ContextRecord = dword ptr 14h.text:0042A2DC PreviousMode = dword ptr 18h.text:0042A2DC bsecondchance = dword ptr 1Ch.text:0042A2DC.text:0042A2DC mov edi, edi.text:0042a2de push ebp.text:0042a2df mov ebp, esp.text:0042a2e1 push ebx.text:0042a2e2 push esi.text:0042a2e2.text:0042a2e3 mov esi, [ebp+exceptionrecord].text:0042a2e6 xor ebx, ebx.text:0042a2e8 cmp [esi+exception_record.exceptioncode], STATUS_BREAKPOINT
DbgPrint 函数流程分析 by 小喂 6.text:0042A2EE jnz short _elseif.text:0042a2ee.text:0042a2f0 cmp [esi+exception_record.numberparameters], ebx.text:0042a2f3 jbe short _elseif.text:0042a2f3.text:0042a2f5 mov eax, [esi+exception_record.exceptioninformation].text:0042a2f8 cmp eax, BREAKPOINT_LOAD_SYMBOLS.text:0042A2FB jz short loc_42a30c.text:0042a2fd cmp eax, BREAKPOINT_UNLOAD_SYMBOLS.text:0042A300 jz short loc_42a30c.text:0042a302 cmp eax, BREAKPOINT_COMMAND_STRING.text:0042A305 jz short loc_42a30c.text:0042a307 cmp eax, BREAKPOINT_PRINT.text:0042A30A jnz short _elseif.text:0042a30c.text:0042a30c mov eax, [ebp+contextrecord].text:0042a30f inc [eax+context._eip].text:0042a315 mov al, 1 ; return TRUE;.text:0042A317 jmp short _exit KdpStub 先判断异常代码是不是 STATUS_BREAKPOINT(int3 断点异常也是这个异常代码, 但第一个参数是 BREAKPOINT_BREAK), 然后判断参数个数 对于当前支持的四种调试服务, 包括输出调试字符串, 都是把 eip 加一, 跳 过 int2d 后面带的 int3 指令, 然后从异常处理中返回, 继续执行 当正在调试时,KiDispatchException 调用的就是 KdpTrap 函数 PAGEKD:006AB5EB stdcall KdpTrap(x, x, x, x, x, x) proc near PAGEKD:006AB6A1 loc_6ab6a1: ; CODE XREF: KdpTrap(x,x,x,x,x,x)+3A j PAGEKD:006AB6A1 mov edx, [ebx+context._ebx] PAGEKD:006AB6A7 lea ecx, [ebp+breturn] PAGEKD:006AB6AA push ecx PAGEKD:006AB6AB push [ebp+exceptionframe] PAGEKD:006AB6AE movzx ecx, word ptr [eax+1ch] ; ExceptionInformation[2] PAGEKD:006AB6B2 push [ebp+trapframe] PAGEKD:006AB6B5 push dword ptr [ebp+previousmode] PAGEKD:006AB6B8 push ecx PAGEKD:006AB6B9 push dword ptr [eax+18h] ; ExceptionInformation[1] PAGEKD:006AB6BC mov ecx, [ebx+context._edi] PAGEKD:006AB6C2 call KdpPrint(x,x,x,x,x,x,x,x) KdpTrap 也会和 KdpStub 一样判断异常代码和参数个数, 以及调试服务号, 根据调试服务号的不同调用不同的处理 函数 针对 BREAKPOINT_PRINT 输出调试信息的情况, 调用的是 KdpPrint 函数 KdpPrint 也会根据 ComponentId 和 Level 值判断一下是否需要屏蔽此次输出 然后判断特权模式, 如果是用户模式 还需要探测字符串内存, 保证可读
DbgPrint 函数流程分析 by 小喂 7 PAGEKD:006AC921 mov [ebp+asbuffer.buffer], edi PAGEKD:006AC924 mov [ebp+asbuffer.length], bx PAGEKD:006AC928 lea eax, [ebp+asbuffer] PAGEKD:006AC92B push eax PAGEKD:006AC92C call KdpLogDbgPrint(x) PAGEKD:006AC931 cmp _KdDebuggerNotPresent, 0 PAGEKD:006AC938 jnz short loc_6ac984 PAGEKD:006AC93A push [ebp+exceptionframe] PAGEKD:006AC93D push [ebp+trapframe] PAGEKD:006AC940 call KdEnterDebugger(x,x) PAGEKD:006AC945 mov [ebp-20h], al PAGEKD:006AC948 lea eax, [ebp+asbuffer] PAGEKD:006AC94B call KdpPrintString(x) KdpPrint 接着调用 KdpLogDbgPrint 在一个循环缓冲区里记录调试字符串, 然后判断是否挂接了调试器, 调用 KdpPrintString 输出调试字符串 KdpPrintString 构造一个调试包, 通过 KdSendPacket 函数发送给调试器 DebugView 实现原理 上一节详细介绍了 DbgPrint 输出调试字符串的流程, 现在来看看 DebugView 工具的实现原理 在 Vista 系统上,DebugView 设置了调试输出回调函数, 从而截获调试字符串 kd> dps nt!rtlpdebugprintcallback L 1 818f41b8 00000000 kd> g ModLoad: 919ef000 919f2d00 Dbgv.sys kd> dps nt!rtlpdebugprintcallback L 1 818f41b8 919efa86 Dbgv+0xa86 在 Vista 以前的系统, 比如 2003 系统上,DbgPrint 函数调用 vdbgprintexwithprefixinternal 函数, 在 vdbgprintexwithprefixinternal 函数里面不是直接 int2d 调用调试服务, 而是通过 DebugPrint 函数再调用调试服务把字符 串输出 DebugView 通过 Hook 函数 DebugPrint 截获调试字符串 kd> u nt!debugprint nt!debugprint: 808356b6 8bff mov edi,edi 808356b8 55 push ebp 808356b9 8bec mov ebp,esp 808356bb ff7510 push dword ptr [ebp+10h] 808356be 8b4508 mov eax,dword ptr [ebp+8] 808356c1 ff750c push dword ptr [ebp+0ch] 808356c4 0fb708 movzx ecx,word ptr [eax] 808356c7 51 push ecx 808356c8 ff7004 push dword ptr [eax+4] 808356cb 6a01 push 1 808356cd e86f460100 call nt!debugservice (80849d41)
DbgPrint 函数流程分析 by 小喂 8 808356d2 5d pop ebp 808356d3 c20c00 ret 0Ch kd> g ModLoad: f6a46000 f6a49d00 Dbgv.sys kd> u nt!debugprint nt!debugprint: 808356b6 ff258c7da4f6 jmp dword ptr [Dbgv+0x1d8c (f6a47d8c)] 808356bc 7510 jne nt!debugprint+0x18 (808356ce) 808356be 8b4508 mov eax,dword ptr [ebp+8] 808356c1 ff750c push dword ptr [ebp+0ch] 808356c4 0fb708 movzx ecx,word ptr [eax] 808356c7 51 push ecx 808356c8 ff7004 push dword ptr [eax+4] 808356cb 6a01 push 1 kd> dps 0xf6a47d8c L 1 f6a47d8c f6a469ac Dbgv+0x9ac ========== 小喂 2007 12 01