Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 3개 있습니다.)
디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
; https://www.sysnet.pe.kr/2/0/12092

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

디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거
; https://www.sysnet.pe.kr/2/0/12114




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 목록이 출력된 결과입니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/22/2024]

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);

------------------------------------------

ntdll_symbols.txt에 포함된 __fltused 심벌의 설명

Understanding the classical model for linking: Taking symbols along for the ride
; https://devblogs.microsoft.com/oldnewthing/20130108-00/?p=5623

정리해 보면, A.obj, B.obj가 있을 때 A.obj에서 B.obj의 어느 하나라도 사용한다면 컴파일러는 B.obj 전체를 포함해 실행 모듈을 만듦. 과거 8086 프로세서 시절에는 부동 소수점 처리를 담당하는 프로세서(8087 프로세서)가 분리돼 있었는데 그게 없는 경우 소프트웨어로 구현한 부동 소수점 연산을 구현한 별도 라이브러리를 링킹.

그 에뮬레이션 라이브러리는 꽤나 무거운 편에 속해서 부동 소수점 연산을 하지 않는 경우에는 가급적 링킹을 하지 않으려고 했는데, 즉, 해당 모듈에 부동 소수점 연산이 있는 경우에는 그것을 나타내기 위해 __fltused 심벌을 추가. 결국 링킹 시에 obj 파일에 그 심벌이 없으면 부동 소수점 연산을 제외할 수 있었다는.
정성태

1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13893정성태2/27/20252225Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
13892정성태2/24/20252983닷넷: 2325. C# - PowerShell과 연동하는 방법파일 다운로드1
13891정성태2/23/20252500닷넷: 2324. C# - 프로세스의 성능 카운터용 인스턴스 이름을 구하는 방법파일 다운로드1
13890정성태2/21/20252329닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)파일 다운로드1
13889정성태2/20/20253054닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI) [1]파일 다운로드1
13888정성태2/17/20252498닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
13887정성태2/17/20253074닷넷: 2320. Blazor의 razor 페이지에서 code-behind 파일로 코드를 분리 및 DI 사용법
13886정성태2/15/20252574VS.NET IDE: 196. Visual Studio - Code-behind처럼 cs 파일을 그룹핑하는 방법
13885정성태2/14/20253236닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
13884정성태2/13/20253522닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용파일 다운로드1
13883정성태2/12/20253266닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/20252581스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/20252832닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/20253172오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20253407오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/20253198오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20253249닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20253708닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20253137스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20253558스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20252983디버깅 기술: 217. WinDbg - PCI 장치 열거파일 다운로드1
13872정성태1/23/20252885오류 유형: 944. WinDbg - 원격 커널 디버깅이 연결은 되지만 Break (Ctrl + Break) 키를 눌러도 멈추지 않는 현상
13871정성태1/22/20253292Windows: 278. Windows - 윈도우를 다른 모니터 화면으로 이동시키는 단축키 (Window + Shift + 화살표)
13870정성태1/18/20253731개발 환경 구성: 741. WinDbg - 네트워크 커널 디버깅이 가능한 NIC 카드 지원 확대
13869정성태1/18/20253456개발 환경 구성: 740. WinDbg - _NT_SYMBOL_PATH 환경 변수에 설정한 경로로 심벌 파일을 다운로드하지 않는 경우
13868정성태1/17/20253112Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...