Time Travel Debugging ── 장기 가동에서 재현되지 않는 결함을 「녹화」해서 되감기

· 업데이트: · · WinDbg, Time Travel Debugging, 디버깅, 결함 조사, Windows, Windows 개발, .NET, C++, 기술 상담

「한 달에 한 번, 심야에만 죽는다」 「같은 조작을 해도 내 환경에서는 두 번 다시 나오지 않는다」 「덤프는 받았지만 죽은 자리를 봐도 『왜 그 값이 되었는지』를 알 수 없다」 ── 장기 가동하는 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~50MB 커지며 상한이 없습니다.24 장기 가동하는 애플리케이션에 무조건 붙여 두는 도구가 아닙니다.
  • 장기 가동에 쓰려면 「녹화할 범위」를 설계합니다.TTD.exe의 -ring/-maxFile(마지막 N MB만 남기기), -module(자사 모듈이 실행되는 동안만 녹화), -recordmode Manual(애플리케이션 쪽에서 녹화 구간을 지정), -monitor(시작될 때마다 녹화)라는 네 개의 입구를 제5장에서 정리합니다.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

수단 장점 단점
라이브 디버깅 대화형이고 실행의 흐름이 보이며 상태를 바꿀 수 있다 사용자의 작업을 멈춘다. 재현을 반복하는 수고. 운영에서는 쓸 수 없는 경우가 많다. 실패 지점에서 원인으로 거슬러 올라가기가 어렵다
덤프 사전 코드 변경이 필요 없다. 침습성이 낮고 트리거로 채취할 수 있다. 쓰지 않으면 오버헤드가 거의 0 연속 스냅숏이라도 「시간 경과」가 보이는 방식은 거칠다
텔레메트리와 로그 가볍다. 업무 시나리오와 연결된다 예상하지 못한 코드 경로에는 로그가 없다. 데이터의 깊이가 부족하고 코드에 정적으로 박힌다
TTD 복잡한 버그에 강하다. 사전 코드 변경이 필요 없다. 오프라인에서 몇 번이든 재생할 수 있고 전부를 기록한다 녹화 시의 오버헤드가 크다. 필요 이상의 데이터를 모으는 경우가 있다. 파일이 커진다

또 하나, TTD의 공식 워크스루가 지적하는 중요한 성질이 있습니다. 실패 지점에서 디버거가 멈췄을 때, 그곳은 진짜 원인에서 몇 단계 앞으로 나아간 오류 처리 코드 안인 경우가 많다.5 덤프는 반드시 이 「몇 단계 뒤」의 위치에서 찍힙니다. TTD라면 거기서 한 명령씩 돌아갈 수 있습니다.

실패 지점과 진짜 원인의 어긋남덤프가 찍히는 실패 지점은 진짜 원인에서 몇 단계 앞으로 나아간 오류 처리 안인 경우가 많고, TTD에서는 그 지점에서 명령 단위로 되감아 원인으로 거슬러 올라갈 수 있다덤프TTD진짜 원인(값을 깨뜨린 명령)몇 단계 앞으로 나아간다실패 지점(예외, 오류 처리)여기서 고정된다p- / t- / g- 로 되돌아간다

그림 3: 덤프는 실패 지점에 고정된다. TTD는 실패 지점에서 원인까지 걸어서 되돌아갈 수 있다.

3. TTD의 구조와 대가

3.1 무엇을 녹화하고 있는가

TTD는 대상 프로세스에 녹화 엔진을 주입해 실행된 명령을 명령 단위로 기록합니다. 공식 문서의 표현으로는 「완전한 명령 수준 트레이스를 평균 1명령당 1바이트 미만으로 부호화한다」는 것이며, 실제로는 1명령당 1비트에서 1바이트 범위에 들어갑니다. 실행하는 함수의 종류가 적고 다루는 데이터도 적은 프로그램일수록 작고, 반대라면 커집니다.24

녹화의 산출물은 둘입니다.1

파일 역할 크기의 기준
.run 트레이스 본체. 녹화 중의 명령 실행을 저장한다 활동 중에는 초당 5~50MB 증가. 유휴 중에는 커지지 않는다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처럼 16진수 둘을 콜론으로 구분한 형태이고, 앞부분이 시퀀싱 번호(시퀀싱 이벤트에 대응), 뒷부분이 그 이벤트에서부터의 대략적인 명령 수입니다.6 FFFFFFFFFFFFFFFE:0은 트레이스의 끝을 뜻합니다.7 위치는 제6장에서 주역이 됩니다.

트레이스 안 위치의 표기법위치는 16진수의 시퀀싱 번호와 스텝 수를 콜론으로 구분한 형태이고, 처음은 0 부근, 끝은 FFFFFFFFFFFFFFFE:0으로 표시되며, 퍼센트 지정으로 대략적인 위치로도 이동할 수 있다위치 xx:yy(16진수)xx: 시퀀싱 번호yy: 그 이벤트에서부터의 명령 수끝은 FFFFFFFFFFFFFFFE:0!tt 50 같은 퍼센트 지정도 가능

그림 5: 위치는 「이벤트 번호:명령 수」. 실제 시각은 아니지만 제6장대로 실제 시각으로 변환할 수 있다.

3.2 대가

TTD는 「침습적인 기술」이라고 공식 문서 자신이 쓰고 있습니다.2 대가를 숫자로 늘어놓습니다.

