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 — что поедает кучу. Чего он не показывает — что произошло по пути к этому состоянию.
flowchart TB
accTitle: Что фиксирует дамп и что фиксирует TTD
accDescr: Аварийный дамп фиксирует только состояние в момент сбоя, а не путь, который к нему привёл. TTD сохраняет всё исполнение инструкций от начала записи до конца, поэтому путь сохраняется вместе с состоянием
dump["Аварийный дамп: состояние в момент сбоя"] --> q1["Не показывает, почему значение стало таким"]
ttd["Трассировка TTD: исполнение инструкций записанного интервала"] --> q2["Можно вернуться к позиции, где значение записали"]
q1 -.-> gap["Этот разрыв и затягивает расследования долгоживущих приложений"]
Рис. 1: Дамп — это состояние, TTD — путь. В ошибках долгоживущих приложений обычно нужен второй.
Типичные случаи выглядят так.
- Одно поле структуры в куче держит невозможное значение. Дамп показывает испорченное значение, но не кто его записал и когда.
- Место исключения известно, но не ясно, почему переданный аргумент был неверным. Поднимаясь выше по вызывающим, функция, которая по пути произвела значение, уже нет на стеке.
- Хендлы или память растут в течение месяца. Один дамп может сказать только «растёт»; пути вызовов, который это вырастил, нет.
flowchart TB
accTitle: Что дамп упускает в типичных долгоживущих случаях
accDescr: Испорченное значение зафиксировано, но не кто его записал; позиция исключения зафиксирована, но функции, которая произвела аргумент, уже нет на стеке; рост ресурса зафиксирован, но не путь, который его вырастил; три вида случаев разделяют это
c1["Испорченное поле"] --> m1["Нет записи, кто и когда записал"]
c2["Исключение на неверном аргументе"] --> m2["Функции, которая произвела значение, уже нет на стеке"]
c3["Ресурс, который растёт месяц"] --> m3["Нет записи пути вызовов, который его вырастил"]
m1 --> same["Общее: состояние есть, пути нет"]
m2 --> same
m3 --> same
Рис. 2: Во всех трёх случаях «состояние есть, пути нет». Больше фотографий путь не заполняют.
Документация Microsoft сравнивает сильные и слабые стороны методов расследования так.1
| Метод | Сильные стороны | Слабые стороны |
|---|---|---|
| Живая отладка | Интерактивна, видно поток исполнения, можно менять состояние | Останавливает работу пользователя. Трудно воспроизводить снова и снова. Часто неприменима в производстве. Сложно идти от точки отказа к причине |
| Дампы | Не требуют заранее менять код. Низкая инвазивность, можно собирать по триггеру. Почти нулевые накладные расходы, пока не используются | Даже с последовательными снимками вид «прошедшего времени» грубый |
| Телеметрия и журналы | Лёгкие. Привязаны к бизнес-сценариям | Нет журналов на непредвиденных путях кода. Недостаточная глубина данных, статически встроены в код |
| TTD | Силён на сложных ошибках. Не требует заранее менять код. Можно воспроизводить офлайн сколько угодно раз и записывает всё | Большие накладные расходы во время записи. Может собрать больше данных, чем нужно. Файлы становятся большими |
Есть ещё одно важное свойство, на которое указывает официальный обзор TTD. Когда отладчик останавливается в точке отказа, эта точка часто находится внутри кода обработки ошибок на несколько шагов дальше настоящей причины.5 Дамп всегда снимается в этой позиции «на несколько шагов позже». С TTD оттуда можно шагнуть назад по одной инструкции.
flowchart TB
accTitle: Разрыв между точкой отказа и настоящей причиной
accDescr: Точка отказа, где снимается дамп, часто находится внутри обработки ошибок на несколько шагов дальше настоящей причины, и с TTD можно перемотать от этой точки инструкция за инструкцией обратно к причине
cause["Настоящая причина (инструкция, которая испортила значение)"] --> steps["Несколько шагов вперёд"]
steps --> fail["Точка отказа (исключение, обработка ошибок)"]
fail -->|"Дамп"| photo["Здесь фиксируется"]
fail -->|"TTD"| back["Назад через p- / t- / g-"]
back --> cause
Рис. 3: Дамп заморожен в точке отказа. TTD может пройти от точки отказа обратно к причине.
3. Как устроен TTD и чем приходится платить
3.1 Что записывается
TTD внедряет движок записи в целевой процесс и записывает исполненные инструкции, инструкция за инструкцией. Словами официальной документации, он «кодирует полную трассировку на уровне инструкций в среднем менее чем в один байт на инструкцию»; на практике это попадает в диапазон от одного бита до одного байта на инструкцию. Программы, которые исполняют мало видов функций и обрабатывают мало данных, дают меньшие трассировки; наоборот — большие.24
Запись даёт два файла.1
| Файл | Роль | Примерный размер |
|---|---|---|
.run |
Сама трассировка. Хранит исполнение инструкций во время записи | Растёт на 5–50 МБ в секунду, пока процесс активен. Не растёт в простое4 |
.idx |
Индекс. Вспомогательные данные, которые позволяют WinDbg эффективно воспроизводить и запрашивать память. Создаётся при остановке записи и также генерируется автоматически, когда WinDbg открывает файл .run |
В 1–2 раза больше трассировки4 |
flowchart TB
accTitle: Поток от записи TTD к воспроизведению
accDescr: Движок записи внедряется в целевой процесс и исполнение инструкций записывается в файл .run; когда WinDbg открывает .run, он создаёт индекс .idx и воспроизводит вперёд и назад через позиции, события и запросы
proc["Целевой процесс"] --> inj["Внедрение движка записи (TTDRecordCPU)"]
inj --> run[".run (запись исполнения инструкций)"]
run --> open["Открыть в WinDbg"]
open --> idx["Сгенерировать .idx (индекс)"]
idx --> play["Воспроизвести вперёд и назад через позиции, события и запросы"]
Рис. 4: Записывают TTD.exe или WinDbg; читает WinDbg. Достаточно делиться только файлом .run.
Время внутри файла .run выражается как «позиция» (position). Форма — два шестнадцатеричных числа, разделённых двоеточием, например 12:0 или 1A0:12F; первая половина — номер секвенирования (соответствует событию секвенирования), вторая — приблизительное число инструкций с этого события.6 FFFFFFFFFFFFFFFE:0 означает конец трассировки.7 Позиции выходят на первый план в главе 6.
flowchart TB
accTitle: Как выражаются позиции в трассировке
accDescr: Позиция — шестнадцатеричный номер секвенирования и счётчик шагов, разделённые двоеточием; начало около 0, конец выражается как FFFFFFFFFFFFFFFE:0, и можно также перейти к приблизительной позиции по проценту
pos["Позиция xx:yy (шестнадцатеричная)"] --> seq["xx: номер секвенирования"]
pos --> step["yy: инструкции с этого события"]
seq --> tail["Конец — FFFFFFFFFFFFFFFE:0"]
step --> pct["Проценты вроде !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 |
flowchart TB
accTitle: Цена записи и её влияние на долгоживущие приложения
accDescr: Замедление во время записи, рост файла, тихое ожидание при исчерпании диска и невозможность отсоединиться после прикрепления — четыре цены, из-за которых нельзя безусловно прикрепить TTD к долгоживущему приложению
cost["Запись TTD"] --> slow["В 5–20 раз медленнее"]
cost --> grow["Растёт на 5–50 МБ в секунду, без потолка"]
cost --> disk["Тихо ждёт, когда диск исчерпан"]
cost --> stuck["После прикрепления нельзя отсоединиться"]
slow --> no["Безусловная постоянная запись невозможна"]
grow --> no
disk --> no
stuck --> no
Рис. 6: Любая из четырёх цен сама по себе исключает постоянную запись. Поэтому нужно «проектирование записи» главы 5.
3.3 Чего оно не умеет
- Только пользовательский режим. Записывается только пользовательское исполнение процесса; код, который исполняется в режиме ядра, например драйверы, отладить нельзя.3
- Защищённые процессы. TTD не может внедриться в защищённые процессы Windows вроде Protected Process Light (PPL).3
- Воспроизведение только для чтения. Можно вернуться в прошлое, но историю изменить нельзя. Команды, которые читают память, работают; команды, которые её меняют, — нет.3
- Несовместимость с антивирусом и ПО мониторинга памяти. Поскольку TTD ставит хуки в процесс, он конфликтует с ПО, которое отслеживает или зеркалирует системные вызовы к памяти. Если запись даёт ошибку, похожую на недостаток прав, временно отключите такое ПО, чтобы локализовать проблему. Фреймворк Electron — известный конфликт; даже если запись удалась, целевой процесс может зайти в взаимную блокировку или упасть.3
- Приложения UWP нельзя запустить и записать (прикрепиться к уже работающему приложению UWP можно). «Необычные процессы», работающие в другом сеансе или другом контексте безопасности, сейчас тоже не поддерживаются.8
flowchart TB
accTitle: Что TTD не может записать или сделать
accDescr: Код режима ядра, защищённые процессы, запись запуска приложений UWP и процессы в другом сеансе или контексте безопасности записать нельзя; во время воспроизведения нельзя менять память; антивирус и Electron могут конфликтовать
no["Ограничения TTD"] --> g1["Нельзя записать"]
no --> g2["Ограничения и конфликты"]
g1 --> k["Код режима ядра (драйверы и т. д.)"]
k --> ppl["Защищённые процессы (PPL)"]
ppl --> uwp["Запись запуска UWP (прикрепление возможно)"]
uwp --> sess["Другие сеансы и контексты безопасности"]
g2 --> ro["Воспроизведение только для чтения"]
ro --> av["Может конфликтовать с антивирусом и 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
flowchart TB
accTitle: Поток записи из UI WinDbg
accDescr: В WinDbg, запущенном от имени администратора, выберите Launch executable (advanced) или Attach to process, отметьте Record with Time Travel Debugging, задайте место сохранения и фильтр модулей в Configure and Record, пройдите диалог записи, и когда приложение завершится, трассировка закроется и проиндексируется автоматически
adm["Запустить WinDbg от имени администратора"] --> pick["Launch executable (advanced) / Attach to process"]
pick --> chk["Отметить Record with Time Travel Debugging"]
chk --> cfg["Configure and Record: место сохранения, фильтр модулей"]
cfg --> rec["Диалог записи (Stop and Debug)"]
rec --> fin["Приложение завершается, трассировка закрывается и индексируется автоматически"]
Рис. 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
flowchart TB
accTitle: Три режима записи TTD.exe
accDescr: launch запускает новый процесс с аргументами и записывает его, но он работает с повышенными правами. attach прикрепляется к работающему процессу по PID. monitor записывает каждый раз, когда стартует указанная программа, и запуск идёт с обычными правами
m["Режимы записи TTD.exe"] --> l["-launch: запустить и записать"]
m --> a["-attach: прикрепиться к работающему PID"]
m --> mo["-monitor: записывать каждый запуск"]
l -.-> lp["Запускается с правами администратора"]
a -.-> ap["Сохраняет обычные права"]
mo -.-> mp["Обычный путь запуска, подходит для автоматизации"]
Рис. 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 даёт четыре средства; выбирайте по характеру симптома.
flowchart TB
accTitle: Выбор охвата записи по симптому долгоживущего приложения
accDescr: Если неизвестно, когда случится, оставляйте только конец кольцевым буфером; если подозреваемый модуль ясен, записывайте только его; если приложение можно менять, задайте интервал API ручной записи; если проявляется только при запуске или на отдельных запусках, записывайте каждый запуск режимом монитора
q{"Характер симптома?"} -->|"Неизвестно, когда случится"| ring["-ring / -maxFile: оставить только конец"]
q -->|"Только при запуске или на отдельных запусках"| mon["-monitor: записывать каждый запуск"]
ring -->|"Подозреваемый модуль ясен"| mod["Добавить -module"]
ring -->|"Приложение можно менять"| man["Задать интервал через -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
flowchart TB
accTitle: Временная шкала записи в кольцевой буфер
accDescr: Запись начинается при прикреплении, старые части выталкиваются из кольцевого буфера, и когда появляется симптом и выдаётся stop, остаётся только объём последних maxFile как трассировка
s["Начать запись через -attach -ring"] --> old["Старые интервалы выталкиваются"]
old --> sym["Появляется симптом"]
sym --> stop["Остановить запись через -stop"]
stop --> keep["Остаётся только объём последних maxFile"]
keep -.-> note["Размерьте так, чтобы моменты до симптома влезли в буфер"]
Рис. 11: Кольцевой буфер — устройство, которое хранит «моменты до симптома». -maxFile считайте назад от «времени от обнаружения симптома до остановки, умноженного на рост в секунду».
Два пункта проектирования.
- Сделайте буфер достаточно большим, чтобы поглотить время от обнаружения до остановки. В активном процессе трассировка растёт на 5–50 МБ в секунду,4 поэтому кольцо 4 ГБ соответствует одной-двум минутам при высокой нагрузке и чуть больше десяти минут при низкой. Нужен механизм, который удерживает лаг между обнаружением симптома (конкретная строка журнала, порог счётчика, оповещение мониторинга) и
-stopвнутри этого окна. - Остановка не отсоединяет.
-stopостанавливает запись, но TTD сам не отсоединяется от целевого процесса.2 Чтобы полностью закончить запись, нужно завершить процесс, поэтому включите в эксплуатацию «перезапустить в следующее окно обслуживания после сбора трассировки».
sequenceDiagram
accTitle: Остановка записи в кольцевой буфер в связке с мониторингом
accDescr: Когда сторона мониторинга обнаруживает симптом через журналы или счётчики, она вызывает TTD.exe stop, забирает готовый файл .run и перезапускает процесс в следующее окно обслуживания, чтобы снять TTD
participant W as Мониторинг (журналы, счётчики)
participant T as TTD.exe
participant P as Целевой процесс
W->>W: Обнаружить симптом
W->>T: -stop PID
T->>P: Остановить запись (процесс продолжается)
T-->>W: .run готов
W->>W: Забрать .run, зашифровать и сохранить
W->>P: Перезапустить в следующее окно обслуживания
Рис. 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 и внутренняя обработка фреймворка обычно дают большую часть исполненных инструкций, поэтому ограничение записи своим модулем само по себе заметно снижает накладные расходы и размер файла.
flowchart TB
accTitle: Как ведёт себя запись с фильтром модуля
accDescr: Целевой процесс работает на полной скорости вне указанного модуля; запись начинается, когда входят в код указанного модуля, продолжается, пока этот модуль вызывает другие модули, и останавливается, когда исполнение покидает модуль
out1["Вне указанного модуля: полная скорость"] --> in1["Вход в указанный модуль: запись начинается"]
in1 --> callee["Другие модули, которые он вызывает: запись продолжается"]
callee --> out2["Выход из указанного модуля: запись останавливается"]
out2 --> out1
Рис. 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
flowchart TB
accTitle: Как ведёт себя режим монитора
accDescr: Параметр monitor ставит драйвер мониторинга запуска процессов, каждый раз при старте указанной программы сужает цель фильтром командной строки, записывает её, создаёт отдельный файл трассировки на запуск и продолжается до Ctrl+C или перезагрузки
drv["Установить драйвер мониторинга запуска"] --> launch["Обнаружить запуск указанной программы"]
launch --> filt{"Совпадает с -cmdLineFilter?"}
filt -->|"Да"| rec["Записать этот запуск (отдельный файл на запуск)"]
filt -->|"Нет"| skip["Не записывать"]
rec --> next["Ждать следующего запуска (до Ctrl+C или перезагрузки)"]
skip --> next
Рис. 14: Режим монитора «подстерегает запуск». Он подходит ошибкам, которые проявляются на каждом запуске, и ошибкам, которые можно сузить условиями запуска.
5.5 Куда класть диск
Для долгоживущих записей кладите трассировки на выделенный том и включите рост файла .run в мониторинг. Как отмечено в разделе 3.2, когда диск заполняется, запись просто тихо ждёт без ошибки. Официальный обходной путь столь же примитивен: «смотреть свободное место в Проводнике» и «проверять, что файл .run регулярно растёт».4 Без мониторинга свободного места вы получаете неполную трассировку, в которой самый нужный момент так и не был записан.
flowchart TB
accTitle: Как исчерпание диска даёт неполную трассировку
accDescr: Когда диск заканчивается во время записи, TTD пишет последнюю страницу и тихо ждёт без ошибки и предупреждения, поэтому симптом, который случается после, не записывается, оставляя неполную трассировку, которая открывается, но без ключевой части. Предотвращайте выделенным томом и мониторингом роста .run
full["Диск заканчивается"] --> wait["Пишет последнюю страницу и тихо ждёт"]
wait --> none["Ни ошибки, ни предупреждения"]
none --> sym["После этого случается симптом"]
sym --> inc["Неполная трассировка без симптома"]
guard["Выделенный том + мониторинг роста .run"] -.->|"предотвращает"| full
Рис. 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, спотыкаетесь здесь.
flowchart TB
accTitle: Проверка и пересборка индекса
accDescr: После открытия трассировки проверьте состояние через !index -status; если это что угодно кроме Index file loaded, пересоберите через !index -force, и если это всё ещё не удаётся, закройте отладчик, удалите файл .idx и снова откройте .run. Пересборка не меняет файл .run
open["Открыть трассировку"] --> st["!index -status"]
st -->|"Index file loaded"| ok["Перейти к анализу"]
st -->|"Что угодно другое"| force["Пересобрать через !index -force"]
force -->|"Неудача"| del["Закрыть, удалить .idx, снова открыть .run"]
del --> ok
force -->|"Успех"| ok
Рис. 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
flowchart TB
accTitle: Сопоставление позиций с метками времени журнала
accDescr: От метки времени в журнале приложения пройдите приблизительное настенное время объекта Position, чтобы найти позицию в трассировке, переместитесь туда через SeekTo и прочитайте исполнение вокруг неё
log["Журнал приложения: время аномалии"] --> match["Найти позицию, чей ToSystemTime близок"]
match --> seek["Переместиться туда через SeekTo"]
seek --> read["Прочитать окружающие вызовы и значения"]
Рис. 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.
flowchart TB
accTitle: Выбор среди обратных команд
accDescr: p- идёт назад на один шаг через вызовы функций, t- идёт назад на одну инструкцию внутрь функций, а g- идёт сразу назад до точки останова, события или начала трассировки. Что останавливает прямой g, останавливает и g-
cur["Текущая позиция"] -->|"p-"| over["Назад на один шаг через вызовы"]
cur -->|"t-"| into["Назад на одну инструкцию внутрь функций"]
cur -->|"g-"| run["Сразу назад до следующего условия остановки"]
run -.-> stop["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
flowchart TB
accTitle: Базовый путь через воспроизведение
accDescr: Откройте трассировку и постройте индекс, от списка событий перейдите к позиции исключения, шагайте назад к причине и при необходимости переместитесь к позиции другого потока через positions
open["Открыть трассировку и проиндексировать"] --> ev["Найти исключение в TTD.Events"]
ev --> seek["Переместиться к позиции через [Time Travel]"]
seek --> back["Назад через t- / p- / g-"]
back --> th["Проверить позиции других потоков через !positions"]
th -.-> caution["~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
flowchart TB
accTitle: Сборка запроса TTD.Calls
accDescr: Соберите вызовы по имени функции, отфильтруйте по возвращаемому значению или аргументам, агрегируйте по коду ошибки и тому подобному, отсортируйте по времени и переместитесь к позиции нужного вызова через ссылку Time Travel
calls["TTD.Calls (имя функции, подстановочные знаки)"] --> where["Where: фильтр по возвращаемому значению или аргументам"]
where --> group["GroupBy: агрегировать по коду ошибки и т. д."]
group --> order["OrderBy: сортировать по времени"]
order --> jump["Переместиться через [Time Travel] на TimeStart"]
Рис. 20: Запросы используют так: начинать не с «где», а с «когда, сколько раз и с какими аргументами».
Слово о символах. TTD определяет число и типы аргументов функции, тип возвращаемого значения и соглашение о вызове из сведений символов PDB. С закрытыми символами (private symbols) вы получаете имя функции и верные аргументы. Только с открытыми символами (public symbols) — имя функции и аргументы по умолчанию (четыре 64-разрядных беззнаковых целых). Модуль без символов вовсе получает имя функции UnknownOrMissingSymbols.1218 О видах PDB и о том, как их хранить, см. «Что такое PDB (Program Database) — отладочная информация, символы и Source Link».
flowchart TB
accTitle: Наличие символов и результаты TTD.Calls
accDescr: С закрытыми символами получаете имя функции и верные аргументы; только с открытыми символами — имя функции и четыре 64-разрядных целых аргумента по умолчанию; без символов имя функции становится UnknownOrMissingSymbols
sym{"Символы модуля?"} -->|"private symbols"| full["Имя функции + верные аргументы и возвращаемое значение"]
sym -->|"public symbols"| pub["Имя функции + аргументы по умолчанию (4 x 64-разрядных целых)"]
sym -->|"нет"| unk["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
- Перейдите к позиции исключения через
TTD.Events - Шагайте назад через
t-и предположите, какая переменная держит испорченное значение - Возьмите адрес этой переменной через
dx &variable - Поставьте точку останова на запись через
ba w4 <address> - Выполните
g-, чтобы сразу вернуться к позиции, где эту переменную записали последней - Проверьте, является ли эта точка (или несколько инструкций раньше) причиной. Если записанное значение пришло из другой переменной, поставьте
baна ту переменную и снова выполнитеg- - Повторяйте, пока не дойдёте до инструкции, которая испортила значение
flowchart TB
accTitle: Прослеживание происхождения значения через ba и g-
accDescr: Поставьте точку останова на запись по адресу испорченного значения и исполняйте в обратную сторону, остановитесь на инструкции, которая записала его последней, и если это значение пришло из другой переменной, повторяйте те же шаги, пока не дойдёте до инструкции, которая испортила значение
bad["Определить адрес испорченного значения"] --> ba["Поставить точку останова на запись через ba w"]
ba --> gb["Исполнять в обратную сторону через g-"]
gb --> writer["Остановиться на инструкции, которая записала последней"]
writer --> q{"Значение пришло из другой переменной?"}
q -->|"Да"| bad
q -->|"Нет"| found["Инструкция, которая испортила = причина"]
Рис. 22: Расследование, которое на дампе заканчивается на «испорчено», с TTD механически идёт до «кто испортил».
В TTD 1.11.553 и новее также добавлен @$curframe.TTD.VariableHistory(), который возвращает историю значений локальных переменных кадра. Он показывает таблицу имён переменных и какие значения каждая переменная держала на каких диапазонах позиций.11 В ситуациях вроде порчи стека, где нужно знать «с какого момента значение неверно», это полезно, чтобы сузить, куда сначала ставить ba.
8. Применить к ошибкам долгоживущих приложений
Теперь применим эти инструменты к трём видам случаев, перечисленным в начале.
flowchart TB
accTitle: Типы ошибок долгоживущих приложений и точка входа TTD для каждого
accDescr: Для прерывистых исключений и порчи данных идите назад от события через ba и g-; для роста ресурсов сопоставляйте вызовы получения и освобождения через TTD.Calls; для «не отвечает» и цепочек ожиданий следите за позициями и настенным временем вызовов API ожидания
t1["Прерывистые исключения, порча данных"] --> a1["TTD.Events, затем ba + g-"]
t2["Рост хендлов и памяти"] --> a2["Сопоставить получение и освобождение через TTD.Calls"]
t3["Не отвечает, цепочки ожиданий"] --> a3["!positions и настенное время API ожидания"]
a2 -.-> pre["Записывать через -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.
flowchart TB
accTitle: Настройка чтения трассировки TTD приложения .NET
accDescr: Откройте трассировку TTD в WinDbg, загрузите 64-разрядное расширение SOS, переместитесь к позиции события исключения и прочитайте управляемое состояние через !clrstack и !pe. Используйте TTD.Calls для вызовов через нативные границы
run["Трассировка TTD (.run)"] --> wd["WinDbg"]
wd --> sos["Расширение SOS (64-разрядное)"]
sos --> ev["Переместиться к позиции исключения через TTD.Events"]
ev --> clr["Читать через !clrstack / !pe"]
wd -.-> calls["TTD.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 | — |
flowchart TB
accTitle: Выбор метода расследования
accDescr: Сначала схватите состояние лёгкими дампами и журналами, сбойные API разбирайте ProcMon, производительность — ETW, и переходите к TTD только когда всё ещё нужно, кто, когда и что передал
start["Симптом"] --> light["Схватить состояние дампами и журналами (легко)"]
light --> api["Сбойные API: ProcMon"]
light --> perf["Производительность: WPR/WPA, PerfView"]
light --> need{"Нужно, кто, когда и что передал?"}
need -->|"Да"| ttd["Спроектировать охват записи и записать через TTD"]
need -->|"Нет"| done["Разобрались лёгкими инструментами"]
Рис. 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
flowchart TB
accTitle: Процедура обращения с трассировками
accDescr: Поскольку записанная трассировка содержит содержимое памяти, поймите, какие сведения она может содержать, сожмите только файл .run и передайте через зашифрованный канал, решите место хранения и срок хранения, а на стороне анализа откройте той же версией WinDbg и постройте индекс
rec["Запись завершена (.run / .idx / .out)"] --> know["Понять, какие сведения могут содержаться"]
know --> share["Сжать только .run, зашифровать и передать"]
share --> keep["Решить место хранения и срок хранения"]
keep --> open["Открыть той же версией 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, поэтому обращайтесь со своими дампами и журналами.
Похожие статьи
- Как читать аварийные дампы в WinDbg + SOS — практика анализа после сбора
- Сбор дампов сбоев Windows — введение: WER, ProcDump, WinDbg
- Что такое PDB (Program Database) — отладочная информация, символы и Source Link
- Утечка хендлов: почему приложение промышленной камеры падает после месяца работы
- «Не отвечает»: как Windows определяет зависание и как проектировать так, чтобы оно не возникало
- Практическое руководство по Process Monitor (ProcMon) — за 10 минут находим «настройки не читаются» и ACCESS DENIED
- WPR/WPA на практике: как разбирать «весь ПК тормозит» по всей системе
- Как найти причину «тормозов» с PerfView и dotnet-trace — практика анализа производительности .NET
- Что остаётся после падения родителя ── дерево дочерних процессов в Job Object
Смежные области консультирования
KomuraSoft LLC занимается расследованием первопричин ошибок приложений Windows, которые проявляются только после долгой работы или прерывисто, сочетая аварийные дампы, журналы и трассировки TTD; построением постановки расследования, включая проектирование охвата записи; и локализацией сбоев, в которых задействованы нативные границы (COM, P/Invoke, SDK устройств). Обращайтесь уже на этапе «дамп есть, до причины не доходим».
- Расследование ошибок и анализ первопричин
- Техническое консультирование и ревью проектирования
- Разработка приложений Windows
- Контакты
Справочные ссылки
-
Microsoft Learn, Time Travel Debugging - Overview. О том, что TTD записывает исполнение процесса и воспроизводит его вперёд и назад, что дампы склонны упускать состояние и путь исполнения, которые привели к сбою, что для записи нужны права администратора, что записи могут содержать персональные и связанные с безопасностью сведения, о сравнительной таблице методов расследования, о ролях
.run/.idxи об отладке управляемого кода расширением SOS в 64-разрядном режиме. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
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 -
Microsoft Learn, Time Travel Debugging - Overview - Things to look out for. О несовместимости с антивирусом, ПО мониторинга памяти и Electron, о том, что это только пользовательский режим, что воспроизведение только для чтения, что нельзя внедриться в защищённые процессы (PPL), и о примерно 10–20-кратном влиянии на производительность во время записи. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Time Travel Debugging - Working with Trace Files. О факторах размера трассировки (от одного бита до одного байта на инструкцию), о росте на 5–50 МБ в секунду в активном состоянии и отсутствии роста в простое, об отсутствии потолка максимального размера, о том, что индекс в 1–2 раза больше трассировки, и о поведении записи и индексации при исчерпании диска вместе с обходным путём. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Time Travel Debugging - Sample App Walkthrough. Об общей процедуре перехода к позиции события исключения и возврата через
baиg-к позиции, где неверное значение записали последним, о том, что точка отказа часто находится внутри обработки ошибок на несколько шагов дальше настоящей причины, о закрытии трассировки при сбое и автоматической индексации WinDbg, и об использованииTTD.Memoryи.Last(). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Microsoft Learn, !tt (time travel). Об указании позиций для
!tt(процент илиxx:yy), о смысле двух компонентов позиции (номер секвенирования и счётчик шагов) и оTTD.PrevRegisterWrite/PrevMemoryAccess/NextMemoryAccess. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, TTD Position Objects. О свойствах объекта Position
Percent/Sequence/Steps, оSeekTo(), оToSystemTime(), возвращающем приблизительное настенное время (UTC), и о том, чтоFFFFFFFFFFFFFFFE:0обозначает конец трассировки. ↩ ↩2 -
Microsoft Learn, Time Travel Debugging - Troubleshooting. О необходимости повышения прав, о том, что запись запуска приложений UWP не поддерживается, что «необычные процессы» в другом сеансе или контексте безопасности вне области, о локализации через
ping.exe/cmd.exe, о замедлении воспроизведения при одновременном использовании Application Verifier и о пересборке индекса через!index -status/!index -force. ↩ ↩2 ↩3 ↩4 ↩5 -
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. ↩
-
Microsoft, WinDbg-Samples - TTD in-process recording API (GitHub). О документации внутрипроцессного API записи, который вместе с
-recordmode Manualпозволяет программе управлять началом и остановкой записи. ↩ -
Microsoft Learn, Time travel debugging release notes. Об исправлении записи программ, которые используют AVX/AVX512, и смене формата индекса (нужна переиндексация) в 1.11.611, и о
@$curframe.TTD.VariableHistory(), добавленном в 1.11.553. ↩ ↩2 ↩3 -
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 -
Microsoft Learn, Time Travel Debugging - Replay a trace. Об обратном исполнении через
p-/t-/g-, о том, чтоg-останавливается на тех же событиях, что прямое исполнение, о!positionsи о том, что~sне меняет позицию в трассировке. ↩ ↩2 ↩3 -
Microsoft Learn, TTD Event Objects. О типах событий (ThreadCreated/ThreadTerminated/ModuleLoaded/ModuleUnloaded/Exception) и дочерних объектах Position, Module, Thread и Exception. ↩
-
Microsoft Learn, TTD Exception Objects. О
Typeобъекта исключения (Software/Hardware),ProgramCounter,Code,FlagsиPosition. ↩ -
Microsoft Learn, WinDbg: Timelines. Об окне Timelines, которое визуализирует исключения, точки останова, обращения к памяти и вызовы функций, и о том, что двойной щелчок по исключению выдаёт
Position.SeekTo(). ↩ -
Microsoft Learn, !positions. Об отображении каждого активного потока и позиции каждого потока в трассировке. ↩ ↩2
-
Microsoft Learn, Introduction to Time Travel Debugging objects. Об объектах
@$curprocess.TTD/@$cursession.TTD, запросах сOrderBy/Where/Select/GroupBy, примерах агрегации ошибокGetLastErrorи поиска последнего вызоваMessageBoxW, смыслеUnknownOrMissingSymbolsи четырёх причинах, по которымCallsничего не возвращает. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, TTD Memory Objects. О типах доступа
TTD.Memory(r/w/rw/e/rwe/ec) и о перемещении к позиции через ссылку[Time Travel]в результатах. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как читать аварийные дампы в WinDbg + SOS — практика анализа после сбора
Разбираем, как читать аварийный дамп Windows в WinDbg с расширением SOS: настройка пути к символам, поиск исключений и утечек через !clrs...
Практическое руководство по Process Monitor (ProcMon) — за 10 минут находим «настройки не читаются» и ACCESS DENIED
Неисправности вроде «настройки поменял, а эффекта нет» разбирают в Process Monitor (ProcMon) по реальным обращениям к файлам и реестру. В...
Что продумать перед заказом разработки Windows-приложения
Перед заказом разработки Windows-приложения разберём, что стоит прояснить: доработка существующего ПО, интеграция с оборудованием, COM/Ac...
Почему ломаются аргументы ── правила аргументов командной строки Windows
В Windows массива аргументов нет: в CreateProcess уходит одна строка, делит её принимающая сторона. Правила деления CommandLineToArgvW, C...
Окончание драйверов принтера Windows ── как готовить печать форм и этикеток в бизнес-приложениях
Microsoft поэтапно прекращает сопровождение драйверов принтера v3/v4; с июля 2026 IPP class driver предпочтут. Что исчезает в Windows pro...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Чем 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.