Time Travel Debugging — записывать и перематывать ошибки, которые не воспроизводятся в долгоживущих приложениях

· Обновлено: · · WinDbg, Time Travel Debugging, Отладка, Расследование сбоев, Windows, Разработка под Windows, .NET, C++, Техническая консультация

История изменений (первая версия, опубликована 2 Sep 2026)
Первая публикация

«Падает раз в месяц, только глубокой ночью.» «Та же операция на моей машине никогда не воспроизводится.» «Дамп получили, но по месту сбоя не понять, почему значение стало таким.» Среди расследований ошибок долгоживущих приложений Windows именно эти случаи съедают больше всего времени. В более ранней статье «Как читать аварийные дампы в WinDbg + SOS — практика анализа после сбора» мы смотрели, как читать аварийный дамп, который является одной фотографией. Эта статья продолжает оттуда и покрывает инструмент для ситуаций, где фотографии недостаточно: Time Travel Debugging (TTD).

TTD — функция WinDbg, которая записывает всё исполнение процесса и позволяет позже воспроизвести его вперёд и назад. Вместо того чтобы снова и снова пытаться воспроизвести ошибку, можно «перемотать» сеанс отладчика.1 Читатели — разработчики и сопровождающие приложений Windows, .NET и C++, которые уже прошли свою долю расследования дампов и журналов и всё ещё имеют случаи, до причины которых не доходят. Предпосылка среды — Windows 10/11 или Windows Server 2016 или новее с текущим WinDbg, и запись требует прав администратора.12 Сложность средняя.

Предпосылки этой статьи

Пункт Содержание
Читатели Разработчики и сопровождающие приложений Windows с долгоживущими или прерывистыми ошибками, до причины которых дампы и журналы не доходят
Предварительные знания Опыт открытия дампа в WinDbg и выполнения !analyze -v или !clrstack. Предполагается содержание статьи об анализе SOS
Предпосылка среды Windows 10/11 или Windows Server 2016/2019/2022/2025, WinDbg (текущая версия), TTD.exe, права администратора2
Вне области Интеграция с Visual Studio Enterprise Snapshot Debugger; режим ядра (TTD только пользовательский режим3)

1. Сначала вывод

  • Дамп сохраняет «состояние»; TTD сохраняет «путь». Официальная документация указывает, что дампы склонны упускать состояние и путь исполнения, которые привели к сбою.1 Если фотография момента сбоя не говорит причину, дальше нужны не ещё фотографии, а запись.
  • Запись тяжела. Во время записи целевой процесс работает в 5–20 раз медленнее или хуже, а файл трассировки растёт на 5–50 МБ в секунду, пока процесс активен, без потолка.24 Это не инструмент, который безусловно прикрепляют к долгоживущему приложению.
  • Для долгоживущих приложений проектируйте «что записывать». Глава 5 покрывает четыре входа, которые даёт TTD.exe: -ring/-maxFile (оставлять только последние N МБ), -module (записывать только пока исполняется свой модуль), -recordmode Manual (пусть приложение задаёт интервал записи) и -monitor (записывать каждый запуск).2
  • Воспроизведение крутится вокруг трёх вещей: позиций (!tt), событий (dx @$curprocess.TTD.Events) и запросов (TTD.Calls / TTD.Memory). Сочетайте ba (точку останова по доступу) с g- (обратным исполнением), и отладчик напрямую отвечает «кто последним записал это значение?».5
  • Трассировка содержит содержимое памяти. Туда могут входить персональные и конфиденциальные сведения вроде путей файлов, данных реестра и содержимого памяти и файлов.1 Проектируйте обмен и хранение как для конфиденциального файла.
  • Знайте, чего TTD не умеет, прежде чем начинать. Нельзя записывать режим ядра, нельзя внедряться в защищённые процессы (PPL), нельзя самому отсоединиться после прикрепления, и во время воспроизведения нельзя менять память.32

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 32, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Где дампа недостаточно

Аварийный дамп — копия памяти и регистров в момент, когда процесс упал (или его остановили). Как описано в статье об анализе SOS, !clrstack показывает, где упало, а !dumpheap -stat — что поедает кучу. Чего он не показывает — что произошло по пути к этому состоянию.

Что фиксирует дамп и что фиксирует TTDАварийный дамп фиксирует только состояние в момент сбоя, а не путь, который к нему привёл. TTD сохраняет всё исполнение инструкций от начала записи до конца, поэтому путь сохраняется вместе с состояниемАварийный дамп: состояние в момент сбояНе показывает, почему значение стало такимТрассировка TTD: исполнение инструкций записанного интервалаМожно вернуться к позиции, где значение записалиЭтот разрыв и затягивает расследования долгоживущих приложений

Рис. 1: Дамп — это состояние, TTD — путь. В ошибках долгоживущих приложений обычно нужен второй.

Типичные случаи выглядят так.

  • Одно поле структуры в куче держит невозможное значение. Дамп показывает испорченное значение, но не кто его записал и когда.
  • Место исключения известно, но не ясно, почему переданный аргумент был неверным. Поднимаясь выше по вызывающим, функция, которая по пути произвела значение, уже нет на стеке.
  • Хендлы или память растут в течение месяца. Один дамп может сказать только «растёт»; пути вызовов, который это вырастил, нет.
Что дамп упускает в типичных долгоживущих случаяхИспорченное значение зафиксировано, но не кто его записал; позиция исключения зафиксирована, но функции, которая произвела аргумент, уже нет на стеке; рост ресурса зафиксирован, но не путь, который его вырастил; три вида случаев разделяют этоИспорченное полеНет записи, кто и когда записалИсключение на неверном аргументеФункции, которая произвела значение, уже нет на стекеРесурс, который растёт месяцНет записи пути вызовов, который его вырастилОбщее: состояние есть, пути нет

Рис. 2: Во всех трёх случаях «состояние есть, пути нет». Больше фотографий путь не заполняют.

Документация Microsoft сравнивает сильные и слабые стороны методов расследования так.1

Метод Сильные стороны Слабые стороны
Живая отладка Интерактивна, видно поток исполнения, можно менять состояние Останавливает работу пользователя. Трудно воспроизводить снова и снова. Часто неприменима в производстве. Сложно идти от точки отказа к причине
Дампы Не требуют заранее менять код. Низкая инвазивность, можно собирать по триггеру. Почти нулевые накладные расходы, пока не используются Даже с последовательными снимками вид «прошедшего времени» грубый
Телеметрия и журналы Лёгкие. Привязаны к бизнес-сценариям Нет журналов на непредвиденных путях кода. Недостаточная глубина данных, статически встроены в код
TTD Силён на сложных ошибках. Не требует заранее менять код. Можно воспроизводить офлайн сколько угодно раз и записывает всё Большие накладные расходы во время записи. Может собрать больше данных, чем нужно. Файлы становятся большими

Есть ещё одно важное свойство, на которое указывает официальный обзор TTD. Когда отладчик останавливается в точке отказа, эта точка часто находится внутри кода обработки ошибок на несколько шагов дальше настоящей причины.5 Дамп всегда снимается в этой позиции «на несколько шагов позже». С TTD оттуда можно шагнуть назад по одной инструкции.

Разрыв между точкой отказа и настоящей причинойТочка отказа, где снимается дамп, часто находится внутри обработки ошибок на несколько шагов дальше настоящей причины, и с TTD можно перемотать от этой точки инструкция за инструкцией обратно к причинеДампTTDНастоящая причина (инструкция, которая испортила значение)Несколько шагов вперёдТочка отказа (исключение, обработка ошибок)Здесь фиксируетсяНазад через p- / t- / g-

Рис. 3: Дамп заморожен в точке отказа. TTD может пройти от точки отказа обратно к причине.

