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...]
정성태

... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
11992정성태7/22/201911634오류 유형: 560. 서비스 관리자 실행 시 "Windows was unable to open service control manager database on [...]. Error 5: Access is denied." 오류 발생
11991정성태7/18/20199168디버깅 기술: 128. windbg - x64 환경에서 닷넷 예외가 발생한 경우 인자를 확인할 수 없었던 사례
11990정성태7/18/201911367오류 유형: 559. Settings / Update & Security 화면 진입 시 프로그램 종료
11989정성태7/18/201910273Windows: 162. Windows Server 2019 빌드 17763부터 Alt + F4 입력시 곧바로 로그아웃하는 현상
11988정성태7/18/201911697개발 환경 구성: 453. 마이크로소프트가 지정한 모든 Root 인증서를 설치하는 방법
11987정성태7/17/201916698오류 유형: 558. 윈도우 - KMODE_EXCEPTION_NOT_HANDLED 블루스크린(BSOD) 문제 [1]
11986정성태7/17/20199502오류 유형: 557. 드라이브 문자를 할당하지 않은 파티션을 탐색기에서 드라이브 문자와 함께 보여주는 문제
11985정성태7/17/20199613개발 환경 구성: 452. msbuild - csproj에 환경 변수 조건 사용 [1]
11984정성태7/9/201917823개발 환경 구성: 451. Microsoft Edge (Chromium)을 대상으로 한 Selenium WebDriver 사용법 [1]
11983정성태7/8/20198889오류 유형: 556. nodemon - 'mocha' is not recognized as an internal or external command, operable program or batch file.
11982정성태7/8/20198893오류 유형: 555. Visual Studio 빌드 오류 - result: unexpected exception occured (-1002 - 0xfffffc16)
11981정성태7/7/201911048Math: 64. C# - 3층 구조의 신경망(분류)파일 다운로드1
11980정성태7/7/201921496개발 환경 구성: 450. Visual Studio Code의 Java 확장을 이용한 간단한 프로젝트 구축파일 다운로드1
11979정성태7/7/201911037개발 환경 구성: 449. TFS에서 gitlab/github등의 git 서버로 마이그레이션하는 방법
11978정성태7/6/201910390Windows: 161. 계정 정보가 동일하지 않은 PC 간의 인증을 수행하는 방법 [1]
11977정성태7/6/201914941오류 유형: 554. git push - error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413 Request Entity Too Large
11976정성태7/4/20199311오류 유형: 553. (잘못 인증 한 후) 원격 git repo 재인증 시 "remote: HTTP Basic: Access denied" 오류 발생
11975정성태7/4/201917810개발 환경 구성: 448. Visual Studio Code에서 콘솔 응용 프로그램 개발 시 "입력"받는 방법
11974정성태7/4/201913168Linux: 22. "Visual Studio Code + Remote Development"로 윈도우 환경에서 리눅스(CentOS 7) C/C++ 개발
11973정성태7/4/201912382Linux: 21. 리눅스에서 공유 라이브러리가 로드되지 않는다면?
11972정성태7/3/201915226.NET Framework: 847. JAVA와 .NET 간의 AES 암호화 연동 [1]파일 다운로드1
11971정성태7/3/201912397개발 환경 구성: 447. Visual Studio Code에서 OpenCvSharp 개발 환경 구성
11970정성태7/2/201910706오류 유형: 552. 웹 브라우저에서 파일 다운로드 후 "Running security scan"이 끝나지 않는 문제
11969정성태7/2/201911128Math: 63. C# - 3층 구조의 신경망파일 다운로드1
11968정성태7/1/201917441오류 유형: 551. Visual Studio Code에서 Remote-SSH 연결 시 "Opening Remote..." 단계에서 진행되지 않는 문제 [1]
11967정성태7/1/201911669개발 환경 구성: 446. Synology NAS를 Windows 10에서 iSCSI로 연결하는 방법
... 61  62  63  64  65  [66]  67  68  69  70  71  72  73  74  75  ...