向C/C++程序员介绍Windbg 脚本
来自Windows调试工具包的所有调试程序都使用相同的引擎dbgeng.dll。它包含一种特殊语言的脚本解释器,我们称之为WinDbg脚本语言以方便使用,我们对WinDbg脚本文件使用WDS文件扩展名。下面是在分析一个脚本时捕获的WinDbg线程的调用堆栈:
0:000> ~1kL 100 ChildEBP RetAddr 037cd084 6dd28cdc dbgeng!TypedData::ForceU64+0x3 037cd0ec 6dcbd08c dbgeng!GetPseudoOrRegVal+0x11c 037cd134 6dcbceff dbgeng!MasmEvalExpression::GetTerm+0x12c 037cd198 6dcbca23 dbgeng!MasmEvalExpression::GetMterm+0x36f 037cd1d4 6dcbc873 dbgeng!MasmEvalExpression::GetAterm+0x13 037cd220 6dcbc783 dbgeng!MasmEvalExpression::GetShiftTerm+0x13 037cd254 6dcbc523 dbgeng!MasmEvalExpression::GetLterm+0x13 037cd2c0 6dcbc443 dbgeng!MasmEvalExpression::GetLRterm+0x13 037cd2f4 6dcbc424 dbgeng!MasmEvalExpression::StartExpr+0x13 037cd308 6dcbbc2f dbgeng!MasmEvalExpression::GetCommonExpression+0xc4 037cd31c 6dccdca3 dbgeng!MasmEvalExpression::Evaluate+0x4f 037cd390 6dccd83d dbgeng!EvalExpression::EvalNum+0x63 037cd3d0 6dd293cc dbgeng!GetExpression+0x5d 037cd458 6dd2a7e2 dbgeng!ScanRegVal+0xfc 037cd4ec 6dd17502 dbgeng!ParseRegCmd+0x422 037cd52c 6dd194e8 dbgeng!WrapParseRegCmd+0x92 037cd608 6dc8ed19 dbgeng!ProcessCommands+0x1278 037cd644 6dc962af dbgeng!DotFor+0x1d9 037cd658 6dd1872e dbgeng!DotCommand+0x3f 037cd738 6dd19b49 dbgeng!ProcessCommands+0x4be 037cd77c 6dc5c879 dbgeng!ProcessCommandsAndCatch+0x49 037cdc14 6dd19cc3 dbgeng!Execute+0x2b9 037cdc64 6dc89db0 dbgeng!ProcessCurBraceBlock+0xa3 037cdc74 6dc962af dbgeng!DotBlock+0x10 037cdc88 6dd1872e dbgeng!DotCommand+0x3f 037cdd68 6dd19b49 dbgeng!ProcessCommands+0x4be 037cddac 6dc5c879 dbgeng!ProcessCommandsAndCatch+0x49 037ce244 6dd173ca dbgeng!Execute+0x2b9 037ce2c4 6dd1863c dbgeng!ParseDollar+0x29a 037ce3a0 6dd19b49 dbgeng!ProcessCommands+0x3cc 037ce3e4 6dc5c879 dbgeng!ProcessCommandsAndCatch+0x49 037ce87c 6dc5cada dbgeng!Execute+0x2b9 037ce8ac 00318693 dbgeng!DebugClient::ExecuteWide+0x6a 037ce954 00318b83 windbg!ProcessCommand+0x143 037cf968 0031ae46 windbg!ProcessEngineCommands+0xa3 037cf97c 76fa19f1 windbg!EngineLoop+0x366 037cf988 77c8d109 kernel32!BaseThreadInitThunk+0xe 037cf9c8 00000000 ntdll!_RtlUserThreadStart+0x23
Hello World
让我们写第一个脚本来打印这个著名的消息。
$$ HelloWorld.wds - Hello World script .block { .printf "Hello World!\n" }
此脚本是多行的,必须使用$><或$$><命令执行:
0:000> $$><c:\scripts\HelloWorld.wds Hello World!
当我们在WinDbg命令窗口中键入或使用$<或$$<命令从文件加载单行脚本时,可以执行这些脚本:
$$ Hello World script; .block { .printf "Hello World!\n" }
我们可以看到,在一行脚本中,除非命令或注释是最终的,否则注释和命令必须以分号结尾。如果命令位于单独的行上,则多行脚本不需要分号。
0:000> $$<c:\scripts\HelloWorld2.wds 0:000> $$ Hello World script; .block { .printf "Hello World!\n" } Hello World!
从现在起,我们将只使用多行脚本,因为它们的可读性。您可能已经注意到,我故意将.printf 放在.block{}括起来,使第一个脚本比需要的更复杂,以显示与C样式函数的相似性:
// Hello World function void helloWorld () { printf ("Hello World!\n"); }
简单算术
考虑一个简单的C样式函数,它打印2个数字的和并使用局部变量:
void sum () { unsigned long t1 = 2; unsigned long t2 = 3; unsigned long t0 = t1 + t2; printf("Sum(%x,%x) = %x\n", t1, t2, t0); }
在WinDbg脚本中,我们可以使用20个不同的用户定义变量,称为伪寄存器。他们的名字是$t0-$t19。如果要获取伪寄存器值,请使用@符号,例如,@$t0。我们可以使用.printf中的%p类型字段字符将该值解释为指针。这是等效的WinDbg脚本及其输出:
$$ Arithmetic1.wds - Calculate the sum of two predefined variables .block { r $t1 = 2 r $t2 = 3 r $t0 = @$t1 + @$t2 .printf "Sum(%p, %p) = %p\n", @$t1, @$t2, @$t0 }
0:000> $$><f:\Arithmetic1.wds Sum(00000002, 00000003) = 00000005
使用硬编码值是没有用的。让我们重写同一个函数来使用参数。与WinDbg脚本中的函数参数等价的是字符串的$arg1-$argN别名。要获取别名值,请将其括在${…}中,例如${$arg1}。但是,如果在某些表达式中使用它,并且参数的类型可以从其他参与操作数推断出来,则不需要将其括起来。
$$ Arithmetic2.wds - Calculate the sum of two function arguments .block { r $t0 = $arg1 + $arg2 .printf "Sum(%p, %p) = %p\n", ${$arg1}, ${$arg2}, @$t0 }
现在我们可以调用脚本并指定参数:
0:000> $$>a<f:\Arithmetic2.wds 1 2 Sum(00000001, 00000002) = 00000003
如果缺少某些参数,则会出现错误:
0:000> $$><f:\Arithmetic2.wds Couldn't resolve error at '$arg1 + $arg2 ; .printf "Sum(%p, %p) = %p\n", ${$arg1}, ${$arg2}, @$t0 ;'
WinDbg允许我们检查是否定义了参数。这可以通过别名解释器${/d:…}的特殊形式完成:
$$ Arithmetic3.wds - Calculate the sum of two optional function arguments .block { r $t1 = 0 .if (${/d:$arg1}) { r $t1 = $arg1 } r $t2 = 0 .if (${/d:$arg2}) { r $t2 = $arg2 } .printf "Sum(%p, %p) = %p\n", @$t1, @$t2, @$t1+@$t2 }
以下是一些参数的脚本输出:
0:000> $$>a<f:\Arithmetic3.wds Sum(00000000, 00000000) = 00000000 0:000> $$>a<f:\Arithmetic3.wds 1 Sum(00000001, 00000000) = 00000001 0:000> $$>a<f:\Arithmetic3.wds 1 2 Sum(00000001, 00000002) = 00000003
递归&循环
让我们编写更复杂的脚本来计算给定数字的阶乘。回想一下阶乘函数的以下定义:
n! = 1*2*3*4*…*(n-2)*(n-1)*n
此函数可以使用以下代码递归计算:
// C-style factorial function using recursion unsigned long factorial (unsigned long n) { unsigned long f = 0; if (n > 1) { f = n*factorial(n-1); } else { f = 1; } return f; }
或者,可以使用while或for循环计算:
// C-style factorial function using a “while” loop unsigned long factorial (unsigned long n) { unsigned long k=1; while (n-1) { k = k * n; --n; } return k; } // C-style factorial function using a “for” loop unsigned long factorial2 (unsigned long n) { unsigned long k=1; for (; n-1; --n) { k = k * n; } return k; }
WinDbg脚本也可以递归调用。我们可以将C风格的代码映射到WinDbg脚本,其中$t0伪寄存器用于模拟函数返回值:
$$ FactorialR.wds - Calculate factorial using recursion .block { .if (${$arg1} > 1) { $$>a<c:\scripts\FactorialR.wds ${$arg1}-1 r $t1 = $arg1 r $t0 = @$t1 * @$t0 } .else { r $t0 = 1 } .printf "Factorial(%p) = %p\n", ${$arg1}, @$t0 }
某些参数的脚本输出:
0:000> $$>a<c:\scripts\FactorialR.wds 1 Factorial(0000000000000001) = 0000000000000001 0:000> $$>a<c:\scripts\FactorialR.wds 2 Factorial(0000000000000001) = 0000000000000001 Factorial(0000000000000002) = 0000000000000002 0:000> $$>a<c:\scripts\FactorialR.wds 3 Factorial(0000000000000001) = 0000000000000001 Factorial(0000000000000002) = 0000000000000002 Factorial(0000000000000003) = 0000000000000006 0:000> $$>a<c:\scripts\FactorialR.wds 4 Factorial(0000000000000001) = 0000000000000001 Factorial(0000000000000002) = 0000000000000002 Factorial(0000000000000003) = 0000000000000006 Factorial(0000000000000004) = 0000000000000018 0:000> $$>a<c:\scripts\FactorialR.wds 10 Factorial(0000000000000001) = 0000000000000001 Factorial(0000000000000002) = 0000000000000002 Factorial(0000000000000003) = 0000000000000006 Factorial(0000000000000004) = 0000000000000018 Factorial(0000000000000005) = 0000000000000078 Factorial(0000000000000006) = 00000000000002d0 Factorial(0000000000000007) = 00000000000013b0 Factorial(0000000000000008) = 0000000000009d80 Factorial(0000000000000009) = 0000000000058980 Factorial(000000000000000a) = 0000000000375f00 Factorial(000000000000000b) = 0000000002611500 Factorial(000000000000000c) = 000000001c8cfc00 Factorial(000000000000000d) = 000000017328cc00 Factorial(000000000000000e) = 000000144c3b2800 Factorial(000000000000000f) = 0000013077775800 Factorial(0000000000000010) = 0000130777758000
现在我们准备使用while循环重写脚本。
$$ FactorialL.wds - Calculate factorial using a "while" loop .block { r $t0 = 1 r $t1 = $arg1 .while (@$t1-1) { r $t0 = @$t0 * @$t1 r $t1 = @$t1 - 1 } .printf "Factorial(%p) = %p\n", ${$arg1}, @$t0 }
脚本参数输出:
0:000> $$>a<f:\FactorialL.wds 10 Factorial(00000010) = 77758000
我们可以使用.for循环令牌简化脚本:
$$ FactorialL2.wds - Calculate factorial using a "for" loop .block { .for (r $t0 = 1, $t1 = $arg1; @$t1-1; r $t1 = @$t1 - 1) { r $t0 = @$t0 * @$t1 } .printf "Factorial(%p) = %p\n", ${$arg1}, @$t0 }
其输出相同:
0:000> $$>a<f:\FactorialL2.wds 10 Factorial(00000010) = 77758000