3. Как устроен TTD и чем приходится платить

3.1 Что записывается

TTD внедряет движок записи в целевой процесс и записывает исполненные инструкции, инструкция за инструкцией. Словами официальной документации, он «кодирует полную трассировку на уровне инструкций в среднем менее чем в один байт на инструкцию»; на практике это попадает в диапазон от одного бита до одного байта на инструкцию. Программы, которые исполняют мало видов функций и обрабатывают мало данных, дают меньшие трассировки; наоборот — большие.24

Запись даёт два файла.1

Файл Роль Примерный размер
.run Сама трассировка. Хранит исполнение инструкций во время записи Растёт на 5–50 МБ в секунду, пока процесс активен. Не растёт в простое4
.idx Индекс. Вспомогательные данные, которые позволяют WinDbg эффективно воспроизводить и запрашивать память. Создаётся при остановке записи и также генерируется автоматически, когда WinDbg открывает файл .run В 1–2 раза больше трассировки4
Поток от записи TTD к воспроизведениюДвижок записи внедряется в целевой процесс и исполнение инструкций записывается в файл .run; когда WinDbg открывает .run, он создаёт индекс .idx и воспроизводит вперёд и назад через позиции, события и запросыЦелевой процессВнедрение движка записи (TTDRecordCPU).run (запись исполнения инструкций)Открыть в WinDbgСгенерировать .idx (индекс)Воспроизвести вперёд и назад через позиции, события и запросы

Рис. 4: Записывают TTD.exe или WinDbg; читает WinDbg. Достаточно делиться только файлом .run.

Время внутри файла .run выражается как «позиция» (position). Форма — два шестнадцатеричных числа, разделённых двоеточием, например 12:0 или 1A0:12F; первая половина — номер секвенирования (соответствует событию секвенирования), вторая — приблизительное число инструкций с этого события.6 FFFFFFFFFFFFFFFE:0 означает конец трассировки.7 Позиции выходят на первый план в главе 6.

Как выражаются позиции в трассировкеПозиция — шестнадцатеричный номер секвенирования и счётчик шагов, разделённые двоеточием; начало около 0, конец выражается как FFFFFFFFFFFFFFFE:0, и можно также перейти к приблизительной позиции по процентуПозиция xx:yy (шестнадцатеричная)xx: номер секвенированияyy: инструкции с этого событияКонец — FFFFFFFFFFFFFFFE:0Проценты вроде !tt 50 тоже работают

Рис. 5: Позиция — это «номер события:число инструкций». Это не настенные часы, но в главе 6 показано, как перевести её в реальное время.

3.2 Цена

Сама официальная документация описывает TTD как «инвазивную технологию».2 Цена в цифрах.

Пункт Содержание
Скорость Целевой процесс во время записи работает в 5–20 раз медленнее или хуже (зависит от приложения и параметров записи). В UI это может остаться незамеченным, но ощущается на тяжёлых операциях вроде диалога открытия файла23
Рост файла 5–50 МБ в секунду, пока процесс активен. Несколько минут записи могут дать несколько ГБ. Потолка нет4
Исчерпание диска Если диск заполняется во время записи, TTD пишет последнюю страницу и затем фактически ждёт, пока снова сможет писать. WinDbg продолжает показывать диалог записи и не выдаёт ни ошибки, ни предупреждения. Результат — неполная трассировка4
Память Запись добавляет накладные расходы виртуальных CPU в память целевого процесса (по умолчанию 55 на x64/ARM64, 32 на x86). Уменьшайте через -numVCpu только когда памяти мало2
Нельзя отсоединиться Однажды прикрепившись, TTD сам себя не отсоединяет. Когда запись закончена, закройте приложение или завершите процесс. Если процесс необходим системе, нужна перезагрузка ОС2
Цена записи и её влияние на долгоживущие приложенияЗамедление во время записи, рост файла, тихое ожидание при исчерпании диска и невозможность отсоединиться после прикрепления — четыре цены, из-за которых нельзя безусловно прикрепить TTD к долгоживущему приложениюЗапись TTDВ 5–20 раз медленнееРастёт на 5–50 МБ в секунду, без потолкаТихо ждёт, когда диск исчерпанПосле прикрепления нельзя отсоединитьсяБезусловная постоянная запись невозможна

Рис. 6: Любая из четырёх цен сама по себе исключает постоянную запись. Поэтому нужно «проектирование записи» главы 5.

3.3 Чего оно не умеет

  • Только пользовательский режим. Записывается только пользовательское исполнение процесса; код, который исполняется в режиме ядра, например драйверы, отладить нельзя.3
  • Защищённые процессы. TTD не может внедриться в защищённые процессы Windows вроде Protected Process Light (PPL).3
  • Воспроизведение только для чтения. Можно вернуться в прошлое, но историю изменить нельзя. Команды, которые читают память, работают; команды, которые её меняют, — нет.3
  • Несовместимость с антивирусом и ПО мониторинга памяти. Поскольку TTD ставит хуки в процесс, он конфликтует с ПО, которое отслеживает или зеркалирует системные вызовы к памяти. Если запись даёт ошибку, похожую на недостаток прав, временно отключите такое ПО, чтобы локализовать проблему. Фреймворк Electron — известный конфликт; даже если запись удалась, целевой процесс может зайти в взаимную блокировку или упасть.3
  • Приложения UWP нельзя запустить и записать (прикрепиться к уже работающему приложению UWP можно). «Необычные процессы», работающие в другом сеансе или другом контексте безопасности, сейчас тоже не поддерживаются.8
Что TTD не может записать или сделатьКод режима ядра, защищённые процессы, запись запуска приложений UWP и процессы в другом сеансе или контексте безопасности записать нельзя; во время воспроизведения нельзя менять память; антивирус и Electron могут конфликтоватьОграничения TTDНельзя записатьОграничения и конфликтыКод режима ядра (драйверы и т. д.)Защищённые процессы (PPL)Запись запуска UWP (прикрепление возможно)Другие сеансы и контексты безопасностиВоспроизведение только для чтенияМожет конфликтовать с антивирусом и Electron

Рис. 7: Ограничения двух видов: «нельзя записать» и «конфликтует». Второе нужно локализовать в каждой среде.

Последний пункт спотыкает тех, кто хочет записать службу. Документация TTD.exe описывает -attach как предназначенный для «расследования служб и долгоживущих приложений», а -monitor — как запись «каждый раз, когда запускается программа или служба»,2 что не просто стыкуется с формулировкой на странице устранения неполадок. На практике безопасный порядок — проверять в каждой среде так: сначала подтвердить, что ping.exe или cmd.exe записываются в той же конфигурации, что производство, затем попробовать целевой процесс.8

4. Запись — UI WinDbg и TTD.exe

У записи два входа: UI WinDbg или командная строка TTD.exe.

4.1 Запись из UI WinDbg

Запустите WinDbg от имени администратора (для TTD повышение прав обязательно1), выберите File > Start debugging > Launch executable (advanced), укажите исполняемый файл и отметьте Record with Time Travel Debugging. Выбор Configure and Record позволяет задать расположение файла трассировки и Record subset of execution (ограничить записываемые модули списком через запятую, например notepad.exe,kernelbase.dll). Для уже работающего процесса выберите File > Start debugging > Attach to process и так же отметьте Record Process with Time Travel Debugging.9

Во время записи появляется маленький диалог с кнопками «Stop and Debug» и «Cancel». Когда приложение завершается (или падает), трассировка закрывается, WinDbg открывает её автоматически и строит индекс.5