항목 내용
속도 녹화 중인 대상 프로세스는 5~20배 이상 느려진다(애플리케이션과 녹화 옵션에 따라 다름). UI에서 눈치채지 못하는 경우도 있지만, 파일을 여는 대화 상자 같은 무거운 처리에서는 체감할 수 있다23
파일 증가 활동 중에는 초당 5~50MB. 몇 분의 녹화로 수 GB가 될 수 있다. 상한은 설정되어 있지 않다4
디스크 소진 녹화 중에 디스크가 다하면 TTD는 마지막 페이지를 쓴 뒤 쓸 수 있게 될 때까지 사실상 기다린다. WinDbg는 녹화 대화 상자를 띄운 채로 오류도 경고도 내지 않는다. 완성되는 것은 불완전한 트레이스4
메모리 녹화는 대상 프로세스의 메모리에 가상 CPU 몫의 오버헤드를 얹는다(기본값으로 x64/ARM64는 55, x86은 32). 메모리가 부족할 때만 -numVCpu로 줄인다2
떨어지지 않음 한 번 연결하면 TTD는 스스로 떨어지지 않는다. 녹화를 마치면 애플리케이션을 닫거나 프로세스를 종료한다. 시스템에 필수인 프로세스라면 OS의 재시작이 필요하다2
녹화의 대가와 그것이 장기 가동에 주는 영향녹화 중의 속도 저하, 파일의 증가, 디스크 소진 시의 조용한 대기, 연결 후 떨어지지 않음이라는 네 가지 대가가 장기 가동 애플리케이션에 무조건 붙여 둘 수 없는 이유가 된다TTD의 녹화5~20배의 속도 저하초당 5~50MB의 증가, 상한 없음디스크 소진 시 조용히 기다린다연결 후에는 떨어지지 않는다무조건 상시 녹화는 불가

그림 6: 네 가지 대가 중 어느 하나만으로도 상시 녹화는 성립하지 않는다. 그래서 제5장의 「녹화할 범위의 설계」가 필요하다.

3.3 할 수 없는 일

  • 사용자 모드 전용.녹화할 수 있는 것은 프로세스의 사용자 모드 실행뿐이고, 드라이버 등 커널 모드에서 실행되는 코드는 디버그할 수 없습니다.3
  • 보호된 프로세스.Protected Process Light(PPL) 등 Windows의 보호 프로세스에는 TTD가 자신을 주입할 수 없습니다.3
  • 재생은 읽기 전용.과거로는 돌아갈 수 있지만 역사는 바꿀 수 없습니다. 메모리를 읽는 명령은 쓸 수 있지만, 바꿔 쓰는 명령은 쓸 수 없습니다.3
  • 백신이나 메모리 감시 계열 소프트웨어와의 비호환.TTD가 프로세스에 후킹하는 방식이기 때문에, 시스템 메모리 호출을 추적하고 섀도잉하는 소프트웨어와 충돌합니다. 녹화 시에 권한 부족 같은 오류가 나온다면 일시적으로 비활성화해 원인을 분리합니다. Electron 프레임워크도 알려진 충돌 사례이며, 녹화되더라도 대상 프로세스의 교착 상태나 크래시가 있을 수 있습니다.3
  • UWP 앱은 시작하면서 녹화할 수 없습니다(이미 동작 중인 UWP 앱에 연결하는 것은 가능). 다른 세션이나 다른 보안 컨텍스트에서 동작하는 「일반적이지 않은 프로세스」도 현재 시점에서는 대상 밖입니다.8
TTD로 녹화할 수 없는 것과 할 수 없는 일커널 모드의 코드, 보호된 프로세스, UWP 앱의 시작 녹화, 다른 세션이나 다른 보안 컨텍스트의 프로세스는 녹화할 수 없고, 재생 중에는 메모리를 바꿔 쓸 수 없으며, 백신이나 Electron과는 충돌할 수 있다TTD의 제약녹화할 수 없는 것제약과 충돌커널 모드의 코드(드라이버 등)보호된 프로세스(PPL)UWP의 시작 녹화(연결은 가능)다른 세션, 다른 컨텍스트재생은 읽기 전용백신, Electron과 충돌할 수 있다

그림 7: 제약은 「녹화할 수 없다」와 「충돌한다」의 두 종류. 후자는 환경마다 원인 분리가 필요하다.

마지막 항목은 서비스를 녹화하고 싶은 사람에게 걸립니다. TTD.exe의 문서는 -attach를 「서비스나 장시간 가동하는 애플리케이션의 조사」용으로, -monitor를 「프로그램이나 서비스가 시작될 때마다 녹화」하는 용도로 설명하고 있어2 문제 해결 페이지의 기술과 단순하게 맞물리지 않습니다. 실무에서는 운영과 같은 구성에서 ping.execmd.exe를 녹화할 수 있는지 확인하고, 다음으로 대상 프로세스에서 시도한다는 순서로 환경마다 확인하는 것이 안전합니다.8

4. 녹화하기 ── WinDbg UI와 TTD.exe

녹화의 입구는 둘입니다. WinDbg의 UI에서 녹화하거나, 명령줄의 TTD.exe로 녹화하거나입니다.

4.1 WinDbg UI에서 녹화하기

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

WinDbg UI에서의 녹화 흐름관리자로 실행한 WinDbg에서 Launch executable (advanced) 또는 Attach to process를 선택하고, Record with Time Travel Debugging에 체크하고, Configure and Record로 저장 위치와 모듈 한정을 설정하고, 녹화 중 대화 상자를 거쳐 애플리케이션 종료 시에 트레이스가 닫히고 자동으로 인덱스가 만들어진다WinDbg를 관리자로 실행Launch executable(advanced) / Attach to processRecord with Time Travel Debugging에 체크Configure and Record: 저장 위치와 모듈 한정녹화 중 대화 상자(Stop and Debug)애플리케이션 종료로 트레이스를 닫고 자동으로 인덱스

그림 8: UI에서의 녹화는 다섯 단계. 「관리자로 실행」을 건너뛰면 첫 대화 상자에서 멈춘다.

4.2 TTD.exe로 녹화하기

WinDbg를 넣을 수 없는 PC에서 녹화하거나 녹화를 자동화하는 장면에서는 TTD.exe 단독으로 사용합니다. https://aka.ms/ttd/download 에서 App Installer를 통해 설치하고, 설치 후에는 ttd.exe -help로 확인합니다. 오프라인 환경용으로는 MSIX 번들을 수동으로 펼쳐 바이너리만 꺼내는 절차(PowerShell 스크립트 포함)도 공식적으로 준비되어 있습니다.2 녹화에는 관리자 권한이 필요하고, 보통은 관리자 명령 프롬프트에서 실행합니다.2

녹화의 모드는 셋입니다.2

