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

C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거

예전에 PDB 파일을 이용해 소스 코드 라인 정보를 다루는 방법을 설명한 적이 있는데요,

PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법
; https://www.sysnet.pe.kr/2/0/1763

이번에는 PDB 파일로부터 Type 정보 및 Symbol을 열거해 보겠습니다.




windbg를 사용하다 보면 dt 명령어로 구조체 정의를 확인하는 경우가 종종 있습니다.

0:007> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   ...[생략]...

위의 출력 결과로부터, _PEB 타입은 ntdll 모듈에 정의되어 있는 것인데 이렇게 windbg가 출력해 줄 수 있는 것은 타입 정보가 PDB 파일에 보관되어 있기 때문입니다. 예전에 소개한 DbgOffset 타입도,

C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법
; https://www.sysnet.pe.kr/2/0/12098

타입의 구조를 모두 보여줄 수 있지만 한 가지 아쉬운 점이 있습니다. 바로 타입의 "전체 크기"를 알 수 없다는 것인데, 실제로 일부 타입은 다음과 같은 식으로 출력이 되기 때문에,

0:007> dt _KPROCESS
ntdll!_KPROCESS
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 ProfileListHead  : _LIST_ENTRY
   +0x028 DirectoryTableBase : Uint8B
   +0x030 ThreadListHead   : _LIST_ENTRY
   ...[생략]...
   +0x288 AddressPolicy    : UChar
   +0x289 Spare2           : [71] UChar
   +0x2d0 InstrumentationCallback : Ptr64 Void
   +0x2d8 SecureState      : <anonymous-tag>

마지막 필드의 크기를 가늠할 수 없습니다. 이런 경우에는 어쩔 수 없이 PDB 파일로부터 타입을 열거해 크기 정보를 받아와야 합니다.




Type 정보에 관해 알아봤으니 이번에는 Symbol 정보에 대해 설명해 보겠습니다. 역시 windbg를 사용하다 보면 "dps" 명령어를 통해 특정 모듈이 제공하는 Symbol의 값을 확인하는 경우가 있습니다. 자주 사용하는 사례로는 stack의 값들 중에서 연관된 symbol을 보여주고 싶은 경우일 텐데요,

0:007> dps @rsp
0000003d`74affa48  00007ffb`649ad4db ntdll!DbgUiRemoteBreakin+0x4b
0000003d`74affa50  00000000`00000000
0000003d`74affa58  00000000`00000000
0000003d`74affa60  00000000`00000000
0000003d`74affa68  00000000`00000000
0000003d`74affa70  00000000`00000000
0000003d`74affa78  00007ffb`63977bd4 KERNEL32!BaseThreadInitThunk+0x14
0000003d`74affa80  00000000`00000000
0000003d`74affa88  00000000`00000000
0000003d`74affa90  00000000`00000000
0000003d`74affa98  00000000`00000000
0000003d`74affaa0  00000000`00000000
0000003d`74affaa8  00007ffb`6494ced1 ntdll!RtlUserThreadStart+0x21
0000003d`74affab0  00000000`00000000
0000003d`74affab8  00000000`00000000
0000003d`74affac0  00000000`00000000

위의 출력에서는 dll이 export하고 있는 함수에 대해서만 출력이 되었지만, 명시적인 export 이외에 내부 전역 변수나 함수 등의 symbol 정보도 출력하는 것이 가능합니다. 가령, _PEB에서 소유하고 있는 KernelCallbackTable 필드의 주소를,

