windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
windbg에서 프로그램을 분석할 때 Hashtable이 나오면 기본 windbg + sos 명령어로는 분석이 (어려운 게 아니고) 지겹습니다. 예를 하나 들어 볼까요? ^^
using System;
using System.Collections;
class Program
{
static unsafe void Main(string[] args)
{
Hashtable hash = new Hashtable();
hash["TEST"] = "1abc";
hash["qwer"] = "2def";
TypedReference tr = __makeref(hash);
IntPtr ptr = **(IntPtr**)(&tr);
Console.WriteLine(ptr.ToInt64().ToString("x")); // 1bfd8672f40
Console.WriteLine(hash);
Console.WriteLine("debug this...");
Console.ReadLine();
}
}
저렇게
TypedReference를 활용하면 객체의 GC Heap 위치(주소)를 알 수 있습니다. 물론 현실적으로 메모리 덤프 파일을 분석하는 경우에는 직접 저 주소를 구해야 하지만,
windbg - x64 덤프 분석 시 메서드의 인자 또는 로컬 변수의 값을 확인하는 방법
; https://www.sysnet.pe.kr/2/0/12069
이번에는 어찌어찌 구했다고 가정하고 진행해 보겠습니다. 일단 windbg로 (1bfd8672f40로 출력된) 객체를 덤프해 보면,
0:005> .loadby sos clr
0:005> !do 1bfd8672f40
Name: System.Collections.Hashtable
MethodTable: 00007ffdc3079118
EEClass: 00007ffdc3184b88
Size: 80(0x50) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdc3074628 4001830 8 ...ashtable+bucket[] 0 instance 000001bfd8672f90 buckets
00007ffdc30780f8 4001831 30 System.Int32 1 instance 2 count
00007ffdc30780f8 4001832 34 System.Int32 1 instance 1 occupancy
00007ffdc30780f8 4001833 38 System.Int32 1 instance 2 loadsize
00007ffdc307e7f0 4001834 3c System.Single 1 instance 0.720000 loadFactor
00007ffdc30780f8 4001835 40 System.Int32 1 instance 2 version
00007ffdc307e598 4001836 44 System.Boolean 1 instance 0 isWriterInProgress
00007ffdc3079aa8 4001837 10 ...tions.ICollection 0 instance 0000000000000000 keys
00007ffdc3079aa8 4001838 18 ...tions.ICollection 0 instance 0000000000000000 values
00007ffdc2fede18 4001839 20 ...IEqualityComparer 0 instance 0000000000000000 _keycomparer
00007ffdc3075f88 400183a 28 System.Object 0 instance 0000000000000000 _syncRoot
Hashtable에 저장된 buckets 필드를 확인할 수 있습니다. 그리고 그 값을 다시 덤프해 보면,
0:005> !DumpObj /d 000001bfd8672f90
Name: System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass: 00007ffdc3183448
Size: 96(0x60) bytes
Array: Rank 1, Number of elements 3, Type VALUETYPE (Print Array)
Fields:
None
위와 같이 나오는데 원래 windbg에서는 "
Debugger Markup Language" 덕분에 "Print Array"가 링크로 제공되므로 사용자는 이후의 작업을 그냥 "Print Array"를 눌러 다음과 같이 bucket [] 배열의 원소 값을 확인할 수 있습니다.
0:005> !DumpArray /d 000001bfd8672f90
Name: System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass: 00007ffdc3183448
Size: 96(0x60) bytes
Array: Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffdc30746a8
[0] 000001bfd8672fa0
[1] 000001bfd8672fb8
[2] 000001bfd8672fd0
그러니까, 다음과 같은 식으로 링크를 따라 이동하므로 편리하게 디버깅을 할 수 있습니다.
그런데, 저 링크에서 배열 요소의 값을 확인하기 위해 링크를 타고 들어가도 "Fields:" 영역은 비어서 나옵니다.
0:005> !DumpVC /d 00007ffdc3074628 000001bfd8672fa0
Name: System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass: 00007ffdc3183448
Size: 24(0x18) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
왜냐하면, 올바르지 않게 DML이 구성되어 있기 때문입니다.
!DumpVC 명령어는 원래 !DumpObject 명령과 출력은 비슷하지만 부가적으로 "MethodTable"을 함께 지정해 주소의 값을 해석하는 기능을 가집니다.
!dumpvc [methodtable] [object_address]
그런데 위의 DumpVC 명령어가 받아들인 methodtable == 00007ffdc3074628 값은 "System.Collections.Hashtable+bucket"이 아닌 "System.Collections.Hashtable+bucket[]" 타입을 가리키기 때문에 정상적으로 배열 요소의 값이 해석이 안 된 것입니다. 따라서 이것을 제대로 해석하려면 "!DumpArray"의 출력 결과에 "Element Methodtable: ...." 값이 나오는데 바로 그 값(예제의 경우 00007ffdc30746a8)이 System.Collections.Hashtable+bucket 타입의 MethodTable이므로 다음과 같이 명령을 내리면 요솟값을 확인할 수 있습니다.
0:005> !DumpVC /d 00007ffdc30746a8 000001bfd8672fa0
Name: System.Collections.Hashtable+bucket
MethodTable: 00007ffdc30746a8
EEClass: 00007ffdc32b42c0
Size: 40(0x28) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdc3075f88 4003483 0 System.Object 0 instance 000001bfd8672e48 key
00007ffdc3075f88 4003484 8 System.Object 0 instance 000001bfd8672e70 val
00007ffdc30780f8 4003485 10 System.Int32 1 instance -1371534266 hash_coll
그리고 출력된 key나 val의 값을 덤프해 들어가면 저장한 값을 개별적으로 확인할 수 있습니다.
0:005> !DumpObj /d 000001bfd8672e48
Name: System.String
MethodTable: 00007ffdc3075b70
EEClass: 00007ffdc2fd6808
Size: 34(0x22) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: TEST
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdc30780f8 4000283 8 System.Int32 1 instance 4 m_stringLength
00007ffdc30769e8 4000284 c System.Char 1 instance 54 m_firstChar
00007ffdc3075b70 4000288 e0 System.String 0 shared static Empty
>> Domain:Value 000001bfd69d5d50:NotInit <<
0:005> !DumpObj /d 000001bfd8672e70
Name: System.String
MethodTable: 00007ffdc3075b70
EEClass: 00007ffdc2fd6808
Size: 34(0x22) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: 1abc
Fields:
MT Field Offset Type VT Attr Value Name
00007ffdc30780f8 4000283 8 System.Int32 1 instance 4 m_stringLength
00007ffdc30769e8 4000284 c System.Char 1 instance 31 m_firstChar
00007ffdc3075b70 4000288 e0 System.String 0 shared static Empty
>> Domain:Value 000001bfd69d5d50:NotInit <<
그런데, Hashtable에 보관한 값이 몇 개 이하라면 저런 식으로 일일이 확인을 해보겠지만 수십 개 이상이 되면 저렇게 확인하는 것이 매우 불편합니다. 따라서 이런 경우에는
pykd를 이용해,
windbg에서 python 스크립트 실행하는 방법 - pykd
; https://www.sysnet.pe.kr/2/0/11227
자동화할 수 있을 것입니다. 그전에, GC Heap 객체가 저장된 특성을 이용하면 pykd를 이용한 텍스트 파싱 작업이 꽤나(그나마) 간단해질 수 있습니다. 우선, DumpArray의 명령에서 본 것처럼,
0:005> !DumpArray /d 000001bfd8672f90
Name: System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass: 00007ffdc3183448
Size: 96(0x60) bytes
Array: Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffdc30746a8
[0] 000001bfd8672fa0
[1] 000001bfd8672fb8
[2] 000001bfd8672fd0
bucket 타입은 struct 형이기 때문에 값의 요소가 GC Heap 메모리에 연속적으로 보관되어 있습니다. 따라서, bucket 타입의 3개 필드 값을 다음과 같은 식으로 한 라인에 요소 하나씩 출력할 수 있습니다.
0:005> dq /c3 000001d500002fa0 L9
000001d5`00002fa0 000001d5`00002e48 000001d5`00002e70 00000000`ae400c46
000001d5`00002fb8 00000000`00000000 00000000`00000000 00000000`00000000
000001d5`00002fd0 000001d5`00002e98 000001d5`00002ec0 00000000`34a6e611
즉, 첫 번째 요소([1] 000001bfd8672fa0)의 값은 key == 000001d5`00002e48, value == 000001d5`00002e70이 됩니다.
그리고 key와 value가 string 타입이라는 것을 알고 있으면, 우리는 string 객체가
GC Heap에 저장될 때 sizeof(int) * 3 만큼의 값이 Object Header에 해당한다는 것을 알고 있기 때문에 개별 문자열은 [주소 + 0x0c] 위치를 이용해 곧바로 덤프할 수 있습니다. 따라서 key, value의 문자열은 이렇게 구할 수 있습니다.
0:005> du 000001d5`00002e48+c
000001d5`00002e54 "TEST"
0:005> du 000001d5`00002e70+c
000001d5`00002e7c "1abc"
이것을 종합하면, Hashtable의 요소가 key = string, value = string으로 이뤄져 있다면 다음의 python 스크립트를 이용해 일괄적으로 bucket 배열의 값을 덤프할 수 있습니다.
import argparse
from pykd import *
def getItem(text, index):
result = text.split(" ")[index + 1].strip()
return result
def isValidAddress(text):
text = text.replace("`", "")
if (int(text, 16) == 0):
return False
return True
def ToValidText(output):
if (len(output.split("\"??????????")) >= 2):
return "(non-string)"
return output
def action(elem0Address, elemCount):
totalCount = elemCount * 3
outputText = pykd.dbgCommand("dq /c3 " + elem0Address + " L" + hex(totalCount))
index = -1
for line in outputText.splitlines():
index = index + 1
keyAddress = getItem(line, 1)
valueAddress = getItem(line, 2)
if (isValidAddress(keyAddress) == False or isValidAddress(valueAddress) == False):
print("[" + str(index) + "] (empty)")
continue
keyText = pykd.dbgCommand("du " + keyAddress + "+c").split(" ")[1].strip()
valueText = pykd.dbgCommand("du " + valueAddress + "+c").split(" ")[1].strip()
keyText = ToValidText(keyText).split("\n")[0]
valueText = ToValidText(valueText).split("\n")[0]
dprintln("[" + str(index) + "] key: <link cmd=\"!do " + keyAddress + " \">" + keyText + "</link>, value: <link cmd=\"!do " + valueAddress + " \">" + valueText + "</link>", True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('address', type=str, help='hashtable.Element[0] address')
parser.add_argument('count', type=int, help='# of Element')
args = parser.parse_args()
action(args.address, args.count)
if __name__ == "__main__":
main()
다음은 위의 스크립트를 이용해 덤프한 결과입니다.
0:005> .load d:\pykd\x64\pykd.dll
0:005> !DumpArray /d 000001bfd8672f90
Name: System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass: 00007ffdc3183448
Size: 96(0x60) bytes
Array: Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffdc30746a8
[0] 000001bfd8672fa0
[1] 000001bfd8672fb8
[2] 000001bfd8672fd0
0:005> !py d:\pykd\do_hashtable.py 000001bfd8672fa0 3
[0] key: "TEST", value: "1abc"
[1] (empty)
[2] key: "qwer", value: "2def"
게다가 출력 결과에 DML을 사용했기 때문에 다음과 같이 개별 값에 대한 "!do"를 실행할 수 있습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]