TTD.exe의 세 가지 녹화 모드launch는 인수를 넘겨 새 프로세스를 시작해 녹화하지만 상승된 권한으로 돌아간다. 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 "문자열"을 붙이면 명령줄에 그 문자열을 포함하는 시작만 녹화합니다.2
  • -children 을 붙이면 자식 프로세스도 녹화하지만, 프로세스마다 별도의 .run이 되고 WinDbg는 한 번에 하나만 열 수 있습니다.2

녹화 중에는 「Tracing Off」(녹화를 멈추고 애플리케이션은 계속)와 「Exit App」(애플리케이션을 닫고 녹화 종료) 두 버튼의 작은 UI가 나옵니다. 자동화에서는 -noUI로 없애고, EULA는 -accepteula로 수락합니다.2 녹화 로그는 .run과 같은 위치의 .out 파일에 남으며, 녹화 시작과 종료의 실제 시각, 녹화 세션의 길이(simulation time), 시작인지 연결인지, OS 버전을 읽을 수 있습니다. 녹화가 잘 되지 않을 때 .out에만 나오는 오류 메시지가 있습니다.2

5. 장기 가동을 위한 녹화 설계

여기가 이 글의 핵심입니다. 제3장의 대가를 전제로 하면, 「언제 일어날지 모르는 결함을 며칠씩 도는 프로세스에서 잡으려면」 녹화 범위의 설계가 필요합니다.TTD.exe가 마련한 수단은 넷이고, 증상의 성질로 고릅니다.

장기 가동의 증상에 따른 녹화 범위 고르기언제 일어날지 모르면 링 버퍼로 마지막 부분만 남기고, 의심스러운 모듈을 알고 있으면 그 모듈만 녹화하고, 애플리케이션을 고칠 수 있으면 수동 녹화 API로 구간을 지정하고, 시작 시나 특정 시작에서만 나오면 감시 모드로 시작될 때마다 녹화한다언제 일어날지 불명시작 시, 특정 시작만의심스러운 모듈이 명확애플리케이션을 고칠 수 있다증상의 성질은?-ring / -maxFile: 마지막만 남긴다-monitor: 시작될 때마다 녹화-module 을 겹친다-recordmode Manual 로 구간 지정

그림 10: 넷은 배타적이지 않다. -ring-module을 겹치는 것이 장기 가동에서는 가장 현실적인 조합이 된다.

5.1 링 버퍼 ── 마지막 N MB만 남기기

-ring을 붙이면 트레이스는 -maxFile로 지정한 크기의 링 버퍼에 쓰이고, 파일은 그 상한을 넘어 커지지 않습니다. 남는 것은 그 크기에 들어가는 녹화의 마지막 부분뿐입니다.2 -maxFile의 단위는 MB이며, 링 버퍼 모드의 기본값은 2,048MB, 최소 1MB, 최대 32,768MB입니다(32비트 프로세스의 인메모리 링은 기본값 256MB).2

:: 동작 중인 감시 애플리케이션에 붙어 최근 4GB 분의 실행만 남긴다
TTD.exe -accepteula -noUI -attach 21440 -ring -maxFile 4096 -out C:\traces\MyApp.run

:: 증상을 감지하면 녹화를 멈춘다(애플리케이션은 계속 돈다)
TTD.exe -stop 21440

-stop은 프로세스 이름, PID, all을 받아 그 녹화를 멈춥니다. -wait <초>는 시스템상의 모든 녹화 세션이 끝날 때까지 기다리며(-1이면 무기한), 자동화 스크립트에서 「멈춘 뒤 파일을 회수한다」는 순서를 만드는 데 사용합니다.2

링 버퍼 녹화의 타임라인연결해 녹화를 시작하면 오래된 부분은 링 버퍼에서 밀려나고, 증상이 나온 시점에 stop하면 최근 maxFile 분만 트레이스로 남는다-attach -ring 으로 녹화 시작오래된 구간은 밀려난다증상이 나온다-stop 으로 녹화 정지최근 maxFile 분만 남는다증상 직전이 버퍼에 들어가는 크기로

그림 11: 링 버퍼는 「증상 직전」을 남기는 장치. -maxFile은 「증상을 알아채고 멈출 때까지의 시간 × 초당 증가량」에서 역산한다.

설계상의 요점은 둘입니다.

  1. 버퍼는 「알아채고 멈출 때까지」를 흡수할 수 있는 크기로 한다.활동 중인 프로세스에서 초당 5~50MB 커지므로4 4GB의 링은 고부하 시에 1~2분, 저부하 시에 10분 남짓에 해당합니다. 증상의 감지(로그의 특정 행, 카운터의 임계값, 모니터링의 이상 통지)에서 -stop까지의 지연을 이 시간 안에 담는 구조가 필요합니다.
  2. 멈춰도 떨어지지 않는다.-stop으로 녹화는 멈추지만, TTD는 대상 프로세스에서 스스로 떨어지지 않습니다.2 녹화를 완전히 그만두려면 프로세스의 종료가 필요하므로, 「트레이스를 회수하면 다음 유지보수 시각에 재시작한다」는 데까지 운영에 포함해 둡니다.
모니터링과 연동한 링 버퍼 녹화의 정지모니터링 쪽이 로그나 카운터로 증상을 감지하면 TTD.exe의 stop을 호출하고, 확정된 .run 파일을 회수하고, 그 후의 유지보수 시각에 프로세스를 재시작해 TTD를 떼어낸다대상 프로세스TTD.exe모니터링(로그, 카운터)대상 프로세스TTD.exe모니터링(로그, 카운터)증상을 감지-stop PID녹화를 정지(프로세스는 계속).run 이 확정.run 을 회수하고 암호화해 보관다음 유지보수 시각에 재시작

그림 12: 감지와 -stop의 지연이 버퍼에 들어가야 비로소 증상 직전이 트레이스에 남는다. 재시작까지가 운영의 일부.

5.2 모듈 한정 ── 자사 코드가 동작하는 동안만 녹화