Поток записи из UI WinDbgВ WinDbg, запущенном от имени администратора, выберите Launch executable (advanced) или Attach to process, отметьте Record with Time Travel Debugging, задайте место сохранения и фильтр модулей в Configure and Record, пройдите диалог записи, и когда приложение завершится, трассировка закроется и проиндексируется автоматическиЗапустить WinDbg от имени администратораLaunch executable (advanced) / Attach to processОтметить Record with Time Travel DebuggingConfigure and Record: место сохранения, фильтр модулейДиалог записи (Stop and Debug)Приложение завершается, трассировка закрывается и индексируется автоматически

Рис. 8: Запись из UI — пять шагов. Пропустите «запуск от имени администратора» — и остановитесь на первом диалоге.

4.2 Запись через TTD.exe

Когда нужно записывать на ПК, куда нельзя поставить WinDbg, или автоматизировать запись, используйте TTD.exe отдельно. Он ставится через App Installer с https://aka.ms/ttd/download, после установки проверяется командой ttd.exe -help. Для офлайн-сред Microsoft также даёт официальную процедуру (со скриптом PowerShell) ручной распаковки пакета MSIX и извлечения только двоичных файлов.2 Для записи нужны права администратора, обычно запускают из командной строки администратора.2

Режимов записи три.2

Три режима записи TTD.exelaunch запускает новый процесс с аргументами и записывает его, но он работает с повышенными правами. attach прикрепляется к работающему процессу по PID. monitor записывает каждый раз, когда стартует указанная программа, и запуск идёт с обычными правамиРежимы записи TTD.exe-launch: запустить и записать-attach: прикрепиться к работающему PID-monitor: записывать каждый запускЗапускается с правами администратораСохраняет обычные праваОбычный путь запуска, подходит для автоматизации

Рис. 9: Аргументы можно передать только через -launch, но он работает с повышенными правами. Чтобы записать поведение, близкое к производству, используйте -attach или -monitor.

:: Запустить и записать (режим по умолчанию; -launch можно опустить)
TTD.exe -out C:\traces MyApp.exe --config prod.json

:: Прикрепиться к работающему процессу (сначала создайте каталог вывода)
TTD.exe -attach 21440 -out C:\traces\MyApp.run

:: Записывать каждый запуск (Ctrl+C завершает мониторинг; -out требует полный путь)
TTD.exe -out C:\traces\ -monitor MyApp.exe
  • -launch — единственный режим, который может передать аргументы, но программа запускается с теми же (администраторскими) правами, что и TTD.exe. Для приложений, чьё поведение меняется с правами, записывайте через -attach или -monitor, чтобы сохранить обычные права.2
  • -attach требует, чтобы каталог вывода уже существовал. Если указываете имя файла, файла с таким именем ещё не должно быть.2
  • -monitor ставит драйвер мониторинга запуска процессов и записывает каждый раз, когда стартует указанная программа (можно задать несколько). Действует до перезагрузки и останавливается Ctrl+C. Преимущества: не нужно собирать запуск самому, цель работает с обычными правами, подходит для автоматизации скриптами. Добавление -cmdLineFilter "string" записывает только запуски, командная строка которых содержит эту строку.2
  • -children записывает и дочерние процессы, но каждый процесс получает свой файл .run, а WinDbg открывает только один за раз.2

Во время записи появляется маленький UI с двумя кнопками: «Tracing Off» (остановить запись и дать приложению продолжить) и «Exit App» (закрыть приложение и закончить запись). Для автоматизации спрячьте его через -noUI и примите EULA через -accepteula.2 Журнал записи хранится в файле .out там же, где .run: там читаются настенные время начала и конца записи, длина сеанса записи (simulation time), запуск это был или прикрепление, и версия ОС. Когда запись срывается, часть сообщений об ошибках появляется только в .out.2

5. Проектирование записи для долгоживущих приложений

Это сердце статьи. Исходя из цены главы 3, чтобы поймать ошибку, которая случается в неизвестный момент в процессе, работающем днями, нужно проектирование записи. TTD.exe даёт четыре средства; выбирайте по характеру симптома.

Выбор охвата записи по симптому долгоживущего приложенияЕсли неизвестно, когда случится, оставляйте только конец кольцевым буфером; если подозреваемый модуль ясен, записывайте только его; если приложение можно менять, задайте интервал API ручной записи; если проявляется только при запуске или на отдельных запусках, записывайте каждый запуск режимом монитораНеизвестно, когда случитсяТолько при запуске или на отдельных запускахПодозреваемый модуль ясенПриложение можно менятьХарактер симптома?-ring / -maxFile: оставить только конец-monitor: записывать каждый запускДобавить -moduleЗадать интервал через -recordmode Manual

Рис. 10: Четыре не взаимоисключающие. Сочетание -ring с -module — самая практичная комбинация для долгоживущих приложений.

5.1 Кольцевой буфер — оставить только последние N МБ

С -ring трассировка пишется в кольцевой буфер размера, заданного -maxFile, и файл никогда не растёт сверх этого предела. Остаётся только последняя часть записи, которая влезает в этот размер.2 Единица -maxFile — МБ; в режиме кольцевого буфера по умолчанию 2 048 МБ, минимум 1 МБ, максимум 32 768 МБ (по умолчанию для кольца в памяти 32-разрядного процесса — 256 МБ).2

:: Прикрепиться к работающему приложению мониторинга и оставить только последние 4 ГБ исполнения
TTD.exe -accepteula -noUI -attach 21440 -ring -maxFile 4096 -out C:\traces\MyApp.run

:: Когда симптом обнаружен, остановить запись (приложение продолжает работать)
TTD.exe -stop 21440

-stop принимает имя процесса, PID или all и останавливает эту запись. -wait <seconds> ждёт, пока на системе закончится каждый сеанс записи (-1 — бесконечно), и в скриптах автоматизации задаёт порядок «остановить, затем забрать файл».2

Временная шкала записи в кольцевой буферЗапись начинается при прикреплении, старые части выталкиваются из кольцевого буфера, и когда появляется симптом и выдаётся stop, остаётся только объём последних maxFile как трассировкаНачать запись через -attach -ringСтарые интервалы выталкиваютсяПоявляется симптомОстановить запись через -stopОстаётся только объём последних maxFileРазмерьте так, чтобы моменты до симптома влезли в буфер

Рис. 11: Кольцевой буфер — устройство, которое хранит «моменты до симптома». -maxFile считайте назад от «времени от обнаружения симптома до остановки, умноженного на рост в секунду».

Два пункта проектирования.

  1. Сделайте буфер достаточно большим, чтобы поглотить время от обнаружения до остановки. В активном процессе трассировка растёт на 5–50 МБ в секунду,4 поэтому кольцо 4 ГБ соответствует одной-двум минутам при высокой нагрузке и чуть больше десяти минут при низкой. Нужен механизм, который удерживает лаг между обнаружением симптома (конкретная строка журнала, порог счётчика, оповещение мониторинга) и -stop внутри этого окна.
  2. Остановка не отсоединяет. -stop останавливает запись, но TTD сам не отсоединяется от целевого процесса.2 Чтобы полностью закончить запись, нужно завершить процесс, поэтому включите в эксплуатацию «перезапустить в следующее окно обслуживания после сбора трассировки».
Остановка записи в кольцевой буфер в связке с мониторингомКогда сторона мониторинга обнаруживает симптом через журналы или счётчики, она вызывает TTD.exe stop, забирает готовый файл .run и перезапускает процесс в следующее окно обслуживания, чтобы снять TTDЦелевой процессTTD.exeМониторинг (журналы, счётчики)Целевой процессTTD.exeМониторинг (журналы, счётчики)Обнаружить симптом-stop PIDОстановить запись (процесс продолжается).run готовЗабрать .run, зашифровать и сохранитьПерезапустить в следующее окно обслуживания

Рис. 12: Только когда лаг между обнаружением и -stop влезает в буфер, моменты до симптома остаются в трассировке. Перезапуск — часть эксплуатации.

