Пре-процессор трассировки программного обеспечения Windows

Пре-проце́ссор трассиро́вки програ́ммного обеспе́чения Windows (англ. Windows software trace preprocessor) — это препроцессор, который упрощает использование трассировки событий через WMI для реализации эффективной программной трассировки software tracing в драйверах и приложениях, предназначенных для операционных систем, начиная с Windows 2000 и новее.

WPP был создан компанией Microsoft и входит в состав Windows DDK. Хотя WPP применим в различных задачах, он не включён в Windows SDK и, в основном, используется для драйверов и вспомогательного программного обеспечения, производимого разработчиками, приобретшими Windows DDK.

Предыстория

Программная трассировка — это специализированный вариант журналирования, используемый для записи информации о выполнении программы. Эти данные обычно применяются для отладки. В отличие от журнала событий, основное предназначение которого — формировать записи о событиях для целей аудита системными администраторами или анализа административными инструментами, программная трассировка служит, в первую очередь, вспомогательным инструментом для разработчиков. В связи с этим многие нефункциональные требования к журналу событий, например, локализация сообщений или стандартизированный формат вывода) не ставятся перед большинством сценариев программной трассировки. С другой стороны, трассировка предъявляет особые требования к производительности, которые обычно не столь критичны для журналирования событий. Примером такого применения является техника трассировки входа/выхода (in/out tracing), при которой сообщения генерируются при входе и выходе из функций или методов, чтобы разработчик мог визуально проследить путь выполнения, включая значения аргументов и результатов, в отладчике или текстовом журнале. Такой подход, полезный для разработчиков, может серьёзно ухудшить производительность, если трассировку нельзя отключить, либо на этапе компиляции через условную компиляцию, либо во время выполнения с помощью флагов[1].

Дополнительные специфические аспекты программной трассировки:

  • В проприетарном ПО трассировочные данные могут содержать чувствительную информацию о исходном коде продукта;
  • Если трассировка может включаться или отключаться во время выполнения, многие способы трассировки требуют включения значительного объёма дополнительных данных в исполняемый файл, что может косвенно ухудшить производительность даже при выключенной трассировке;
  • Если включение или отключение трассировки осуществляется на этапе компиляции, получение трассировочных данных с пользовательского компьютера зависит от наличия и установки особой, трассировочной сборки программы;
  • Некоторые виды программного обеспечения должны отвечать строгим требованиям к производительности даже при активной трассировке.

Из-за первых двух соображений традиционно для программной трассировки используется условная компиляция для включения или удаления трассировочных сообщений и соответствующих данных. Например, с помощью C-препроцессора можно определить макрос DebugOut следующим образом:

#ifdef _DEBUG
#define DebugOut(msg, ...) \
    DebugPrintf(__FUNCTION__ "(" __FILE__ ":" TO_STRING(__LINE__) ")\t" \
        msg, __VA_ARGS__)
#else
#define DebugOut(msg, ...)
#endif

где TO_STRING — макрос для преобразования номера строки (__LINE__) в строку, а DebugPrintf — функция, подобная printf, выводящая сообщения в отладчик.

Тогда следующая строка кода:

DebugOut("Error %d occurred\n", error_code);

выведет сообщение примерно такого вида (только в отладочной сборке):

SomeFunction(file.c:78)    Error 217 occurred

Другой подход, особенно для трассировки входа/выхода, заключается в использовании инструментирования. Этот способ может решить большую часть упомянутых проблем, но обычно доступен только для управляемого кода.