-module <모듈 이름>은 지정한 모듈(실행 파일 자신이어도 되고 로드되는 DLL이어도 되며 여러 개 지정 가능)과 그 모듈이 호출하는 코드만 녹화합니다. 대상 프로세스는 지정 모듈의 코드가 실행될 때까지 최고 속도로 돌고, 모듈에 들어가면 녹화가 시작되며, 모듈에서 나가면 녹화가 멈추고 다시 최고 속도로 돌아갑니다. 녹화의 켜고 끄기는 비용이 크므로, 지정 모듈이 프로세스 내의 다른 모듈을 호출하는 동안에는 녹화를 켜 둔 채로 유지합니다.2

:: 자사의 계측 로직 DLL이 동작하는 동안만 녹화(링과 병용)
TTD.exe -accepteula -noUI -attach 21440 -module MeasureCore.dll -ring -maxFile 2048 -out C:\traces\

이 방식의 트레이스는 녹화가 멈춰 있던 구간을 「다음 명령은 녹화 재개 후의 첫 명령」으로 건너뛸 뿐이며, 디버그하는 방법은 전체를 녹화한 트레이스와 다르지 않습니다.2 장기 가동하는 애플리케이션에서는 UI의 유휴 루프나 프레임워크의 내부 처리가 실행 명령의 대부분을 차지하기 쉬우므로, 자사 모듈로 한정하는 것만으로도 오버헤드와 파일 크기가 크게 줄어듭니다.

모듈 한정 녹화의 동작대상 프로세스는 지정 모듈 밖에서는 최고 속도로 돌고, 지정 모듈의 코드에 들어가면 녹화가 시작되며, 그 모듈이 부르는 다른 모듈 동안에도 녹화가 이어지고, 모듈에서 나가면 녹화가 멈춘다지정 모듈 밖: 최고 속도지정 모듈에 진입: 녹화 시작호출 대상인 다른 모듈: 녹화 지속지정 모듈에서 이탈: 녹화 정지

그림 13: 자사 DLL이 부르는 Win32 API나 런타임 내부까지 녹화되므로, 「자사 코드가 OS에 무엇을 넘겼는가」를 좇기에는 충분하다.

5.3 수동 녹화 ── 애플리케이션 쪽에서 구간을 지정한다

-recordmode Manual을 지정하면 TTD가 주입된 뒤에도 프로세스는 최고 속도로 돌고, 프로그램이 TTD의 인프로세스 녹화 API를 호출했을 때만 녹화합니다(기본값인 Automatic은 주입 직후부터 녹화). 2 API의 문서와 샘플은 GitHub의 WinDbg-Samples 리포지토리에 있습니다.10

애플리케이션을 고칠 수 있다면 이것이 가장 낭비가 적은 방식입니다. 「통신 재시도가 3회 연속으로 실패했다」 「큐의 적체가 임계값을 넘었다」처럼 애플리케이션 자신이 이상의 전조를 아는 장면에서 녹화를 시작하고, 정상으로 돌아오면 멈추는 형태로 넣을 수 있습니다. 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가 돌아, keyframe(인덱스용으로 자동 생성되는 트레이스 안의 위치. 큰 트레이스일수록 많다)을 세면서 .idx를 만듭니다. 큰 트레이스일수록 시간이 걸립니다.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을 다시 연다. 재생성은 .run을 변경하지 않는다Index 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 로

위치는 시퀀싱 번호:스텝 수의 16진수 두 쌍입니다.6 Position 객체에는 Percent(트레이스 안의 비율), Sequence, Steps 속성과 그 위치로 이동하는 SeekTo(), 그리고 대략적인 실제 시각(UTC)을 반환하는 ToSystemTime() 이 있습니다.7 장기 가동의 조사에서는 이 ToSystemTime()이 효과를 발휘합니다. 애플리케이션의 로그에 남은 시각과 트레이스 안의 위치를 맞춰 볼 수 있기 때문입니다. TTD.Calls의 결과에도 SystemTimeStart / SystemTimeEnd가 나옵니다.12

위치와 로그 시각의 대조애플리케이션의 로그에 있는 시각에서 Position 객체의 대략적인 실제 시각을 따라 트레이스 안의 위치를 특정하고, 그 위치로 SeekTo로 이동해 주변의 실행을 읽는다애플리케이션의 로그: 이상의 시각ToSystemTime 의 실제 시각이 가까운 위치를 찾는다SeekTo 로 그 위치로주변의 호출과 값을 읽는다

그림 17: 「로그의 이 행 직전에 무엇을 하고 있었는가」를 위치와 실제 시각의 대응으로 뽑을 수 있다.

위치는 공유에도 쓸 수 있습니다. 트레이스를 동료에게 건넬 때 !tt x:y의 위치를 덧붙이면, 상대는 같은 시점부터 보기 시작할 수 있습니다. 버그 티켓에 위치의 범위를 써 두는 운영도 공식적으로 권장됩니다.2

6.3 되감기

보통의 스텝 실행 명령 끝에 -를 붙이면 시간이 역방향으로 진행합니다.13

명령 의미 리본의 버튼
p- 1명령(또는 소스 1행) 되돌아간다. 함수 호출은 1스텝 취급 Step Over Back
t- 1명령(또는 소스 1행) 되돌아간다. 함수 호출의 내부도 따라간다 Step Into Back
g- 역방향으로 실행한다. 중단점에 걸리거나 이벤트에서 멈추거나 트레이스의 처음에서 멈춘다 Go Back

g-를 멈추는 이벤트는 순방향의 g를 멈추는 것과 같습니다.13ba(액세스 중단점)나 bp를 걸고 g-를 치면, 「그 조건이 마지막으로 성립한 위치」까지 단숨에 되돌아갈 수 있습니다. 이것이 제7장의 기본 동작입니다.

역방향 명령의 구분 사용p-는 함수 호출을 건너뛰어 1스텝 되돌아가고, t-는 함수 안까지 1명령씩 되돌아가며, g-는 중단점이나 이벤트나 트레이스 처음까지 단숨에 되돌아간다. 순방향의 g를 멈추는 조건은 g-도 멈춘다p-t-g-현재의 위치호출을 건너뛰어 하나 되돌아간다함수 안까지 1명령 되돌아간다다음에 멈출 조건까지 단숨에 되돌아간다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-로 3명령 되돌아가 값을 확인합니다.5 WinDbg의 Timelines 창은 예외, 중단점, 메모리 액세스, 함수 호출을 타임라인으로 시각화하며, 예외를 더블클릭하면 같은 SeekTo()가 발행됩니다.16