5.2 Фильтр модуля — записывать только пока исполняется свой код

-module <module name> записывает только указанный модуль (сам исполняемый файл или загруженную DLL; можно задать несколько) и код, который этот модуль вызывает. Целевой процесс работает на полной скорости, пока не исполнится код указанного модуля; запись начинается, когда исполнение входит в модуль, останавливается, когда выходит, и процесс снова идёт на полной скорости. Включение и выключение записи дорого, поэтому запись остаётся включённой, пока указанный модуль вызывает другие модули в процессе.2

:: Записывать только пока работает наша DLL измерительной логики (вместе с кольцом)
TTD.exe -accepteula -noUI -attach 21440 -module MeasureCore.dll -ring -maxFile 2048 -out C:\traces\

Трассировка, сделанная так, просто пропускает интервалы, где запись была выключена, считая «следующей инструкцией» первую инструкцию после возобновления записи, и отлаживается ничем не иначе, чем трассировка всего процесса.2 В долгоживущих приложениях цикл простоя UI и внутренняя обработка фреймворка обычно дают большую часть исполненных инструкций, поэтому ограничение записи своим модулем само по себе заметно снижает накладные расходы и размер файла.

Как ведёт себя запись с фильтром модуляЦелевой процесс работает на полной скорости вне указанного модуля; запись начинается, когда входят в код указанного модуля, продолжается, пока этот модуль вызывает другие модули, и останавливается, когда исполнение покидает модульВне указанного модуля: полная скоростьВход в указанный модуль: запись начинаетсяДругие модули, которые он вызывает: запись продолжаетсяВыход из указанного модуля: запись останавливается

Рис. 13: Поскольку Win32 API и среда выполнения, которые вызывает ваша DLL, тоже записываются, этого достаточно, чтобы проследить «что наш код передал ОС».

5.3 Ручная запись — пусть приложение задаёт интервал

С -recordmode Manual процесс продолжает работать на полной скорости даже после внедрения TTD, и запись идёт только когда программа вызывает внутрипроцессный API записи TTD (по умолчанию Automatic записывает с момента внедрения).2 Документация API и образцы лежат в репозитории WinDbg-Samples на GitHub.10

Если приложение можно менять, это наименее расточительный способ. Можно встроить логику, которая начинает запись, когда само приложение знает, что аномалия близка, и останавливает, когда всё возвращается к норме: «три подряд повторные попытки связи не удались», «очередь превысила порог» и так далее. В конфигурации процесса-наблюдателя вроде той, что разобрана в статье о Job Object, интервал может задать наблюдатель.

5.4 Режим монитора — записывать каждый запуск

Для ошибок, которые проявляются только сразу после запуска или на отдельных запусках, подходит -monitor. Официальная таблица сравнения тоже описывает режим монитора как предназначенный «ловить прерывистые проблемы и проблемы запуска».2 Когда одну и ту же программу записывают много раз, имена файлов по умолчанию с порядковыми номерами (MyApp01.run, MyApp02.run и так далее) становятся неэффективными, потому что приходится сканировать существующие файлы, поэтому используйте -timestampFilename для имён с меткой времени. Число одновременных записей можно ограничить через -maxConcurrentRecordings.2

Как ведёт себя режим монитораПараметр monitor ставит драйвер мониторинга запуска процессов, каждый раз при старте указанной программы сужает цель фильтром командной строки, записывает её, создаёт отдельный файл трассировки на запуск и продолжается до Ctrl+C или перезагрузкиДаНетУстановить драйвер мониторинга запускаОбнаружить запуск указанной программыСовпадает с -cmdLineFilter?Записать этот запуск (отдельный файл на запуск)Не записыватьЖдать следующего запуска (до Ctrl+C или перезагрузки)

Рис. 14: Режим монитора «подстерегает запуск». Он подходит ошибкам, которые проявляются на каждом запуске, и ошибкам, которые можно сузить условиями запуска.

5.5 Куда класть диск

Для долгоживущих записей кладите трассировки на выделенный том и включите рост файла .run в мониторинг. Как отмечено в разделе 3.2, когда диск заполняется, запись просто тихо ждёт без ошибки. Официальный обходной путь столь же примитивен: «смотреть свободное место в Проводнике» и «проверять, что файл .run регулярно растёт».4 Без мониторинга свободного места вы получаете неполную трассировку, в которой самый нужный момент так и не был записан.

Как исчерпание диска даёт неполную трассировкуКогда диск заканчивается во время записи, TTD пишет последнюю страницу и тихо ждёт без ошибки и предупреждения, поэтому симптом, который случается после, не записывается, оставляя неполную трассировку, которая открывается, но без ключевой части. Предотвращайте выделенным томом и мониторингом роста .runпредотвращаетДиск заканчиваетсяПишет последнюю страницу и тихо ждётНи ошибки, ни предупрежденияПосле этого случается симптомНеполная трассировка без симптомаВыделенный том + мониторинг роста .run

Рис. 15: Трассировка, которая «открывается, но без ключевой части», рождается из дыры в мониторинге диска.

6. Воспроизведение — позиции, события и перемотка

6.1 Открытие

Когда вы открываете файл .run в WinDbg, если индекса нет, автоматически выполняется !index и строится файл .idx, считая keyframe (позиции в трассировке, которые автоматически генерируются для индексации; в больших трассировках их больше). Чем больше трассировка, тем дольше это занимает.5 Состояние индекса проверяется через !index -status; если там что угодно кроме «Index file loaded», пересоберите через !index -force. Если и это не помогает, закройте отладчик, удалите файл .idx и снова откройте .run. Пересборка индекса не меняет файл .run, поэтому данные не теряются.8

Оговорка: когда в TTD 1.11.611 улучшили индексацию больших трассировок, формат индекса изменился, и существующие трассировки нужно переиндексировать.11 Если вы таскаете старые файлы .idx, спотыкаетесь здесь.

Проверка и пересборка индексаПосле открытия трассировки проверьте состояние через !index -status; если это что угодно кроме Index file loaded, пересоберите через !index -force, и если это всё ещё не удаётся, закройте отладчик, удалите файл .idx и снова откройте .run. Пересборка не меняет файл .runIndex file loadedЧто угодно другоеНеудачаУспехОткрыть трассировку!index -statusПерейти к анализуПересобрать через !index -forceЗакрыть, удалить .idx, снова открыть .run

Рис. 16: Пересборка индекса не трогает файл .run. Если сомневаетесь, удалите и откройте снова.

6.2 Перемещение по позиции

Передача позиции в !tt перемещает к этому моменту времени.6

!tt 0          ; начало трассировки
!tt 50         ; примерно позиция 50%
!tt 100        ; конец трассировки
!tt 1A0:12F    ; к позиции 1A0:12F

Позиция — пара шестнадцатеричных чисел, номер секвенирования:счётчик шагов.6 У объекта Position есть свойства Percent (доля трассировки), Sequence и Steps, плюс SeekTo(), который перемещает к этой позиции, и ToSystemTime(), который возвращает приблизительное настенное время (UTC).7 В расследованиях долгоживущих приложений именно ToSystemTime() делает разницу, потому что позволяет сопоставить метки времени в журнале приложения с позициями в трассировке. Результаты TTD.Calls тоже несут SystemTimeStart / SystemTimeEnd.12

Сопоставление позиций с метками времени журналаОт метки времени в журнале приложения пройдите приблизительное настенное время объекта Position, чтобы найти позицию в трассировке, переместитесь туда через SeekTo и прочитайте исполнение вокруг неёЖурнал приложения: время аномалииНайти позицию, чей ToSystemTime близокПереместиться туда через SeekToПрочитать окружающие вызовы и значения

