KD extension DLLs & KDCOM protocol
WinDbg debugger allows you to debug all modern versions of Windows using a built-in kernel debugger and either COM or IEEE1394 port. Let's see how is it implemented. To start windows in Kernel Debugging mode, you specify additional parameters in boot.ini file that look like this:
Let's now see what actually happens when NTOSKRNL detects that it was started with /DEBUG parameter. First of all, it analyzes the /DEBUGPORT parameter from boot.ini and determines what packet-level plugin (KD extension DLL in Microsoft terminology) to load. For COM-based debugging the plugin DLL is called KDCOM.DLL, for IEEE1394-based debugging it is called KD1394.DLL. Fortunately, when you specify something like /DEBUGPORT=FOO, NTOSKRNL will try to load KDFOO.DLL and use it as a KD extension DLL. As we are providing our own DLL making a fast interface to WinDbg, that expects a named pipe from a virtual COM port, we need to solve two problems here:
- The problem of creating a valid KD extension DLL, i.e. providing the same set of exported functions working in an expected way.
- The problem of understanding and reimplementing KDCOM protocol, i.e. providing the same data at the end of our fast pipe, as the original KDCOM.DLL provides.
Note that in Windows Vista the kernel debugging flags are specified using bcdedit.exe utility and cannot specify a non-standard KD extension DLL. The only way to load KDVMWare DLL to kernel is to replace a standard one, for example, KD1394.DLL.
KD extension DLLs
Let's explore the structure of a typical KD extension DLL. A quick analysis of KDCOM.DLL shows that it exports the following functions:
8 exported name(s), 8 export addresse(s). Ordinal base is 1.
Sorted by Name:
RVA Ord. Hint Name
-------- ---- ---- ----
00000386 1 0000 KdD0Transition
00000386 2 0001 KdD3Transition
000003A6 3 0002 KdDebuggerInitialize0
0000044C 4 0003 KdDebuggerInitialize1
00000F4C 5 0004 KdReceivePacket
00000460 6 0005 KdRestore
00000456 7 0006 KdSave
000011B2 8 0007 KdSendPacket
Let's analyze how this functions work.
Initialization
As it is evident from their names, two functions are used to initialize a KD extension DLL: KdDebuggerInitialize0() and KdDebuggerInitialize1(). Fortunately, Microsoft provides PDB file for WinXP version of KDCOM.DLL. Additionally, some of the functions are described by Ken Johnson (http://www.nynaeve.net/?p=169). Let's use the PDB to recover declarations for the initialization functions:
NTSTATUS NTAPI KdDebuggerInitialize1(PLOADER_PARAMETER_BLOCK lpLoaderParameterBlock);
The first function performs initial initialization of a KD extension DLL. For example, it can read the parameters specified in BOOT.INI using the LOADER_PARAMETER_BLOCK::LoadOptions field. For example, KDCOM.DLL can get determine the COM port number to use and its baud rate. Both initialization functions return a NTSTATUS value with STATUS_SUCCESS corresponding to successful completion. Note that if an initialization function such as KdDebuggerInitialize0() returns an unsuccessful status, kernel is started without debugging support and the DLL is not actually used any more.
Sending and receiving packets
All communication between kernel and a kernel debugger is packet-based. The following rules describe the packet behavior:
- All KD extension DLL calls are called synchronously, i.e. should not leave any executing code after they return.
- A KD extension DLL guarantees successful packet delivery, i.e. retries sending a packet when KdSendPacket() was called until debugger acknowledges it.
- Kernel uses polling model to check whether new packets are available. For that purpose it calls KdReceivePacket() with a special parameter.
- When kernel wants to receive a packet, it knows the type of the packet to receive. It passes this type to KdReceivePacket() that drops all packets of other types (and sends corresponding resend requests).
- Each send/receive operation uses 2 buffers for data transfer. Typically, the first buffer contains some fixed-size message header and the second one contains variable-sized message body. However, this is a typical use case, not the only one. Basically, in a send operation the two buffers are simply sent one after another as a single data block with no additional indication where the first one ends and the second one starts. When a receive function is called with first buffer having a size of N bytes, the first N bytes of the message are put to it, while the rest is put to the second buffer.
Each buffer is represented by a STRING structure defined in NTDEF.H (on x64 systems the data pointer is aligned at 8-byte boundary):
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} STRING, *PKD_BUFFER;
In KDVMWare this structure is redefined as KD_BUFFER.
A special structure called KD_CONTEXT maintains the global state for KD packet layer:
{
ULONG RetryCount;
BOOLEAN BreakInRequested;
} KD_CONTEXT, *PKD_CONTEXT;
The RetryCount member is set before a call to KdSendPacket() and specifies the number of retries for a droppable packet to set. A droppable packet is a packet that can be simply dropped if no acknowledgment comes from WinDbg after some number of retries (KdSendPacket() will just return). The BreakInRequested is set to TRUE by KdReceivePacket() if WinDbg has requested a kernel breakpoint (ctrl+break was pressed, or WinDbg was just started). The break-in request is not a part of a packet and is transferred separately (see KDCOM protocol description below).
Here are the definitions for packet sending and receiving functions:
__in PKD_BUFFER FirstBuffer,
__in_opt PKD_BUFFER SecondBuffer,
__inout PKD_CONTEXT KdContext);
KD_RECV_CODE NTAPI KdReceivePacket(__in ULONG PacketType,
__inout_opt PKD_BUFFER FirstBuffer,
__inout_opt PKD_BUFFER SecondBuffer,
__out_opt PULONG PayloadBytes,
__inout_opt PKD_CONTEXT KdContext);
The KdReceivePacket() return value can be defined as a following enumeration:
{
KD_RECV_CODE_OK = 0,
KD_RECV_CODE_TIMEOUT = 1,
KD_RECV_CODE_FAILED = 2
} KD_RECV_CODE, *PKD_RECV_CODE;
The PacketType parameter specifies the type of the packet being sent or being received (all packets with other types should be ignored), however there is one exception. When PacketType is set to 8 in a KdReceivePacket() call, the function checks whether there is any data available (for example, whether the COM port buffer is non-empty), and returns immediately either KD_RECV_CODE_OK or KD_RECV_CODE_TIMEOUT.
Additional support functions
A KD extension DLL exports some additional functions that are not directly involved in packet sending/receiving and can simply return STATUS_SUCCESS in most of implementations:
NTSTATUS NTAPI KdD3Transition(); //Called when the debug port device should be powered off
NTSTATUS NTAPI KdSave(BOOL SleepTransaction); //Saves the debug port state before standby or hibernation
NTSTATUS NTAPI KdRestore(BOOL SleepTransaction); //Restores originally saved debug port state
The information about these functions was taken from RectOS documentation pages. Although Microsoft implementation can be different from ReactOS one, just returning STATUS_SUCCESS from these functions should work.
KDCOM protocol
Another problem to be solved in order to connect kernel and WinDbg using a custom KD extension DLL is the protocol that KDCOM.DLL uses to transfer packets over a COM port. As WinDbg receives and sends KDCOM packets when connected to a kernel using a named pipe, our tool should be able to produce and to parse such packets. In KDVMWare these packets are processed in KDCLIENT.DLL on host side, however, in Microsoft implementation, all packet processing logic is implemented inside KDCOM.DLL. Let's see, how it works.
First of all, there are two kinds of packets: control packets and data packets. Data packets directly transfer KdSendPacket()/KdReceivePacket() data, while control packets signalize receive acknowledgment, retry requests, resync requests, etc. Each control packet consists of a packet header, a data block, and a terminating byte (0xAA). A data block contains contents of two buffers, one after another, with no indication of where one ends and another starts. Moreover, sender and receiver can use split the packet data in different ways:
Let's define a C structure describing the packet header:
{
ULONG Signature;
USHORT PacketType;
USHORT TotalDataLength;
ULONG PacketID;
ULONG Checksum;
} KD_PACKET_HEADER, *PKD_PACKET_HEADER;
Packet signature is either 0x30303030 ('0000') for data packets, or 0x69696969 for control packets ('iiii'). Packet type specifies the exact type of the packet. Types for control and data packets are members of the same enumeration:
{
KdPacketType3 = 3,
KdPacketAcknowledge = 4,
KdPacketRetryRequest = 5,
KdPacketResynchronize = 6,
KdPacketType7 = 7,
KdCheckForAnyPacket = 8,
KdPacketType11 = 11,
};
As it was described before, packet type 8 is not used as a packet type. Instead, when KdReceivePacket() is called with that value, it checks whether any data can be received from WinDbg and returns immediately.
Packet ID is used to detect if a single packet was missed, as the least significant bit of a packet ID toggles with every new packet sent. The initial packet ID is 0x80800800, however, resync command sets it to 0x80800000. Checksum is just an arithmetic sum of all bytes from the data section of the packet.
As I have discovered after development of KDVMWare, a file named windbgkd.h was included in Windows 2000 DDK and contained information about KDCOM protocol internals. The ReactOS version containing most of the information from it can be found here. According to that file, the following packet types are actually used:
#define PACKET_TYPE_KD_STATE_CHANGE32 1
#define PACKET_TYPE_KD_STATE_MANIPULATE 2
#define PACKET_TYPE_KD_DEBUG_IO 3
#define PACKET_TYPE_KD_ACKNOWLEDGE 4
#define PACKET_TYPE_KD_RESEND 5
#define PACKET_TYPE_KD_RESET 6
#define PACKET_TYPE_KD_STATE_CHANGE64 7
#define PACKET_TYPE_KD_POLL_BREAKIN 8
#define PACKET_TYPE_KD_TRACE_IO 9
#define PACKET_TYPE_KD_CONTROL_REQUEST 10
#define PACKET_TYPE_KD_FILE_IO 11
#define PACKET_TYPE_MAX 12
Let's discuss the types of control packets and their roles in KDCOM protocol:
- Acknowledgment packets are sent by both Kernel and WinDbg when a data packet was successfully received.
- Resend packet is sent when Kernel or WinDbg has received a damaged packet, a packet with wrong ID, or a packet with unexpected type.
- Resync packet is sent by WinDbg when it is initially connected to kernel. The kernel acknowledges resync operation by sending back another resync packet.
To illustrate, how KDCOM packet layer works, let's check out some examples:
- Normal operation. Kernel continiously checks for new packets. When a packet is found, kernel receives it (assuming it knows the type for the packet).
- Normal packet sending. Kernel sends a packet to WinDbg. KdSendPacket() waits for acknowledgment packet from WinDbg.
- Packet sending with retry. Kernel sends a packet to WinDbg, however the latter does not receive it. KDCOM then resends the packet after timeout.
- Packet collision. Kernel sends a packet to WinDbg when WinDbg sends a
packet to kernel. Kernel sends a resend request to WinDbg. The latter gets
the data packet instead of acknowledgment and buffers it. Then acknowledges
it, receives a resend request for the first packet and sends it once again.
KDCOM receives acknowledgement and returns control to kernel, that calls
KdReceivePacket() to get packet from WinDbg
(if expects one).
- Resynchronization. Kernel receives a packet
and encounters a resync request from WinDbg.
Droppable packets
Some packets are "droppable". It means that KdSendPacket() may return control when such a packet was not acknowledged by WinDbg after some number of retries. KDCOM.DLL treats the following packets as droppable:
- Type 3, subtype 0x3230
- Type 7, subtype 0x3031
- Type 11, subtype 0x3430
Packet subtype (ApiNumber) is the first DWORD in the packet data block. The types referenced here is defined in the following way in windbgkd.h:
#define DbgKdLoadSymbolsStateChange 0x00003031
Resync bounce problem
There is one significant detail in original KDCOM implementation. When the KDCOM.DLL initializes, it reinitializes the COM port and resets its buffer. A named pipe implementation should do the same. In other case, the following scenario is possible:
- WinDbg connects to a named pipe with no kernel listening and sends a few resync packets.
- Kernel loads our KD DLL, it receives first resync packet and replies with a resync.
- WinDbg resynchronizes and sends some data.
- Kernel receives another resync from buffer and resyncs again, replying with a resync.
- WinDbg receives an unexpected resync, resynchronizes and sends another resync packet to acknowledge resynchronization.
- The WinDbg/kernel couple will continue producing resync packets till the end of time and will never synchronize normally.
To avoid this problem, KDVMWare simply clears the named pipe receive buffer when it receives a resync packet.
Implementation in KDVMWare
All KDCOM-related functionality is implemented in the KdComDispatcher class. Feel free to explore its documentation using the link above.