6.5 스레드와 위치

!positions는 현재 위치에서 활성인 모든 스레드와 각각의 트레이스 안 위치를 표시합니다.17 여기에 함정이 하나 있습니다. ~<번호>s로 스레드를 전환해도 트레이스 안의 위치는 움직이지 않습니다. 디버거가 메모리를 읽을 때 사용하는 위치는 바뀌지 않으므로, 다른 스레드의 「그 시점」의 메모리를 보고 싶다면 !positions의 출력에 있는 위치 링크나 !tt x:y로 이동합니다.13

재생의 기본 동선트레이스를 열어 인덱스를 만들고, 이벤트 목록에서 예외의 위치로 이동하고, 역방향 스텝으로 원인까지 거슬러 올라가고, 필요하면 positions로 다른 스레드의 위치로 옮긴다트레이스를 열고 인덱스TTD.Events 로 예외를 찾는다[Time Travel] 로 위치로 이동t- / p- / g- 로 거슬러 올라간다!positions 로 다른 스레드의 위치를 확인~s 로는 트레이스 위치가 움직이지 않는다

그림 19: 「이벤트로 들어가 역방향으로 걷는다」가 재생의 기본 동선. 스레드 전환은 위치 이동이 아니다.

7. 쿼리로 「언제」를 찾기 ── TTD.Calls와 TTD.Memory

재생의 진짜 강점은 트레이스 전체를 질의할 수 있다는 점입니다. 디버거의 데이터 모델(dx 명령)에 TTD의 객체가 실려 있어 LINQ 식으로 좁히고 정렬하고 집계할 수 있습니다.18

7.1 TTD.Calls ── 함수 호출을 검색하기

@$cursession.TTD.Calls("module!symbol")은 지정한 함수의 호출을 트레이스 전체에서 모읍니다. 와일드카드를 쓸 수 있고, 각 호출에는 시작과 종료 위치(TimeStart / TimeEnd), 스레드 ID(재사용되지 않는 UniqueThreadId 포함), 인수(Parameters[]), 반환값(ReturnValue), 반환 주소(ReturnAddress)가 들어 있습니다.12

공식 문서의 예는 GetLastError입니다. 반환값이 0이 아닌 호출을 오류 코드별로 집계하면, 트레이스 중에 어떤 오류가 몇 번 나왔는지가 목록이 됩니다.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()로 마지막 호출을 취하고, 그 TimeStart[Time Travel] 링크로 이동합니다.18

TTD.Calls 쿼리의 조립함수 이름으로 호출을 모으고, 반환값이나 인수로 좁히고, 오류 코드 등으로 집계하고, 시각으로 정렬하고, 목적한 호출의 위치로 Time Travel 링크로 이동한다TTD.Calls(함수 이름, 와일드카드)Where: 반환값과 인수로 좁힌다GroupBy: 오류 코드 등으로 집계OrderBy: 시각으로 정렬TimeStart 의 [Time Travel] 로 이동

그림 20: 「어디서」가 아니라 「언제, 몇 번, 어떤 인수로」에서 들어가는 것이 쿼리의 사용법.

심볼과의 관계를 짚어 둡니다. TTD는 PDB의 심볼 정보에서 함수의 인수 개수와 형, 반환값의 형, 호출 규약을 정합니다. private symbols가 있으면 함수 이름과 올바른 인수가 나옵니다. public symbols만 있으면 함수 이름과 기본 인수(64비트 부호 없는 정수 4개)가 됩니다. 심볼이 전혀 없는 모듈은 함수 이름이 UnknownOrMissingSymbols가 됩니다.1218 PDB의 종류와 보관에 대해서는 「PDB란 무엇인가」를 참조하십시오.

심볼의 유무와 TTD.Calls의 결과private symbols가 있으면 함수 이름과 올바른 인수를 얻을 수 있고, public symbols만 있으면 함수 이름과 기본값인 4개의 64비트 정수 인수가 되며, 심볼이 없으면 함수 이름이 UnknownOrMissingSymbols가 된다private symbolspublic symbols없음모듈의 심볼은?함수 이름 + 올바른 인수와 반환값함수 이름 + 기본 인수(64비트 정수 4개)UnknownOrMissingSymbols

그림 21: 자사 모듈의 PDB를 보관하고 있는지가 쿼리의 실용성을 그대로 결정한다.

Calls는 계산을 수반하므로 트레이스가 클수록 시간이 걸리고 CPU 사용률이 올라갑니다. 결과는 메모리에 캐시되어 같은 함수에 대한 두 번째 이후의 쿼리는 빨라집니다.12 쿼리가 아무것도 반환하지 않을 때의 원인은 넷으로, 호출을 쓰는 방식(모듈 이름은 x 명령으로 확인. 대문자로 돌아오면 그것을 사용), 대상 DLL이 그 위치에서는 아직 로드되지 않음(로드 후의 위치로 이동해 다시 실행), 함수가 인라인 확장되어 있음(추적 불가), 와일드카드가 너무 넓음(좁힌다)입니다.18

7.2 TTD.Memory ── 메모리 액세스를 검색하기

@$cursession.TTD.Memory(시작 주소, 종료 주소, "액세스 종류")는 지정 범위의 메모리에 대한 액세스를 트레이스 전체에서 모읍니다. 종류는 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", 주소, 크기) / NextMemoryAccess가 가볍고, 여러 범위를 한 번에 지정할 수 있습니다. 레지스터가 바뀐 위치는 @$curthread.TTD.PrevRegisterWrite("rcx")로 찾을 수 있습니다.6

7.3 ba + g- ── 「누가 깨뜨렸는가」를 대답하게 만들기

