感觉有必要学习一下写一个windbg插件

windows官方有一个插件的示例,便从该示例学习

https://github.com/tpn/winsdk-10/tree/master/Debuggers/x64/sdk/samples/exts

目录结构:

1
2
3
4
5
6
7
8
9
dbgexts.cpp
dbgexts.def
dbgexts.h
dbgexts.rc
exts.cpp
exts.vcxproj
makefile
readme.txt
sources

先看一下dbgexts.h中的几个部分

1
2
3
#define INIT_API()                             \
HRESULT Status; \
if ((Status = ExtQuery(Client)) != S_OK) return Status;

这里面,ExtQurey是在dbgexts.cpp中定义的函数,它的参数Client可能是调试器,因为使用交叉引用并不能找得到它的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern "C" HRESULT
ExtQuery(PDEBUG_CLIENT4 Client)
{
HRESULT Status;

if ((Status = Client->QueryInterface(__uuidof(IDebugControl),
(void **)&g_ExtControl)) != S_OK)
{
goto Fail;
}
if ((Status = Client->QueryInterface(__uuidof(IDebugSymbols2),
(void **)&g_ExtSymbols)) != S_OK)
{
goto Fail;
}
g_ExtClient = Client;

return S_OK;

Fail:
ExtRelease();
return Status;
}

https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/writing-an-analysis-extension-to-extend--analyze 中所说,Client->QueryInterface 获取IDebugControlIDebugSymbols2接口,然后将该接口赋值给第二个参数,那么接下来就可以通过第二个参数来使用该接口中的函数了

接下来是ExtRelease,位于dbgexts.cpp

1
2
3
4
5
6
7
8
// Cleans up all debugger interfaces.
void
ExtRelease(void)
{
g_ExtClient = NULL;
EXT_RELEASE(g_ExtControl);
EXT_RELEASE(g_ExtSymbols);
}

其中,EXT_RELEASE是一个宏,位于dbgexts.h

1
2
#define EXT_RELEASE(Unk) \
((Unk) != NULL ? ((Unk)->Release(), (Unk) = NULL) : NULL)

原来是调用了接口的Release函数,关于该函数微软并没有给出官方的解释,不过顾名思义,该函数的作用应该就是释放资源

接下来是ExtOut,位于dbgexts.cpp

1
2
3
4
5
6
7
8
9
10
// Normal output.
void __cdecl
ExtOut(PCSTR Format, ...)
{
va_list Args;

va_start(Args, Format);
g_ExtControl->OutputVaList(DEBUG_OUTPUT_NORMAL, Format, Args);
va_end(Args);
}

只见其使用了接口的OutputVaList函数,该函数设置字符串格式,并将结果发送到在引擎的客户端中注册的输出回调。大概就是可以依靠这个函数输出内容到调试器的显示界面或者交互界面

接下来是重头戏,位于dbgexts.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
extern "C"
HRESULT
CALLBACK
DebugExtensionInitialize(PULONG Version, PULONG Flags)
{
IDebugClient *DebugClient;
PDEBUG_CONTROL DebugControl;
HRESULT Hr;

*Version = DEBUG_EXTENSION_VERSION(1, 0);
*Flags = 0;
Hr = S_OK;

if ((Hr = DebugCreate(__uuidof(IDebugClient),
(void **)&DebugClient)) != S_OK)
{
return Hr;
}

if ((Hr = DebugClient->QueryInterface(__uuidof(IDebugControl),
(void **)&DebugControl)) == S_OK)
{

//
// Get the windbg-style extension APIS
//
ExtensionApis.nSize = sizeof (ExtensionApis);
Hr = DebugControl->GetWindbgExtensionApis64(&ExtensionApis);

DebugControl->Release();

}
DebugClient->Release();
return Hr;
}

DebugExtensionInitialize是一个回调函数,当调试器引擎导入了一个DbgEng拓展之后会被自动调用,目的是对拓展进行初始化

DEBUG_EXTENSION_VERSION返回一个版本号

1
2
3
4
// Returns a version with the major version in
// the high word and the minor version in the low word.
#define DEBUG_EXTENSION_VERSION(Major, Minor) \
((((Major) & 0xffff) << 16) | ((Minor) & 0xffff))

DebugCreate函数会创建一个新的client,其中第一个参数指定所需的调试器引擎客户端接口的接口标识符,函数执行成功会返回S_OK

ExtensionApis的定义为

1
WINDBG_EXTENSION_APIS   ExtensionApis;

