Microsoft MVP성태의 닷넷 이야기
.NET Framework: 932. C# - ETW 관련 Win32 API 사용 예제 코드 (1) [링크 복사], [링크+제목 복사],
조회: 21625
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 11개 있습니다.)
.NET Framework: 475. ETW(Event Tracing for Windows)를 C#에서 사용하는 방법
; https://www.sysnet.pe.kr/2/0/1804

.NET Framework: 483. 코드로 살펴 보는 ETW의 활성화 시점
; https://www.sysnet.pe.kr/2/0/1815

.NET Framework: 915. ETW(Event Tracing for Windows)를 이용한 닷넷 프로그램의 내부 이벤트 활용
; https://www.sysnet.pe.kr/2/0/12244

.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시
; https://www.sysnet.pe.kr/2/0/12255

.NET Framework: 932. C# - ETW 관련 Win32 API 사용 예제 코드 (1)
; https://www.sysnet.pe.kr/2/0/12292

.NET Framework: 933. C# - ETW 관련 Win32 API 사용 예제 코드 (2) NT Kernel Logger
; https://www.sysnet.pe.kr/2/0/12296

.NET Framework: 934. C# - ETW 관련 Win32 API 사용 예제 코드 (3) ETW Consumer 구현
; https://www.sysnet.pe.kr/2/0/12299

.NET Framework: 935. C# - ETW 관련 Win32 API 사용 예제 코드 (4) CLR ETW Consumer
; https://www.sysnet.pe.kr/2/0/12300

.NET Framework: 936. C# - ETW 관련 Win32 API 사용 예제 코드 (5) - Private Logger
; https://www.sysnet.pe.kr/2/0/12302

개발 환경 구성: 504. ETW - 닷넷 프레임워크 기반의 응용 프로그램을 위한 명령행 도구 etrace 소개
; https://www.sysnet.pe.kr/2/0/12303

.NET Framework: 994. C# - (.NET Core 2.2부터 가능한) 프로세스 내부에서 CLR ETW 이벤트 수신
; https://www.sysnet.pe.kr/2/0/12474




C# - ETW 관련 Win32 API 사용 예제 코드 (1)

예전에 ETW의 Provider 측 코드를 작성하는 방법을 소개했고,

ETW(Event Tracing for Windows)를 C#에서 사용하는 방법
; https://www.sysnet.pe.kr/2/0/1804

닷넷 프레임워크 관련한 ETW 이벤트 활용과 함께 Consumer 측에 대해서도 다뤘으니,

ETW(Event Tracing for Windows)를 이용한 닷넷 프로그램의 내부 이벤트 활용
; https://www.sysnet.pe.kr/2/0/12244

ETW 관련한 명령어들을 Win32 API 수준으로 내려 살펴보는 시간을 갖겠습니다. ^^




우선, ETW provider를 나열하는 "xperf -providers" 명령어를 코드로 구현해 볼까요?

ETW provider 목록
; https://www.sysnet.pe.kr/2/0/10909

정식 BCL에 이 기능은 없지만 "Microsoft.Diagnostics.Tracing.TraceEvent" 패키지의 소스 코드를 보면 Win23 API와 Interop하는 소스 코드를 볼 수 있습니다.

// perfview/src/TraceEvent/
// ; https://github.com/microsoft/perfview/blob/master/src/TraceEvent/TraceEventSession.cs#L1533

static unsafe void PrintProviders()
{
    var providersByName = new SortedDictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
    int buffSize = 0;
    var hr = NativeMethods.TdhEnumerateProviders(null, ref buffSize);
    Debug.Assert(hr == 122);     // ERROR_INSUFFICIENT_BUFFER
    var buffer = stackalloc byte[buffSize];
    var providersDesc = (PROVIDER_ENUMERATION_INFO*)buffer;

    hr = NativeMethods.TdhEnumerateProviders(providersDesc, ref buffSize);
    if (hr != 0)
    {
        Trace.WriteLine("TdhEnumerateProviders failed HR = " + hr);
        providersDesc->NumberOfProviders = 0;
    }

    var providers = (TRACE_PROVIDER_INFO*)&providersDesc[1];
    for (int i = 0; i < providersDesc->NumberOfProviders; i++)
    {
        var name = new string((char*)&buffer[providers[i].ProviderNameOffset]);
        providersByName[name] = providers[i].ProviderGuid;
    }

    foreach (var item in providersByName)
    {
        Console.WriteLine(item.Key + ": " + item.Value);
    }
}