0:007> dt _PEB @$peb
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   ...[생략]...
   +0x050 ProcessImagesHotPatched : 0y0
   +0x050 ReservedBits0    : 0y000000000000000000000000 (0)
   +0x054 Padding1         : [4]  ""
   +0x058 KernelCallbackTable : 0x00007ffb`63537330 Void
   ...[생략]...

dps로 덤프해 보면 user32.dll에 포함된 symbol의 주소와 연결된 것을 볼 수 있습니다.

0:007> dps 0x00007ffb`63537330 L5
00007ffb`63537330  00007ffb`634b5160 USER32!_fnCOPYDATA
00007ffb`63537338  00007ffb`6352ec60 USER32!_fnCOPYGLOBALDATA
00007ffb`63537340  00007ffb`634d2720 USER32!_fnDWORD
00007ffb`63537348  00007ffb`634d61d0 USER32!_fnNCDESTROY
00007ffb`63537350  00007ffb`634dc830 USER32!_fnDWORDOPTINLPMSG

즉, 우리가 원하는 것은 바로 저 symbol 목록을 얻고 싶은 것입니다.




사실, 이에 관해 검색해 보면 C/C++ 소스 코드로 다음과 같이 친절하게 ^^ 구현되어 있습니다.

mridgers/pdbdump.c - Small tool to list and query symbols in PDB files.
; https://gist.github.com/mridgers/2968595

소스 코드가 상당히 간결하기 때문에 C#으로의 마이그레이션도 어렵지 않은데요, 위의 코드에 따라 SymInitialize까지 호출하는 코드를 다음과 같이 작성하고,

uint options = NativeMethods.SymGetOptions();
Console.WriteLine($"SymGetOptions: {options}");

options &= ~(uint)SymOpt.SYMOPT_DEFERRED_LOADS;
options |= (uint)SymOpt.SYMOPT_LOAD_LINES;
options |= (uint)SymOpt.SYMOPT_IGNORE_NT_SYMPATH;
#if ENABLE_DEBUG_OUTPUT
options |= (uint)SymOpt.SYMOPT_DEBUG;
#endif
options |= (uint)SymOpt.SYMOPT_UNDNAME;

NativeMethods.SymSetOptions(options);

int pid = Process.GetCurrentProcess().Id;
IntPtr processHandle = NativeMethods.OpenProcess(ProcessAccessRights.PROCESS_QUERY_INFORMATION | ProcessAccessRights.PROCESS_VM_READ, false, pid);

if (NativeMethods.SymInitialize(processHandle, null, false) == false)
{
    return;
}

SymLoadModuleEx를 호출하면 되는데, 이때 PDB 파일의 경로가 필요합니다.

IntPtr baseAddress = new IntPtr((long)NativeMethods.SymLoadModuleEx(processHandle,
    IntPtr.Zero, pdbFilePath, null, baseAddress.ToInt64(), moduleSize, null, 0));

해보진 않았지만 pdbFilePath 인자에 dll 경로를 넣는 경우 symchk.exe가 가졌던 DLL들이 필요할 것이므로 그런 의존성을 제거하고 싶다면 직접 PDB 파일을 다운로드하는 방법을 쓰면 됩니다. 그러고 보니 ^^ 저번에 이런 기능을 하는 코드를 작성해 두었었죠.

C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
; https://www.sysnet.pe.kr/2/0/12094

따라서 이 코드를 곁들이면 다음과 같이 SymLoadModuleEx 처리가 완료됩니다.

PEImage pe = null;
string moduleName = "ntdll.dll";
string pdbFilePath = null;

{
    string rootPathToSave = Path.Combine(Environment.CurrentDirectory, "sym");

    pe = PEImage.FromLoadedModule(moduleName);
    pdbFilePath = pe.DownloadPdb(pe.ModulePath, rootPathToSave); // ntdll.dll에 대한 pdb 파일을 다운로드 하고,
}

IntPtr baseAddress = new IntPtr((long)NativeMethods.SymLoadModuleEx(processHandle,
    IntPtr.Zero, pdbFilePath, null, pe.BaseAddress.ToInt64(), pe.MemorySize, null, 0));

이쯤에서 "mridgers/pdbdump.c" 코드를 보면 재미있는 것이 하나 있는데, 위에서 사용한 processHandle, pe.BaseAddress, pe.MemorySize들이 꼭 정확할 필요는 없다는 것입니다. 실제로 원 저작자의 코드에서는 관련 값들을 다음과 같이 하드 코딩했고,