Рис. 17: «Что оно делало непосредственно перед этой строкой журнала?» можно найти через соответствие позиций и настенного времени.

Позиции также полезны для обмена. Когда передаёте трассировку коллеге, приложите позицию !tt x:y — и собеседник начнёт смотреть с того же момента. Писать диапазоны позиций в отчётах об ошибках тоже официально рекомендуется.2

6.3 Перемотка

Если к обычным командам пошагового исполнения добавить - в конец, время идёт назад.13

Команда Смысл Кнопка ленты
p- Назад на одну инструкцию (или одну строку исходника). Вызов функции считается одним шагом Step Over Back
t- Назад на одну инструкцию (или одну строку исходника). Заходит внутрь вызовов функций Step Into Back
g- Исполнять в обратную сторону. Останавливается, когда срабатывает точка останова, на событии или в начале трассировки Go Back

События, которые останавливают g-, те же, что останавливают прямой g.13 Иными словами, поставьте ba (точку останова по доступу) или bp и выполните g- — и вы сразу вернётесь к «последней позиции, где это условие выполнялось». Это базовый ход главы 7.

Выбор среди обратных командp- идёт назад на один шаг через вызовы функций, t- идёт назад на одну инструкцию внутрь функций, а g- идёт сразу назад до точки останова, события или начала трассировки. Что останавливает прямой g, останавливает и g-p-t-g-Текущая позицияНазад на один шаг через вызовыНазад на одну инструкцию внутрь функцийСразу назад до следующего условия остановкиba / bp / событие / начало трассировки

Рис. 18: Чтобы внимательно смотреть рядом, используйте t-; чтобы прыгнуть к далёкой причине, используйте ba + g-.

6.4 Вход через события

Если неясно, откуда начинать читать, начните со списка событий. @$curprocess.TTD.Events перечисляет как события создание и завершение потоков, загрузку и выгрузку модулей и исключения.14

dx -g @$curprocess.TTD.Events
dx @$curprocess.TTD.Events.Where(t => t.Type == "Exception").Select(e => e.Exception)

Событие исключения содержит позицию, тип (Software / Hardware), код исключения и счётчик команд на тот момент; щелчок по ссылке [Time Travel] в выводе перемещает к этой позиции.15 Официальный обзор идёт по этому потоку: прыгает к позиции нарушения доступа (0xc0000005), подозревает порчу стека, потому что указатель стека и базовый указатель не совпадают, и шагает назад на три инструкции через t-, чтобы проверить значения.5 Окно Timelines в WinDbg визуализирует исключения, точки останова, обращения к памяти и вызовы функций как шкалу времени; двойной щелчок по исключению выдаёт тот же SeekTo().16

6.5 Потоки и позиции

!positions показывает каждый активный поток в текущей позиции вместе с позицией каждого потока в трассировке.17 Здесь есть ловушка. Переключение потоков через ~<number>s не двигает позицию в трассировке. Позиция, которую отладчик использует для чтения памяти, не меняется, поэтому чтобы посмотреть память другого потока «в тот момент», переместитесь туда ссылкой позиции в выводе !positions или через !tt x:y.13

Базовый путь через воспроизведениеОткройте трассировку и постройте индекс, от списка событий перейдите к позиции исключения, шагайте назад к причине и при необходимости переместитесь к позиции другого потока через positionsОткрыть трассировку и проиндексироватьНайти исключение в TTD.EventsПереместиться к позиции через [Time Travel]Назад через t- / p- / g-Проверить позиции других потоков через !positions~s не двигает позицию трассировки

Рис. 19: «Войти через событие, идти назад» — базовый путь через воспроизведение. Переключение потоков — не перемещение позиций.

7. Искать «когда» запросами — TTD.Calls и TTD.Memory

Настоящая сила воспроизведения в том, что можно запросить всю трассировку. Объекты TTD выставлены через модель данных отладчика (команда dx), и их можно фильтровать, сортировать и агрегировать в стиле LINQ.18

7.1 TTD.Calls — поиск вызовов функций

@$cursession.TTD.Calls("module!symbol") собирает вызовы указанной функции со всей трассировки. Допускаются подстановочные знаки, и каждый вызов несёт позиции начала и конца (TimeStart / TimeEnd), идентификатор потока (с UniqueThreadId, который никогда не переиспользуется), аргументы (Parameters[]), возвращаемое значение (ReturnValue) и адрес возврата (ReturnAddress).12

Пример в официальной документации — GetLastError. Соберите вызовы с ненулевым возвращаемым значением по коду ошибки — и получите список, какие ошибки сколько раз случились за трассировку.18

dx -g @$cursession.TTD.Calls("kernelbase!GetLastError").Where(x => x.ReturnValue != 0).GroupBy(x => x.ReturnValue).Select(x => new { ErrorNumber = x.First().ReturnValue, ErrorCount = x.Count() }).OrderByDescending(p => p.ErrorCount),d

Для «откуда последний раз вызывали MessageBox?» возьмите последний вызов через OrderBy(c => c.TimeStart).Last() и переместитесь туда ссылкой [Time Travel] его TimeStart.18

Сборка запроса TTD.CallsСоберите вызовы по имени функции, отфильтруйте по возвращаемому значению или аргументам, агрегируйте по коду ошибки и тому подобному, отсортируйте по времени и переместитесь к позиции нужного вызова через ссылку Time TravelTTD.Calls (имя функции, подстановочные знаки)Where: фильтр по возвращаемому значению или аргументамGroupBy: агрегировать по коду ошибки и т. д.OrderBy: сортировать по времениПереместиться через [Time Travel] на TimeStart

Рис. 20: Запросы используют так: начинать не с «где», а с «когда, сколько раз и с какими аргументами».

Слово о символах. TTD определяет число и типы аргументов функции, тип возвращаемого значения и соглашение о вызове из сведений символов PDB. С закрытыми символами (private symbols) вы получаете имя функции и верные аргументы. Только с открытыми символами (public symbols) — имя функции и аргументы по умолчанию (четыре 64-разрядных беззнаковых целых). Модуль без символов вовсе получает имя функции UnknownOrMissingSymbols.1218 О видах PDB и о том, как их хранить, см. «Что такое PDB (Program Database) — отладочная информация, символы и Source Link».

Наличие символов и результаты TTD.CallsС закрытыми символами получаете имя функции и верные аргументы; только с открытыми символами — имя функции и четыре 64-разрядных целых аргумента по умолчанию; без символов имя функции становится UnknownOrMissingSymbolsprivate symbolspublic symbolsнетСимволы модуля?Имя функции + верные аргументы и возвращаемое значениеИмя функции + аргументы по умолчанию (4 x 64-разрядных целых)UnknownOrMissingSymbols

Рис. 21: Храните ли вы PDB своих модулей, напрямую определяет, насколько полезны запросы.

Calls связан с вычислениями, поэтому чем больше трассировка, тем дольше это занимает и тем выше загрузка CPU. Результаты кэшируются в памяти, поэтому второй и последующие запросы к той же функции быстрее.12 Когда запрос ничего не возвращает, причин четыре: как записан вызов (проверьте имя модуля командой x; если вернулось в верхнем регистре, используйте его), целевая DLL на этой позиции ещё не загружена (перейдите к позиции после загрузки и выполните снова), функция встроена (inlined, отследить нельзя) или подстановочный знак слишком широкий (сузьте).18

7.2 TTD.Memory — поиск обращений к памяти

@$cursession.TTD.Memory(start address, end address, "access type") собирает обращения к указанному диапазону памяти со всей трассировки. Типы: r (чтение), w (запись), rw, e (исполнение), rwe и ec (исполнение/изменение).19 На «кто последним записал эту переменную?» отвечают, собрав с "w", взяв .Last() и переместившись к этой позиции.5