internal struct PROVIDER_ENUMERATION_INFO
{
    public int NumberOfProviders;
    public int Padding;
}

unsafe class NativeMethods
{
    [DllImport("tdh.dll")]
    internal static extern int TdhEnumerateProviders(PROVIDER_ENUMERATION_INFO* pBuffer, ref int pBufferSize);

}

그러니까, TdhEnumerateProviders Win32 API 하나로 ETW provider 목록을 모두 열람할 수 있습니다. (참고로, ETW Provider 목록은 레지스트리에 등록되므로 "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers\"의 하위 키를 열람해도 됩니다.)




"logman query -ets" 명령을 실행하면 현재 StartTrace로 실행해 둔 ETW 세션이 있는지 확인할 수 있습니다. 제 PC의 경우 최초 로그인 시 다음과 같은 세션들이 있었습니다.

C:\Windows\System32> logman query -ets

Data Collector Set                      Type                          Status
-------------------------------------------------------------------------------
Circular Kernel Context Logger          Trace                         Running
AppModel                                Trace                         Running
DiagLog                                 Trace                         Running
Diagtrack-Listener                      Trace                         Running
EventLog-Application                    Trace                         Running
EventLog-Microsoft-Windows-Sysmon-Operational Trace                         Running
EventLog-RemoteDesktopServices-RemoteFX-SessionLicensing-Debug Trace                         Running
EventLog-System                         Trace                         Running
LwtNetLog                               Trace                         Running
Microsoft-Windows-Rdp-Graphics-RdpIdd-Trace Trace                         Running
NetCore                                 Trace                         Running
NtfsLog                                 Trace                         Running
RadioMgr                                Trace                         Running
UBPM                                    Trace                         Running
WdiContextLog                           Trace                         Running
WiFiSession                             Trace                         Running
SgrmEtwSession                          Trace                         Running
UserNotPresentTraceSession              Trace                         Running
CldFltLog                               Trace                         Running
Admin_PS_Provider                       Trace                         Running
NetCfgTrace                             Trace                         Running
WindowsUpdate_trace_log                 Trace                         Running
MpWppTracing-20200819-152610-00000003-ffffffff Trace                         Running
ScreenOnPowerStudyTraceSession          Trace                         Running
SHS-08192020-152637-7-7f                Trace                         Running
Cloud Files Diagnostic Event Listener   Trace                         Running
8696EAC4-1288-4288-A4EE-49EE431B0AD9    Trace                         Running
Microsoft-VisualStudio-Telemetry-PerfWatson2-19040 Trace                         Running
Microsoft-VisualStudio-Telemetry-PerfWatson2-18384 Trace                         Running

The command completed successfully.

이 목록은, Win32 API로는 QueryAllTraces로 구할 수 있는데 Interop 과정이 좀 복잡한 것을 제외하고는 API 문서에 나온 기능을 C#으로도 다음과 같이 옮길 수 있습니다.

public static List<EventTraceProperitesManaged> QueryAllSessions()
{
    List<EventTraceProperitesManaged> list = new List<EventTraceProperitesManaged>();

    unsafe
    {
        IntPtr ptrBuf = Marshal.AllocHGlobal(EventTraceProperties.MaxSessionBufferSize);

        try
        {
            IntPtr[] arrayBuf = new IntPtr[EventTraceProperties.MAX_SESSIONS];

            for (int i = 0; i < EventTraceProperties.MAX_SESSIONS; i++)
            {
                EventTraceProperties* pProp = (EventTraceProperties*)(((byte*)ptrBuf.ToPointer()) + EventTraceProperties.RecordSize * i);

                IntPtr elemPtr = new IntPtr(pProp);
                arrayBuf[i] = elemPtr;

                pProp->Initialize();
            }

            GCHandle gcHandle = GCHandle.Alloc(arrayBuf, GCHandleType.Pinned);

            try
            {
                IntPtr elem0 = gcHandle.AddrOfPinnedObject();

                uint activeSessionCount = 0;
                int status = NativeMethods.QueryAllTraces(elem0, EventTraceProperties.MAX_SESSIONS, ref activeSessionCount);
                if (status == 0)
                {
                    for (int i = 0; i < activeSessionCount; i++)
                    {
                        EventTraceProperties* pProp = (EventTraceProperties*)(((byte*)ptrBuf.ToPointer()) + EventTraceProperties.RecordSize * i);
                        list.Add(pProp->ReadAsManaged());
                    }
                }
                else
                {
                    Console.WriteLine(status);
                }
            }
            finally
            {
                gcHandle.Free();
            }
        }
        finally
        {
            Marshal.FreeHGlobal(ptrBuf);
        }
    }

    return list;
}

대개의 경우, ETW Consumer를 만들 때 저런 식으로 열람할 일은 없을 것입니다. 대신 자신이 만드는 세션 이름으로 이미 등록된 적이 있는지 알아야 할 필요는 있는데 이를 위해 QueryTrace Win32 API를 사용할 수 있습니다.

// EventTraceWatcher
// ; https://www.nuget.org/packages/EventTraceWatcher/
// ; ./src/EventTraceWatcher.cs LoadExistingEventTraceProperties

public static bool IsSessionActive(string sessionName)
{
    EventTraceProperties prop = new EventTraceProperties(true);
    int status = NativeMethods.QueryTrace(0, sessionName, ref prop);

    if (status == 0)
    {
        return true;
    }
    else if (status == EventTraceProperties.ERROR_WMI_INSTANCE_NOT_FOUND)
    {
        // The instance name passed was not recognized as valid by a WMI data provider.
        return false;
    }
              
    throw new System.ComponentModel.Win32Exception(status);
}

/*
Console.WriteLine("Active == " + IsSessionActive("EventLog-Application"));
// Active == True
*/




예전에 썼던 글에서 EventTraceWatcher를 소개한 적이 있는데요, 해당 클래스를 사용해 보면,

// How to consume ETW events from C#
// ; https://learn.microsoft.com/en-us/archive/blogs/danielvl/how-to-consume-etw-events-from-c

using System;
using Microsoft.Samples.Eventing;

class Program
{
    static void Main()
    {
        try
        {
            new Program().Run();
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine(ex);
        }
    }

    private void Run()
    {
        Guid RewriteProviderId = new Guid("0469abfa-1bb2-466a-b645-e3e15a02f38b");

        using (EventTraceWatcher watcher = new EventTraceWatcher("Rewrite", RewriteProviderId))
        {

            watcher.EventArrived += delegate (object sender, EventArrivedEventArgs e) {

                if (e.Error != null)
                {
                    Console.Error.WriteLine(e.Error);
                    Environment.Exit(-1);
                }

                Console.WriteLine("Event Name: " + e.EventId);

                foreach (var p in e.Properties)
                {
                    Console.WriteLine("\t" + p.Key + " -- " + p.Value);
                }

                Console.WriteLine();
            };

            watcher.Start();

            Console.WriteLine("Press <Enter> to exit");
            Console.ReadLine();

            watcher.Stop();
        }
    }
}

프로세스가 정상 종료하면서 watcher.Stop() 코드가 실행되었음에도 여전히 "Rewrite"라는 이름의 세션이 살아 있는 것을 "logman query -ets" 명령어로 확인할 수 있습니다. 사실 다른 커널 리소스 핸들과 달리, ETW 핸들은 (EXE) 프로세스의 종료에 상관이 없어야 하는 것이 맞습니다. 하지만 위의 경우에는 프로세스가 종료하면 ETW 세션도 닫히기를 원하는 상황인데, 게다가 소스 코드로 디버깅해 보면 분명히 using 문으로 인한 finally에 의해 Dispose가 실행되었고, 그에 따라 StopTrace와 CloseTrace가 실행되었지만 여전히 ETW 세션이 안 닫힙니다.

이 경우 StopTrace의 반환값은, 0x1069(0n4201) 값으로 C++ 헤더 파일에서 다음과 같이 설명하고 있습니다.

//
// MessageId: ERROR_WMI_INSTANCE_NOT_FOUND
//
// MessageText:
//
// The instance name passed was not recognized as valid by a WMI data provider.
//
#define ERROR_WMI_INSTANCE_NOT_FOUND     4201L

어쨌든, 이렇게 (의도치 않게) 살아 있는 세션을 닫으려면 logman을 이용해 다음과 같은 식의 명령어를 실행해야 합니다.

logman stop <SessionName> -ets

예) logman stop Rewrite -ets