Трассировка событий WMI — это пример технологии, специально оптимизированной для трассировки в производительно чувствительном коде, таком как драйверы. Она также позволяет управлять распространением чувствительных трассировочных данных, так как человекочитаемые строки сообщений («Error %d occurred\n» в примере выше) определяются отдельно от кода и, таким образом, не попадают в сам продукт (в коде используется номер сообщения). Однако имеются следующие ограничения:

  • Трассировка событий WMI не позволяет автоматически генерировать часть сообщения вида «SomeFunction(file.c:78)». Это свойственно всем подобным технологиям, а не только трассировке событий WMI;
  • Требование отделять человекочитаемую часть трассировочного сообщения от кода может снижать читабельность исходного кода;
  • Использование такого подхода может привести к существенной дополнительной работе даже для единичных трассировочных сообщений.

Принцип работы WPP

Пре-процессор трассировки программного обеспечения Windows (WPP) запускается до этапа компиляции, то есть даже раньше C-препроцессора, и для каждого обрабатываемого файла генерирует заголовок трассировочных сообщений (по умолчанию — файл filename.tmh, где filename — имя исходного файла). Этот заголовочный файл требуется явно включать в исходный код, например:

// Файл: file.cxx
// Пример использования WPP
#include "file.tmh"

Понимание C/C++-синтаксиса у WPP весьма ограничено: он не разворачивает макросы (кроме особых случаев, когда это необходимо), не обрабатывает препроцессорные директивы (pragmas) и не выполняет семантического анализа.

Разработчик указывает один или несколько трассировочных макросов, которые должно обрабатывать WPP, с помощью конфигурационного файла, специальных аннотаций в комментариях, параметров командной строки либо их комбинации. Каждый раз, когда WPP находит указанный макрос в исходном коде, он генерирует для него соответствующий макрос сообщения трассировки. Иными словами, если, к примеру, макрос трассировки называется DoTrace, WPP создаёт отдельный макрос сообщения для каждого его вхождения. Формально эти макросы различаются по имени файла и номеру строки, а сам WPP определяет исходный макрос таким образом, чтобы при каждом вхождении он раскрывался в отдельный макрос сообщения.

То, как генерируются макросы сообщений трассировки, определяется шаблонным файлом (его формат не задокументирован). Стандартные шаблоны, прилагаемые к WPP, предполагают включение строки трассировочного сообщения в виде аннотации (используется возможность __annotation компилятора Microsoft). Эти строки не попадают в компилируемый код, но включаются в файл символов отладчика в таком формате, который понимают утилиты, входящие в состав WPP. Макросы сообщений трассировки содержат также логику для включения/выключения трассировки по флагам и обращения к API трассировки событий WMI.

Ограничения

  • WPP не разворачивает макросы и не распознаёт вызов трассировочного макроса внутри определения другого макроса. Например, если DoTrace — трассировочный макрос, а CheckForErrors определён так:
#define CheckForErrors(error_code) \
    if (IsError(error_code)) \
    { \
        DoTrace("Error %d occurred\n", err); \
        HandleError(error_code); \
    }

то для DoTrace внутри CheckForErrors WPP не сгенерирует соответствующие макросы сообщений трассировки. Существует отдельный обходной механизм для подобных случаев, но определённый класс макросов остаётся невыражаемым даже с его помощью.

  • Стандартный шаблон генерирует код, корректно работающий только с компилятором Microsoft. Хотя это не является фундаментальным ограничением самого препроцессора, отсутствие документации по формату шаблонов на практике приводит к тому, что WPP применяется только с этим компилятором;
  • В ранних версиях WPP возникали ошибки компиляции при включении в исходный файл более одного заголовочного файла трассировки (например, если исходный файл с трассировкой включал заголовок, содержащий трассировку во встроенных функциях). В последних версиях эта проблема решена. Следует отметить, что это ограничение обусловлено шаблоном, а не самим WPP;
  • Так как макросы сообщений трассировки различаются по файлу и номеру строки, в каждой строке исходного кода может содержаться только один трассировочный макрос.

Примечания

  1. Кожевников Александр Николаевич. Система анализа длительности ключевых операций на основе трассировки программного обеспечения // Инновации в науке. — 2019. — Вып. 6 (94). — ISSN 2308-6009.

Литература