dx -g @$cursession.TTD.Memory(0x00a4fca0, 0x00a4fca4, "w")
dx @$cursession.TTD.Memory(0x00a4fca0, 0x00a4fca4, "w").Last().TimeStart.SeekTo()

Если нужно искать только вперёд или назад от текущей позиции, @$curprocess.TTD.PrevMemoryAccess("w", address, size) / NextMemoryAccess легче и принимают несколько диапазонов сразу. Позицию, где изменился регистр, можно найти через @$curthread.TTD.PrevRegisterWrite("rcx").6

7.3 ba + g- — заставить отладчик ответить «кто испортил?»

Сжав процедуру официального обзора в форму, пригодную для расследований долгоживущих приложений, получаем следующее.5

  1. Перейдите к позиции исключения через TTD.Events
  2. Шагайте назад через t- и предположите, какая переменная держит испорченное значение
  3. Возьмите адрес этой переменной через dx &variable
  4. Поставьте точку останова на запись через ba w4 <address>
  5. Выполните g-, чтобы сразу вернуться к позиции, где эту переменную записали последней
  6. Проверьте, является ли эта точка (или несколько инструкций раньше) причиной. Если записанное значение пришло из другой переменной, поставьте ba на ту переменную и снова выполните g-
  7. Повторяйте, пока не дойдёте до инструкции, которая испортила значение
Прослеживание происхождения значения через ba и g-Поставьте точку останова на запись по адресу испорченного значения и исполняйте в обратную сторону, остановитесь на инструкции, которая записала его последней, и если это значение пришло из другой переменной, повторяйте те же шаги, пока не дойдёте до инструкции, которая испортила значениеДаНетОпределить адрес испорченного значенияПоставить точку останова на запись через ba wИсполнять в обратную сторону через g-Остановиться на инструкции, которая записала последнейЗначение пришло из другой переменной?Инструкция, которая испортила = причина

Рис. 22: Расследование, которое на дампе заканчивается на «испорчено», с TTD механически идёт до «кто испортил».

В TTD 1.11.553 и новее также добавлен @$curframe.TTD.VariableHistory(), который возвращает историю значений локальных переменных кадра. Он показывает таблицу имён переменных и какие значения каждая переменная держала на каких диапазонах позиций.11 В ситуациях вроде порчи стека, где нужно знать «с какого момента значение неверно», это полезно, чтобы сузить, куда сначала ставить ba.

8. Применить к ошибкам долгоживущих приложений

Теперь применим эти инструменты к трём видам случаев, перечисленным в начале.

Типы ошибок долгоживущих приложений и точка входа TTD для каждогоДля прерывистых исключений и порчи данных идите назад от события через ba и g-; для роста ресурсов сопоставляйте вызовы получения и освобождения через TTD.Calls; для «не отвечает» и цепочек ожиданий следите за позициями и настенным временем вызовов API ожиданияПрерывистые исключения, порча данныхTTD.Events, затем ba + g-Рост хендлов и памятиСопоставить получение и освобождение через TTD.CallsНе отвечает, цепочки ожиданий!positions и настенное время API ожиданияЗаписывать через -module, ограниченный своей DLL

Рис. 23: Точка входа зависит от типа. Общее — сузить охват записи, прежде чем начинать читать.

Тип 1: прерывистые исключения и порча данных. Оставьте моменты до симптома кольцевым буфером раздела 5.1, прыгните к позиции исключения из списка событий раздела 6.4 и проследите происхождение значения через ba + g- раздела 7.3. Отличие от расследования дампа в том, что расследование не заканчивается, когда вы находите «испорченную переменную»; оттуда вы механически идёте назад.

Тип 2: рост хендлов и памяти. Это тот вид случая, который разобран в «Утечка хендлов: почему приложение промышленной камеры падает после месяца работы». Месяц записать нельзя, поэтому ограничьте запись своей DLL через -module раздела 5.2, совместите с -ring и оставьте «несколько минут, пока растёт». На трассировке соберите TTD.Calls("kernelbase!CreateFileW") и TTD.Calls("kernelbase!CloseHandle") и сопоставьте возвращаемое значение CreateFileW (значение хендла) с первым аргументом CloseHandle (Parameters[0]). ReturnValue у CloseHandle — булев флаг успеха, поэтому для сопоставления он бесполезен. Если значение хендла осталось незакрытым, ReturnAddress (вызывающий) этого вызова CreateFileW даёт вызывающего, который утекает.12 При этом наблюдать, есть ли утечка и насколько она велика, дешевле через Application Verifier или счётчики хендлов; правильное разделение труда — выносить TTD на этап, когда нужно закрепить, какой путь утекает. Заметьте: запись при включённом Application Verifier заметно ухудшает производительность воспроизведения из-за того, как используется память, поэтому на время записи отключайте его.8

Тип 3: «не отвечает» и цепочки ожиданий. Как написано в ««Не отвечает»: как Windows определяет зависание и как проектировать так, чтобы оно не возникало», зависание — вопрос, кто кого ждёт. Трассировка не растёт в простое,4 поэтому время ожидания потока, который ждёт в ядре, не появляется как инструкции. Что появляется — вызов непосредственно перед входом в ожидание и инструкции после возврата. Смотрите позицию каждого потока через !positions17 и выстройте «какой поток начал ждать чего и когда» из SystemTimeStart / SystemTimeEnd у TTD.Calls("kernelbase!WaitForSingleObject") и подобных; сопоставление с метками времени журнала позволяет восстановить цепочку ожиданий.12 «Ошибки, зависящие от порядка», вроде DllMain и блокировки загрузчика или ложных пробуждений условных переменных, — область, где TTD, который записывает сам порядок, хорошо подходит.

9. TTD в приложениях .NET

Официальная документация TTD указывает, что управляемый код можно отлаживать через TTD в WinDbg с расширением SOS (sos.dll), работающим в 64-разрядном режиме.1 Загрузка та же, что в главе 3 статьи об анализе SOS (.loadby sos coreclr или автоматическая загрузка), и !clrstack и !pe работают в каждой позиции трассировки. Базовый путь — переместиться к позиции исключения через TTD.Events, затем прочитать управляемый стек через !clrstack.

Настройка чтения трассировки TTD приложения .NETОткройте трассировку TTD в WinDbg, загрузите 64-разрядное расширение SOS, переместитесь к позиции события исключения и прочитайте управляемое состояние через !clrstack и !pe. Используйте TTD.Calls для вызовов через нативные границыТрассировка TTD (.run)WinDbgРасширение SOS (64-разрядное)Переместиться к позиции исключения через TTD.EventsЧитать через !clrstack / !peTTD.Calls для вызовов через нативные границы

Рис. 24: Управляемое состояние через SOS, поиск вызовов на нативных границах. Разделите роли — и не заблудитесь.

Две оговорки. Первая: Microsoft гарантирует «SOS в 64-разрядном режиме». Про приложения .NET, собранные под x86, ничего не сказано, поэтому по возможности запускайте объект расследования как x64. Вторая: TTD.Calls опирается на сведения символов PDB.12 Не рассчитывайте искать JIT-скомпилированные управляемые методы по имени; надёжное использование — следить за вызовами через нативные границы вроде целей P/Invoke в нативных DLL, COM и Win32 API. Проблемы управляемой кучи вроде тех, что в «.NET: как отличить ожидание GC от утечки памяти», сначала преследуют через dotnet-counters / dotnet-gcdump / !gcroot на дампе, а TTD выносят на этап, где задействована нативная граница.

10. Выбор между дампами, журналами, ETW и TTD

TTD — не панацея и не заменяет существующие инструменты. Здесь он выстроен рядом с инструментами, которые мы разбирали в статьях.

