Alter.Org.UA
 << Back Home UK uk   Donate Donate

NT Debug message support

Kernel KdPrint()/DebugPrint() behavior

NT OSes are capable of generating and collecting text Debug Messages. In kernel-mode it is usually done via KdPrint(). User-mode applications use OutputDebugString().

KdPrint() is defined in ntddk.h as follow:

#ifdef DBG
#define KdPrint(_x_) DbgPrint _x_
#else
#define KdPrint(_x_)
#endif

DbgPrint() is defined so:

extern "C"  // for C++ projects
ULONG
_cdecl
DbgPrint(
    PCH Format,
    ...
    );

DbgPrint() is exported from ntoskrnl.exe. In some drivers (for example in ScsiPort/ScsiMiniPort) DbgPrint() is redefined. But in any case it works in same way. Formatted string with parameters is printed to buffer UCHAR[512] with vsnprintf(). The buffer is allocated on stack and has size of 512 bytes (at least in all systems those I seen). Under NT 3.51, NT 4 and 2000 it happens in DbgPrint() itself. Then this buffer is passed in some way (read below) to DebugPrint() as PSTRING. Since XP DebugPrint() is replaced with DebugPrintEx().

#define BREAKPOINT_PRINT 1

NTSTATUS
DebugPrint(
    IN PSTRING Msg
    )
{
    return DebugService( BREAKPOINT_PRINT,
                   Msg, 0 );
}
 
NTSTATUS DebugPrintEx(
    IN PSTRING Msg,
    IN ULONG ComponentId,
    IN ULONG Level)
{
    return DebugServiceEx( BREAKPOINT_PRINT,
                   Msg->Buffer, Msg->Length,
                   ComponentId, Level );
}

Experiments show, that DbgPrint() is available not only in в kernel-mode. You can use it in user-mode (Native, Win32). In this case DbgPrint() is exported from ntdll.dll.

Numega SoftIce hooks this call and at first this PSTRING comes to DbgMsg.sys driver. This driver is intended for capturing DbgPrint() messages. Looks like SoftIce patches call point of DebugPrint() (it changes the address in CALL XXXXXXXX instruction).

Under w2k DbgPrint() has own exception handler. Among this it checks after vsnprintf() call if the requested string was successfully fit in buffer. If not, the message is dropped.

XP DbgPrint() behavior is more complicated. New function vDbgPrintExWithPrefix() appeared. DbgPrint(), DbgPrintEx() and vDbgPrintEx() work as wrappers for this function.

ULONG
vDbgPrintExWithPrefix(
    IN PCH Prefix,
    IN ULONG ComponentId,
    IN ULONG Level,
    IN PCH Format,
    IN va_list arglist // va_list == PCH[]*
    );

Also, XP has more parameters for DebugPrint(), so I call it DebugPrintEx(). I don't know exactly what do they mean. But seems that they are involved in built-id message filtering. And I think we have to RTFM for DbgSetDebugFilterState()/DbgQueryDebugFilterState().

2003-R2 made me even more happy with new internal function, lets call it DebugDispatch(). Along with sending debug messages it can break execution and invoke debugger. This additional capability is used by DbgPrintReturnControlC() function. Now all mentioned above functions and vDbgPrintExWithPrefix() wraps DebugDispatch().

NTSTATUS DebugDispatch(
    IN PCH Prefix,
    IN ULONG ComponentId,
    IN ULONG Level,
    IN PCH Format,
    IN va_list arglist, // va_list == PCH[]*
    IN BOOLEAN ContinueExecution);

In all cases PSTRING comes to DebugPrint()/DebugPrintEx() function, which calls DebugService()/DebugServiceEx(). Lets look inside them:

NTSTATUS DebugService(
    ULONG   Type,
    PVOID   Param1,
    PVOID   Param2)
{
    mov     eax, Type
    mov     ecx, Param1
    mov     edx, Param2

    int     2dh
    int     3

    mov     RetValue, eax
}


 
NTSTATUS DebugServiceEx(
    ULONG   Type,
    PVOID   Param1, ULONG   Param2,
    ULONG   Param3, ULONG   Param4)
{
    mov     eax, Type
    mov     ecx, Param1
    mov     edx, Param2
    mov     ebx, Param3
    mov     edi, Param4

    int     2dh
    int     3

    mov     RetValue, eax
}

int 2d raises exception. In KdpTrap() exception handler the following checks are performed:

  1. if KdDebuggerNotPresent && Type != BREAKPOINT_PROMPT, exit
  2. Previous execution mode is checked. If call comes from UserMode, an additional buffer validation is performed with ProbeForRead().

When all checks are passed, KdpPrintString() is invoked:

BOOLEAN
KdpPrintString (
    IN PSTRING Output
    );

After checks buffer is passed to KdpSendPacket() for sending to remote debugger. Inside this function packet header and packed data blocks are generated. Header and data are sent by separate calls to KdpSendString(). KdpSendString() uses exported function KdPortPutByte().

Here is a picture that shows call references. Exported functions are printed in bold.

NT 3.51, NT4, 2000
XP, 2003
2003-R2
DbgPrint()

  

  
    DebugPrint()
      DebugService()
        KdpTrap()
          KdpPrintString()
            KdpSendPacket()
              KdpSendString()
                KdPortPutByte()
DbgPrint()
DbgPrintEx()
vDbgPrintEx()
  vDbgPrintExWithPrefix()

    DebugPrintEx()
      DebugServiceEx()
        KdpTrap()
          KdpPrintString()
            KdpSendPacket()
              KdpSendString()
                KdPortPutByte()
DbgPrint()
DbgPrintEx()
vDbgPrintEx()
vDbgPrintExWithPrefix()
  DebugDispatch()
    DebugPrintEx()
      DebugServiceEx()
        KdpTrap()
          KdpPrintString()
            KdpSendPacket()
              KdpSendString()
                KdPortPutByte()
Win32 OutputDebugString() behavior

Unicode-version OutputDebugStringW() converts Unicode characters to ANSI and calls OutputDebugStringA(). OutputDebugStringA() registers own exception handler and raises exception via RaiseException():

WINBASEAPI
VOID
WINAPI
RaiseException(
    DWORD dwExceptionCode,
    DWORD dwExceptionFlags,
    DWORD nNumberOfArguments,
    CONST DWORD *lpArguments
    );

dwExceptionCode    = 0x40010006
dwExceptionFlags   = 0
nNumberOfArguments = 2
lpArguments        = PSTRING
Then function JUMPs to exit. PSTRING is generated from input string. OutputDebugString() doesn't pass control to the kernel debug functions described above.

If there is registered system debugger, it handles the exception. Otherwise own OutputDebugString()'s handler is invoked. It does the following: Attempts to open 'DBWIN_DATA_READY' and 'DBWIN_BUFFER_READY' Named Events, then Named File Mapping 'DBWIN_BUFFER'. If any of these operations fails, the message is dropped.

'DBWIN_BUFFER_READY' is set by capturing application. It signals that application is ready to receive next message. 'DBWIN_DATA_READY' is set by OutputDebugString() and signals that new message is copied to 'DBWIN_BUFFER'. 'DBWIN_BUFFER' has size of 4k (1 page). It has the following structure:

typedef struct _DBG_OUTPUT_DEBUG_STRING_BUFFER {
    ULONG ProcessId;
    UCHAR Msg[4096-sizeof(ULONG)];
} DBG_OUTPUT_DEBUG_STRING_BUFFER, *PDBG_OUTPUT_DEBUG_STRING_BUFFER;

Msg string is always NULL-terminated.


See also:

<< Back designed by Alter aka Alexander A. Telyatnikov powered by Apache+PHP under FBSD © 2002-2024