공식 워크스루가 제시하는 절차를 장기 가동의 조사에 쓸 수 있는 형태로 줄이면 다음과 같습니다.5

  1. TTD.Events로 예외의 위치로 이동한다
  2. t-로 되돌아가면서 깨진 값을 가진 변수를 가정한다
  3. 그 변수의 주소를 dx &변수로 얻는다
  4. ba w4 <주소>로 쓰기 중단점을 건다
  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로 획득과 해제의 호출을 맞춰 보고, 응답 없음이나 대기의 연쇄는 positions와 대기 API 호출의 실제 시각으로 좇는다간헐적인 예외와 데이터 파손TTD.Events → ba + g-핸들과 메모리의 증가TTD.Calls 로 획득과 해제를 맞춘다응답 없음과 대기의 연쇄!positions 와 대기 API 의 실제 시각-module 로 자사 DLL 에 한정해 녹화

그림 23: 유형마다 입구가 다르다. 공통되는 것은 녹화 범위를 먼저 좁힌 뒤에 읽기 시작하는 것.

유형 1: 간헐적인 예외와 데이터 파손.제5.1절의 링 버퍼로 증상 직전을 남기고, 제6.4절의 이벤트 목록에서 예외의 위치로 날아가, 제7.3절의 ba + g-로 값의 출처를 거슬러 올라갑니다. 덤프 조사와의 차이는, 「깨진 변수」를 찾은 시점에 조사가 끝나지 않고 거기서부터 기계적으로 앞으로 나아갈 수 있다는 점입니다.

유형 2: 핸들과 메모리의 증가.핸들 누수로 한 달 뒤에 죽는다」에서 해부한 종류의 프로젝트입니다. 한 달 분을 녹화할 수는 없으므로, 제5.2절의 -module로 자사 DLL에 한정하고 -ring과 겹쳐 「늘어나는 도중의 몇 분」을 남깁니다. 트레이스 위에서는 TTD.Calls("kernelbase!CreateFileW")TTD.Calls("kernelbase!CloseHandle")를 모아, CreateFileW의 반환값(핸들 값)과 CloseHandle의 첫 번째 인수(Parameters[0])를 맞춰 봅니다. CloseHandleReturnValue는 성패를 나타내는 불리언 값이므로 대조에는 쓸 수 없습니다. 닫히지 않은 핸들 값이 남으면, 그 CreateFileW 호출의 ReturnAddress(호출한 쪽)를 보면 누수시키고 있는 호출 지점을 뽑을 수 있습니다.12 다만 누수의 「유무」와 「양」의 관측은 Application Verifier나 핸들 카운트 쪽이 가볍고, TTD는 「어느 경로가 누수시키고 있는가」를 확정하는 단계에서 쓰는 것이 역할 분담으로 올바릅니다. 또한 Application Verifier를 활성화한 채로 녹화하면 메모리 사용 방식의 관계로 재생 시의 성능이 눈에 띄게 나빠지므로, 녹화 시에는 비활성화합니다.8

유형 3: 「응답 없음」과 대기의 연쇄.「응답 없음」의 정체」에서 쓴 대로, 중단은 「누가 누구를 기다리고 있는가」의 문제입니다. 트레이스는 유휴 중에는 커지지 않으므로4 커널에서 기다리는 스레드의 대기 시간 자체는 명령으로 찍히지 않습니다. 찍히는 것은 대기에 들어가기 직전의 호출과 돌아온 뒤의 명령입니다. !positions로 각 스레드의 위치를 보고17, TTD.Calls("kernelbase!WaitForSingleObject") 등의 SystemTimeStart / SystemTimeEnd에서 「어느 스레드가 언제 무엇을 기다리기 시작했는가」를 늘어놓으면, 로그의 시각과 맞춰 대기의 연쇄를 조립할 수 있습니다.12 DllMain과 로더 락이나 조건 변수의 spurious wakeup 같은 「순서에 의존하는 결함」은, 순서 그 자체가 녹화에 남는 TTD와 궁합이 좋은 영역입니다.

9. .NET 애플리케이션에서의 TTD

TTD의 공식 문서는 64비트 모드로 동작하는 SOS 확장(sos.dll)을 사용해 WinDbg의 TTD에서 관리 코드를 디버그할 수 있다고 서술합니다.1 읽어 들이는 방법은 SOS 분석 글의 제3장과 같고(.loadby sos coreclr 또는 자동 로드), 트레이스의 각 위치에서 !clrstack이나 !pe를 쓸 수 있습니다. 기본 동선은 TTD.Events로 예외의 위치로 이동한 뒤 !clrstack으로 관리 스택을 읽는 조합입니다.

.NET 애플리케이션의 TTD 트레이스를 읽는 구성TTD 트레이스를 WinDbg로 열고 64비트의 SOS 확장을 읽어 들여 예외 이벤트의 위치로 이동한 뒤 !clrstack이나 !pe로 관리 상태를 읽는다. TTD.Calls는 네이티브 경계의 호출에 사용한다TTD 트레이스(.run)WinDbgSOS 확장(64비트)TTD.Events 로 예외의 위치로!clrstack / !pe 로 읽는다TTD.Calls 는 네이티브 경계의 호출에

그림 24: 관리 상태는 SOS로, 호출의 검색은 네이티브 경계에서. 역할을 나누면 헤매지 않는다.

주의점을 둘. 첫째, 공식이 보장하는 것은 「64비트 모드의 SOS」입니다. x86으로 빌드한 .NET 애플리케이션에 대해서는 명시가 없으므로, 가능하면 조사 대상을 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에 넘긴 인수가 어떻게 만들어졌는지까지 필요할 때
PC 전체나 장시간의 성능 WPR/WPA, PerfView(ETW) 성능이 아니라 정확성의 문제이고 명령 단위의 순서가 필요할 때
업무상의 경과 애플리케이션의 로그 로그가 없는 코드 경로에서 일어났을 때(TTD는 사전 코드 변경 없이 전부를 기록한다1)
누가 그 값을 썼는가, 호출의 순서 TTD ──
조사 수단 고르기먼저 가벼운 덤프와 로그로 상태를 파악하고, 실패한 API는 ProcMon, 성능은 ETW로 조사하고, 그래도 「누가 언제 무엇을 넘겼는가」가 필요할 때 TTD로 나아간다아니오증상덤프와 로그로 상태를 파악(가벼움)실패한 API 는 ProcMon성능은 WPR/WPA, PerfView누가 언제 무엇을 넘겼는지가 필요?TTD 로 녹화 범위를 설계해 녹화가벼운 도구로 결착