HANDLE g_handle = (HANDLE)0x493;
uintptr_t base_addr = 0x400000;

base_addr = (size_t)SymLoadModuleEx(g_handle, NULL, buffer, NULL,
        base_addr, 0x7fffffff, NULL, 0)

그래도 잘 동작하는 것을 확인할 수 있습니다. 하지만, 제 경우에는 그냥 PEImage 정보가 있기 때문에 그걸 이용해 정확한 값을 설정했습니다.




일단, 여기까지 마무리되었으면 이제 남은 작업은 심벌과 타입 정보를 열거하는 것입니다. 이 작업은 각각에 해당하는 함수를 호출하는 것으로 간단하게 해결됩니다.

// Symbol 열거
NativeMethods.SymEnumSymbols(processHandle, (ulong)_baseAddress.ToInt64(), "*", enum_proc, IntPtr.Zero);

// 타입 열거
NativeMethods.SymEnumTypes(processHandle, (ulong)baseAddress.ToInt64(), enum_proc, IntPtr.Zero);

private static unsafe bool enum_proc(IntPtr pinfo, uint size, IntPtr pUserContext)
{
    SYMBOL_INFO info = SYMBOL_INFO.Create(pinfo);

    // info.Name == symbol 이름
    // info.Size == 구조체 열거인 경우 크기 정보
    // info.Address == Symbol 열거인 경우 메모리 상의 위치

    return true;
}

끝입니다. 위와 같이 실행하면 Symbol 또는 Type 하나마다 enum_proc 메서드가 callback 방식으로 호출되고 첫 번째 인자로 들어오는 pinfo 정보로부터 SYMBOL_INFO 구조체의 값을 구하면 됩니다.




위의 내용들을 종합해 github에 PdbDump.cs 파일을 추가했으니,

DotNetSamples/WinConsole/PEFormat/WindowsPE/PdbDump.cs
; https://github.com/stjeong/DotNetSamples/blob/master/WinConsole/PEFormat/WindowsPE/PdbDump.cs

다음과 같이 간단하게 사용하시면 됩니다.

using KernelStructOffset;
using System;
using System.IO;
using WindowsPE;

namespace ConsoleApp1
{
    // mridgers/pdbdump.c
    // https://gist.github.com/mridgers/2968595

    // Install-Package WindowsPE
    class Program
    {
        static void Main(string[] args)
        {
            PEImage pe = null;
            string moduleName = "ntdll.dll";
            string pdbFilePath = null;

            {
                string rootPathToSave = Path.Combine(Environment.CurrentDirectory, "sym");
                pe = PEImage.FromLoadedModule(moduleName);
                pdbFilePath = pe.DownloadPdb(pe.ModulePath, rootPathToSave);
            }

            {
                PdbStore symbolStore = PdbDump.CreateSymbolStore(pdbFilePath, pe.BaseAddress, pe.MemorySize);

                foreach (SYMBOL_INFO si in symbolStore.Enumerate())
                {
                    Console.WriteLine(si.Name + " at " + si.Address.ToString("x"));
                }
            }

            {
                PdbStore typeStore = PdbDump.CreateTypeStore(pdbFilePath, pe.BaseAddress, pe.MemorySize);

                foreach (SYMBOL_INFO si in typeStore.Enumerate())
                {
                    Console.WriteLine(si.Name + ", sizeof() == " + si.Size);
                }
            }
        }
    }
}

첨부 파일은 위의 샘플 코드ntdll.dll의 Symbol 목록Type 목록이 출력된 결과입니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/13/2020]

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

비밀번호

댓글 작성자
 



2020-02-09 11시57분
PEImage.FromLoadedModule 호출 시점에 없는 DLL의 경우, 그냥 사전에 LoadLibrary를 호출해 주면 됩니다.

        [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
        static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);

        IntPtr baseAddress = LoadLibrary(moduleName);