Что нужно узнать Инструмент, который использовать первым Когда выходит TTD
Состояние в момент сбоя Аварийный дамп (WER LocalDumps / ProcDump) Когда дамп показывает «испорчено», но не кто испортил
Какой файл или ключ реестра не удался Process Monitor Когда нужно ещё, как были построены аргументы, переданные сбойному API
Производительность всего ПК за долгое время WPR/WPA, PerfView (ETW) Когда это проблема правильности, а не производительности, и нужен порядок на уровне инструкций
Последовательность событий на уровне бизнеса Журналы приложения Когда случилось на пути кода без журналирования (TTD записывает всё без предварительных изменений кода1)
Кто записал это значение, порядок вызовов TTD
Выбор метода расследованияСначала схватите состояние лёгкими дампами и журналами, сбойные API разбирайте ProcMon, производительность — ETW, и переходите к TTD только когда всё ещё нужно, кто, когда и что передалДаНетСимптомСхватить состояние дампами и журналами (легко)Сбойные API: ProcMonПроизводительность: WPR/WPA, PerfViewНужно, кто, когда и что передал?Спроектировать охват записи и записать через TTDРазобрались лёгкими инструментами

Рис. 25: Сначала схватите «результат» лёгкими инструментами и выносите TTD только на те случаи, где оказывается нужен «путь».

Порядок — «сначала самое лёгкое». Дампы и журналы почти ничего не стоят при сборе, поэтому держите их постоянно; TTD выносите, после проектирования главы 5, на случаи, где чтение дампа показало, что нужен путь. Учитывая накладные расходы TTD и то, что трассировки содержат конфиденциальные сведения, нет причины разворачивать этот порядок.

11. Замечания по эксплуатации

  • Обращайтесь с трассировками как с конфиденциальными файлами. Запись содержит содержимое памяти и может включать персональные и связанные с безопасностью сведения вроде путей файлов, данных реестра и содержимого памяти и файлов.12 Когда записываете в среде заказчика, заранее поймите, что может войти (строки подключения, токены, данные клиентов), и решите зашифрованный канал передачи, место хранения и срок хранения.
  • Делитесь только файлом .run. Файл .idx примерно такого же размера, как .run, и генерируется автоматически, когда WinDbg его открывает. Файлы .run хорошо сжимаются. Когда сообщаете об ошибке в самом TTD, приложите и файл .out.2
  • Выравнивайте версии. TTD продолжают обновлять вместе с WinDbg; в 1.11.611 есть исправление сбоев записи в программах, которые используют AVX/AVX512, и смена формата индекса.11 Смешение старой версии на стороне записи или воспроизведения означает переиндексацию или повторную запись.
  • Проверяйте -replayCpuSupport, когда воспроизводите на другом CPU. По умолчанию приоритет у переносимости, а MostConservative предусмотрен для случаев, когда CPU записи и CPU воспроизведения различаются (например, воспроизведение трассировки Intel на arm64). Наоборот, если известно, что CPU воспроизведения равен или лучше, можно выбрать меньшую и более быструю запись.2
  • Windows Server тоже можно записывать. TTD.exe поддерживает Windows Server 2016/2019/2022/2025.2
  • Локализация сбоя записи. Сначала попробуйте, записываются ли ping.exe или cmd.exe; если нет, подозревайте конфликт с инвазивным ПО вроде антивируса или виртуализации приложений.8
Процедура обращения с трассировкамиПоскольку записанная трассировка содержит содержимое памяти, поймите, какие сведения она может содержать, сожмите только файл .run и передайте через зашифрованный канал, решите место хранения и срок хранения, а на стороне анализа откройте той же версией WinDbg и постройте индексЗапись завершена (.run / .idx / .out)Понять, какие сведения могут содержатьсяСжать только .run, зашифровать и передатьРешить место хранения и срок храненияОткрыть той же версией WinDbg и сгенерировать .idx

Рис. 26: Передача трассировки — передача конфиденциального файла. Решите процедуру до того, как записывать.

12. Итог

  • Дамп — «состояние»; TTD — «путь». В случаях, где нужно происхождение испорченного значения или порядок вызовов, TTD превращает расследование в механическую работу
  • Запись в 5–20 раз медленнее, растёт на 5–50 МБ в секунду и не отсоединяется. Для долгоживущих приложений сначала спроектируйте охват записи через -ring/-maxFile, -module, -recordmode Manual и -monitor
  • Воспроизведение — «войти через событие, идти назад»: TTD.Events, затем [Time Travel], затем t-/g-, и ba + g-, чтобы отладчик ответил «кто испортил»
  • TTD.Calls и TTD.Memory — запросы по всей трассировке. Сопоставляйте с метками времени журнала через ToSystemTime() и SystemTimeStart
  • Для .NET читайте состояние 64-разрядным SOS, а TTD.Calls используйте на нативных границах
  • Трассировка — конфиденциальный файл. Делитесь только файлом .run и сначала решите канал и хранение

Случаи, где дамп сняли, но до причины не доходят, или где работа встала, потому что ошибка не воспроизводится, довольно часто закрываются TTD, как только можно спроектировать охват записи. Мы также можем взять на себя всё от проектирования записи до анализа файла .run, поэтому обращайтесь со своими дампами и журналами.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается расследованием первопричин ошибок приложений Windows, которые проявляются только после долгой работы или прерывисто, сочетая аварийные дампы, журналы и трассировки TTD; построением постановки расследования, включая проектирование охвата записи; и локализацией сбоев, в которых задействованы нативные границы (COM, P/Invoke, SDK устройств). Обращайтесь уже на этапе «дамп есть, до причины не доходим».

