Давненько у нас в бложеке не появлялось новых статей! Что ж, ETW, настало твое время!
Возможно, кто-то из вас и раньше сталкивался с этим сокращением - ETW. Что же это такое? Расшифровывается эта аббревиатура как Event Tracing for Windows. ETW - это системный компонент ОС Windows, который используется для диагностики, отладки и исследования производительности тех или иных частей ОС, а также приложений. Появился он где-то с Windows 2000, но полноценно его использовать стало возможным примерно с Windows Vista.
- А мне какое до этого дело? - спросите вы. А вот какое: в Windows несколько сотен (в Win10 - около тысячи) провайдеров событий, которые жаждут вам что-нибудь рассказать о своей работе, нужно лишь подписаться! Вот, например, как можно штатными средствами Windows посмотреть, что Internet Explorer отсылает по HTTP. А вот эти ребята сумели с помощью ETW поснифать, что пользователь набирает на своей USB-клавиатуре. И это все - безо всяких хуков, инжектов и драйверов! ETW просто пронизывает Windows, его использует и Event Viewer, и такие утилиты, как xPerf, и Visual Studio во время отладки приложения, чтобы отображать вам статистику потребляемой памяти и процессорного времени, и даже штатный Resource Monitor, который вы можете открыть из таскменеджера!
Уже интересно?
Тогда сначала быстренько рассмотрим архитектуру ETW, его плюсы и минусы. Вот что из себя представляет этот компонент:
Как показано на картинке, в системе зарегистрировано некоторое количество провайдеров событий (можно зарегистрировать свой провайдер). Провайдером может быть ядро системы, драйвера или user-mode приложения. Существуют контроллеры ETW, которые могут создавать, модифицировать и удалять сессии логирования. Сессии ETW - это глобальные объекты ОС Windows. Сессия потребляет события каких-либо ETW-провайдеров, после чего записывает их в файл или же выдает всем заинтересованным потребителям (consumers) в реальном времени. Цель ETW как API - быть максимально быстрым (чтобы даже драйвера и ядро могли вести свои логи, не сильно напрягаясь), не нагружать провайдер, если никто не подписан на его ивенты, и иметь возможность логировать события даже на самых ранних этапах загрузки ОС. Далее я приведу достоинства и недостатки ETW с точки зрения потребителя событий.
Плюсы ETW:
- Позволяет единообразно потреблять события из сотен источников.
- Полноценно работает на Windows Vista (или даже Win 7) и новее.
- Не требует хуков, инжектов, драйверов. Все события можно получать в user mode.
Минусы ETW:
- Требует привилегий администратора. Увы, не получится работать с объектами ядра (сессиями) от имени простого пользователя.
- Не будет работать на Windows XP. Также, не все провайдеры, которые доступны в более новых ОС, присутствуют в более старых.
- Созданные вами сессии будут палиться в Computer Management'е (описано далее), а также любое приложение сможет перечислить имеющиеся в системе сессии; их можно будет остановить и удалить извне. Разумеется, для этого тоже нужны будут права администратора.
В этой статье я буду рассматривать только создание потребителей событий ETW. Провайдеры - это отдельная большая тема, с которой при желании можно познакомиться, почитав соответствующие статьи в MSDN.
Само ETW API - не сахар. Оно неочевидное, не слишком хорошо документировано, многие вещи приходится додумывать или искать в Интернете примеры использования той или иной функции. Статей на эту тему написано не так много (некоторые дополнительные ссылки я приведу в конце статьи). Я постараюсь прояснить большинство вопросов, которые у нас возникнут по ходу разбора этого API.
Перед тем, как я перейду к коду, который позволяет создавать сессии и получать события, я опишу несколько штатных системных утилит Windows, которые помогут нам в работе с ETW. Вначале, конечно, неплохо было бы изучить список провайдеров, доступных на конкретной ОС. Для этого можно использовать следующую команду:
1 |
logman query providers |
Запускать нужно, как я уже говорил, от имени администратора. Команда выдаст нам полный список доступных в системе провайдеров ETW. Вы увидите что-то вроде этого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Provider GUID ------------------------------------------------------------------------------- .NET Common Language Runtime {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4} ACPI Driver Trace Provider {DAB01D4D-2D48-477D-B1C3-DAAD0CE6F06B} Active Directory Domain Services: SAM {8E598056-8993-11D2-819E-0000F875A064} Active Directory: Kerberos Client {BBA3ADD2-C229-4CDB-AE2B-57EB6966B0C4} Active Directory: NetLogon {F33959B4-DBEC-11D2-895B-00C04F79AB69} ADODB.1 {04C8A86F-3369-12F8-4769-24E484A9E725} ADOMD.1 {7EA56435-3F2F-3F63-A829-F0B35B5CAD41} Application Popup {47BFA2B7-BD54-4FAC-B70B-29021084CA8F} Application-Addon-Event-Provider {A83FA99F-C356-4DED-9FD6-5A5EB8546D68} ASP.NET Events {AFF081FE-0247-4275-9C4E-021F3DC1DA35} ATA Port Driver Tracing Provider {D08BD885-501E-489A-BAC6-B7D24BFE6BBF} AuthFw NetShell Plugin {935F4AE6-845D-41C6-97FA-380DAD429B72} ... |
Есть также возможность посмотреть, какие провайдеры подключены к тому или иному процессу, чтобы узнать, какую информацию через ETW-события этот самый процесс может нам предоставлять:
1 |
logman query providers -pid <PID процесса> |
Вот, например, вывод для Notepad++ на Win10:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Provider GUID ------------------------------------------------------------------------------- Microsoft-Windows-AsynchronousCausality {19A4C69A-28EB-4D4B-8D94-5F19055A1B5C} Microsoft-Windows-COM-Perf {B8D6861B-D20F-4EEC-BBAE-87E0DD80602B} Microsoft-Windows-Crypto-BCrypt {C7E089AC-BA2A-11E0-9AF7-68384824019B} Microsoft-Windows-Deplorch {B9DA9FE6-AE5F-4F3E-B2FA-8E623C11DC75} Microsoft-Windows-Direct3D11 {DB6F6DDB-AC77-4E88-8253-819DF9BBF140} Microsoft-Windows-DirectComposition {C44219D0-F344-11DF-A5E2-B307DFD72085} Microsoft-Windows-DNS-Client {1C95126E-7EEA-49A9-A3FE-A378B03DDB4D} Microsoft-Windows-Documents {C89B991E-3B48-49B2-80D3-AC000DFC9749} Microsoft-Windows-Dwm-Api {292A52C4-FA27-4461-B526-54A46430BD54} Microsoft-Windows-DXGI {CA11C036-0102-4A2D-A6AD-F03CFED5D3C9} Microsoft-Windows-Immersive-Shell-API {5F0E257F-C224-43E5-9555-2ADCB8540A58} Microsoft-Windows-KnownFolders {8939299F-2315-4C5C-9B91-ABB86AA0627D} Microsoft-Windows-Networking-Correlation {83ED54F0-4D48-4E45-B16E-726FFD1FA4AF} Microsoft-Windows-RPC {6AD52B32-D609-4BE9-AE07-CE8DAE937E39} Microsoft-Windows-RPC-Events {F4AED7C7-A898-4627-B053-44A7CAA12FCD} Microsoft-Windows-Shell-Core {30336ED4-E327-447C-9DE0-51B652C86108} Microsoft-Windows-TSF-msctf {4FBA1227-F606-4E5F-B9E8-FAB9AB5740F3} Microsoft-Windows-User-Diagnostic {305FC87B-002A-5E26-D297-60223012CA9C} Microsoft-Windows-UxTheme {422088E6-CD0C-4F99-BD0B-6985FA290BDF} Microsoft-Windows-WinRT-Error {A86F8471-C31D-4FBC-A035-665D06047B03} Microsoft-Windows-Winsock-NameResolution {55404E71-4DB9-4DEB-A5F5-8F86E46DDE56} ... |
Провайдеры, которые попали в процесс из загруженных в него внешних DLL-модулей, также будут включены в список. Просмотрев список провайдеров ОС или конкретного процесса, можно найти что-нибудь интересное. Для себя я выбрал Microsoft-Windows-Kernel-Process
, логично предположив, что он нам может рассказать что-то о процессах в системе. На нем мы и будем тренироваться получать и парсить события. Выполняем следующую команду:
1 |
wevtutil gp Microsoft-Windows-Kernel-Process /ge /gm |
На выходе имеем практически полную информацию о провайдере: его GUID, список каналов, уровней логирования, ключевых слов (об этом далее) и событий. Собственно, события-то нас в первую очередь и интересуют, ведь это как раз то, что мы можем получить от провайдера. Вот, например, пара событий, которые сообщают о том, что в системе появился новый или, соответственно, завершился процесс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
events: event: value: 1 version: 0 opcode: 1 channel: 16 level: 4 task: 1 keywords: 0x8000000000000010 message: Process %1 started at time %2 by parent %3 running in session %4 with name %6. event: value: 2 version: 0 opcode: 2 channel: 16 level: 4 task: 2 keywords: 0x8000000000000010 message: Process %1 (which started at time %2) stopped at time %3 with exit code %4. ... |
Здесь value
- это идентификатор события, он нам потребуется далее, чтобы отличать полученные события друг от друга. Хочу заметить, что многие провайдеры также содержат недокументированные события (без каких-либо сообщений), а также существуют полностью недокументированные (безымянные) провайдеры. Это очень большое поле для исследований, но пока что мы остановимся на том, что хорошо описано.
Теперь неплохо было бы получить эти события и посмотреть, что же в них содержится. К сожалению, я не нашел годного и быстрого способа просмотреть метаданные событий без того, чтобы их получать (возможно, такой способ все же есть, если вы о нем знаете, напишите комментарий). На самом деле, провайдер может предоставлять события в куче различных форматов (MOF, WPP, Manifest-based и появившийся в Win10 TraceLogging; подробнее о них здесь). Пока это нас не сильно интересует: мы воспользуемся стандартными средствами Windows, чтобы создать сессию ETW и получить события в файл журнала. Итак, начнем. Сначала запустим оснастку "Computer Management" / "Управление компьютером" (Win+R -> compmgmt.msc
) и перейдем в раздел "Performance" -> "Data Collector Sets" -> "Event Trace Sessions" ("Производительность" -> "Группы сборщиков данных" -> "Сеансы отслеживания событий"). Здесь мы увидим список имеющихся сессий ETW:
Давайте создадим свою для провайдера Microsoft-Windows-Kernel-Process
. Правой кнопкой мыши кликнем на свободном поле в списке сессий, выберем "New" -> "Data Collector Set" ("Создать" -> "Группа сборщиков данных"). Откроется окно создания сессии. Укажем имя сессии и выберем "Create manually (Advanced)" ("Создать вручную (для опытных)", мы же с вами опытные ребята):
Теперь пришло время добавить интересующие нас провайдеры в сессию. У нас он всего один, выбираем его из списка, нажав "Add..." ("Добавить..."). Указываем интересующие нас ключевые слова в поле "Keywords(Any)" ("Ключевые слова(Любые)"). Помните, раньше мы уже видели список ключевых слов провайдера, когда запускали wevtutil
? Я добавлю всего одно ключевое слово 0x10
- события, имеющие отношение к процессу:
1 2 3 4 5 6 |
keywords: keyword: name: WINEVENT_KEYWORD_PROCESS mask: 10 message: ... |
Если, опять-таки, посмотреть на вывод wevtutil
, то мы увидим, что этому ключевому слову соответствует всего два события: старт и остановка процесса (ID 1 и 2). Для полноценного исследования абсолютно всех событий провайдера можно указать 0xFFFFFFFFFFFFFFFF
. Далее указываем уровень логирования (опять-таки, для надежности можно указать 0xFF
, чтобы ничего не пропустить, но мы укажем 0x4
- Informational
). Есть также возможность указать прочие настройки сессии (например фильтр), но нам пока что хватит того, что мы уже настроили, чтобы получить интересующие нас события.
Нажимаем "Next" ("Далее"). Выбираем путь, по которому будет сохраняться файл журнала сессии. Снова нажимаем "Next". Выбираем флажок "Start this data collector set now" ("Запустить группу сборщиков данных сейчас") и нажимаем "Finish" ("Готово"):
Теперь сессия создана, запущена и собирает интересующие нас события. Для теста запустим notepad.exe
, а потом закроем его. Остановим сессию, кликнув на ее имени правой кнопкой мыши и выбрав "Stop" ("Остановить") (сессию можно после этого удалить, выбрав "Delete" ("Удалить")). Перейдем в папку, которую мы указали для сохранения журнала сессии. Там будет находиться файл с именем имя_сессии.etl
(в моем случае - my session.etl
. Нам нужно его разобрать. Для этого существует еще одна стандартная утилита Windows. Запускаем ее следующим образом, предварительно перейдя в консоли в папку с etl-файлом:
1 |
tracerpt "my session.etl" -o "result.xml" |
Получим в той же папке файл в XML-формате result.xml
. Можно открыть его в любом текстовом редакторе, чтобы увидеть интересующие нас события запуска и остановки notepad.exe
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"> <System> <Provider Name="Microsoft-Windows-Kernel-Process" Guid="{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}" /> <EventID>1</EventID> <Version>2</Version> <Level>4</Level> <Task>1</Task> <Opcode>1</Opcode> <Keywords>0x8000000000000010</Keywords> <TimeCreated SystemTime="2017-03-13T22:53:05.043418000+0259" /> <Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" /> <Execution ProcessID="4508" ThreadID="1804" ProcessorID="5" KernelTime="30" UserTime="15" /> <Channel>Microsoft-Windows-Kernel-Process/Analytic</Channel> <Computer /> </System> <EventData> <Data Name="ProcessID"> 2392</Data> <Data Name="CreateTime">2017-03-13T19:53:05.043397100Z</Data> <Data Name="ParentProcessID"> 4508</Data> <Data Name="SessionID"> 1</Data> <Data Name="Flags"> 0</Data> <Data Name="ImageName">\Device\HarddiskVolume2\Windows\System32\notepad.exe</Data> <Data Name="ImageChecksum">0x47C64</Data> <Data Name="TimeDateStamp">0x5789986A</Data> <Data Name="PackageFullName"></Data> <Data Name="PackageRelativeAppId"></Data> </EventData> <RenderingInfo Culture="ru-RU"> <Level>Information </Level> <Opcode>Start </Opcode> <Keywords> <Keyword>WINEVENT_KEYWORD_PROCESS</Keyword> </Keywords> <Task>ProcessStart</Task> <Message>Process 2392 started at time ‎2017‎-‎03‎-‎13T19:53:05.043397100Z by parent 4508 running in session 1 with name \Device\HarddiskVolume2\Windows\System32\notepad.exe. </Message> <Channel>Microsoft-Windows-Kernel-Process/Analytic</Channel> <Provider>Microsoft-Windows-Kernel-Process </Provider> </RenderingInfo> </Event> <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"> <System> <Provider Name="Microsoft-Windows-Kernel-Process" Guid="{22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716}" /> <EventID>2</EventID> <Version>1</Version> <Level>4</Level> <Task>2</Task> <Opcode>2</Opcode> <Keywords>0x8000000000000010</Keywords> <TimeCreated SystemTime="2017-03-13T22:53:08.995285700+0259" /> <Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" /> <Execution ProcessID="2392" ThreadID="7448" ProcessorID="0" KernelTime="0" UserTime="0" /> <Channel>Microsoft-Windows-Kernel-Process/Analytic</Channel> <Computer /> </System> <EventData> <Data Name="ProcessID"> 2392</Data> <Data Name="CreateTime">2017-03-13T19:53:05.043397100Z</Data> <Data Name="ExitTime">2017-03-13T19:53:08.995104700Z</Data> <Data Name="ExitCode"> 0</Data> <Data Name="TokenElevationType"> 1</Data> <Data Name="HandleCount"> 74</Data> <Data Name="CommitCharge">2469888</Data> <Data Name="CommitPeak">2666496</Data> <Data Name="CPUCycleCount">183003329</Data> <Data Name="ReadOperationCount"> 2</Data> <Data Name="WriteOperationCount"> 0</Data> <Data Name="ReadTransferKiloBytes"> 6</Data> <Data Name="WriteTransferKiloBytes"> 0</Data> <Data Name="HardFaultCount"> 0</Data> <Data Name="ImageName">notepad.exe</Data> </EventData> <RenderingInfo Culture="ru-RU"> <Level>Information </Level> <Opcode>Stop </Opcode> <Keywords> <Keyword>WINEVENT_KEYWORD_PROCESS</Keyword> </Keywords> <Task>ProcessStop</Task> <Message>Process 2392 (which started at time ‎2017‎-‎03‎-‎13T19:53:05.043397100Z) stopped at time ‎2017‎-‎03‎-‎13T19:53:08.995104700Z with exit code 0. </Message> <Channel>Microsoft-Windows-Kernel-Process/Analytic</Channel> <Provider>Microsoft-Windows-Kernel-Process </Provider> </RenderingInfo> </Event> |
Здесь мы уже видим имена и значения тех свойств, которые приходят с каждым ивентом (например, ProcessID
, ExitCode
и т.д.). К сожалению, здесь по-прежнему не видны типы данных этих свойств, и о них можно лишь догадываться, изучая значения этих свойств.
Что ж, теперь перейдем к коду. Как же сделать так, чтобы наше приложение получало события от нужных провайдеров в режиме реального времени, а затем выколупывало из них все интересующие нас свойства? Писать будем на C++14
. Я сделал небольшую библиотеку, с помощью которой можно реализовать все перечисленное, далее я буду приводить выдержки кода из нее с комментариями. Полный код библиотеки вместе с тестовыми приложениями можно будет скачать в конце статьи. Наш код будет выступать в роли контроллера сессии и потребителя событий одновременно.
Для начала сделаем класс, который будет подготавливать структуру настроек для сессии, которую мы потом создадим.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class event_trace_session_properties { public: //У каждой сессии должно быть имя; //имя - это уникальный идентификатор сессии в системе explicit event_trace_session_properties(const std::wstring& session_name); //PEVENT_TRACE_PROPERTIES - указатель на структуру, которую подготовит //этот класс. operator PEVENT_TRACE_PROPERTIES() noexcept { return reinterpret_cast<PEVENT_TRACE_PROPERTIES>(data_.data()); } operator const EVENT_TRACE_PROPERTIES*() const noexcept { return reinterpret_cast<const EVENT_TRACE_PROPERTIES*>(data_.data()); } private: using props_type = std::vector<std::uint8_t>; props_type data_; }; //В конструкторе мы аллоцируем память под структуру EVENT_TRACE_PROPERTIES //и имя сессии. Потом мы заполняем необходимые поля структуры. event_trace_session_properties::event_trace_session_properties(const std::wstring& session_name) : data_(sizeof(EVENT_TRACE_PROPERTIES) + (session_name.size() + 1) * sizeof(std::wstring::value_type)) { auto& sessionProperties = *static_cast<PEVENT_TRACE_PROPERTIES>(*this); constexpr const ULONG QueryPerformanceCounterClientContext = 1; //Используем QueryPerformanceCounter для вычисления временных меток событий sessionProperties.Wnode.ClientContext = QueryPerformanceCounterClientContext; sessionProperties.Wnode.BufferSize = data_.size(); //Будем получать события в режиме реального времени sessionProperties.LogFileMode = EVENT_TRACE_REAL_TIME_MODE; //По этому смещению от начала структуры будет располагаться имя сессии sessionProperties.LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES); } |
Структура не совсем простая - после нее будет записано имя сессии, поэтому нам нужно будет выделить соответствующий объем памяти под это имя, помимо заполнения полей структуры. Копировать имя в выделенную память не нужно, это за нас будет делать ETW API. Теперь мы можем подготовить структуру, описывающую нашу сессию. Далее нужно эту самую сессию создать и запустить. Вот интерфейс класса сессии:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
class event_trace_session { public: //Уровень логирования enum class trace_level : std::uint8_t { none = TRACE_LEVEL_NONE, critical = TRACE_LEVEL_CRITICAL, fatal = TRACE_LEVEL_FATAL, error = TRACE_LEVEL_ERROR, warning = TRACE_LEVEL_WARNING, information = TRACE_LEVEL_INFORMATION, verbose = TRACE_LEVEL_VERBOSE }; public: //Сохраняем имя сессии, затем запускаем ее template<typename String> explicit event_trace_session(String&& session_name) : session_name_(std::forward<String>(session_name)) { create_or_replace_trace_session(); } event_trace_session(const event_trace_session&) = delete; event_trace_session& operator=(const event_trace_session&) = delete; ~event_trace_session(); //Закрыть и удалить сессию void close_trace_session(); //Следующие две функции позволяют добавить к сессии провайдер по GUID'у. //После добавления провайдера сессия будет выдавать события этого провайдера //всем потребителями. void enable_trace(const ms_guid& provider_guid, trace_level level); void enable_trace(const ms_guid& provider_guid, trace_level level, std::uint64_t match_any_keywords); //Получить имя сессии const std::wstring& get_name() const noexcept { return session_name_; } private: void create_or_replace_trace_session(); TRACEHANDLE handle_ = 0; std::wstring session_name_; }; |
Начнем разбор имплементации с создания и завершения сессии:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
void event_trace_session::create_or_replace_trace_session() { //Заполняем свойства сессии, используя ранее созданный класс: event_trace_session_properties props(session_name_); ULONG result = 0; bool closed = false; for (int i = 0; i < 2; ++i) { //Сначала пытаемся создать сессию switch (result = ::StartTraceW(&handle_, session_name_.c_str(), props)) { case ERROR_SUCCESS: return; case ERROR_ALREADY_EXISTS: if (i) break; //Если сессия уже существует - закрываем ее и пробуем //создать ее еще раз close_trace_session(); continue; default: break; } } //Если ничего не вышло - кидаем исключение. //event_trace_error - простой класс, унаследованный от std::runtime_error, //который позволяет помимо текста сообщения прикопать еще и код ошибки. throw event_trace_error("Unable to start trace session", result); } //Закрытие сессии void event_trace_session::close_trace_session() { event_trace_session_properties props(session_name_); //Закрываем сессию с помощью ControlTrace auto result = ::ControlTraceW(0, session_name_.c_str(), props, EVENT_TRACE_CONTROL_STOP); //Если сессия закрылась, или же ее не существовало, то все ОК if (ERROR_SUCCESS != result && ERROR_WMI_INSTANCE_NOT_FOUND != result) throw event_trace_error("Unable to stop trace session", result); } |
Здесь я отмечу еще раз, что сессия - глобальный объект ОС Windows. Ее имя является ее же уникальным идентификатором. Логично, что сессия с тем именем, которое мы устанавливаем, может уже существовать в системе. Поэтому мы в этом случае закрываем старую сессию и пытаемся после этого еще раз создать новую. Стоит также помнить, что сессию после использования нужно обязательно закрывать (например, при выходе из приложения), потому что в противном случае сессия останется висеть до перезагрузки компьютера и потреблять его ресурсы.
Теперь перейдем к функции добавления провайдера к сессии:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void event_trace_session::enable_trace(const ms_guid& provider_guid, trace_level level, std::uint64_t match_any_keywords) { ENABLE_TRACE_PARAMETERS params{}; params.Version = ENABLE_TRACE_PARAMETERS_VERSION_2; //Добавляем провайдер к сессии с помощью функции EnableTraceEx2 auto result = ::EnableTraceEx2(handle_, provider_guid.native_ptr(), EVENT_CONTROL_CODE_ENABLE_PROVIDER, static_cast<UCHAR>(level), match_any_keywords, 0, 0, ¶ms); if (ERROR_SUCCESS != result) throw event_trace_error("Unable to enable trace for provider", result); } void event_trace_session::enable_trace(const ms_guid& provider_guid, trace_level level) { //По умолчанию включаем все ключевые слова событий return enable_trace(provider_guid, level, (std::numeric_limits<std::uint64_t>::max)()); } |
Тут мы можем указать желаемый уровень логирования, а также набор ключевых слов для провайдера, события, соответствующие которым, мы будем получать. Про то, что такое ключевые слова, я уже говорил выше.
Подробнее прочитать про использованные WinAPI-функции можно здесь: StartTrace, ControlTrace и EnableTraceEx2 (последняя доступна только на Windows 7 и новее, если требуется совместимость с Vista, то нужно обратить внимание на функцию EnableTraceEx).
Отмечу, что события real-time-сессии мы будем получать все же не мгновенно, как только они возникли, а в пределах одной секунды, когда сессия будет сбрасывать буферы событий. Ускорить этот таймер нельзя, замедлить - можно. Подробнее можно прочитать об этом в документации на параметр FlushTimer
структуры EVENT_TRACE_PROPERTIES.
Теперь мы сможем создать и настроить сессию. Переходим к последней части работы с сессиями: потребление событий от запущенной real-time-сессии. Здесь мы меняем нашу роль с контроллера сессии на потребителя событий. Рассмотрим класс event_trace
, который реализует сказанное. Здесь используется библиотека boost.signals2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
class event_trace { public: //Событие, когда приходит новый ивент от сессии using event_processor = void(PEVENT_RECORD record); using event_processor_signal = boost::signals2::signal<event_processor>; //Событие при возникновении ошибки using error_processor = void(std::uint32_t error); using error_processor_signal = boost::signals2::signal<error_processor>; //Событие, когда потребление ивентов завершено using stop_processor = void(); using stop_processor_signal = boost::signals2::signal<stop_processor>; public: //Создание потребителя событий для какой-либо сессии explicit event_trace(const event_trace_session& session); event_trace(const event_trace&) = delete; event_trace& operator=(const event_trace&) = delete; ~event_trace(); //Подключает обработчик ивентов сессии template<typename Handler> boost::signals2::connection on_trace_event(Handler&& handler) { return on_event_.connect(std::forward<Handler>(handler)); } //Подключает обработчик ошибок template<typename Handler> boost::signals2::connection on_error(Handler&& handler) { return on_error_.connect(std::forward<Handler>(handler)); } //Подключает обработчик ивентов сессии для конкретного провайдера template<typename Handler> boost::signals2::connection on_trace_event(const ms_guid& trace_provider, Handler&& handler) { return on_provider_event_[trace_provider].connect(std::forward<Handler>(handler)); } //Подключает обработчик ивентов сессии для конкретного провайдера и идентификатора событий. //(только для событий, определенных через манифест) template<typename Handler> boost::signals2::connection on_trace_event(const ms_guid& trace_provider, USHORT event_id, Handler&& handler) { return on_provider_event_[{ trace_provider, event_id }].connect(std::forward<Handler>(handler)); } //Подключает обработчик события об остановке получения ивентов template<typename Handler> boost::signals2::connection on_stop_trace(Handler&& handler) { return on_stop_trace_.connect(std::forward<Handler>(handler)); } //Запустить получение событий в отдельном потоке void run_async(); //Запустить получение событий синхронно (вызов блокируется до тех пор, //пока не выполнена функция stop()) void run(); //Остановить получение событий сессии void stop(); private: //Создать потребитель событий void open_trace(); //Начать получение событий void start_monitoring(bool throw_error); //Функция-коллбэк, которую вызывает ОС Windows, когда провайдер предоставляет нам //новое событие static void __stdcall static_process_trace_event(PEVENT_RECORD record); void process_trace_event(PEVENT_RECORD record, std::uint32_t error) noexcept; private: struct event_key { event_key(const ms_guid& guid) : guid(guid) { } event_key(const ms_guid& guid, USHORT event_id) : guid(guid) , event_id(event_id) { } friend bool operator<(const event_key& left, const event_key& right) noexcept { return left.guid < right.guid || (left.guid == right.guid && left.event_id < right.event_id); } ms_guid guid; boost::optional<USHORT> event_id; }; std::wstring session_name_; event_processor_signal on_event_; error_processor_signal on_error_; stop_processor_signal on_stop_trace_; std::map<event_key, event_processor_signal> on_provider_event_; event_trace_handle trace_handle_; std::thread event_processor_; std::atomic_flag started_ = ATOMIC_FLAG_INIT; }; |
Вопрос, который мог возникнуть после чтения этого кода - зачем нам событие об остановке сессии, ведь мы сами знаем, когда ее останавливаем? Снова напоминаю, что так как сессия - глобальный объект, ее может остановить кто угодно с соответствующими правами. Поэтому нам стоит знать, если логирование вдруг остановилось извне.
Еще интересен метод on_trace_event
, который, помимо прочих, принимает параметр USHORT event_id
. Этот метод позволяет подписаться на конкретное событие конкретного провайдера. Однако, не все события будут иметь идентификатор. Если провайдер присылает события в формате Manifest-based, то этот идентификатор будет выставлен (см. комментарии к структуре EVENT_DESCRIPTOR). Если же события описаны в формате MOF (DecodingSourceWbem
), то нужно смотреть на TRACE_EVENT_INFO::EventGuid
(о том, как получить эту структуру, далее). Все системные провайдеры, на которые я подписывался, предоставляли события в формате Manifest-based (DecodingSourceXMLFile
), поэтому в библиотеку я добавил удобную подписку по идентификаторам ивентов, которые в этом случае выставляются. Как вы сейчас уже осознаете, API для ETW действительно не самое стройное: оно предлагает нам огромные структуры, отдельные части из которых могут использоваться лишь в особых случаях.
Начнем разбор имплементации с кода создания потребителя событий. Для этого используется WinAPI-функция OpenTrace. Стоит отметить, что и OpenTrace
, и StartTrace
возвращают TRACEHANDLE
, но их не нужно путать: первая имеет отношение к потребителю событий имеющейся сессии, а вторая создает саму сессию. Как я уже говорил, ETW - говнистое неочевидное API, но тут ничего не поделаешь.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void event_trace::open_trace() { //Заполняем структуру для создания потребителя событий сессии EVENT_TRACE_LOGFILEW trace{}; auto session_name = session_name_; trace.LoggerName = &session_name[0]; //Потребляем события в режиме реального времени. //Флаг PROCESS_TRACE_MODE_EVENT_RECORD говорит о том, что //мы будем использовать коллбэк EventRecordCallback для получения событий trace.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD; //Коллбэк, который вызывается при получении события trace.EventRecordCallback = static_process_trace_event; //Контекст, будет передан в EventRecordCallback trace.Context = this; //Создаем потребителя событий сессии! //trace_handle_ - это объект класса event_trace_handle, который //автоматически закрывает трейс с помощью функции CloseTrace //в деструкторе. Его код я приводить в статье не буду. trace_handle_.reset(::OpenTraceW(&trace)); if (!trace_handle_.is_valid()) throw event_trace_error("Unable to open trace", ::GetLastError()); } |
Наконец, начинаем обработку приходящих к нам событий с помощью функции ProcessTrace:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
//Значение флага throw_error выставляется в true при синхронной //обработке событий и в false при асинхронной. void event_trace::start_monitoring(bool throw_error) { auto handle = trace_handle_.get(); //1 - колчество хендлов consumer'ов, у нас он один auto result = ::ProcessTrace(&handle, 1, 0, 0); if (ERROR_SUCCESS != result && ERROR_CANCELLED != result) { if (throw_error) throw event_trace_error("Unable to start trace monitoring", result); process_trace_event(nullptr, result); } else { if (throw_error) { on_stop_trace_(); return; } try { on_stop_trace_(); } catch (const std::exception&) { assert(false); } } } |
Функция ProcessTrace блокируется до тех пор, пока кто-то не закроет сессию либо не освободит (CloseTrace
) хендл потребителя событий. Поэтому класс event_trace
предоставляет возможность получать события как синхронно, так и асинхронно.
Как же мы обрабатываем получаемые события? А вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
void __stdcall event_trace::static_process_trace_event(PEVENT_RECORD record) { //В UserContext у нас указатель на event_trace if (record->UserContext) static_cast<event_trace*>(record->UserContext)->process_trace_event(record, 0u); } void event_trace::process_trace_event(PEVENT_RECORD record, std::uint32_t error) noexcept { try { if (error) { on_error_(error); return; } //Заголовки ивентов пропускаем. Такой заголовок вы могли видеть раньше в //XML-файле, который мы сгенерировали из ETL-файла с помощью tracerpt ранее. //Впрочем, real-time сессии не генерируют заголовков, поэтому это перестраховка. if (record->EventHeader.ProviderId == EventTraceGuid) return; //Далее вызываем соответствующие коллбэки, на которые могут подписаться другие классы, //заинтересованные в тех или иных событиях. on_event_(record); auto it = on_provider_event_.find({ record->EventHeader.ProviderId }); if (it != on_provider_event_.cend()) (*it).second(record); it = on_provider_event_.find({ record->EventHeader.ProviderId, record->EventHeader.EventDescriptor.Id }); if (it != on_provider_event_.cend()) (*it).second(record); } catch (...) { assert(false); } } |
Вот, собственно, и вся суть класса event_trace
. Мы можем теперь создать сессию, настроить ее, добавив к ней необходимые провайдеры, а потом получить события этой сессии. Но рано расслабляться, ведь это - только половина дела (а может, и меньше)! Нам же нужно еще разобрать полученные события, получив из них интересующие нас свойства. В этом нам поможет библиотека tdh.dll
. Она позволяет унифицировано распарсивать события ETW. Единственный формат, с которым могут возникнуть сложности - это WPP (невозможно будет получить форматированные строки событий, потому что для парсинга необходимы tmf-файлы), но это нас не сильно волнует, потому что формат WPP используется только для отладки приложений, а все системные провайдеры предоставляют события в других форматах.
Устройство каждого ивента в рамках tdh.dll
таково:
- Событие может содержать единственную строку, и никаких свойств в нем не будет.
- Событие может содержать одно или более именованных свойств разных типов.
- Каждое свойство может быть массивом переменной длины (по сути, все свойства - это массивы, но у большинства из них единственный элемент).
- Каждое свойство может быть структурой.
- Структура содержит набор свойств разных типов (включая массивы переменной длины), но не другие структуры.
Со всем этим добром нам и придется работать. Я написал класс event_info
, который извлекает разные параметры события, включая свойства (простые, массивы и структуры) в нетипизированном виде, аж 500 строк кода, и класс event_property
, который представляет конкретное значение свойства (это значение обычного свойства, один из элементов массива-свойства, либо значение параметра свойства-структуры) еще на 700 строк (точнее, сам класс маленький, а вот код конвертации нетипизированных свойств в типизированные - длинный, это класс event_property_converter
). Я не буду приводить этот код полностью. Для начала я приведу интерфейсы с описанием, затем - несколько примеров кода преобразования типов, а потом дам комментарии по поводу использованных WinAPI-функций. Скажу сразу: интерфейс для работы с ивентами, предоставляемый библиотекой tdh.dll
, во многих отношениях еще более неочевидный и запутанный чем то, что мы уже изучили. Начнем с event_info
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
class event_info { public: //Получает информацию о ивенте из непосредственно полученной //записи события explicit event_info(PEVENT_RECORD record); //Информация об ивенте (низкоуровневая) operator PTRACE_EVENT_INFO() noexcept { return reinterpret_cast<PTRACE_EVENT_INFO>(data_.data()); } operator const TRACE_EVENT_INFO*() const noexcept { return reinterpret_cast<const TRACE_EVENT_INFO*>(data_.data()); } operator PEVENT_RECORD() noexcept { return record_; } operator const EVENT_RECORD*() const noexcept { return record_; } //Если ивент содержит простую UNICODE-строку без имени и без свойств, //то этот метод вернёт true. В этом случае все методы для работы со //свойствами не применяются (они будут кидать исключение), //и строка получается с помощью метода get_event_string(). bool has_string_only() const noexcept; event_property get_event_string() const; //Получить имя top-level свойства по его имени. //Top-level - это свойство, вложенное непосредственно в событие, //а не в какую-либо структуру. const wchar_t* get_property_name(ULONG top_level_index) const; //Получить сообщение события. Может вернуть nullptr, если сообщение отсутствует. const wchar_t* get_event_message() const noexcept; //Получить низкоуровневый дескриптор события const EVENT_DESCRIPTOR& get_event_descriptor() const noexcept; //Получить идентификатор события USHORT get_event_id() const noexcept; //Получить количество top-level свойств события ULONG get_top_level_property_count() const noexcept; //Проверить, структура ли какое-либо свойство или нет, по его индексу bool is_property_struct(ULONG top_level_index) const; //Если свойство является структурой, то этот метод вернет вспомогательный класс //для доступа к этой структуре event_info_structure get_structure(ULONG top_level_index) const; //Получить индекс top-level свойства по его имени bool find_property_index(const std::wstring& name, ULONG& top_level_index) const; //Получить размер массива top-level свойства. Для большинства свойств это //будет единица, как я уже писал выше ULONG get_array_property_size(ULONG top_level_index) const; //Получить значение top-level свойства по его индексу. Если свойство - массив, в котором //более одного элемента, кинет исключение. event_property get_plain_property_value(ULONG top_level_index) const; //Получить значение top-level свойства по его индексу. Поддерживает как обычные свойства, //так и массивы. event_property get_array_property_value(ULONG top_level_index, ULONG element_index) const; //Далее идут аналогичные методы, но для структур: //Получить индекс свойства структуры по его имени bool find_property_index(const event_info_structure& structure, const std::wstring& name, ULONG& index) const; //Получить размер массива свойства структуры. Если свойство не является массивом, //то вернется 1 ULONG get_array_property_size(const event_info_structure& structure, ULONG struct_member_index) const; //Получить значение свойства структуры по его индексу. Если свойство - массив, в котором //более одного элемента, кинет исключение. event_property get_plain_property_value(const event_info_structure& structure, ULONG struct_member_index) const; //Получить значение свойства структуры по его индексу. Если свойство - массив, в котором //более одного элемента, кинет исключение. Этот метод поддерживает свойства, в которых //top-level свойство является массивом структур. event_property get_plain_property_value(const event_info_structure& structure, ULONG struct_index, ULONG struct_member_index) const; //Получить значение свойства структуры по его индексу. Поддерживает как обычные свойства, //так и массивы. Top-level свойство может быть как единственной структурой, так и массивом структур. event_property get_array_property_value(const event_info_structure& structure, ULONG struct_index, ULONG struct_member_index, ULONG struct_element_index) const; //Далее идут хелперы, которые позволяют получить типизированное свойство //по его имени или индексу. template<typename PropertyType> bool get_plain_property_value(const std::wstring& name, PropertyType& value) const { ULONG top_level_index = 0u; if (!find_property_index(name, top_level_index)) return false; value = event_property_converter<PropertyType>::convert(get_plain_property_value(top_level_index)); return true; } template<typename PropertyType> auto get_plain_property_value(const std::wstring& name) const { ULONG top_level_index = 0u; if (!find_property_index(name, top_level_index)) throw event_trace_error("Property was not found in event"); return event_property_converter<PropertyType>::convert(get_plain_property_value(top_level_index)); } template<typename PropertyType> auto get_plain_property_value(ULONG top_level_index) const { return event_property_converter<PropertyType>::convert(get_plain_property_value(top_level_index)); } template<typename PropertyType> auto get_array_property_value(ULONG top_level_index, ULONG element_index) const { return event_property_converter<PropertyType>::convert( get_array_property_value(top_level_index, element_index)); } template<typename PropertyType> auto get_plain_property_value(const event_info_structure& structure, ULONG struct_member_index) const { return event_property_converter<PropertyType>::convert( get_plain_property_value(structure, struct_member_index)); } template<typename PropertyType> auto get_plain_property_value(const event_info_structure& structure, ULONG struct_index, ULONG struct_member_index) const { return event_property_converter<PropertyType>::convert( get_plain_property_value(structure, struct_index, struct_member_index)); } template<typename PropertyType> auto get_array_property_value(const event_info_structure& structure, ULONG struct_index, ULONG struct_member_index, ULONG struct_element_index) const { return event_property_converter<PropertyType>::convert( get_array_property_value(structure, struct_index, struct_member_index, struct_element_index)); } private: //Несколько обобщенных методов для получения свойств ивентов event_property get_array_property_value(ULONG top_level_index, ULONG element_index, bool is_array) const; event_property get_array_property_value(const event_info_structure& structure, ULONG struct_index, ULONG struct_member_index, ULONG struct_element_index, bool is_array, bool is_struct_member_array) const; event_property get_array_property_value(ULONG top_level_index, ULONG element_index, ULONG struct_member_index, bool is_array, bool is_struct_member_array, PROPERTY_DATA_DESCRIPTOR* data_descriptors, ULONG descriptor_count) const; //Вспомогательная функция, кидает исключение, если ивент не содержит //свойств, а только UNICODE-строку. void check_if_has_properties() const; private: std::vector<uint8_t> data_; PEVENT_RECORD record_; }; //Вывод всех свойств события в удобоваривом виде в поток std::wostream& operator<<(std::wostream& stream, const event_info& info); |
Здесь целая куча всяких методов, но нам понадобится всего несколько. Шаблонные хелперы позволяют получить сразу типизированное значение свойства. Для конвертации используется класс event_property_converter
, который определен рядом с event_property
. Кратенько рассмотрим класс event_property
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class event_property { public: //Изначально значение свойства содержится в нетипизированном буфере using raw_value_type = std::vector<std::uint8_t>; event_property(std::uint16_t in_type, std::uint16_t out_type, bool wide_pointer, raw_value_type&& value, std::wstring&& name) noexcept; //Получить тип свойства на входе std::uint16_t get_in_type() const noexcept { return in_type_; } //Получить тип свойства на выходе std::uint16_t get_out_type() const noexcept { return out_type_; } //Получить имя свойства const std::wstring& get_name() const noexcept { return name_; } //Получить нетипизированное значение свойства const raw_value_type& get_raw_value() const noexcept { return value_; } //Получить тип указателя свойства bool is_wide_pointer() const noexcept { return wide_pointer_; } //Преобразовать значение свойства в строку std::wstring to_wstring() const; private: std::uint16_t in_type_; std::uint16_t out_type_; bool wide_pointer_; raw_value_type value_; std::wstring name_; }; |
- Какие еще типы на входе и выходе, окаянный ты демон? Что за размеры указателей?! - Вот что вы, возможно, думаете после прочтения этой части кода. Все по порядку! Итак, тип данных на входе - это, непосредственно, тип данных свойства. Такой, как INT32
, UNICODESTRING
или GUID
. Он указывает, что содержится в значении raw_value_type value_
. А вот тип на выходе нужен лишь для отображения свойства в виде строки. Этот тип - исключительно информативный, это скорее способ форматирования свойства. Например, тип на входе может быть UINT32
(т.е. 4 байта в little endian), а на выходе - PID
, или IPv4
, или, например, Win32Error
. Это говорит потребителю о том, как форматировать этот самый 32-разрядный unsigned integer, чтобы отобразить его пользователю. Моя библиотека не поддерживает форматирование в соответствии с типами данных на выходе и форматирует их так, как сама считает нужным. Просмотреть список всех возможных типов на входе и соответствующих им типов на выходе можно здесь и здесь.
Теперь, что там у нас с указателем (is_wide_pointer
)? Если тип свойства на входе - SIZET
или POINTER
, то is_wide_pointer
указывает, сколько байтов занимает этот тип - 4 или 8. Логично предположить, что 64-разрядные приложения будут предавать 8-байтовые значения, а 32-разрядные - 4-байтовые.
Переходим к парсингу свойств, чтобы преобразовать их к конкретным C++
-типам. Я написал шаблонный класс с целой кучей специализаций, который позволяет конвертировать нетипизированные свойства (объекты класса event_property
) в типизированные значения C++
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
template<typename T> class event_property_converter { }; //Специализация для получения значения типа bool //Соответствующий тип tdh: TDH_INTYPE_BOOLEAN template<> class event_property_converter<bool> { public: static bool convert(const event_property& prop); }; //Специализация для получения значения типа std::uint64_t //Соответствующий тип tdh: TDH_INTYPE_UINT8, TDH_INTYPE_UINT16, //TDH_INTYPE_UINT32 или TDH_INTYPE_UINT64 - все они влезут в //std::uint64_t без потери точности, и все они беззнаковые. template<> class event_property_converter<std::uint64_t> { public: static std::uint64_t convert(const event_property& prop); }; // ... еще дофига специализаций ... //Специализация для получения значения типа std::wstring //Соответствующий тип tdh: TDH_INTYPE_UNICODESTRING, TDH_INTYPE_COUNTEDSTRING, //TDH_INTYPE_REVERSEDCOUNTEDSTRING или TDH_INTYPE_NONNULLTERMINATEDSTRING template<> class event_property_converter<std::wstring> { public: static std::wstring convert(const event_property& prop); }; //Специализация для получения значения типа std::string //Соответствующий тип tdh: TDH_INTYPE_ANSISTRING, TDH_INTYPE_COUNTEDANSISTRING, //TDH_INTYPE_REVERSEDCOUNTEDANSISTRING или TDH_INTYPE_NONNULLTERMINATEDANSISTRING template<> class event_property_converter<std::string> { public: static std::string convert(const event_property& prop); }; //Специализация для получения значения типа std::chrono::system_clock::time_point //Соответствующий тип tdh: TDH_INTYPE_SYSTEMTIME или TDH_INTYPE_FILETIME template<> class event_property_converter<std::chrono::system_clock::time_point> { public: static std::chrono::system_clock::time_point convert(const event_property& prop); }; // ... и так далее ... |
Все эти специализации позволяют со всеми возможными проверками безопасно преобразовать класс event_property
в понятное и пригодное для использования значение, например, std::uint64_t
. Для этого типа соответствующая специализация проверит, какой тип свойство имеет на входе (нам в случае с std::uint64_t
подходят TDH_INTYPE_UINT8
, TDH_INTYPE_UINT16
, TDH_INTYPE_UINT32
и TDH_INTYPE_UINT64
- все они помещаются в std::uint64_t
без потери точности, и все они беззнаковые, как и std::uint64_t
) и корректный ли размер оно имеет. Если все условия выполнены, то будет произведена конвертация, в противном случае мы получим исключение.
Для конвертации числовых и некоторых других типов я написал общую шаблонную функцию, которая сравнивает тип на входе переданного ей нетипизированного свойства (event_property
) с теми типами, которые мы задали. Если тип совпадает, то проверяем размер переданного типизированного значения. Если все совпадает, производим конвертацию (с помощью обычного reinterpret_cast
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//Вспомогательная структура, хранящая тип, в который мы конвертируем //нетипизированное свойство, а также значение его типа на входе в виде константы TDH_INTYPE_***, template<typename Type, std::uint16_t TdhType> struct EventTypeInfo { using type = Type; static constexpr const std::uint16_t tdh_type = TdhType; }; //Эта функция вызовется, если тип свойства не совпадает //ни с одним из переданных нами типов template<typename ResultType> [[noreturn]] ResultType convert_property_data(const event_property&) { throw event_trace_error("Incorrect property type"); } //Это, непосредственно, хелпер для конвертации нетипизированного свойства //в типизированное значение template<typename ResultType, typename EventType, typename... Args> ResultType convert_property_data(const event_property& prop) { //Сверяем тип на входе if (prop.get_in_type() == EventType::tdh_type) { //Если совпал - сверяем еще и размер using SourceType = EventType::type; if (prop.get_raw_value().size() != sizeof(SourceType)) throw event_trace_error("Invalid property value size"); //Если все совпало - конвертируем return static_cast<ResultType>(*reinterpret_cast<const SourceType*>(prop.get_raw_value().data())); } //Иначе идем дальше по списку типов return convert_property_data<ResultType, Args...>(prop); } |
А вот как это применяется на примере конвертации в std::uint32_t
:
1 2 3 4 5 6 7 8 9 10 11 |
std::uint32_t event_property_converter<std::uint32_t>::convert(const event_property& prop) { //В std::uint32_t можно конвертировать либо TDH_INTYPE_UINT32 размера sizeof(std::uint32_t), //либо TDH_INTYPE_UINT16 размера sizeof(std::uint16_t), либо же TDH_INTYPE_UINT8 //размера sizeof(std::uint8_t). Нам нужно, чтобы не терялась точность, и чтобы знаковость //совпадала - все типы тут беззнаковые. return convert_property_data<std::uint32_t, EventTypeInfo<std::uint32_t, TDH_INTYPE_UINT32>, EventTypeInfo<std::uint16_t, TDH_INTYPE_UINT16>, EventTypeInfo<std::uint8_t, TDH_INTYPE_UINT8>>(prop); } |
А вот как это применяется на примере конвертации в std::uint32_t
:
1 2 3 4 5 6 7 8 9 10 11 |
std::uint32_t event_property_converter<std::uint32_t>::convert(const event_property& prop) { //В std::uint32_t можно конвертировать либо TDH_INTYPE_UINT32 размера sizeof(std::uint32_t), //либо TDH_INTYPE_UINT16 размера sizeof(std::uint16_t), либо же TDH_INTYPE_UINT8 //размера sizeof(std::uint8_t). Нам нужно, чтобы не терялась точность, и чтобы знаковость //совпадала - все типы тут беззнаковые. return convert_property_data<std::uint32_t, EventTypeInfo<std::uint32_t, TDH_INTYPE_UINT32>, EventTypeInfo<std::uint16_t, TDH_INTYPE_UINT16>, EventTypeInfo<std::uint8_t, TDH_INTYPE_UINT8>>(prop); } |
Аналогичным образом мы можем получить, скажем, float
:
1 2 3 4 5 6 |
float event_property_converter<float>::convert(const event_property& prop) { //Здесь только один подходящий тип в списке return convert_property_data<float, EventTypeInfo<FLOAT, TDH_INTYPE_FLOAT>>(prop); } |
Разумеется, со строковыми типами такое не прокатит (а в событии строка может содержаться аж в восьми разных форматах), поэтому мне пришлось писать еще одну шаблонную функцию для конвертации строк в std::string
/ std::wstring
. Ее я приводить не буду, код можно посмотреть, скачав солюшен в конце статьи. Еще отдельные функции написаны для конвертации типов SYSTEMTIME
и FILETIME
. Все остальное в той или иной мере использует описанные заготовки.
Нам осталось рассмотреть набор WinAPI-функций из библиотеки tdh.dll
, которые я использовал. Предположим, что у нас изначально нет ничего, кроме указателя PEVENT_RECORD
- это то, что мы получили в свой коллбэк после вызова ProcessTrace
.
Есть отдельный случай, когда ивент содержит единственную строку, и ничего более. В этом случае в записи события (PEVENT_RECORD
) будет выставлен соответствующий флаг EVENT_HEADER_FLAG_STRING_ONLY
:
1 2 3 4 |
if ((record_->EventHeader.Flags & EVENT_HEADER_FLAG_STRING_ONLY) == EVENT_HEADER_FLAG_STRING_ONLY) { //Берем строку из record_->UserData, длина ее (в байтах!) лежит в record_->UserDataLength } |
В этом случае нам не нужно ничего парсить. На практике я с такими событиями не сталкивался. Далее рассмотрим, что делать, если наш ивент всё же содержит набор свойств разных типов. Для начала нам необходимо получить некую дополнительную информацию о событии. Для этого используется функция TdhGetEventInformation. Первый раз мы ее вызываем, передав нулевой буфер, получаем на выходе необходимый размер буфера, выделяем буфер подходящего размера и вызываем эту функцию еще раз. Эта функция загружает необходимую для разбора события информацию. Если потребуется, она даже загрузит DLL-файл провайдера, в котором содержатся метаданные для разбираемого события, и считает их сама. Это достаточно удобно!
Полученная структура (TRACE_EVENT_INFO
, ее я уже упоминал) содержит некоторые интересные нам свойства. Это, например, TopLevelPropertyCount
- сколько свойств верхнего уровня содержится в событии. Напомню, что свойства верхнего уровня - это свойства, которые не вложены в структуру. Также она содержит имена всех свойств события. Получить их можно так:
1 2 3 4 |
//Здесь info - это указатель на TRACE_EVENT_INFO, //а index - индекс интересующего нас свойства return reinterpret_cast<const WCHAR*>(reinterpret_cast<const BYTE*>(info) + info->EventPropertyInfoArray[index].NameOffset) |
Не самый красивый код, но и это неплохо. А как же нам получить данные какого-либо свойства? Допустим, мы уже знаем его индекс index
. Но мы ведь еще не определили, является ли это свойство обычным значением или структурой? Содержится ли в этом свойстве всего одно значение, или их несколько (т.е. свойство - массив значений)? Вот как мы можем определить, структура ли свойство:
1 2 3 |
//Здесь info - это указатель на TRACE_EVENT_INFO, //а index - индекс интересующего нас свойства (как и в предыдущем сниппете) return (info->EventPropertyInfoArray[index].Flags & PropertyStruct) == PropertyStruct; |
Здесь пока все понятно: если в наборе флагов имеется PropertyStruct
, значит, мы имеем дело со структурой. Теперь нам нужно определить количество элементов свойства (ведь оно по-прежнему может быть массивом значений). А вот это уже сложнее:
1 2 3 4 5 6 7 8 9 |
//Количество элементов свойства может содержаться в ОТДЕЛЬНОМ СВОЙСТВЕ! if ((info->EventPropertyInfoArray[index].Flags & PropertyParamCount) == PropertyParamCount) { //Если это так - то нам ничего не остается, как считать это свойство! return get_plain_property_value<std::uint32_t>( info->EventPropertyInfoArray[index].countPropertyIndex); } return info->EventPropertyInfoArray[index].count; |
Итак, количество элементов массива может храниться как в специальной переменной count
, так и в отдельном свойстве типа UINT16
или UINT32
, индекс которого лежит в countPropertyIndex
. Мозг уже закипает, а ведь я еще не успел рассмотреть, как же получить нетипизированные данные свойства (которые содержат его значение)!
Сперва рассмотрим простой случай, когда у нас свойство - это обычное значение (не структура). Сначала необходимо заполнить массив структур PROPERTY_DATA_DESCRIPTOR. В нашем случае такая структура будет всего одна:
1 2 3 4 5 6 7 |
PROPERTY_DATA_DESCRIPTOR data_descriptor; //Сюда передаем указатель на имя свойства (не его индекс!) //Как получить имя свойства, я рассмотрел чуть выше. data_descriptor.PropertyName = reinterpret_cast<ULONGLONG>(get_property_name(index)); //А сюда передаем индекс массива. Если в свойстве всего один элемент, //то тут будет, соответственно, 0. data_descriptor.ArrayIndex = element_index; |
Теперь, подготовив структуру, мы можем вызвать TdhGetPropertySize и получить, наконец-то, этот злосчастный размер свойства:
1 2 3 4 5 6 7 |
ULONG property_size = 0; //record_ - PEVENT_RECORD //1 - количество структур в массиве data_descriptor, у нас она одна //(зачем их может быть больше, расскажу далее) auto status = ::TdhGetPropertySize(record_, 0, nullptr, 1, &data_descriptor, &property_size); if (ERROR_SUCCESS != status) throw event_trace_error("Unable to get property size", status); |
Наконец-то в переменной property_size
мы имеем размер свойства. Теперь мы можем подготовить буфер нужного размера, чтобы затем скопировать туда данные интересующего свойства с помощью TdhGetProperty.
1 2 3 4 5 6 7 |
//Это обычный std::vector<std::uint8_t> event_property::raw_value_type raw_value; //Аллоцируем буфер подходящего размера raw_value.resize(property_size); status = ::TdhGetProperty(record_, 0, nullptr, 1, &data_descriptor, property_size, raw_value.data()); if (ERROR_SUCCESS != status) throw event_trace_error("Failed to get property value", status); |
Вот и всё! Осталась пара мелочей - получить типы свойства на входе и на выходе, а также размер указателя (на случай, если тип свойства - SIZET
или POINTER
), но это уже пустяки:
1 2 3 |
auto in_type = info->EventPropertyInfoArray[index].nonStructType.InType; auto out_type = info->EventPropertyInfoArray[index].nonStructType.OutType; bool wide_pointer = !(record_->EventHeader.Flags & EVENT_HEADER_FLAG_32_BIT_HEADER); |
Фууух! Этот API действительно крепкий орешек. А что там со свойствами-структурами? Все еще интереснее! Вот тут нам и понадобится подготавливать массив из ДВУХ элементов PROPERTY_DATA_DESCRIPTOR
. Но сперва я расскажу, как эти самые свойства-структуры хранятся в ивенте. По сути, структура - это диапазон индексов свойств. Например, у нас с индекса 4 по индекс 6 лежат три свойства разных типов. Структура объединяет эти три свойства в одно. Соответственно, эти индексы (4, 5, 6) не будут фигурировать в TopLevelPropertyCount
, эти индексы не считаются индексами свойств верхнего уровня, но информация о них все равно хранится в событии (в массиве EventPropertyInfoArray
). Таким образом, структура - это просто условная группировка нескольких обычных свойств под одним именем структуры. Вложенными структуры быть не могут (и на том спасибо!). Итак, как же нам получить диапазон этих самых индексов?
1 2 3 |
//Здесь index - это индекс свойства-структуры auto struct_start_index = info->EventPropertyInfoArray[index].structType.StructStartIndex; auto member_count = info->EventPropertyInfoArray[index].structType.NumOfStructMembers; |
Здесь struct_start_index
будет содержать первый индекс свойства, входящего в структуру, а member_count
- количество свойств в структуре. Для наглядности приведу пример лейаута всего этого добра, чтобы было понятнее:
В этом примере свойства верхнего уровня имеют индекс с 0 до 3 включительно. Свойства 0, 1, 3 - это простые свойства, а вот 2 - структура. В структуре указан начальный индекс входящих в нее свойств (4) и количество свойств (3). Таким образом, свойства с индексами 4, 5, 6 (это уже не свойства верхнего уровня) включены в структуру с индексом 2.
Нужно помнить, что сама структура может быть массивом, плюс каждое свойство внутри структуры также может быть массивом! Теперь мы готовы заполнить PROPERTY_DATA_DESCRIPTOR
:
1 2 3 4 5 6 7 8 9 10 |
PROPERTY_DATA_DESCRIPTOR data_descriptors[2]{}; //Указатель на имя структуры: data_descriptors[0].PropertyName = reinterpret_cast<ULONGLONG>(name_of_structure); //Индекс самой структуры, если у нас массив таких структур: data_descriptors[0].ArrayIndex = struct_index; //Теперь имя свойства внутри структуры, которое мы хотим получить: data_descriptors[1].PropertyName = reinterpret_cast<ULONGLONG>( get_property_name(struct_start_index + struct_member_index)); //И индекс этого свойства, если свойство является массивом: data_descriptors[1].ArrayIndex = struct_element_index; |
Такие дела. Далее, в общем-то, все аналогично - вызываем сначала TdhGetPropertySize, передав туда этот массив структур PROPERTY_DATA_DESCRIPTOR
и не забыв указать, что у нас их теперь две, а затем TdhGetProperty. При получении типа на входе и на выходе не забываем, что индекс теперь нужно использовать тот, который указывает не на структуру, а на то свойство структуры, которое мы получаем:
1 2 |
auto in_type = info->EventPropertyInfoArray[struct_start_index + struct_member_index].nonStructType.InType; auto out_type = info->EventPropertyInfoArray[struct_start_index + struct_member_index].nonStructType.OutType; |
Вот теперь точно все! Очевидно, TDH API - не самое приятное из имеющихся в Windows, но оно на самом деле достаточно облегчает дело получения свойств из событий. Стоит упомянуть, что существует даже функция TdhFormatProperty, которая за вас может отформатировать свойство в нужный формат (она смотрит на тип свойства на выходе) и преобразовать его в строку. Останется только вывести на экран! Вот пример ее использования.
Мы подошли к концу изучения ETW API с точки зрения потребителя ивентов (consumer) и контроллера сессии (controller). Время переходить к примеру его использования. В конце-концов, я ведь хотел отслеживать процессы в системе. Я набросал простую консольную программку (используя свою библиотеку для работы с ETW), которая выводит получаемые ивенты, все их свойства с именами, значениями и типами. Посмотрим на ее код, а точнее, на самую его мякотку в функции main
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
//Получаем GUID провайдера по его имени auto process_provider_guid = event_provider_list().get_guid(L"Microsoft-Windows-Kernel-Process"); //Стартуем сессию ETW event_trace_session session(L"Kaimi.io test session"); //Эти ключевые слова мы видели, когда игрались с wevtutil static constexpr const std::uint64_t keyword_process = 0x10; static constexpr const std::uint64_t keyword_thread = 0x20; static constexpr const std::uint64_t keyword_image = 0x40; //Включаем трассирование нашего провайдера, передав заодно интересующие нас //ключевые слова: session.enable_trace(process_provider_guid, event_trace_session::trace_level::verbose, keyword_process | keyword_thread | keyword_image); //Начинаем получать события из сессии event_trace trace(session); //Это то, что мы будем делать, когда приходит новое событие //от нашего провайдера Microsoft-Windows-Kernel-Process: trace.on_trace_event(process_provider_guid, [](auto record) { try { //Мы выводим все свойства события в консоль //(а также его идентификатор и сообщение) std::wcout << event_info(record) << std::endl; } catch (const std::exception& e) { std::cout << "Error parsing event: " << e.what() << std::endl; } }); //Поехали! trace.run(); |
Из новенького в этом коде - получение GUID
провайдера по его имени. Это делается с помощью перечисления всех провайдеров системы (функция TdhEnumerateProviders), код запрятан внутрь класса event_provider_list
. Оператор <<
для класса event_info
я перегрузил, он выводит все свойства события с их типами. После запуска программы я могу открыть, например, notepad.exe
и увидеть в логе следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
Event ID = 1, Message = "Process %1 started at time %2 by parent %3 running in session %4 with name %6. " [UINT32] ProcessID = 4516 [FTIME] CreateTime = 2017-03-14 09:02:40 UTC [UINT32] ParentProcessID = 4984 [UINT32] SessionID = 1 [UINT32] Flags = 0 [USTR] ImageName = \Device\HarddiskVolume2\Windows\System32\notepad.exe [UINT32] ImageChecksum = 293988 [UINT32] TimeDateStamp = 1468635242 [USTR] PackageFullName = [USTR] PackageRelativeAppId = Event ID = 3, Message = "Thread %2 (in Process %1) started. " [UINT32] ProcessID = 4516 [UINT32] ThreadID = 6140 [PTR] StackBase = 18446653368338317312 [PTR] StackLimit = 18446653368338292736 [PTR] UserStackBase = 931952656384 [PTR] UserStackLimit = 931952586752 [PTR] StartAddr = 140700295923664 [PTR] Win32StartAddr = 140700295923664 [PTR] TebBase = 931954188288 [UINT32] SubProcessTag = 0 Event ID = 5, Message = "Process %3 had an image loaded with name %7. " [PTR] ImageBase = 140700295823360 [PTR] ImageSize = 266240 [UINT32] ProcessID = 4516 [UINT32] ImageCheckSum = 293988 [UINT32] TimeDateStamp = 1468635242 [PTR] DefaultBase = 140700295823360 [USTR] ImageName = \Device\HarddiskVolume2\Windows\System32\notepad.exe ... |
Неплохо получилось! Мы видим, что запустился notepad.exe
, потом, что в нем стартанул какой-то поток, затем видим, какие модули загружаются в этот процесс. Разумеется, это далеко не все созданные в процессе потоки, и модулей там тоже гораздо больше, но я это все опустил для краткости. Когда мы notepad.exe
закроем, мы увидим, как все эти потоки останавливаются, модули выгружаются и, наконец, сам процесс завершается.
Для пущей наглядности я написал утилитку с GUI, которая мониторит в реальном времени все создаваемые процессы, смотрит, какие у них имеются потоки и какие модули в них загружены. Ее код - также в солюшене в конце статьи, а пока что - ее скриншот:
Написана она на чистом WinAPI, без каких-либо GUI-библиотек. Работоспособность утилиты проверена на Win7 x64 и Win10 x64. Конечно, эта утилита не покажет те процессы, которые были запущены до того, как она сама запустилась, но мы видим все те процессы, которые стартанули позже, а также всё, что происходит с их потоками и модулями.
Надеюсь, вы получили для себя какие-то новые знания об ОС Windows и работе с ETW! Конечно, я не покрыл и половины темы про Event Tracing for Windows (например, не рассказал про kernel logger, с которым работа ведется совсем по-другому, и ничего не пояснил про выдачу ивентов с точки зрения провайдера). Но эта тема очень обширна, и имеющегося материала уже хватило на огромную статью!
Напоследок приведу еще несколько полезных ссылок на всякие релевантные ресурсы помимо тех, которые указаны в самом начале статьи:
- Самый худший из когда-либо созданных API - перевод статьи про ETW-трейсинг системного логгера (kernel logger). Рекомендую также ознакомиться с комментариями. Вот оригинал статьи.
- Using Events to Calculate CPU Usage - пример от Microsoft, как использовать ETW для подсчета уровня загрузки процессора.
- Using TdhGetProperty to Consume Event Data - Пример от Microsoft, как использовать TdhGetProperty для получения всех свойств события ETW.
- Код из проекта Performance Co-Pilot, который работает с ETW как consumer.
- Исходники библиотеки Microsoft.Diagnostics.Tracing.TraceEvent (.NET). Хорошие примеры кода, умеет парсить WPP-события.
- Небольшой FAQ по ETW.
И, наконец, исходники моей библиотеки для разбора событий, создания сессий, получения событий, а также программ-примеров использования (консольный- и GUI-отслеживатель процессов): Скачать примеры и библиотеку для работы с ETW (исходники), Скачать примеры в собранном виде (пароль на архив - kaimi.io), Исходники всех проектов и библиотеки на GitHub.
Я года 2 назад пересел с с++ на c#, и глядя на код еще раз убедился что не зря ненавижу плюсы))
Спасибо, статья действительно очень большая и сложная, но полезная. Честно говоря не знал о таком функционале до сегодняшнего дня. Читая статью поразился, как у человека столько знаний в голове помещается)) Мало того, хороший программист далеко не всегда хороший учитель, а тут еще и так подробно все расписано.
Я так понимаю, частично информацию о событиях процесса можно посмотреть, когда например программа крешит, и в Управление компьютером -> Просмотр событий -> Сводка административных событий можно увидеть что-то типа такого:
Имя сбойного приложения: Explorer.EXE, версия: 6.1.7601.17514, отметка времени: 0x4ce7a144
Имя сбойного модуля: bcryptprimitives.dll, версия: 6.1.7601.17514, отметка времени 0x4ce7c4f0
Код исключения: 0xc0000005
Смещение ошибки: 0x0000000000007a94
Идентификатор сбойного процесса: 0x8d4
Время запуска сбойного приложения: 0x01d2a0a11f06420a
Путь сбойного приложения: C:\Windows\Explorer.EXE
Вся эта информация там появляется при помощи ETW?
Спасибо за лестный комментарий :)
По поводу некрависого кода - дело не в C++, а в низкоуровневом ETW API. Если писать обертки над этими API на C#, то лапша не меньше будет, чем на плюсах. Например, вот - тут и функции на 300 строк, и магические константы, и работа с указателями в unsafe-функциях... А конечный интерфейс для пользователя можно красивым сделать на любом языке.
По поводу крешей - да, насколько я знаю, начиная с WinXP журнал событий Windows работает исключительно с ETW. События о падении приложений туда может отсылать ядро ОС. В списке ETW-сессий (как их посмотреть, описано в статье) на Win7 я вижу, например, EventLog-Application, EventLog-System и другие подобные. Думаю, что именно они используются для формирования журналов событий Windows.
Мощная статья, всю не осилил, но взял на заметку, что в винде есть такое, и где, если понадобится, можно освежить память по теме.
Спасибо.
Привет, такой вопрос. Что думаете по поводу языка Golang. В нем можно делать много зеленых потоков и отличная работа с I/O. Нормально ли подойдет для для создания различных прожек рассыльщиков сообщений по сайтам, парсерам и т.д? Или лучше взять что-нибудь типо java. Но в Golang еще нормальная вещь, что можно нормально компилировать в бинарник и запускать на линукс серверах без дополнительного софта наподобие интерпретаторов.
Привет, go только краем глаза видел, но ничего на нем не писал. Но, думаю, что для подобного софта подойдет. А в Java, конечно, готовых библиотек всяких разных побольше будет. Так что я бы сначала посмотрел, есть ли для языка написанные готовые либы, которые могут пригодиться, или придется их самому писать ручками.
Увидев у тебя win10, я в тебе крупно разочаровался.
Обратись к http://blog.cr4.sh/p/blog-page.html, он тебе пояснит, что в Win10, с точки зрения безопасности, множество вещей реализовано лучше, чем в MacOS и типичных Linux дистрах.
Та же Enterprise версия практически не содержит ненужного барахла и в ней легко отключается отправка данных, если волнует этот момент.
Респект за статью, отличная работа
Хорошая статья для изучения ETW API. Большое спасибо автору.
Спасибо за статью, пробую что-то подобное по процессам написать на Go и прихожу к ETW
Как успехи с Go?
Добрый день, подскажите, пожалуйста:
1) как по имени сессии etw узнать, имя файла трассировки и путь до него
2) как найти приложение (контроллер), который отвечает за данную сесиию?
1) Думаю, подойдёт функция ControlTrace. В
TraceHandle
можно передатьNULL
, а вInstanceName
, соответственно, имя интересующей сессии.ControlCode
должен быть, видимо,EVENT_TRACE_CONTROL_QUERY
. ВProperties
заполнитьWnode.BufferSize, Wnode.Guid, LoggerNameOffset, and LogFileNameOffset
. Далее после вызова функции по переданному смещению будет имя файла.2). А вот тут, вероятно, никак. Сессия не принадлежит приложению и остаётся открытой и рабочей в том случае, если приложение уже закрылось, но не вызвало
CloseTrace
. Кроме того, одну сессию может слушать несколько приложений одновременно.Добрый день, dx, спасибо Вам большое за предыдущий ответ!
Я решил посмотреть текущие сессии через xperf -loggers. Меня удивил тот факт, что не у каждой работающей сессии ETW есть файл журнала логирования (*.etl)
Как так, ведь сессия работает?
Сессия может быть настроена без журнала логирования. Приложение-пример, которое в статье разбирается, именно такую сессию и создает. Такая сессия используется только для потребления событий приложениями в реальном времени. Можно глянуть документацию на EVENT_TRACE_REAL_TIME_MODE.
Понял, спасибо огромное!!!
Доброго времени суток!
Спасибо большое за статью, отличное введение в ETW.
А не знаете как можно достать другую информацию о событии? Например, TimeCreated и Execution (картинку того, что имею ввиду прилагаю)
https://yapx.ru/v/UL7vi
Приветствую, эти данные находятся в структуре
EVENT_HEADER
. Туда можно добраться изEVENT_RECORD::EventHeader
.TimeStamp
- это, видимо, TimeCreated. Из Execution:KernelTime
,UserTime
,ThreadId
,ProcessId
. ProcessorId из Execution можно получить с помощьюGetEventProcessorIndex
(определена вevntcons.h
):Спасибо) разобрался. А вот я по полученному ProcessId хотел еще имя процесса получать, допустим открыл я файл в ворде, и чтобы мне залогировалось, что это ворд открыл. Однако на все такие события, по ProcessId я получаю имя процесса "explorer.exe", а не конкретную программу. Можно ли через ETW выудить имя приложения, которое задействовало файл?
Мой пример вроде бы корректно отображает имена процессов. Открыл ворд, вижу в списке отслеживаемых процессов WINWORD.exe, а explorer не вижу.
Прошу прощения, не указал, что я использую Microsoft-Windows-Kernel-File провайдер.
С помощью него я слежу за I/O операциями над определенным файлом. Но этот провайдер может лишь выдать Process ID. По этому ID я пытаюсь получить имя процесса, но к сожалению мне выдается только "explorer.exe". Можно ли в таком случае как то добыть имя процесса?
Так может быть
explorer.exe
и выполняет эти I/O операции? Плюс, у многих ивентов в этом провайдере есть полеThreadId
/IssuingThreadId
. Может быть, можно по нему определить, какой процесс выполнил операцию (вызвав, например,OpenThread
и потомGetProcessIdOfThread
. Или, воспользовавшись ToolHelp API, поддерживать актуальный список всех процессов и их потоков, и там искать по ID потока).