그림 25: 가벼운 도구로 「결과」를 파악한 뒤, 「경로」가 필요하다고 판명된 프로젝트에만 TTD를 꺼낸다.

순서는 「가벼운 것부터」입니다. 덤프와 로그는 채취 비용이 거의 0이므로 상시 두고, TTD는 「덤프를 읽은 결과 경로가 필요하다고 판명된」 프로젝트에서 제5장의 설계를 한 뒤에 꺼냅니다. TTD의 오버헤드와 트레이스가 기밀 정보를 포함하는 성질을 생각하면, 이 순서를 뒤집을 이유는 없습니다.

11. 운영상의 주의

  • 트레이스는 기밀 파일로 다룬다.녹화에는 메모리 내용이 포함되고, 파일 경로와 레지스트리, 메모리나 파일의 내용 등 개인 정보와 보안 관련 정보가 들어갈 수 있습니다.12 고객 환경에서 녹화한다면 무엇이 포함될 수 있는지(연결 문자열, 토큰, 고객 데이터)를 사전에 파악하고, 암호화된 경로와 보관처, 보존 기간을 정합니다.
  • 공유하는 것은 .run만..idx.run과 비슷한 정도로 크고, WinDbg가 열 때 자동으로 생성됩니다. .run은 압축이 잘 듣습니다. TTD 자체의 결함을 보고할 때는 .out도 첨부합니다.2
  • 버전을 맞춘다.TTD는 WinDbg와 함께 계속 갱신되고 있으며, 1.11.611에서는 AVX/AVX512 대응 프로그램의 녹화 크래시 수정과 인덱스 형식의 변경이 들어갔습니다.11 녹화 쪽과 재생 쪽에 오래된 버전이 섞이면 재인덱싱이나 재녹화가 됩니다.
  • 다른 CPU에서 재생한다면 -replayCpuSupport를 확인한다.기본값은 이식성을 우선한 설정이고, 녹화한 CPU와 재생하는 CPU가 다른(Intel의 트레이스를 arm64에서 재생하는 등) 경우에는 MostConservative가 마련되어 있습니다. 반대로 동등 이상의 CPU에서 재생한다고 알고 있다면, 더 작고 빠른 녹화도 고를 수 있습니다.2
  • Windows Server에서도 녹화할 수 있다.TTD.exe는 Windows Server 2016/2019/2022/2025를 지원합니다.2
  • 녹화되지 않을 때의 원인 분리.먼저 ping.execmd.exe가 녹화되는지 시도하고, 되지 않으면 백신이나 애플리케이션 가상화 같은 침습적인 소프트웨어와의 충돌을 의심합니다.8
트레이스의 취급 절차녹화한 트레이스는 메모리 내용을 포함하므로 포함될 수 있는 정보를 파악하고, .run만 압축해 암호화된 경로로 건네고, 보관처와 보존 기간을 정하고, 분석 쪽에서는 같은 버전의 WinDbg로 열어 인덱스를 만든다녹화 완료(.run / .idx / .out)포함될 수 있는 정보를 파악.run 만 압축하고 암호화해 건넨다보관처와 보존 기간을 정한다같은 버전의 WinDbg 로 열어 .idx 를 생성

그림 26: 트레이스의 전달은 「기밀 파일의 전달」. 절차를 정하고 나서 녹화한다.

12. 정리

  • 덤프는 「상태」, TTD는 「경로」. 깨진 값의 출처나 호출의 순서가 필요한 프로젝트에서 TTD는 조사를 기계적인 작업으로 바꾼다
  • 녹화는 5~20배 느리고 초당 5~50MB 커지며 떨어지지 않는다. 장기 가동에 쓴다면 -ring/-maxFile, -module, -recordmode Manual, -monitor로 녹화할 범위를 먼저 설계한다
  • 재생은 「이벤트로 들어가 역방향으로 걷는다」. TTD.Events[Time Travel]t-/g-, 그리고 ba + g-로 「누가 깨뜨렸는가」를 대답하게 만든다
  • TTD.CallsTTD.Memory는 트레이스 전체에 대한 질의. ToSystemTime()SystemTimeStart로 로그의 시각과 맞춘다
  • .NET은 64비트의 SOS로 상태를 읽고, TTD.Calls는 네이티브 경계에 쓴다
  • 트레이스는 기밀 파일. 공유는 .run만, 경로와 보관을 정하고 나서

덤프는 받았는데 원인에 닿지 못한다, 재현되지 않아 손이 멈춰 있다는 프로젝트는, 녹화 범위의 설계만 할 수 있으면 TTD로 결착되는 경우가 적지 않습니다. 녹화의 설계부터 .run의 분석까지 맡을 수도 있으니, 덤프와 로그를 첨부해 상담해 주십시오.

관련 글

관련 상담 영역

합동회사 코무라소프트에서는 장기 가동 후나 간헐적으로만 발생하는 Windows 애플리케이션의 결함에 대해, 크래시 덤프와 로그와 TTD 트레이스를 조합한 원인 조사, 녹화 범위의 설계를 포함한 조사 체제의 구축, 네이티브 경계(COM, P/Invoke, 장치 SDK)가 얽히는 장애의 원인 분리를 다루고 있습니다. 「덤프는 받았지만 원인에 닿지 못한다」는 단계부터 상담해 주십시오.