정성태

... 61  62  63  64  65  66  67  [68]  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
11947정성태6/17/201912678Linux: 14. 리눅스 환경에서 TFS 서버 연동
11946정성태6/17/201913260개발 환경 구성: 445. C# - MathNet으로 정규 분포를 따르는 데이터를 생성, PLplot으로 Histogram 표현파일 다운로드1
11945정성태6/17/201911657Linux: 13. node.js에서 syslog로 출력하는 방법
11944정성태6/16/201918430Linux: 12. Ubuntu 16.04/18.04에서 node.js 최신 버전 설치 방법
11943정성태6/15/201911254.NET Framework: 844. C# - 박싱과 언박싱 [1]
11942정성태6/13/201917665개발 환경 구성: 444. 로컬의 Visual Studio Code로 원격 리눅스 머신에 접속해 개발하는 방법 [1]
11941정성태6/13/201910984오류 유형: 546. "message NETSDK1057: You are using a preview version of .NET Core" 빌드 경고 없애는 방법
11940정성태6/13/201910921개발 환경 구성: 443. Visual Studio의 Connection Manager 기능(Remote SSH 관리)을 위한 명령행 도구파일 다운로드1
11939정성태6/13/20199992오류 유형: 545. Managed Debugging Assistant 'FatalExecutionEngineError'
11938정성태6/12/201911625Math: 59. C# - 웨이트 벡터 갱신식을 이용한 퍼셉트론 분류파일 다운로드1
11937정성태6/11/201918081개발 환경 구성: 442. .NET Core 3.0 preview 5를 이용해 Windows Forms/WPF 응용 프로그램 개발 [1]
11936정성태6/10/201911326Math: 58. C# - 최소 자승법의 1차, 2차 수렴 그래프 변화 확인 [2]파일 다운로드1
11935정성태6/9/201912020.NET Framework: 843. C# - PLplot 출력을 파일이 아닌 Window 화면으로 변경
11934정성태6/7/201913223VC++: 133. typedef struct와 타입 전방 선언으로 인한 C2371 오류파일 다운로드1
11933정성태6/7/201913242VC++: 132. enum 정의를 C++11의 enum class로 바꿀 때 유의할 사항파일 다운로드1
11932정성태6/7/201911850오류 유형: 544. C++ - fatal error C1017: invalid integer constant expression파일 다운로드1
11931정성태6/6/201911957개발 환경 구성: 441. C# - CairoSharp/GtkSharp 사용을 위한 프로젝트 구성 방법
11930정성태6/5/201912549.NET Framework: 842. .NET Reflection을 대체할 System.Reflection.Metadata 소개 [1]
11929정성태6/5/201912242.NET Framework: 841. Windows Forms/C# - 클립보드에 RTF 텍스트를 복사 및 확인하는 방법 [1]
11928정성태6/5/201910884오류 유형: 543. PowerShell 확장 설치 시 "Catalog file '[...].cat' is not found in the contents of the module" 오류 발생
11927정성태6/5/201911868스크립트: 15. PowerShell ISE의 스크립트를 복사 후 PPT/Word에 붙여 넣으면 한글이 깨지는 문제 [1]
11926정성태6/4/201913268오류 유형: 542. Visual Studio - pointer to incomplete class type is not allowed
11925정성태6/4/201912086VC++: 131. Visual C++ - uuid 확장 속성과 __uuidof 확장 연산자파일 다운로드1
11924정성태5/30/201913870Math: 57. C# - 해석학적 방법을 이용한 최소 자승법 [1]파일 다운로드1
11923정성태5/30/201913495Math: 56. C# - 그래프 그리기로 알아보는 경사 하강법의 최소/최댓값 구하기파일 다운로드1
11922정성태5/29/201911570.NET Framework: 840. ML.NET 데이터 정규화파일 다운로드1
... 61  62  63  64  65  66  67  [68]  69  70  71  72  73  74  75  ...