Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일
 

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@outlook.com

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12059정성태11/20/2019736디버깅 기술: 131. windbg/Visual Studio - HeapFree x86의 동작 분석
12058정성태11/19/2019712디버깅 기술: 130. windbg - CoTaskMemFree/FreeCoTaskMem에서 발생한 덤프 분석 사례
12057정성태11/18/2019502오류 유형: 579. Visual Studio - Memory 창에서 유효한 주소 영역임에도 "Unable to evaluate the expression." 오류 출력
12056정성태11/18/2019939개발 환경 구성: 464. "Microsoft Visual Studio Installer Projects" 프로젝트로 EXE 서명 및 MSI 파일 서명 방법파일 다운로드1
12055정성태11/17/2019507개발 환경 구성: 463. Visual Studio의 Ctrl + Alt + M, 1 (Memory 1) 등의 단축키가 동작하지 않는 경우
12054정성태11/15/2019929.NET Framework: 869. C# - 일부러 GC Heap을 깨뜨려 GC 수행 시 비정상 종료시키는 예제
12053정성태11/15/2019571Windows: 164. 윈도우 10 - 명령행 창(cmd.exe) 속성에 (DotumChe, GulimChe, GungsuhChe 등의) 한글 폰트가 없는 경우
12052정성태11/15/2019509오류 유형: 578. Azure - 일정(schedule)에 등록한 runbook이 1년 후 실행이 안 되는 문제(Reason - The key used is expired.)
12051정성태12/2/20191198개발 환경 구성: 462. 시작하자마자 비정상 종료하는 프로세스의 메모리 덤프 - procdump [1]
12050정성태11/14/2019755Windows: 163. AcLayers의 API 후킹과 FaultTolerantHeap
12049정성태11/13/2019565.NET Framework: 868. (닷넷 프로세스를 대상으로) 디버거 방식이 아닌 CLR Profiler를 이용해 procdump.exe 기능 구현
12048정성태11/12/2019777Windows: 163. GUID 이름의 볼륨에 해당하는 파티션을 찾는 방법
12047정성태11/12/20191258Windows: 163. 안전하게 eject시킨 USB 장치를 물리적인 재연결 없이 다시 인식시키는 방법
12046정성태11/9/2019697오류 유형: 577. windbg - The call to LoadLibrary(...\sos.dll) failed, Win32 error 0n193
12045정성태10/27/2019565오류 유형: 576. mstest.exe 실행 시 "Visual Studio Enterprise is required to execute the test." 오류 - 두 번째 이야기
12044정성태10/27/2019712오류 유형: 575. mstest.exe - System.Resources.MissingSatelliteAssemblyException: The satellite assembly named "Microsoft.VisualStudio.ProductKeyDialog.resources.dll, ..."
12043정성태10/27/2019712오류 유형: 574. Windows 10 설치 시 오류 - 0xC1900101 - 0x4001E
12042정성태10/26/2019648오류 유형: 573. OneDrive 하위에 위치한 Documents, Desktop 폴더에 대한 권한 변경 시 "Unable to display current owner"
12041정성태10/23/2019512오류 유형: 572. mstest.exe - The load test results database could not be opened.
12040정성태10/23/2019878오류 유형: 571. Unhandled Exception: System.Net.Mail.SmtpException: Transaction failed. The server response was: 5.2.0 STOREDRV.Submission.Exception:SendAsDeniedException.MapiExceptionSendAsDenied
12039정성태10/22/2019662스크립트: 16. cmd.exe의 for 문에서는 ERRORLEVEL이 설정되지 않는 문제
12038정성태10/17/2019528오류 유형: 570. SQL Server 2019 RC1 - SQL Client Connectivity SDK 설치 오류
12037정성태10/15/2019841.NET Framework: 867. C# - Encoding.Default 값을 바꿀 수 있을까요?파일 다운로드1
12036정성태10/21/20191492.NET Framework: 866. C# - 고성능이 필요한 환경에서 GC가 발생하지 않는 네이티브 힙 사용파일 다운로드1
12035정성태10/13/2019716개발 환경 구성: 461. C# 8.0의 #nulable 관련 특성을 .NET Framework 프로젝트에서 사용하는 방법파일 다운로드1
12034정성태1/31/2020991개발 환경 구성: 460. .NET Core 환경에서 (프로젝트가 아닌) C# 코드 파일을 입력으로 컴파일하는 방법 [1]
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...