Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 7개 있습니다.)
개발 환경 구성: 319. windbg에서 python 스크립트 실행하는 방법 - pykd
; https://www.sysnet.pe.kr/2/0/11227

디버깅 기술: 96. windbg - 풀 덤프에 포함된 모든 닷넷 모듈을 파일로 저장하는 방법
; https://www.sysnet.pe.kr/2/0/11297

디버깅 기술: 103. windbg - .NET 4.0 이상의 환경에서 모든 DLL에 대한 심벌 파일을 로드하는 파이썬 스크립트
; https://www.sysnet.pe.kr/2/0/11331

디버깅 기술: 143. windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
; https://www.sysnet.pe.kr/2/0/12084

디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
; https://www.sysnet.pe.kr/2/0/12087

디버깅 기술: 178. windbg - 디버그 시작 시 스크립트 실행
; https://www.sysnet.pe.kr/2/0/12472

개발 환경 구성: 621. windbg에서 python 스크립트 실행하는 방법 - pykd (2)
; https://www.sysnet.pe.kr/2/0/12899




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

... 91  92  93  94  95  96  97  [98]  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11483정성태4/10/201827888개발 환경 구성: 358. "Let's Encrypt"에서 제공하는 무료 SSL 인증서를 IIS에 적용하는 방법 (2) [1]
11482정성태4/10/201820368VC++: 126. CUDA Core 수를 알아내는 방법
11481정성태4/10/201831994개발 환경 구성: 357. CUDA의 인덱싱 관련 용어 - blockIdx, threadIdx, blockDim, gridDim
11480정성태4/9/201822012.NET Framework: 736. C# - API를 사용해 Azure에 접근하는 방법 [2]파일 다운로드1
11479정성태4/9/201817693.NET Framework: 735. Azure - PowerShell로 Access control(IAM)에 새로운 계정 만드는 방법
11478정성태11/8/201919912디버깅 기술: 115. windbg - 덤프 파일로부터 PID와 환경변수 등의 정보를 구하는 방법 [1]
11477정성태4/8/201817398오류 유형: 460. windbg - sos 명령어 수행 시 c0000006 오류 발생
11476정성태4/8/201818920디버깅 기술: 114. windbg - !threads 출력 결과로부터 닷넷 관리 스레드(System.Threading.Thread) 객체를 구하는 방법
11475정성태3/28/201821191디버깅 기술: 113. windbg - Thread.Suspend 호출 시 응용 프로그램 hang 현상에 대한 덤프 분석
11474정성태3/27/201819332오류 유형: 459. xperf: error: TEST.Event: Invalid flags. (0x3ec).
11473정성태3/22/201824534.NET Framework: 734. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상파일 다운로드2
11472정성태3/22/201818486개발 환경 구성: 356. GTX 1070, GTX 960, GT 640M의 cudaGetDeviceProperties 출력 결과
11471정성태3/20/201821897VC++: 125. CUDA로 작성한 RGB2RGBA 성능 [1]파일 다운로드1
11470정성태3/20/201823909오류 유형: 458. Visual Studio - CUDA 프로젝트 빌드 시 오류 C1189, expression must have a constant value
11469정성태3/19/201816909오류 유형: 457. error MSB3103: Invalid Resx file. Could not load file or assembly 'System.Windows.Forms, ...' or one of its dependencies.
11468정성태3/19/201816466오류 유형: 456. 닷넷 응용 프로그램 실행 시 0x80131401 예외 발생
11467정성태3/19/201815995오류 유형: 455. Visual Studio Installer - 업데이트 실패
11466정성태3/18/201817088개발 환경 구성: 355. 한 대의 PC에서 2개 이상의 DirectX 게임을 실행하는 방법
11463정성태3/15/201819517.NET Framework: 733. 스레드 간의 read/write 시에도 lock이 필요 없는 경우파일 다운로드1
11462정성태3/14/201822341개발 환경 구성: 354. HTTPS 호출에 대한 TLS 설정 확인하는 방법 [1]
11461정성태3/13/201824939오류 유형: 454. 윈도우 업데이트 설치 오류 - 0x800705b4 [1]
11460정성태3/13/201817415디버깅 기술: 112. windbg - 닷넷 메모리 덤프에서 전역 객체의 내용을 조사하는 방법
11459정성태3/13/201818210오류 유형: 453. Debug Diagnostic Tool에서 mscordacwks.dll을 찾지 못하는 문제
11458정성태2/21/201819257오류 유형: 452. This share requires the obsolete SMB1 protocol, which is unsafe and could expose your system to attack. [1]
11457정성태2/17/201823960.NET Framework: 732. C# - Task.ContinueWith 설명 [1]파일 다운로드1
11456정성태2/17/201829680.NET Framework: 731. C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법 [7]파일 다운로드1
... 91  92  93  94  95  96  97  [98]  99  100  101  102  103  104  105  ...