코드로 이것을 구현하려면 ControlTrace API를 사용하는데,

// ControlTraceW function
// ; https://learn.microsoft.com/en-us/windows/win32/api/evntrace/nf-evntrace-controltracew

public const uint EVENT_TRACE_CONTROL_QUERY = 0;
public const uint EVENT_TRACE_CONTROL_STOP = 1;
public const uint EVENT_TRACE_CONTROL_UPDATE = 2;

public static void CloseActiveSession(string sessionName)
{
    if (IsSessionActive(sessionName, out _) == true)
    {
        EventTraceProperties prop = new EventTraceProperties();
        prop.Initialize();

        NativeMethods.ControlTrace(0, sessionName, ref prop, EVENT_TRACE_CONTROL_STOP);
    }
}

public static int CloseActiveSession(ulong sessionHandle)
{
    EventTraceProperties prop = new EventTraceProperties();
    prop.Initialize();

    return NativeMethods.ControlTrace(sessionHandle, null, ref prop, EVENT_TRACE_CONTROL_STOP);
}

문서에 보면 ControlTrace가 StopTrace 함수를 대체한다고 쓰여있기는 합니다. 하지만 그렇다고 해도 StopTrace 호출이 세션을 닫지 못한다는 것은 이해가 안 됩니다. 참고로, 보다 더 최근에 나왔던 "Microsoft.Diagnostics.Tracing.TraceEvent" 라이브러리에서는 StopTrace는 아예 호출도 안 하고 ControlTrace로 세션을 닫고 있습니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




