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);
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13598정성태4/16/2024210닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드1
13597정성태4/15/2024293닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/2024510닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/2024512닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/2024744닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/2024938닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241185C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241152닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241067Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241131닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241185닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신파일 다운로드1
13587정성태3/27/20241133오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241263Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241089Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241042개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241145Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241218Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241366개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241131닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241493오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241620닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241850닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20241539닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20241662닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20241552닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...