Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 3개 있습니다.)

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

그러니까, 다음과 같은 식으로 링크를 따라 이동하므로 편리하게 디버깅을 할 수 있습니다.

dump_hashtable_1.png

그런데, 저 링크에서 배열 요소의 값을 확인하기 위해 링크를 타고 들어가도 "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"를 실행할 수 있습니다.

dump_hashtable_2.png




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/21/2019]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2020-12-22 04시29분
rodneyviana/netext
; https://github.com/rodneyviana/netext#whash---dump-items-in-a-hash-table

!whash [...addr_of_hashtable_instance...]
정성태

... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12702정성태7/11/20218722오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/20218356.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/20218775.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/20217550VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202111517오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/20217413개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/20217962VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
12695정성태7/3/20218025VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
12694정성태7/2/20219934Java: 24. Azure - Spring Boot 앱을 Java SE(Embedded Web Server)로 호스팅 시 로그 파일 남기는 방법 [1]
12693정성태6/30/20217713오류 유형: 731. Azure Web App Site Extension - Failed to install web app extension [...]. {1}
12692정성태6/30/20217588디버깅 기술: 180. Azure - Web App의 비정상 종료 시 남겨지는 로그 확인
12691정성태6/30/20218454개발 환경 구성: 573. 테스트 용도이지만 테스트에 적합하지 않은 Azure D1 공유(shared) 요금제
12690정성태6/28/20219260Java: 23. Azure - 자바(Java)로 만드는 Web App Service - Tomcat 호스팅
12689정성태6/25/20219803오류 유형: 730. Windows Forms 디자이너 - The class Form1 can be designed, but is not the first class in the file. [1]
12688정성태6/24/20219499.NET Framework: 1073. C# - JSON 역/직렬화 시 리플렉션 손실을 없애는 JsonSrcGen [2]파일 다운로드1
12687정성태6/22/20217516오류 유형: 729. Invalid data: Invalid artifact, java se app service only supports .jar artifact
12686정성태6/21/20219934Java: 22. Azure - 자바(Java)로 만드는 Web App Service - Java SE (Embedded Web Server) 호스팅
12685정성태6/21/202110154Java: 21. Azure Web App Service에 배포된 Java 프로세스의 메모리 및 힙(Heap) 덤프 뜨는 방법
12684정성태6/19/20218615오류 유형: 728. Visual Studio 2022부터 DTE.get_Properties 속성 접근 시 System.MissingMethodException 예외 발생
12683정성태6/18/202110110VS.NET IDE: 166. Visual Studio 2022 - Windows Forms 프로젝트의 x86 DLL 컨트롤이 Designer에서 오류가 발생하는 문제 [1]파일 다운로드1
12682정성태6/18/20217800VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
12681정성태6/18/20217124오류 유형: 727. .NET 2.0 ~ 3.5 + x64 환경에서 System.EnterpriseServices 참조 시 CS8012 경고
12680정성태6/18/20218233오류 유형: 726. python2.7.exe 실행 시 0xc000007b 오류
12679정성태6/18/20218854COM 개체 관련: 23. CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법파일 다운로드1
12678정성태6/17/20218081.NET Framework: 1072. C# - CoCreateInstance 관련 Inteop 오류 정리파일 다운로드1
12677정성태6/17/20219558VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석파일 다운로드1
... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...