在前面的文章使用PowerDbg自动化Windbg调试过程里,简单介绍了如何使用PowerDbg自动化一些Windbg的命令。但问题是,PowerDbg自身提供的命令太少了,幸好PowerDbg提供了源代码,可以让我们了解它是如何工作的,因此我也有机会自己扩展PowerDbg。
上次在用Windbg调试一个问题的时候,需要查看一个Dictionary对象里面所有的值,用来确认有些特殊的值是否被正确的加入到Dictionary对象里。本来是用Visual Studio调试这个问题的,但是后面发现Visual Studio实在是太慢了—其实我调试的程序就是Visual Studio本身,只不过用另外一个Visual Studio调试它而已。在没有符号文件和源代码的情况下,在Visual Studio里面设置一个托管代码的函数断点的速度的确很慢。不得已,只好切换到Windbg,但是同时就没有了Visual Studio强大的变量显示的功能(Visualizer)。
在Windbg里面,如果要查看Dictionary对象的值,一般是通过下面几个命令实现的:
# 1. 查看Dictionary对象本身的值,找到保存元素的槽(bucket)。
0:000> !do 018e2e0c Name: System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]] MethodTable: 002b201c EEClass: 62e00e18 Size: 52(0x34) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) Fields: MT Field Offset Type VT Attr Value Name 6304aa5c 40009af 4 System.Int32[] 0 instance 018e30e0 buckets 00000000 40009b0 8 SZARRAY 0 instance 018e30f8 entries 6304ab0c 40009b1 20 System.Int32 1 instance 2 count 6304ab0c 40009b2 24 System.Int32 1 instance 2 version 6304ab0c 40009b3 28 System.Int32 1 instance -1 freeList 6304ab0c 40009b4 2c System.Int32 1 instance 0 freeCount 00000000 40009b5 c 0 instance 018e2e7c comparer 00000000 40009b6 10 0 instance 00000000 keys 00000000 40009b7 14 0 instance 00000000 values 630484dc 40009b8 18 System.Object 0 instance 00000000 _syncRoot 6302f3d0 40009b9 1c ...SerializationInfo 0 instance 00000000 m_siInfo # 2. 使用!DumpArray打印数组并且显示每个元素的详细信息。 0:000> !da -details 018e30f8 Name: System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]][] MethodTable: 002b23e8 EEClass: 002b2368 Size: 60(0x3c) bytes Array: Rank 1, Number of elements 3, Type VALUETYPE Element Methodtable: 002b2318 # 每个元素的详细信息 [0] 018e3100 Name: System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]] MethodTable 002b2318 EEClass: 62e00f5c Size: 24(0x18) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) Fields: MT Field Offset Type VT Attr Value Name 6304ab0c 40009ba 8 System.Int32 1 instance 1497890914 hashCode 6304ab0c 40009bb c System.Int32 1 instance -1 next # 找到键值对以及他们的地址 63048530 40009bc 0 System.__Canon 0 instance 018e2c08 key 63048530 40009bd 4 System.__Canon 0 instance 018e2e88 value [1] 018e3110 Name: System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]] MethodTable 002b2318 EEClass: 62e00f5c Size: 24(0x18) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) Fields: MT Field Offset Type VT Attr Value Name 6304ab0c 40009ba 8 System.Int32 1 instance 1306722402 hashCode 6304ab0c 40009bb c System.Int32 1 instance -1 next 63048530 40009bc 0 System.__Canon 0 instance 018e2dbc key 63048530 40009bd 4 System.__Canon 0 instance 018e3134 value [2] 018e3120 Name: System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib]] MethodTable 002b2318 EEClass: 62e00f5c Size: 24(0x18) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) Fields: MT Field Offset Type VT Attr Value Name 6304ab0c 40009ba 8 System.Int32 1 instance 0 hashCode 6304ab0c 40009bb c System.Int32 1 instance 0 next 63048530 40009bc 0 System.__Canon 0 instance 00000000 key 63048530 40009bd 4 System.__Canon 0 instance 00000000 value # 3. 查看键值对的详细信息,这一步可以通过!DumpObject命令完成。 0:000> !do 018e2c08 Name: System.String MethodTable: 630488c0 EEClass: 62e0a498 Size: 26(0x1a) bytes (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: key1 Fields: MT Field Offset Type VT Attr Value Name 6304ab0c 4000096 4 System.Int32 1 instance 5 m_arrayLength 6304ab0c 4000097 8 System.Int32 1 instance 4 m_stringLength 630495a0 4000098 c System.Char 1 instance 6b m_firstChar 630488c0 4000099 10 System.String 0 shared static Empty >> Domain:Value 003829d0:018e1198 << 630494f0 400009a 14 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 003829d0:018e1924 << # 4. 针对Dictionary对象的每一个元素,重复第二步和第三步。 |
从上表里面可以看出,在Windbg里面查看一个Dictionary对象的确不是一件轻松的事情,特别是在Dictionary对象的Value参数也是一个 Dictionary对象的时候,那就更痛苦了。
我当时调试那个问题的时候,就是使用!DumpArray、!DumpObject以及DU(用来显示字符串)命令手工遍历一个40个元素的Dictionary对象。分析完那一个问题以后,我决定再也不做类似的事情了!
因此我用PowerShell结合PowerDbg已有的命令写了下面一个脚本,用来自动递归打印Dictionary对象里面所有的元素(里面有必要的注释),如果要使用这个脚本,只需要把下面的代码合并到PowerDbg的源代码里面就好了。如果你连合并都懒得做,没关系,下面的链接是已经合并好的代码:
/Files/killmyday/ParseDumpDic.zip
# # 这个函数就是入口函数,你需要提供一个Dictionary对象的地址,这个地址你需要自己去查看Windbg+SOS # 的输出才能找到,它只支持x86平台,如果需要支持x64平台,那你需要自己修改一下下面的脚本 # function Parse-PowerDbgDUMPDIC([string] $address = $(throw "Error! You must provide the address of Dictionaryo object.")) { set-psdebug -strict $ErrorActionPreference = "stop" trap {"Error message: $_"}
# 根据Dictionary对象的地址组合一个SOS命令(!DumpArray),因为Dictionary对象保存键值对数组 # 的属性距离Dictionary对象的地址有8个字节的位置(如果是64位系统,就是16个字节)。 # # poi命令是Windbg用来获取一个指针指向的内存的内容。这是因为Dictionary对象的地址加上8个字节 # 的偏移量,只是获取了键值对数组的属性的地址,而不是键值对数组的地址。 $cmd = "!da -details poi(0x{0:x8}+8)" -f $address # 保存最后格式化的结果,创建的是一个.NET的StringBuilder对象 $builder = New-Object System.Text.StringBuilder $builder.AppendLine("key,value")
Invoke-WindbgDUMPARR $cmd $builder 0 return $builder.ToString() }
function Invoke-WindbgDUMPARR([string] $cmd = $(throw "Error! You must provide windbg !DumpArray command to invoke."), [System.Text.StringBuilder] $builder = $(throw "Error! You must provide buffer for result."), [int] $level) {
# 将格式化好的Windbg命令发送到windbg远程调试服务器中执行 Invoke-WinDbgCommand $cmd # 执行完毕以后,$global:g_commandOutput全局变量保存了Windbg的输出 # 注意,Invoke-WinDbgCommand只会将上一次命令的输出保存在这个变量里面 # 至于它是如何做到的,你可以阅读Invoke-WinDbgCommand的源代码 $stringReader = [System.IO.StringReader] $global:g_commandOutput
# 一行行处理Windbg输出,使用正则表达式提取出我们需要的信息,例如 # 变量类型,变量地址甚至是变量的值 while(($line = $stringReader.ReadLine()) -ne $null) { # 递归处理键值对数组里面的每一个键值对 if ($line -match "^\s*\[\d+\]\s+(?<addr>[0-9a-fA-F]+)$") { # 获取键值对的地址,后面我们可以用!DumpObject命令来处理它 $addr = $matches["addr"]
$line = $stringReader.ReadLine(); if ( $line -eq $null ) { throw "Errors! There is error in Windbg output. Expect more output for dictionary entry." }
# 获取键值对的类型,因为我期望这个程序可以处理尽量多的从Dictionary<,>派生出来 # 的类型。 if ( $line -match "Name:\s+(?<type>(.+))" ) { Parse-PowerDbgDictionary $addr $matches["type"] $builder $level } } } }
# 这个命令模板用来打印键值对里面的键(Key)的值,注意,它只处理字符串类型 # 如果指定的键值对地址是一个空值(NULL),则什么都不做。因为Dictionary对象采取 # 和ArrayList相似的动态扩展内存的逻辑,键值对数组并不一定都是满的 $global:g_WindbgViewKeyCmd = "j poi(0x{0:8})=0 ;du poi(0x{0:8})+c" # 这个命令模板用来打印键值对里面的值(Value)的值,注意,它只处理字符串类型 $global:g_WindbgViewValueCmd = "j poi(0x{0:8})=0 ;du poi(0x{0:8}+4)+c" # 如果Dictionary对象的值类型不是字符串(String)类型的话,而是另外一个Dictionary对象的话 # 下面这个命令模板用来递归处理这个Dictionary对象 $global:g_WindbgDumpArrCmd = "j poi(0x{0:8})=0 ;!da -details poi(poi(0x{0:8}+4)+8)" function Parse-PowerDbgDictionary( [string] $objAddr = $(throw "Error! You must provide the address of Dictionaryo object."), [string] $typeName = $(throw "Error! you must provide full type name of Dictionary entry."), [System.Text.StringBuilder] $builder = $(throw "Error! You must provide buffer for result."), [int] $level) { # 根据键值对的类型获取键(Key)的类型和值(Value)的类型 $result = Parse-PowerDbgDictionaryEntry $typeName $keyType = $result[0] $valueType = $result[1]
# 只处理键(Key)类型为字符串的情况 if ( [String]::Compare($keyType, 0, "System.String", 0, "System.String".Length) -eq 0 ) { $cmd = $global:g_WindbgViewKeyCmd -f $objAddr Invoke-WinDbgCommand $cmd
if ( $global:g_commandOutput -match "[0-9A-Za-z]{8}\s+""(?<text>.+)""\s*$" ) { $builder.AppendLine("") for ( [int]$i = 0; $i -lt $level; $i++ ) { $builder.Append(" ,") }
$builder.Append($matches["text"]) } }
# 如果值(Value)类型为字符串的话,打印出它的值 if ( [String]::Compare($valueType, 0, "System.String", 0, "System.String".Length) -eq 0 ) { $cmd = $global:g_WindbgViewValueCmd -f $objAddr Invoke-WinDbgCommand $cmd
if ( $global:g_commandOutput -match "[0-9A-Za-z]{8}\s+""(?<text>.+)""\s*$" ) { $builder.Append(" = ") $builder.Append($matches["text"]) } } # 看看值(Value)类型是不是另外一个Dictionary对象 elseif ([String]::Compare($valueType, 0, "System.Collections.Generic.Dictionary", 0, "System.Collections.Generic.Dictionary".Length) -eq 0) { $cmd = $global:g_WindbgDumpArrCmd -f $objAddr $level = $level + 1 # 是的话,递归处理这个Dictionary对象,然后再打印下一个键值对 Invoke-WindbgDUMPARR $cmd $builder $level } else { throw "Value type {0} is not supported." -f $valueType } }
$global:g_WindbgGenericDicName = "System.Collections.Generic.Dictionary"
function Parse-PowerDbgDictionaryEntry( [string] $typeName = $(throw "Error! you must provide full type name of Dictionary entry.")) { if([String]::Compare($typeName, 0, $global:g_WindbgGenericDicName, 0, $global:g_WindbgGenericDicName.Length) -ne 0) { throw "Error! Just Dictionary or generic Dictionary are supported." }
$typeName = $typeName -replace "\[\]", "" if ( $typeName -match "^[^\[\]]*(((?'Open'\[)(?<key>[^\[\]]*))+((?'Close-Open'\])[^\[\]]*)+)*$" ) { $keyType = $matches["key"] $valueType = $matches["Close"] # skip [$keyType], and get the result $valueType = $valueType.SubString($keyType.Length + 3) $valueType = $valueType.SubString(1, $valueType.Length - 2)
# output the type of DictionaryEntry.Key $keyType # output the type of DictionaryEntry.Value $valueType } else { throw "Error! Parenthese in input DictionaryEntry's type name are not balanced." } }
# 将里面的函数导出,这样可以在PowerShell里面使用下面这几个函数 Export-ModuleMember -Function Parse-PowerDbgDUMPDIC Export-ModuleMember -Function Parse-PowerDbgDictionaryEntry Export-ModuleMember -Function Invoke-WindbgDUMPARR Export-ModuleMember -Function Parse-PowerDbgDictionary |
下面是一个输出的例子 ( 脚本里面有一个Bug,不知道为什么那个Capacity等东西被PowerShell打印出来了,但是不影响我的使用,就放在那里没理它了):
> $result = Parse-PowerDbgDUMPDIC "018e2e0c"
> $result
Capacity MaxCapacity Length
-------- ----------- ------
512 2147483647 275
key,value
key1
,subkey1 = value1
,subkey2 = value2
,subkey3 = value3
,subkey4 = value4
,subkey5 = value5
,subkey6 = value6
key2
,subkey-1 = value-1
,subkey-2 = value-2
,subkey-3 = value-3
,subkey-4 = value-4
,subkey-5 = value-5
,subkey-6 = value-6