Избавляемся от EXCEPTION_ACCESS_VIOLATION при отладке suspended-процессов на Windows
Расскажу о решении одной загадочной проблемы, с которой я столкнулся при написании собственного отладчика для Windows.
Задача состояла в том, чтобы присоединить отладчик (разрабатываемый с помощью
Windows Debugger API) к процессу, который был только что запущен с
флагом CREATE_SUSPENDED
.
Стандартным (и в общем-то единственным) подходом тут является вызов функции
DebugActiveProcess
. В соответствии с документацией,
эта функция должна остановить все потоки в процессе, сгенерировать некоторое
количество отладочных сообщений (которые затем будут нами получены при помощи
WaitForDebugEvent
), и продолжит выполнение процесса.
Отдельно упоминается, что эта функция также сгенерирует событие
EXCEPTION_DEBUG_EVENT
.
Применительно к процессу в состоянии suspended это, увы, неправда.
- Потоки, находящиеся в состоянии suspended, не будут автоматически продолжены
после завершения вызова
DebugActiveProcess
. Они останутся suspended. Да, это логично и я бы этого и ожидал, но документация этого не упоминает — а не помешало бы! - Для процесса, запущенного с флагом
CREATE_SUSPENDED
, не будет сгенерировано событиеEXCEPTION_DEBUG_EVENT
. Это удивляет многих, кто занимался до этого написанием отладчика, но это правда — не будет и всё тут.
Помимо того, что документация неполна и просто не покрывает наш случай, она ещё
и не упоминает одного важного момента: DebugActiveProcess
создаст в целевом
процессе дополнительный поток, этот поток будет запущен и выполнит какой-то код.
Но ведь это не должно быть проблемой, правда?
Неправда.
С некоторой вероятностью, зависящей от архитектуры процесса и версии ОС, это может привести к незамедлительному падению процесса. Я экспериментировал на 64-битной Windows 10 (1903) и выполнял тесты на 32-битных и 64-битных процессах. Для 64-битных процессов проблема не воспроизводилась, но 32-битные процессы падают примерно в 1.5% случаев. Похоже, что это относится к любым процессам: к нативным и к .NET, к графическим и консольным. В ответе на мой вопрос на Stack Overflow (к которому мы ещё вернёмся) пользователь @RbMm также утверждает, что на Windows XP это приводило к падению процесса всегда.
Вот какова последовательность событий при нормальной работе процесса:
CREATE_PROCESS_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
(которое, кажется, сообщает о загрузкеntdll.dll
, но имени этой библиотеки простым путём получить не удаётся — что задокументировано, так что всё в порядке)CREATE_THREAD_DEBUG_EVENT
- после этого идёт множество событий
LOAD_DLL_DEBUG_EVENT
и прочих, которые свидетельствуют о нормальной работе отладчика
Если же вам не повезло, то последовательность получаемых отладчиком событий будет выглядеть так:
CREATE_PROCESS_DEBUG_EVENT
LOAD_DLL_DEBUG_EVENT
CREATE_THREAD_DEBUG_EVENT
EXCEPTION_DEBUG_EVENT
:EXCEPTION_ACCESS_VIOLATION
(в информации об исключении будет нулевой адрес и значение8
в массивеEXCEPTION_RECORD::ExceptionInformation
, что, в соответствии с документацией, обозначает ошибку DEP)
После этого выполнение отлаживаемого процесса будет прекращено, он обречён.
Какое-то время провозившись с этой проблемой (мне-то нужен надёжно работающий отладчик!), я задал вопрос на Stack Overflow, на который удивительно быстро были оставлены комментарии, а затем и дан ответ пользователем @RbMm (который, похоже, один отвечает на все странные вопросы про Windows Debugger API — большое спасибо ему за это).
Итак, почему же это происходит? Из пояснений @RbMm и того, что я прочитал
про отладчик, складывается примерно такая картина. Системный рантайм (возможно,
CRT?) требуют от процесса, чтобы он всегда выполнял некоторую инициализацию.
Инициализация выполняется первым потоком, который начинает вызывать функции
системного рантайма, и в обычной ситуации всё в порядке, если подключать
отладчик к уже запущенному процессу. Но в случае с процессом в состоянии
suspended эти функции часто начинает первым вызывать именно поток отладчика,
который был создан в процессе благодаря вызову функции DebugActiveProcess
. А
проводить эту инициализацию в любом потоке приложения, кроме главного,
нежелательно — это и приводит к означенным проблемам.
Замечательно, а что делать? Единственная документированная для нас функция для отладки приложений имеет нежелательное поведение. Ну что ж, давайте тогда используем недокументированную функцию! Серьёзно, это на данный момент единственное надёжное решение проблемы.
В системной библиотеке ntdll
есть недокументированные функции
NtDebugActiveProcess
и NtWaitForDebugEvent
, которые отличаются от
документированных DebugActiveProcess
и WaitForDebugEvent
довольно
незначительно: во-первых, они получают контекст отладки не неявным образом, а
требуют явного управления объектом DebugObjectHandle
(тогда как официальный
подход состоит в неявном поддержании контекста в thread-local storage), а
во-вторых, NtDebugActiveProcess
не создаёт лишних потоков в отлаживаемом
приложении. То, что нужно! Не забудьте прилинковать к своему приложению
библиотеку ntdll.lib
: она нужна для использования этих функций.
Собрать работающий код из недокументированных функций было непросто: пришлось буквально по кусочкам сшивать сигнатуры функций, описания структур и enum'ов, но в итоге всё получилось, и проблему на итоговом коде я больше воспроизвести не могу.
Вот проект на GitHub, иллюстрирующий проблему и решение: в ветке
simplified
приведён проблемный код, а в ветке
ntdebugactiveprocess
— исправленная версия,
использующая функцию NtDebugActiveProcess
(код довольно грязноват, был быстро
написан в качестве proof-of-concept).
Для запуска проекта нужно его скомпилировать и выполнить бинарник, перенаправив вывод в файл:
$ Debug\NetRuntimeWaiter.exe > log.txt
Перенаправление в файл обязательно, т.к. иначе ведение лога работы программы (которых там немало!) начнёт занимать слишком большое время, и проблема будет воспроизводиться значительно реже.