WINDBG_EXTENSION_APIS是一个结构体,其定义位于WDBGEXTS.H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _WINDBG_EXTENSION_APIS64 {
ULONG nSize;
PWINDBG_OUTPUT_ROUTINE lpOutputRoutine;
PWINDBG_GET_EXPRESSION64 lpGetExpressionRoutine;
PWINDBG_GET_SYMBOL64 lpGetSymbolRoutine;
PWINDBG_DISASM64 lpDisasmRoutine;
PWINDBG_CHECK_CONTROL_C lpCheckControlCRoutine;
PWINDBG_READ_PROCESS_MEMORY_ROUTINE64 lpReadProcessMemoryRoutine;
PWINDBG_WRITE_PROCESS_MEMORY_ROUTINE64 lpWriteProcessMemoryRoutine;
PWINDBG_GET_THREAD_CONTEXT_ROUTINE lpGetThreadContextRoutine;
PWINDBG_SET_THREAD_CONTEXT_ROUTINE lpSetThreadContextRoutine;
PWINDBG_IOCTL_ROUTINE lpIoctlRoutine;
PWINDBG_STACKTRACE_ROUTINE64 lpStackTraceRoutine;
} WINDBG_EXTENSION_APIS64, *PWINDBG_EXTENSION_APIS64;

但是微软官方文档却并未对其有所说明

GetWindbgExtensionApis64函数会填充ExtensionApis这个结构,据文档所说:

If you are including Wdbgexts.h in your extension code, you should call this method during the initialization of the extension DLL

如果你的拓展代码中include了Wdbgexts.h,就应该在初始化拓展dll的时候调用这个函数

实际上,在DebugExtensionInitialize函数中,所做的操作就是各种初始化,初始化你的拓展所需要的全局变量、各种结构体等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
extern "C"
void
CALLBACK
DebugExtensionNotify(ULONG Notify, ULONG64 Argument)
{
UNREFERENCED_PARAMETER(Argument);

//
// The first time we actually connect to a target
//

if ((Notify == DEBUG_NOTIFY_SESSION_ACCESSIBLE) && (!Connected))
{
IDebugClient *DebugClient;
HRESULT Hr;
PDEBUG_CONTROL DebugControl;

if ((Hr = DebugCreate(__uuidof(IDebugClient),
(void **)&DebugClient)) == S_OK)
{
//
// Get the architecture type.
//

if ((Hr = DebugClient->QueryInterface(__uuidof(IDebugControl),
(void **)&DebugControl)) == S_OK)
{
if ((Hr = DebugControl->GetActualProcessorType(
&TargetMachine)) == S_OK)
{
Connected = TRUE;
}

NotifyOnTargetAccessible(DebugControl);

DebugControl->Release();
}

DebugClient->Release();
}
}


if (Notify == DEBUG_NOTIFY_SESSION_INACTIVE)
{
Connected = FALSE;
TargetMachine = 0;
}

return;
}

当会话更改其活动或可访问状态时,引擎将调用DebugExtensionNotify回调函数来通知扩展DLL。

文档中说,该函数是可选的,如果要在会话状态更改时,拓展能得到通知,则DbgEng扩展DLL需要导出此函数

该函数发生在会话开始和结束之间,或者目标执行的开始和结束之间

对于该函数的逻辑,我的理解是:

如果当前会话是可访问的,即当前会话处于暂停状态,应该就是windbg的break状态,就得到并输出当前物理处理器的处理器类型。如果没有会话是活动的,那么就重置存储当前会话的架构的变量

至于这里的实现有什么用呢?我认为其用处只是获取当前会话的架构,例如IA64X86