Справочные ссылки

  1. Microsoft Learn, Time Travel Debugging - Overview. О том, что TTD записывает исполнение процесса и воспроизводит его вперёд и назад, что дампы склонны упускать состояние и путь исполнения, которые привели к сбою, что для записи нужны права администратора, что записи могут содержать персональные и связанные с безопасностью сведения, о сравнительной таблице методов расследования, о ролях .run/.idx и об отладке управляемого кода расширением SOS в 64-разрядном режиме.  2 3 4 5 6 7 8 9 10

  2. Microsoft Learn, Time Travel Debugging - TTD.exe command line utility. О замедлении в 5–20 раз и более, о невозможности отсоединиться после прикрепления, о поддержке Windows Server 2016–2025, об установке и офлайн-развёртывании, о трёх режимах -launch/-attach/-monitor, о параметрах -out/-noUI/-accepteula/-stop/-wait/-tracingOff/-children/-cmdLineFilter/-timestampFilename/-ring/-maxFile/-maxConcurrentRecordings/-numVCpu/-replayCpuSupport/-module/-recordmode, о чтении файла .out и о советах по обмену трассировками.  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

  3. Microsoft Learn, Time Travel Debugging - Overview - Things to look out for. О несовместимости с антивирусом, ПО мониторинга памяти и Electron, о том, что это только пользовательский режим, что воспроизведение только для чтения, что нельзя внедриться в защищённые процессы (PPL), и о примерно 10–20-кратном влиянии на производительность во время записи.  2 3 4 5 6 7

  4. Microsoft Learn, Time Travel Debugging - Working with Trace Files. О факторах размера трассировки (от одного бита до одного байта на инструкцию), о росте на 5–50 МБ в секунду в активном состоянии и отсутствии роста в простое, об отсутствии потолка максимального размера, о том, что индекс в 1–2 раза больше трассировки, и о поведении записи и индексации при исчерпании диска вместе с обходным путём.  2 3 4 5 6 7 8 9

  5. Microsoft Learn, Time Travel Debugging - Sample App Walkthrough. Об общей процедуре перехода к позиции события исключения и возврата через ba и g- к позиции, где неверное значение записали последним, о том, что точка отказа часто находится внутри обработки ошибок на несколько шагов дальше настоящей причины, о закрытии трассировки при сбое и автоматической индексации WinDbg, и об использовании TTD.Memory и .Last() 2 3 4 5 6 7

  6. Microsoft Learn, !tt (time travel). Об указании позиций для !tt (процент или xx:yy), о смысле двух компонентов позиции (номер секвенирования и счётчик шагов) и о TTD.PrevRegisterWrite/PrevMemoryAccess/NextMemoryAccess 2 3 4

  7. Microsoft Learn, TTD Position Objects. О свойствах объекта Position Percent/Sequence/Steps, о SeekTo(), о ToSystemTime(), возвращающем приблизительное настенное время (UTC), и о том, что FFFFFFFFFFFFFFFE:0 обозначает конец трассировки.  2

  8. Microsoft Learn, Time Travel Debugging - Troubleshooting. О необходимости повышения прав, о том, что запись запуска приложений UWP не поддерживается, что «необычные процессы» в другом сеансе или контексте безопасности вне области, о локализации через ping.exe/cmd.exe, о замедлении воспроизведения при одновременном использовании Application Verifier и о пересборке индекса через !index -status/!index -force 2 3 4 5

  9. Microsoft Learn, Time Travel Debugging - Record a trace. О записи из Launch executable (advanced) / Attach to process в UI WinDbg, флажке Record with Time Travel Debugging, задании места сохранения через Configure and Record и ограничении модулей через Record subset of execution

  10. Microsoft, WinDbg-Samples - TTD in-process recording API (GitHub). О документации внутрипроцессного API записи, который вместе с -recordmode Manual позволяет программе управлять началом и остановкой записи. 

  11. Microsoft Learn, Time travel debugging release notes. Об исправлении записи программ, которые используют AVX/AVX512, и смене формата индекса (нужна переиндексация) в 1.11.611, и о @$curframe.TTD.VariableHistory(), добавленном в 1.11.553.  2 3

  12. Microsoft Learn, TTD Calls Objects. Об аргументах TTD.Calls, свойствах ThreadId/UniqueThreadId/Function/ReturnValue/ReturnAddress/Parameters[]/TimeStart/TimeEnd/SystemTimeStart/SystemTimeEnd, значениях по умолчанию при отсутствии сведений символов PDB (четыре 64-разрядных беззнаковых целых аргумента, UnknownOrMissingSymbols) и о том, что вычисление занимает время, а результаты кэшируются.  2 3 4 5 6 7

  13. Microsoft Learn, Time Travel Debugging - Replay a trace. Об обратном исполнении через p-/t-/g-, о том, что g- останавливается на тех же событиях, что прямое исполнение, о !positions и о том, что ~s не меняет позицию в трассировке.  2 3

  14. Microsoft Learn, TTD Event Objects. О типах событий (ThreadCreated/ThreadTerminated/ModuleLoaded/ModuleUnloaded/Exception) и дочерних объектах Position, Module, Thread и Exception. 

  15. Microsoft Learn, TTD Exception Objects. О Type объекта исключения (Software/Hardware), ProgramCounter, Code, Flags и Position

  16. Microsoft Learn, WinDbg: Timelines. Об окне Timelines, которое визуализирует исключения, точки останова, обращения к памяти и вызовы функций, и о том, что двойной щелчок по исключению выдаёт Position.SeekTo()

  17. Microsoft Learn, !positions. Об отображении каждого активного потока и позиции каждого потока в трассировке.  2

  18. Microsoft Learn, Introduction to Time Travel Debugging objects. Об объектах @$curprocess.TTD / @$cursession.TTD, запросах с OrderBy/Where/Select/GroupBy, примерах агрегации ошибок GetLastError и поиска последнего вызова MessageBoxW, смысле UnknownOrMissingSymbols и четырёх причинах, по которым Calls ничего не возвращает.  2 3 4 5

  19. Microsoft Learn, TTD Memory Objects. О типах доступа TTD.Memory (r/w/rw/e/rwe/ec) и о перемещении к позиции через ссылку [Time Travel] в результатах. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Чем Time Travel Debugging (TTD) отличается от аварийного дампа?
Аварийный дамп — фотография памяти в момент сбоя: показывает состояние в этот миг, но не путь, который к нему привёл. Трассировка TTD — полная запись исполнения инструкций процесса, которую позже можно воспроизвести вперёд и назад, поэтому можно перемотать и напрямую проверить, кто последним записал переменную или что вызывалось непосредственно перед исключением. Официальная документация Microsoft также указывает, что дампы склонны упускать состояние и путь исполнения, которые привели к сбою. Цена — процесс во время записи работает в 5–20 раз медленнее, а файл трассировки растёт примерно на 5–50 МБ в секунду, пока процесс активен.
Можно ли оставить TTD прикреплённым к производственному приложению, которое работает днями?
Как есть — нет. TTD сильно замедляет процесс во время записи, трассировка активного процесса растёт на 5–50 МБ в секунду, и потолка размера файла нет. Использование на долгоживущем приложении предполагает проект записи: кольцевой буфер TTD.exe -ring с -maxFile, чтобы оставлять только последние N МБ, -module, чтобы записывать только пока работает свой модуль, или -recordmode Manual, чтобы приложение задавало интервал записи. Также, однажды прикрепившись, TTD сам себя не отсоединяет, поэтому заранее нужно решить, как закончить запись, а это значит завершить (перезапустить) процесс.
Можно ли записывать службы и процессы в других сеансах?
Документация TTD.exe описывает -attach как предназначенный для расследования служб и долгоживущих приложений, а -monitor — как запись каждый раз, когда запускается программа или служба. С другой стороны, страница устранения неполадок указывает, что необычные процессы, работающие в другом сеансе или другом контексте безопасности, в настоящее время не поддерживаются для записи. Сработает ли на самом деле, зависит от среды, поэтому сначала запишите простой процесс вроде ping.exe или cmd.exe в той же конфигурации, что производство, затем попробуйте целевой процесс и только потом встраивайте в эксплуатацию.
Можно ли использовать TTD на трассировках приложений .NET?
Да. Официальная документация указывает, что расширение SOS (sos.dll), работающее в 64-битном режиме, можно использовать на трассировке TTD в WinDbg для отладки управляемого кода. Базовый шаблон — выполнять команды SOS вроде !clrstack и !pe на каждой позиции трассировки, переходя к позиции события исключения и затем читая управляемый стек. Запрос TTD.Calls, который ищет вызовы по имени символа, опирается на сведения символов PDB, поэтому в приложениях .NET надёжное использование — следить за вызовами через нативные границы вроде P/Invoke, COM и Win32 API.
Безопасно ли отправлять файл трассировки (.run) другой компании или за пределы организации?
Как есть — нет. Запись TTD содержит содержимое памяти процесса, и официальная документация прямо указывает, что туда могут входить персональные или конфиденциальные сведения вроде путей файлов, данных реестра и содержимого памяти и файлов. Если отправляете, сначала поймите, что записано (строки подключения, токены, данные клиентов и так далее), затем решите зашифрованный канал передачи и место хранения. Достаточно делиться только файлом .run; индексный файл (.idx) генерируется автоматически, когда WinDbg его открывает.
TTD.Calls ничего не возвращает, когда ищу функцию. Почему?
Есть четыре основные причины. Первая — символы: функции модуля без PDB называются UnknownOrMissingSymbols, и имя модуля может быть в верхнем регистре, поэтому проверьте фактическое имя символа командой x. Вторая — целевая DLL на этой позиции ещё может быть не загружена; перейдите к позиции после загрузки DLL и запросите снова. Третья — если функция встроена (inlined), движок запроса не может её отследить. Четвёртая — шаблон может быть слишком широким и совпадать со слишком многими функциями; сузьте его.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог