转自:https://msdn.microsoft.com/en-us/magazine/gg650659.aspx
Troubleshooting production issues can be one of the most frustrating jobs that any engineer can do. It can also be one of the most rewarding jobs. Working in Microsoft Support, I’m faced with this every day. Why did the application crash? Why did it hang? Why is it having a performance issue?
Learning how to debug can be a daunting task, and is one of those skills that requires many hours of regular practice to stay proficient. But it’s a crucial skill for being an all-around developer. Still, by bottling the skills of a few debugging experts, debugger engineers of all skill levels can execute extremely complex debugging logic as easy-to-run commands.
Myriad troubleshooting techniques can be used to get to the root cause of a crash, but the most valuable—and most fruitful to engineers with debugging skills—is a process dump. Process dumps contain a snapshot of a process memory at the time of capture. Depending on the dumping tool, this could be the entire address space or just a subset.
Windows automatically creates a minidump through Windows Error Reporting (WER) when any application throws an unhandled exception. In addition, you can manually create a dump file via the Userdump.exe tool. The Sysinternals tool ProcDump (technet.microsoft.com/sysinternals/dd996900) is becoming the preferred process-dumping tool of Microsoft Support because it can capture a dump based upon a large variety of triggers and can generate dumps of various sizes. But once you have the dump data, what can you do with it to aid debugging?
Various versions of Visual Studio support opening dump files (.dmp), but the best tool to use is a debugger from Debugging Tools for Windows. These tools are all based on a single debugging engine that supports two debugger extension APIs. In this article, I’m going to cover the basics of building a custom debugger extension so you can analyze these dump files (and also live systems) with ease.
Debugging Tools for Windows is an installable and redistributable component of the Windows SDK and Windows Driver Kit (WDK). As I write this, the current version is 6.12, and it’s available in version 7.1 of the Windows SDK or the WDK. I recommend using the most-recent version, as the debugging engine has many valuable additions, including better stack walking.
The Debugging Tools for Windows guidelines say that you should compile debugger extensions using the WDK build environment. I use the latest release of the WDK (version 7.1.0 build 7600.16385.1), but any version of the WDK or its previous incarnation as the Driver Development Kit (DDK) will suffice. When building an extension using the WDK, you use x64 and x86 Free Build environments.
With a little bit of effort you can also adapt my projects to build in the Windows SDK build environment or Visual Studio.
One note of warning: The WDK doesn’t like spaces in path names. Make sure you compile from an unbroken path. For example, use something like C:\Projects instead of C:\Users\Andrew Richards\Documents\Projects.
Regardless of how you build the extension, you’ll need the header and library files of the Debugging Tools SDK, which is a component of Debugging Tools for Windows. The examples in this article use my x86 path (C:\debuggers_x86\sdk) when referencing the header and library files. If you choose to install the debugger elsewhere, remember to update the path and add quotes when necessary to accommodate spaces in the path names.
The Debugging Tools for Windows debuggers are architecture-agnostic. Any edition of the debugger can debug any target architecture. A common example is using the x64 debugger to debug an x86 application. The debugger is released for x86, x64 (amd64) and IA64, but it can debug x86, x64, IA64, ARM, EBC and PowerPC (Xbox) applications. You can install all of the debugger editions side-by-side.
This agility isn’t universally understood, though. Not all debugger extensions adapt to the target architecture as well as the debugger engine does. Some debugger extensions assume that the pointer size of the target will be the same as the pointer size of the debugger. Similarly, they use the wrong hardcoded register (esp in place of rsp, for example) instead of a pseudo-register such as $csp.
If you’re having an issue with a debugger extension, you should try running the debugger designed for the same architecture as the target environment. This might overcome the assumptions of the poorly written extension.
Each application build type and associated processor architecture comes with its own set of debugging headaches. The assembler generated for a debug build is relatively linear, but the assembler generated for a release build is optimized and can resemble a bowl of spaghetti. On x86 architectures, Frame Pointer Omission (FPO) plays havoc with call stack reconstruction (the latest debugger handles this well). On x64 architectures, function parameters and local variables are stored in registers. At the time of dump capture, they may have been pushed onto the stack, or may no longer exist due to register reuse.
Experience is the key here. To be accurate, one person’s experience is the key here. You just need to bottle their know-how in a debugger extension for the rest of us. It only takes a few repetitions of a similar debugging sequence before I automate it as a debugger extension. I’ve used some of my extensions so much that I forget how I did the same thing using the underlying debugging commands.
There are two debugger extension APIs: the deprecated WdbgExts API (wdbgexts.h) and the current DbgEng API (dbgeng.h).
WdbgExts extensions are based on a global call that’s configured at initialization (WinDbgExtensionDllInit):
WINDBG_EXTENSION_APIS ExtensionApis;
The global provides the functionality required to run functions such as dprintf(“\n”) and GetExpression(“@$csp”) without any namespace. This type of extension resembles the code you’d write when doing Win32 programming.
DbgEng extensions are based on debugger interfaces. The IDebugClient interface is passed to you by the debug engine as a parameter of each call. The interfaces support QueryInterface for access to the ever-increasing range of debugger interfaces. This type of extension resembles the code you’d write when doing COM programming.
It’s possible to make a hybrid of the two extension types. You expose the extension as DbgEng, but add the functionality of the WdbgExts API at run time via a call to IDebugControl::GetWindbgExtensionApis64. As an example, I’ve written the classic “Hello World” as a DbgEng extension in C. If you prefer C++, refer to the ExtException class in the Debugging Tools SDK (.\inc\engextcpp.cpp).
Compile the extension as MyExt.dll (TARGETNAME in the sources file shown in Figure 1). It exposes a command called !helloworld. The extension dynamically links to the Microsoft Visual C runtime (MSVCRT). If you want to use static, change the USE_MSVCRT=1 statement to USE_LIBCMT=1 in the sources file.
TARGETNAME=MyExt TARGETTYPE=DYNLINK _NT_TARGET_VERSION=$(_NT_TARGET_VERSION_WINXP) DLLENTRY=_DllMainCRTStartup !if "$(DBGSDK_INC_PATH)" != "" INCLUDES = $(DBGSDK_INC_PATH);$(INCLUDES) !endif !if "$(DBGSDK_LIB_PATH)" == "" DBGSDK_LIB_PATH = $(SDK_LIB_PATH) !else DBGSDK_LIB_PATH = $(DBGSDK_LIB_PATH)\$(TARGET_DIRECTORY) !endif TARGETLIBS=$(SDK_LIB_PATH)\kernel32.lib \ $(DBGSDK_LIB_PATH)\dbgeng.lib USE_MSVCRT=1 UMTYPE=windows MSC_WARNING_LEVEL = /W4 /WX SOURCES= dbgexts.rc \ dbgexts.cpp \ myext.cpp
The DebugExtensionInitialize function (see Figure 2) is called when the extension is loaded. Setting the Version parameter is a simple matter of using the DEBUG_EXTENSION_VERSION macro with the EXT_MAJOR_VER and EXT_MINOR_VER #defines I’ve added to the header file:
// dbgexts.h #include#include #define EXT_MAJOR_VER 1 #define EXT_MINOR_VER 0
// dbgexts.cpp #include "dbgexts.h" extern "C" HRESULT CALLBACK DebugExtensionInitialize(PULONG Version, PULONG Flags) { *Version = DEBUG_EXTENSION_VERSION(EXT_MAJOR_VER, EXT_MINOR_VER); *Flags = 0; // Reserved for future use. return S_OK; } extern "C" void CALLBACK DebugExtensionNotify(ULONG Notify, ULONG64 Argument) { UNREFERENCED_PARAMETER(Argument); switch (Notify) { // A debugging session is active. The session may not necessarily be suspended. case DEBUG_NOTIFY_SESSION_ACTIVE: break; // No debugging session is active. case DEBUG_NOTIFY_SESSION_INACTIVE: break; // The debugging session has suspended and is now accessible. case DEBUG_NOTIFY_SESSION_ACCESSIBLE: break; // The debugging session has started running and is now inaccessible. case DEBUG_NOTIFY_SESSION_INACCESSIBLE: break; } return; } extern "C" void CALLBACK DebugExtensionUninitialize(void) { return; }
The Version value is reported as the API version in the debugger .chain command. To change the File Version, File Description, Copyright and other values you need to edit the dbgexts.rc file:
myext.dll: image 6.1.7600.16385, API 1.0.0, built Wed Oct 13 20:25:10 2010 [path: C:\Debuggers_x86\myext.dll]
The Flags parameter is reserved and should be set to zero. The function needs to return S_OK.
The DebugExtensionNotify function is called when the session changes its active or accessible status. The Argument parameter is wrapped with the UNREFERENCED_PARAMETER macro to eliminate the unused parameter compiler warning.
I’ve added the switch statement for the Notify parameter for completeness, but I haven’t added any functional code in this area. The switch statement processes four session state changes:
The DebugExtensionUninitialize function is called when the extension is unloaded.
Each extension command to be exposed is declared as a function of type PDEBUG_EXTENSION_CALL. The name of the function is the name of the extension command. Because I’m writing “Hello World,” I’ve named the function helloworld (see Figure 3).
// MyExt.cpp #include "dbgexts.h" HRESULT CALLBACK helloworld(PDEBUG_CLIENT pDebugClient, PCSTR args) { UNREFERENCED_PARAMETER(args); IDebugControl* pDebugControl; if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), (void **)&pDebugControl))) { pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Hello World!\n"); pDebugControl->Release(); } return S_OK; }
Note that the convention is to use lower-case function names. Because I’m using the WDK build environment, the myext.def file also needs to be changed. The name of the extension command needs to be added so that it’s exported:
;------------- ; MyExt.def ;------------- EXPORTS helloworld DebugExtensionNotify DebugExtensionInitialize DebugExtensionUninitialize
The args parameter contains a string of the arguments for the command. The parameter is passed as a null-terminated ANSI string (CP_ACP).
The pDebugClient parameter is the IDebugClient interface pointer that allows the extension to interact with the debugging engine. Even though the interface pointer looks like it’s a COM Interface pointer, it can’t be marshaled, nor accessed at a later time. It also can’t be used from any other thread. To do work on an alternate thread, a new debugger client (a new interface pointer to IDebugClient) must be created on that thread using IDebugClient::CreateClient. This is the only function that can be run on an alternate thread.
The IDebugClient interface (like all interfaces) is derived from IUnknown. You use QueryInterface to access the other DbgEng interfaces, be they later versions of the IDebugClient interface (IDebugClient4) or different interfaces (IDebugControl, IDebugRegisters, IDebugSymbols, IDebugSystemObjects and so on). To output text to the debugger, you need the IDebugControl interface.
I have two non-SDK files in the folder to help with development. The make.cmd script adds the Debugger SDK inc and lib paths to the WDK build environment, then runs the appropriate build command:
@echo off set DBGSDK_INC_PATH=C:\Debuggers_x86\sdk\inc set DBGSDK_LIB_PATH=C:\Debuggers_x86\sdk\lib set DBGLIB_LIB_PATH=C:\Debuggers_x86\sdk\lib build -cZMg %1 %2
Note that the WDK build environment itself determines whether an x86 or x64 binary will be built. If you want to build for multiple architectures, you’ll need to open multiple prompts and run make.cmd in each. The building can be done concurrently.
Once built, I use the (x86) test.cmd script to copy the compiled i386 binaries into the x86 debugger folder (c:\Debuggers_x86), then launch an instance of Notepad with the debugger attached and the extension loaded:
@echo off copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86 copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86 \Debuggers_x86\windbg.exe -a myext.dll -x notepad
If everything has gone as planned, I can type “!helloworld” in the debugger command prompt and see a “Hello World!” response:
0:000> !helloworld Hello World!
The “Hello World” application may be amazing, but you can do better. I’m now going to use this infrastructure to add a command that actually interacts with the target and would help you do some analysis. The simple test01 application has a global pointer that’s assigned a value:
// test01.cpp #includevoid* g_ptr; int main(int argc, char* argv[]) { g_ptr = "This is a global string"; Sleep(10000); return 0; }
The new !gptr command in MyExt.cpp (see Figure 4) will resolve the test01!g_ptr global, read the pointer and then output the values found in the same format as “x test01!g_ptr”:
0:000> x test01!g_ptr 012f3370 Test01!g_ptr = 0x012f20e4 0:000> !gptr 012f3370 test01!g_ptr = 0x012f20e4
HRESULT CALLBACK gptr(PDEBUG_CLIENT pDebugClient, PCSTR args) { UNREFERENCED_PARAMETER(args); IDebugSymbols* pDebugSymbols; if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugSymbols), (void **)&pDebugSymbols))) { // Resolve the symbol. ULONG64 ulAddress = 0; if (SUCCEEDED(pDebugSymbols->GetOffsetByName("test01!g_ptr", &ulAddress))) { IDebugDataSpaces* pDebugDataSpaces; if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugDataSpaces), (void **)&pDebugDataSpaces))) { // Read the value of the pointer from the target address space. ULONG64 ulPtr = 0; if (SUCCEEDED(pDebugDataSpaces->ReadPointersVirtual(1, ulAddress, &ulPtr))) { PDEBUG_CONTROL pDebugControl; if (SUCCEEDED(pDebugClient->QueryInterface(__uuidof(IDebugControl), (void **)&pDebugControl))) { // Output the values. pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "%p test01!g_ptr = 0x%p\n", ulAddress, ulPtr); pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "%ma\n", ulPtr); pDebugControl->Release(); } } pDebugDataSpaces->Release(); } pDebugSymbols->Release(); } } return S_OK; }
The first step is to determine the location of the test01!g_ptr pointer. The pointer will be in a different location each time the application runs because Address Space Layout Randomization (ASLR) will change the module load address. To get the location, I use QueryInterface to get the IDebugSymbols interface, and then use GetOffsetByName. The GetOffsetByName function takes a symbol name and returns the address as a 64-bit pointer. The debugger functions always return 64-bit pointers (ULONG64) so that 64-bit targets can be debugged with a 32-bit debugger.
Remember, this is the address of the pointer in the target address space, not your own. You can’t just read from it to determine its value. To get the value of the pointer, I use QueryInterface again to get the IDebugDataSpaces interface, and then use ReadPointersVirtual. This reads the pointer from the target address space. ReadPointersVirtual automatically adjusts for pointer size and endian differences. You don’t need to manipulate the pointer returned.
IDebugControl::Output takes the same format string as printf, but also has formatters that allow you to reference the target address space. I use the %ma format to print out the ANSI string that the global pointer points to in the target address space. The %p format is pointer-size-aware and should be used for pointer output (you must pass a ULONG64).
I’ve modified the test script to load a dump file of the x86 version of test01 instead of launching Notepad:
@echo off copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86 copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86 \Debuggers_x86\windbg.exe -a myext.dll -y "..\Test01\x86;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..\Test01\x86\Test01.dmp
I’ve also set the symbol path to the test01 x86 folder and the Microsoft Public Symbol Server so that everything can be resolved. Additionally, I’ve made an x64 test script that does the same as the x86 test script, but with a dump file of the x64 version of the test application:
@echo off copy objfre_win7_x86\i386\myext.dll c:\Debuggers_x86 copy objfre_win7_x86\i386\myext.pdb c:\Debuggers_x86 \Debuggers_x64\windbg.exe -a myext.dll -y "..\Test01\x64;SRV*c:\symbols*http://msdl.microsoft.com/download/symbols" -z ..\Test01\x64\Test01.dmp
When I run the scripts, the x86 debugger is launched, the appropriate dump file is opened, the x86 version of the extension is loaded and symbols are resolvable.
Once again, if everything has gone to plan, I can type “x test01!g_ptr” and !gptr in the debugger command prompt and see similar responses:
// x86 Target 0:000> x test01!g_ptr 012f3370 Test01!g_ptr = 0x012f20e4 0:000> !gptr 012f3370 test01!g_ptr = 0x012f20e4 This is a global string // x64 Target 0:000> x test01!g_ptr 00000001`3fda35d0 Test01!g_ptr = 0x00000001`3fda21a0 0:000> !gptr 000000013fda35d0 test01!g_ptr = 0x000000013fda21a0 This is a global string
If you repeat the test using the x64 debugger, the amd64-compiled version of the debugger extension and the x86 or x64 dump files, you’ll get the same results. That is, the extension is architecture-agnostic.
I’m now going to extend this infrastructure once again. Let’s add a command that finds the duration of a Sleep call on the current thread stack. The !sleepy command (see Figure 5) will resolve the call stack symbols, look for the Sleep function and read the DWORD that represents the milliseconds to delay, and then will output the delay value (if found).
HRESULT CALLBACK sleepy(PDEBUG_CLIENT4 Client, PCSTR args) { UNREFERENCED_PARAMETER(args); BOOL bFound = FALSE; IDebugControl* pDebugControl; if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugControl), (void **)&pDebugControl))) { IDebugSymbols* pDebugSymbols; if (SUCCEEDED(Client->QueryInterface(__uuidof(IDebugSymbols), (void **)&pDebugSymbols))) { DEBUG_STACK_FRAME* pDebugStackFrame = (DEBUG_STACK_FRAME*)malloc( sizeof(DEBUG_STACK_FRAME) * MAX_STACK_FRAMES); if (pDebugStackFrame != NULL) { // Get the Stack Frames. memset(pDebugStackFrame, 0, (sizeof(DEBUG_STACK_FRAME) * MAX_STACK_FRAMES)); ULONG Frames = 0; if (SUCCEEDED(pDebugControl->GetStackTrace(0, 0, 0, pDebugStackFrame, MAX_STACK_FRAMES, &Frames)) && (Frames > 0)) { ULONG ProcessorType = 0; ULONG SymSize = 0; char SymName[4096]; memset(SymName, 0, 4096); ULONG64 Displacement = 0; if (SUCCEEDED(pDebugControl->GetEffectiveProcessorType( &ProcessorType))) { for (ULONG n=0; nGetNameByOffset( pDebugStackFrame[n].InstructionOffset, SymName, 4096, &SymSize, &Displacement)) && (SymSize > 0)) { if ((ProcessorType == IMAGE_FILE_MACHINE_I386) && (_stricmp(SymName, "KERNELBASE!Sleep") == 0) && (Displacement == 0xF)) { // Win7 x86; KERNELBASE!Sleep+0xF is usually in frame 3. IDebugDataSpaces* pDebugDataSpaces; if (SUCCEEDED(Client->QueryInterface( __uuidof(IDebugDataSpaces), (void **)&pDebugDataSpaces))) { // The value is pushed immediately prior to // KERNELBASE!Sleep+0xF DWORD dwMilliseconds = 0; if (SUCCEEDED(pDebugDataSpaces->ReadVirtual( pDebugStackFrame[n].StackOffset, &dwMilliseconds, sizeof(dwMilliseconds), NULL))) { pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Sleeping for %ld msec\n", dwMilliseconds); bFound = TRUE; } pDebugDataSpaces->Release(); } if (bFound) break; } else if ((ProcessorType == IMAGE_FILE_MACHINE_AMD64) && (_stricmp(SymName, "KERNELBASE!SleepEx") == 0) && (Displacement == 0xAB)) { // Win7 x64; KERNELBASE!SleepEx+0xAB is usually in frame 1. IDebugRegisters* pDebugRegisters; if (SUCCEEDED(Client->QueryInterface( __uuidof(IDebugRegisters), (void **)&pDebugRegisters))) { // The value is in the 'rsi' register. ULONG rsiIndex = 0; if (SUCCEEDED(pDebugRegisters->GetIndexByName( "rsi", &rsiIndex))) { DEBUG_VALUE debugValue; if (SUCCEEDED(pDebugRegisters->GetValue( rsiIndex, &debugValue)) && (debugValue.Type == DEBUG_VALUE_INT64)) { // Truncate to 32bits for display. pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Sleeping for %ld msec\n", debugValue.I32); bFound = TRUE; } } pDebugRegisters->Release(); } if (bFound) break; } } } } } free(pDebugStackFrame); } pDebugSymbols->Release(); } if (!bFound) pDebugControl->Output(DEBUG_OUTPUT_NORMAL, "Unable to determine if Sleep is present\n"); pDebugControl->Release(); } return S_OK; }
To add some complexity to the command, the command will support the x86 and x64 versions of the test01 application. Because the calling convention is different for x86 and x64 applications, the command will have to be aware of the target’s architecture as it progresses.
The first step is to get the stack frames. To get the frames, I use QueryInterface to get the IDebugControl interface, and then use GetStackTrace to retrieve information about each stack frame. GetStackTrace takes an array of DEBUG_STACK_FRAME structures. I always allocate the array of DEBUG_STACK_FRAME structures on the heap so that I don’t cause a stack overflow. If you’re retrieving a stack overflow thread of a target, you’ll probably hit your own stack limit if the array is allocated on your stack.
If GetStackTrace succeeds, the array will be populated with information for each frame that was walked. Success here doesn’t necessarily mean that the frame information is correct. The debugger does its best to walk the stack frames, but mistakes can be made when the symbols aren’t correct (when they’re missing or have been forced). If you’ve used “.reload /f /i” to force the symbol load, poor symbol alignment will probably occur.
To effectively use the contents of each of the DEBUG_STACK_FRAME structures, I need to know the target’s effective processor type. As mentioned previously, the target architecture can be completely different from the debugger extension architecture. The effective processor type (.effmach) is the architecture that the target is currently using.
The processor type can also be a different processor type than that used by the target’s host. The most common example of this is when the target is an x86 application that’s running via Windows 32-bit on Windows 64-bit (WOW64) on an x64 edition of Windows. The effective processor type is IMAGE_FILE_MACHINE_I386. The actual type is IMAGE_FILE_MACHINE_AMD64.
This means you should consider an x86 application to be an x86 application regardless of whether it’s running on an x86 edition of Windows or an x64 edition of Windows. (The only exception to this rule is when you’re debugging the WOW64 calls that surround the x86 process.)
To get the effective processor type, I use the IDebugControl interface that I already have, and then use GetEffectiveProcessorType.
If the effective processor type is i386, I need to look for the KERNELBASE!Sleep+0xf function. If all the symbols are resolved correctly, the function should be in frame 3:
0:000> knL4 # ChildEBP RetAddr 00 001bf9dc 76fd48b4 ntdll!KiFastSystemCallRet 01 001bf9e0 752c1876 ntdll!NtDelayExecution+0xc 02 001bfa48 752c1818 KERNELBASE!SleepEx+0x65 03 001bfa58 012f1015 KERNELBASE!Sleep+0xf
If the effective processor type is AMD64, I look for the KERNELBASE!SleepEx+0xab function. If all the symbols are resolved correctly, the function should be in frame 1:
0:000> knL2 # Child-SP RetAddr Call Site 00 00000000'001cfc08 000007fe'fd9b1203 ntdll!NtDelayExecution+0xa 01 00000000'001cfc10 00000001'3fda101d KERNELBASE!SleepEx+0xab
However, based on the level of symbol resolution available, the function symbol I’m looking for may or may not be in the expected frame. If you open the test01 x86 dump file and don’t specify a symbol path, you can see an example of this. The KERNELBASE!Sleep call will be in frame 1 instead of frame 3:
0:000> knL4 # ChildEBP RetAddr WARNING: Stack unwind information not available. Following frames may be wrong. 00 001bfa48 752c1818 ntdll!KiFastSystemCallRet 01 001bfa58 012f1015 KERNELBASE!Sleep+0xf 02 001bfaa4 75baf4e8 Test01+0x1015 03 001bfab0 76feaf77 kernel32!BaseThreadInitThunk+0x12
The debugger warns you of this possible mistake. If you want to have your extension adapt to these types of issues, you should iterate over the frames as I have, instead of just looking at the expected frame.
To determine the existence of the Sleep function, I need to look up the symbol for each frame. If the effective processor type and symbol make a valid pair, the function has been found. Note that this logic is fragile and is being used to simplify the example. The symbol may change between builds and platforms. For example, Windows Server 2008 is kernel32!Sleep+0xf, but Windows 7 is KERNELBASE!Sleep+0xf.
To get the symbol, I use QueryInterface to get the IDebugSymbol interface. I then use GetNameByOffset to get the symbol of the instruction offset address.
There are two parts to the symbol: the symbol name (KERNELBASE!Sleep) and the displacement (0xf). The symbol name is an amalgamation of the module name and the function name (
If there are no symbols, the function will be reported as just the module name with a large displacement (Test01+0x1015).
Once I’ve found the frame, the next step is to extract the delay. When the target is x86 based, the delay will be in a DWORD that has been pushed onto the stack immediately prior to the function call (note that this is fragile logic):
// @$csp is the pseudo-register of @esp 0:000> dps @$csp001bfa4c 752c1818 KERNELBASE!Sleep+0xf 001bfa50 00002710
The StackOffset member of the DEBUG_STACK_FRAME structure actually points to this address already, so no pointer arithmetic is necessary. To get the value, I use QueryInterface to get the IDebugDataSpaces interface, and then use ReadVirtual to read the DWORD from the target address space.
If the target is x64 based, the delay isn’t in the stack—it’s in the rsi register (this is also fragile logic due to its frame-context dependency):
0:000> r @rsi rsi=0000000000002710
To get the value, I use QueryInterface to get the IDebugRegisters interface. I first need to use GetIndexByName to get the index of the rsi register. I then use GetValue to read the register value from the target registers. Because rsi is a 64-bit register, the value is returned as an INT64. Because the DEBUG_VALUE structure is a union, you can simply reference the I32 member instead of the I64 member to get the truncated version that represents the DWORD passed to Sleep.
Once again, in both cases, I use the IDebugControl::Output function to output the result.
In this article, I barely scratched the surface of what can be achieved. Stacks, symbols, registers, memory I/O and environmental information are but a few of the many things that you can interrogate and change from within an extension.
In a future article I’ll delve deeper into the relationship a debugger extension can have with the debugger. I’ll cover debugger clients and debugger callbacks, and I’ll use these to encapsulate the SOS debugger extension so that you can write an extension that can debug managed applications without having to have any knowledge of the underlying .NET structures.