규칙을 알 수 없지만, 가끔씩 logman query에 오류가 발생하는 경우가 있습니다.

C:\Windows\System32> logman query -ets

Data Collector Set                      Type                          Status
-------------------------------------------------------------------------------

Error:
The GUID passed was not recognized as valid by a WMI data provider.

그래서 그런지, 저렇게 열거하는 QueryAllTraces API도 가끔씩 호출이 실패하는 경우가 있는데 ^^; 원인을 모르겠습니다.




ETW 관련 오류 코드들은 "C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\winerror.h" 헤더 파일에서 찾을 수 있으며 주로 다음의 오류코드들을 볼 수 있을 것입니다.

int result = NativeMethods.CloseTrace(this.traceHandle);

//
// MessageId: ERROR_CTX_CLOSE_PENDING
//
// MessageText:
//
// A close operation is pending on the session.
//
#define ERROR_CTX_CLOSE_PENDING          7007L 

int result = NativeMethods.StopTrace(this.sessionHandle, this.loggerName, out properties /*as statistics*/);

//
// MessageId: ERROR_BAD_LENGTH
//
// MessageText:
//
// The program issued a command but the command length is incorrect.
//
#define ERROR_BAD_LENGTH                 24L

//
// MessageId: ERROR_INVALID_HANDLE
//
// MessageText:
//
// The handle is invalid.
//
#define ERROR_INVALID_HANDLE             6L

//
// MessageId: ERROR_INVALID_PARAMETER
//
// MessageText:
//
// The parameter is incorrect.
//
#define ERROR_INVALID_PARAMETER          87L    // dderror




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/15/2024]

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

비밀번호

댓글 작성자
 




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/20252981닷넷: 2325. C# - PowerShell과 연동하는 방법파일 다운로드1
13891정성태2/23/20252500닷넷: 2324. C# - 프로세스의 성능 카운터용 인스턴스 이름을 구하는 방법파일 다운로드1
13890정성태2/21/20252320닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)파일 다운로드1
13889정성태2/20/20253050닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI) [1]파일 다운로드1
13888정성태2/17/20252498닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
13887정성태2/17/20253070닷넷: 2320. Blazor의 razor 페이지에서 code-behind 파일로 코드를 분리 및 DI 사용법
13886정성태2/15/20252572VS.NET IDE: 196. Visual Studio - Code-behind처럼 cs 파일을 그룹핑하는 방법
13885정성태2/14/20253235닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
13884정성태2/13/20253521닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용파일 다운로드1
13883정성태2/12/20253261닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/20252577스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/20252832닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/20253167오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20253406오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/20253197오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20253249닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
13876정성태1/25/20253706닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20253133스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20253555스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20252983디버깅 기술: 217. WinDbg - PCI 장치 열거파일 다운로드1
13872정성태1/23/20252883오류 유형: 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/20253109Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...