另外NotifyOnTargetAccessible函数是自定义的,并不是文档中的,位于exts.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
extern "C"
HRESULT
CALLBACK
KnownStructOutput(
_In_ ULONG Flag,
_In_ ULONG64 Address,
_In_ PSTR StructName,
_Out_writes_opt_(*BufferSize) PSTR Buffer,
_Inout_opt_ _When_(Flag == DEBUG_KNOWN_STRUCT_GET_NAMES, _Notnull_) _When_(Flag == DEBUG_KNOWN_STRUCT_GET_SINGLE_LINE_OUTPUT, _Notnull_) PULONG BufferSize
)
{
const char* KnownStructs[] = {"_LARGE_INTEGER", "_SYSTEMTIME", NULL};
HRESULT Hr = S_OK;

if ((Flag == DEBUG_KNOWN_STRUCT_GET_NAMES) && (Buffer != NULL) && (BufferSize != NULL))
{
//
// Return names of known structs in multi string
//
ULONG SizeRemaining = *BufferSize, SizeNeeded = 0, Length;
PCHAR CopyAt = Buffer;

for (ULONG i=0; KnownStructs[i] != NULL; i++)
{
if (SizeRemaining > (Length = (ULONG)strlen(KnownStructs[i]) + 1) &&
Hr == S_OK)
{
Hr = StringCbCopy(CopyAt, SizeRemaining, KnownStructs[i]);

SizeRemaining -= Length;
CopyAt += Length;
}
else
{
Hr = S_FALSE;
}
SizeNeeded += Length;
}
// Terminate multistring and return size copied
*CopyAt = 0;
*BufferSize = SizeNeeded+1;
}
else if ((Flag == DEBUG_KNOWN_STRUCT_GET_SINGLE_LINE_OUTPUT) && (Buffer != NULL) && (BufferSize != NULL))
{
if (!strcmp(StructName, KnownStructs[0]))
{
ULONG64 Data;
ULONG ret;

if (ReadMemory(Address, &Data, sizeof(Data), &ret))
{
Hr = StringCbPrintf(Buffer, *BufferSize, " { %lx`%lx }", (ULONG) (Data >> 32), (ULONG) Data);
}
else
{
Hr = E_INVALIDARG;
}
}
else if (!strcmp(StructName, KnownStructs[1]))
{
SYSTEMTIME Data;
ULONG ret;

if (ReadMemory(Address, &Data, sizeof(Data), &ret))
{
Hr = StringCbPrintf(Buffer, *BufferSize, " { %02lu:%02lu:%02lu %02lu/%02lu/%04lu }",
Data.wHour,
Data.wMinute,
Data.wSecond,
Data.wMonth,
Data.wDay,
Data.wYear);
}
else
{
Hr = E_INVALIDARG;
}
}
else
{
Hr = E_INVALIDARG;
}
}
else if (Flag == DEBUG_KNOWN_STRUCT_SUPPRESS_TYPE_NAME)
{
if (!strcmp(StructName, KnownStructs[0]))
{
// Do not print type name for KnownStructs[0]
Hr = S_OK;
}
else
{
// Print the type name
Hr = S_FALSE;
}

if (Buffer)
{
Buffer[0] = 0;
}
}
else
{
Hr = E_INVALIDARG;
}
return Hr;
}

关于文件中的KnownStructOutput函数,windows的文档并未说明,但是的确为其留了一席之地,希望后续可以进行说明

但是,在 http://www.dbgtech.net/windbghelp/hh/debugger/extensions_ref_dfff8fda-36a7-42ab-9ad7-1698c352c028.xml.htm 中对其有说明

接下来重点转到exts.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
HRESULT CALLBACK
cmdsample(PDEBUG_CLIENT4 Client, PCSTR args)
{
CHAR Input[256];
INIT_API();

UNREFERENCED_PARAMETER(args);

//
// Output a 10 frame stack
//
g_ExtControl->OutputStackTrace(DEBUG_OUTCTL_ALL_CLIENTS | // Flags on what to do with output
DEBUG_OUTCTL_OVERRIDE_MASK |
DEBUG_OUTCTL_NOT_LOGGED,
NULL,
10, // number of frames to display
DEBUG_STACK_FUNCTION_INFO | DEBUG_STACK_COLUMN_NAMES |
DEBUG_STACK_ARGUMENTS | DEBUG_STACK_FRAME_ADDRESSES);
//
// Engine interface for print
//
g_ExtControl->Output(DEBUG_OUTCTL_ALL_CLIENTS, "\n\nDebugger module list\n");

//
// list all the modules by executing lm command
//
g_ExtControl->Execute(DEBUG_OUTCTL_ALL_CLIENTS |
DEBUG_OUTCTL_OVERRIDE_MASK |
DEBUG_OUTCTL_NOT_LOGGED,
"lm", // Command to be executed
DEBUG_EXECUTE_DEFAULT );

//
// Ask for user input
//
g_ExtControl->Output(DEBUG_OUTCTL_ALL_CLIENTS, "\n\n***User Input sample\n\nEnter Command to run : ");
GetInputLine(NULL, &Input[0], sizeof(Input));
Input[ARRAYSIZE(Input) - 1] = 0; // Ensure null termination
g_ExtControl->Execute(DEBUG_OUTCTL_ALL_CLIENTS |
DEBUG_OUTCTL_OVERRIDE_MASK |
DEBUG_OUTCTL_NOT_LOGGED,
Input, // Command to be executed
DEBUG_EXECUTE_DEFAULT );

EXIT_API();
return S_OK;
}

该例向我们展示了如何获取从命令行进行的输入

从这个函数中我们可以学到,OutputStackTrace函数可以打印出类似kb这种命令的效果

Output函数可以打印输出

Execute函数可以执行指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
HRESULT CALLBACK
structsample(PDEBUG_CLIENT4 Client, PCSTR args)
{
ULONG64 Address;
INIT_API();

Address = GetExpression(args);

DWORD Buffer[4], cb;

// Read and display first 4 dwords at Address
if (ReadMemory(Address, &Buffer, sizeof(Buffer), &cb) && cb == sizeof(Buffer)) {
dprintf("%p: %08lx %08lx %08lx %08lx\n\n", Address,
Buffer[0], Buffer[1], Buffer[2], Buffer[3]);
}

//
// Method 1 to dump a struct
//
dprintf("Method 1:\n");
// Inititalze type read from the Address
if (InitTypeRead(Address, _EXCEPTION_RECORD) != 0) {
dprintf("Error in reading _EXCEPTION_RECORD at %p", // Use %p to print pointer values
Address);
} else {
// read and dump the fields
dprintf("_EXCEPTION_RECORD @ %p\n", Address);
dprintf("\tExceptionCode : %lx\n", (ULONG) ReadField(ExceptionCode));
dprintf("\tExceptionAddress : %p\n", ReadField(ExceptionAddress));
dprintf("\tExceptionInformation[1] : %I64lx\n", ReadField(ExceptionInformation[1]));
// And so on...
}

//
// Method 2 to read a struct
//
ULONG64 ExceptionInformation_1, ExceptionAddress, ExceptionCode;
dprintf("\n\nMethod 2:\n");
// Read and dump the fields by specifying type and address individually
if (GetFieldValue(Address, "_EXCEPTION_RECORD", "ExceptionCode", ExceptionCode)) {
dprintf("Error in reading _EXCEPTION_RECORD at %p\n",
Address);
} else {
// Pointers are read as ULONG64 values
GetFieldValue(Address, "_EXCEPTION_RECORD", "ExceptionAddress", ExceptionAddress);
GetFieldValue(Address, "_EXCEPTION_RECORD", "ExceptionInformation[1]", ExceptionInformation_1);
// And so on..

dprintf("_EXCEPTION_RECORD @ %p\n", Address);
dprintf("\tExceptionCode : %lx\n", ExceptionCode);
dprintf("\tExceptionAddress : %p\n", ExceptionAddress);
dprintf("\tExceptionInformation[1] : %I64lx\n", ExceptionInformation_1);
}

ULONG64 Module;
ULONG i, TypeId;
CHAR Name[MAX_PATH];
//
// To get/list field names
//
g_ExtSymbols->GetSymbolTypeId("_EXCEPTION_RECORD", &TypeId, &Module);
dprintf("Fields of _EXCEPTION_RECORD\n");
for (i=0; ;i++) {
HRESULT Hr;
ULONG Offset=0;

Hr = g_ExtSymbols->GetFieldName(Module, TypeId, i, Name, MAX_PATH, NULL);
if (Hr == S_OK) {
g_ExtSymbols->GetFieldOffset(Module, TypeId, Name, &Offset);
dprintf("%lx (+%03lx) %s\n", i, Offset, Name);
} else if (Hr == E_INVALIDARG) {
// All Fields done
break;
} else {
dprintf("GetFieldName Failed %lx\n", Hr);
break;
}
}

//
// Get name for an enumerate
//
// typedef enum {
// Enum1,
// Enum2,
// Enum3,
// } TEST_ENUM;
//
ULONG ValueOfEnum = 0;
g_ExtSymbols->GetSymbolTypeId("TEST_ENUM", &TypeId, &Module);
g_ExtSymbols->GetConstantName(Module, TypeId, ValueOfEnum, Name, MAX_PATH, NULL);
dprintf("Testenum %I64lx == %s\n", ExceptionCode, Name);
// This prints out, Testenum 0 == Enum1

//
// Read an array
//
// typedef struct FOO_TYPE {
// ULONG Bar;
// ULONG Bar2;
// } FOO_TYPE;
//
// FOO_TYPE sampleArray[20];
ULONG Bar, Bar2;
CHAR TypeName[100];
for (i=0; i<20; i++) {
sprintf_s(TypeName, sizeof(TypeName), "sampleArray[%lx]", i);
if (GetFieldValue(0, TypeName, "Bar", Bar))
break;
GetFieldValue(0, TypeName, "Bar2", Bar2);
dprintf("%16s - Bar %2ld Bar2 %ld\n", TypeName, Bar, Bar2);
}

EXIT_API();
return S_OK;
}

