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

... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12711정성태7/15/20218532개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/202110330개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/20216917Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202110327Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202177592오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/20218750.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [2]파일 다운로드1
12705정성태7/13/20218926VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/20218065개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/202113711개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/20218857오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/20218492.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/20218885.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/20217691VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202111654오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/20217548개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/20218147VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
12695정성태7/3/20218180VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
12694정성태7/2/202110142Java: 24. Azure - Spring Boot 앱을 Java SE(Embedded Web Server)로 호스팅 시 로그 파일 남기는 방법 [1]
12693정성태6/30/20217885오류 유형: 731. Azure Web App Site Extension - Failed to install web app extension [...]. {1}
12692정성태6/30/20217769디버깅 기술: 180. Azure - Web App의 비정상 종료 시 남겨지는 로그 확인
12691정성태6/30/20218600개발 환경 구성: 573. 테스트 용도이지만 테스트에 적합하지 않은 Azure D1 공유(shared) 요금제
12690정성태6/28/20219415Java: 23. Azure - 자바(Java)로 만드는 Web App Service - Tomcat 호스팅
12689정성태6/25/20219980오류 유형: 730. Windows Forms 디자이너 - The class Form1 can be designed, but is not the first class in the file. [1]
12688정성태6/24/20219654.NET Framework: 1073. C# - JSON 역/직렬화 시 리플렉션 손실을 없애는 JsonSrcGen [2]파일 다운로드1
12687정성태6/22/20217614오류 유형: 729. Invalid data: Invalid artifact, java se app service only supports .jar artifact
12686정성태6/21/202110068Java: 22. Azure - 자바(Java)로 만드는 Web App Service - Java SE (Embedded Web Server) 호스팅
... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...