참고 링크

  1. Microsoft Learn, Time Travel Debugging - Overview. TTD가 프로세스의 실행을 녹화해 앞뒤로 재생할 수 있다는 점, 덤프가 실패에 이르는 상태와 실행 경로를 놓치기 쉽다는 점, 녹화에 관리자 권한이 필요하다는 점, 녹화에 개인 정보와 보안 관련 정보가 포함될 수 있다는 점, 조사 수단의 비교 표, .run/.idx의 역할, 64비트 모드의 SOS 확장으로 관리 코드를 디버그할 수 있다는 점에 대해.  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. 트레이스 크기의 요인(1명령당 1비트~1바이트), 활동 중에는 초당 5~50MB 커지고 유휴 중에는 커지지 않는다는 점, 최대 크기의 상한이 없다는 점, 인덱스가 트레이스의 1~2배가 된다는 점, 디스크 소진 시의 녹화와 인덱스 생성의 동작 및 회피책에 대해.  2 3 4 5 6 7 8 9

  5. Microsoft Learn, Time Travel Debugging - Sample App Walkthrough. 예외 이벤트의 위치로 이동해 bag-로 잘못된 값을 마지막으로 쓴 위치까지 거슬러 올라가는 일반 절차, 실패 지점이 진짜 원인에서 몇 단계 앞으로 나아간 오류 처리 안에 있는 경우가 많다는 점, 크래시 시에 트레이스가 닫히고 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(), 대략적인 실제 시각(UTC)을 반환하는 ToSystemTime(), 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. WinDbg UI의 Launch executable (advanced) / Attach to process 로부터의 녹화, Record with Time Travel Debugging 체크 박스, Configure and Record 에 의한 저장 위치의 설정, Record subset of execution 에 의한 모듈 한정에 대해. 

  10. Microsoft, WinDbg-Samples - TTD in-process recording API (GitHub). -recordmode Manual과 조합해 프로그램 쪽에서 녹화의 시작과 정지를 제어하는 인프로세스 녹화 API의 문서에 대해. 

  11. Microsoft Learn, Time travel debugging release notes. 1.11.611에서의 AVX/AVX512 대응 프로그램의 녹화 수정과 인덱스 형식의 변경(재인덱싱이 필요), 1.11.553에서 추가된 @$curframe.TTD.VariableHistory()에 대해.  2 3

  12. Microsoft Learn, TTD Calls Objects. TTD.Calls의 인수와 ThreadId/UniqueThreadId/Function/ReturnValue/ReturnAddress/Parameters[]/TimeStart/TimeEnd/SystemTimeStart/SystemTimeEnd의 각 속성, PDB의 심볼 정보가 없는 경우의 기본값(64비트 부호 없는 정수 4인수, 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~50MB 정도 커집니다.
며칠씩 계속 돌리는 운영 애플리케이션에 TTD를 붙여 둘 수 있습니까?
그대로는 무리입니다. TTD는 녹화 중의 속도 저하가 크고, 활동 중인 프로세스에서는 트레이스가 초당 5~50MB 커지며 파일 크기에 상한도 없습니다. 장기 가동에 쓰려면 TTD.exe의 -ring(링 버퍼)과 -maxFile로 「마지막 N MB만 남기기」, -module로 자사 모듈이 동작하는 동안만 녹화하기, -recordmode Manual로 애플리케이션 쪽에서 녹화 구간을 지정하기 같은 녹화 범위의 설계가 전제가 됩니다. 또한 한 번 연결한 TTD는 스스로 떨어지지 않으므로, 녹화를 그만두려면 프로세스를 종료(재시작)하는 운영까지 포함해 정해 두어야 합니다.
서비스나 다른 세션의 프로세스도 녹화할 수 있습니까?
TTD.exe의 문서는 -attach를 「서비스나 장시간 가동하는 애플리케이션의 조사」용으로, -monitor를 「프로그램이나 서비스가 시작될 때마다 녹화」하는 용도로 설명합니다. 한편 문제 해결 페이지에는 다른 세션이나 다른 보안 컨텍스트에서 동작하는 「일반적이지 않은 프로세스」는 현재 시점에서 녹화 대상이 아니라는 기술도 있습니다. 실제로 녹화되는지는 환경에 따라 다르므로, 운영과 같은 구성에서 먼저 ping.exe나 cmd.exe 같은 단순한 프로세스를 녹화하고, 다음으로 대상 프로세스에서 시도해 확인한 뒤에 운영에 넣으십시오.
.NET 애플리케이션의 트레이스에서도 TTD를 쓸 수 있습니까?
쓸 수 있습니다. 공식 문서는 64비트 모드로 동작하는 SOS 확장(sos.dll)을 WinDbg의 TTD 트레이스 위에서 사용해 관리 코드를 디버그할 수 있다고 서술합니다. 트레이스의 각 위치에서 !clrstack이나 !pe 같은 SOS 명령을 실행하고, 예외 이벤트의 위치로 이동한 뒤 관리 스택을 읽는 조합이 기본입니다. TTD.Calls 쿼리로 심볼 이름에서 호출을 검색하는 기능은 PDB의 심볼 정보를 전제로 하므로, .NET 애플리케이션에서는 P/Invoke나 COM, Win32 API 같은 네이티브 경계의 호출을 좇는 사용법이 확실합니다.
트레이스 파일(.run)을 다른 회사나 사외로 보내도 괜찮습니까?
그대로는 위험합니다. TTD의 녹화에는 프로세스의 메모리 내용이 포함되며, 파일 경로, 레지스트리, 메모리나 파일의 내용 등 개인 정보나 비밀 정보가 들어갈 수 있다고 공식 문서가 명시합니다. 보낸다면 무엇이 녹화되어 있는지(연결 문자열, 토큰, 고객 데이터 등)를 파악한 뒤에 암호화된 경로와 보관처를 정하십시오. 공유하는 것은 .run만으로 충분하고, 인덱스 파일(.idx)은 WinDbg가 열 때 자동으로 생성됩니다.
TTD.Calls로 함수를 검색해도 아무것도 반환되지 않습니다. 왜입니까?
원인은 주로 네 가지입니다. 첫째는 심볼의 문제로, PDB가 없는 모듈의 함수는 「UnknownOrMissingSymbols」라는 이름이 되고 모듈 이름도 대문자일 때가 있으므로, x 명령으로 실제 심볼 이름을 확인합니다. 둘째는 대상 DLL이 그 시점에 아직 로드되지 않은 경우로, DLL이 로드된 뒤의 위치로 이동한 다음 다시 쿼리합니다. 셋째로 함수가 인라인 확장되어 있으면 쿼리 엔진이 추적할 수 없습니다. 넷째는 와일드카드가 너무 넓어 일치하는 함수가 너무 많은 경우로, 패턴을 좁힙니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기