GetExpression函数可以返回表达式的值,因为该函数的目的是从指定地址读取数据然后将其按照结构体的格式输出,所以地址可能是一个表达式,因此这里使用了GetExpression函数来获取参数

ReadMemory函数可以获取指定地址的数据

InitTypeRead宏就文档所说,是可以将第一个参数即地址挂载为结构体类型即第二个参数,该函数执行完毕之后就可以使用ReadField宏来使用字段名来获取其对应的数据,这也是该示例函数所介绍的第一种打印结构体的方法

第二种方法是使用GetFieldValue函数来获取字段数据,挺直白的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/*
Extension to look at locals through IDebugSymbolGroup interface

Usage: !symgrptest [args]

To demontrate local/args lookup, this will go through the stack
and set scope with the instruction of the stack frame and
enumerate all the locals/arguments for the frame function
*/
HRESULT CALLBACK
symgrptest(PDEBUG_CLIENT4 Client, PCSTR args)
{
HRESULT hRes;
IDebugSymbolGroup *pDbgSymGroup;
ULONG ulCount, nFrames;
DEBUG_STACK_FRAME Stack[50];
ULONG SymGroupType;

INIT_API();

if (!_stricmp(args, "args"))
{
// Disply only the arguments
SymGroupType = DEBUG_SCOPE_GROUP_ARGUMENTS;
} else
{
// Display all the locals
SymGroupType = DEBUG_SCOPE_GROUP_LOCALS;
}

//
// Get the current stack
//
if ((hRes = g_ExtControl->GetStackTrace(0, 0, 0, &Stack[0], 50, &nFrames)) != S_OK) {
dprintf("Stacktrace failed - %lx\n", hRes);
nFrames = 0;
}

// Create a local symbol group client
if ((hRes = g_ExtSymbols->
GetScopeSymbolGroup(SymGroupType,
NULL, &pDbgSymGroup)) == E_NOTIMPL)
{
EXIT_API();
return S_OK;
}

while (nFrames) {
//
// Enumerate locals for this frame
//
--nFrames;

// Set scope at this frame
g_ExtSymbols->SetScope(0, &Stack[nFrames], NULL, 0);

// refresh the symbol group with current scope
if ((hRes = g_ExtSymbols->
GetScopeSymbolGroup(DEBUG_SCOPE_GROUP_LOCALS,
pDbgSymGroup, &pDbgSymGroup)) == E_NOTIMPL)
{
break;
}
hRes =
pDbgSymGroup->GetNumberSymbols ( &ulCount);

dprintf("\n\n>Scope Frame %lx: %lx Symbols\n",nFrames,ulCount);

PDEBUG_SYMBOL_PARAMETERS pSymParams = new DEBUG_SYMBOL_PARAMETERS[ ulCount ] ;
if (ulCount)
{
// Get all symbols for the frame
hRes =
pDbgSymGroup->GetSymbolParameters (0,
ulCount ,
pSymParams);
}
if ( S_OK == hRes )
{
for ( ULONG i = 0 ; i < ulCount ; i++ )
{
TCHAR szName[ MAX_PATH ], *pName;
ULONG ulSize;
DEBUG_VALUE Value;

// Lookup symbol name and print
pName = &szName[1];
hRes = pDbgSymGroup->GetSymbolName ( i,
pName,
MAX_PATH - 1,
&ulSize ) ;
if (SUCCEEDED(hRes))
{
// Prefix ! so this is evaluated as symbol
szName[0] = '!';
hRes = g_ExtControl->Evaluate(szName,
DEBUG_VALUE_INVALID,
&Value, NULL);
if (SUCCEEDED(hRes))
{
dprintf("%lx: %32s = 0x%p\n", i, pName, Value.I64);
}
}
}
}
delete []pSymParams;
}
pDbgSymGroup->Release();
EXIT_API();
return S_OK;
}

注释说,该函数的作用是通过IDebugSymbolGroup接口查找函数的参数

它首先通过GetStackTrace函数得到了栈帧,然后又通过GetScopeSymbolGroup函数得到了IDebugSymbolGroup接口

通过SetScope来设置当前栈帧的范围,我不懂的是为什么要设置为0?

通过GetScopeSymbolGroup来重新刷新接口,然后就可以通过GetNumberSymbols来得到当前参数个数,当然这些必须是在有符号的前提下

最后就可以通过GetSymbolParameters函数来获取参数的符号了,对应的,在参数名前加!并用Evalute函数,可以得到其值