Agentic Debugging Using Time Travel - Greg Law - ACCU York

1. Time Travel Debugging의 핵심 개념

Time Travel Debugging은 프로그램 실행의 “과거”를 관찰할 수 있게 하는 기술이다. 전통적인 디버거가 “현재 무슨 일이 일어나고 있는가(what’s happening)“를 보여준다면, Time Travel Debugging은 “무슨 일이 일어났는가(what happened)“를 보여준다. 이는 HG Wells의 타임머신처럼 과거를 관찰하는 것이지, 과거를 변경하는 것이 아니다.

Printf 디버깅 vs 전통적 디버거의 한계

강연자는 디버깅 방식을 두 가지로 분류한다:

Printf 디버깅(Console Logging)

  • 과거에 무슨 일이 일어났는지 “이야기(story)“를 제공
  • 하지만 새로운 질문을 하려면 코드를 수정하고 재컴파일, 재실행해야 함

전통적 디버거

  • 프로그램 상태에 대해 즉석에서 어떤 질문이든 할 수 있음
  • 하지만 “현재” 상태만 볼 수 있고, 과거는 backtrace 정도만 제공

Time Travel Debugging의 장점

  • 과거에 일어난 일을 볼 수 있으면서(printf의 장점)
  • 코드 수정 없이 즉석에서 질문할 수 있음(디버거의 장점)
  • 특히 비결정적(non-deterministic)이고 재현이 어려운 버그에 효과적

2. DOOM 실전 시연: 데이터 흐름 역추적

강연자는 실제 DOOM 게임을 녹화(recording)한 후, 좀비를 죽인 순간을 역추적하는 과정을 시연한다.

디버깅 시나리오

초기 상태: 게임이 종료된 시점의 비디오 버퍼

iVideoBuffer[320][200]  // 2D 배열

목표: 화면의 특정 픽셀(피 묻은 부분)이 언제, 어떻게 변경되었는지 추적

역추적 프로세스

  1. 픽셀 좌표 확인: 마우스 호버로 좌표 172, 115 확인

  2. last 커맨드 사용: 해당 배열 요소가 마지막으로 변경된 시점으로 이동

    • 실행 시간의 99% 지점으로 이동
    • draw_column 함수 내에서 픽셀이 업데이트됨을 확인
  3. 콜스택 역행(uncall): draw_sprite 함수로 올라감

    • 일반적인 스택 팝(pop)과 유사하지만, 실제로 시간을 되감고 있음
    • 모든 전역 상태가 과거로 복원됨
  4. sprite 데이터 추적: x1, x2 좌표가 마지막으로 이동한 시점 확인

    • 93% 시점, project_sprite 함수
  5. health 변수 발견: thing.health = -5 (죽은 상태)

    • last thing.health 명령으로 health가 변경된 시점으로 이동
    • 87% 시점, health가 10에서 -5로 변경됨
  6. 북마크(bookmark) 설정: 이 중요한 시점을 “die”로 북마크

  7. 디스플레이 업데이트 시점 찾기:

    • 이전에 본 d_display 함수로 돌아감
    • d_display에 브레이크포인트 설정
    • 포워드 실행으로 디스플레이 업데이트 시점 확인
    • 비디오 프레임을 보면 좀비가 죽는 순간을 정확히 캡처

핵심 기법: 데이터 흐름과 코드 흐름을 따라가면서(following data and code flow), 코드베이스에 대한 사전 지식 없이도 버그의 원인을 찾을 수 있음

3. 구현 원리: 어떻게 과거를 기억하는가

Time Travel Debugging의 핵심 질문은 “이전 상태가 무엇이었는가(what was the previous state)“이다. 이에 대한 두 가지 접근법이 있다:

접근법 1: 상태 저장(Saving State)

모든 명령어 실행마다 변경된 상태를 저장하는 방식.

문제점:

  • 매 명령어마다 최소 8~16바이트 이상의 데이터 저장 필요
  • 초당 수십억 개의 명령어 실행 → 엄청난 저장 공간과 I/O 오버헤드
  • 실용적이지 않음

접근법 2: 재계산(Recomputing)

기본 아이디어: 컴퓨터는 기본적으로 거대한 결정론적 함수

f(x) → next_state

여기서 x는 프로그램의 전체 상태

Reversible Logic의 한계:

  • a = a + 1은 겉보기엔 가역적이지만, 플래그(flags)에 영향을 미침
  • a = b는 a의 이전 값을 복원할 방법이 없음
  • 이전 명령어가 무엇이었는지 알 수 없음(점프 가능)

해결책: 재실행(Re-execution)

  • 1000번째 명령어에서 1 스텝 뒤로 가려면?
  • → 처음부터 999번째 명령어까지 재실행
  • 실제로는 스냅샷을 활용하여 효율성 향상

스냅샷(Snapshot) 전략

[Snapshot 0] --instructions--> [Snapshot 1] --instructions--> [Snapshot 2] ... [Snapshot N]

동작 방식:

  • 녹화 중 약 30개의 스냅샷 생성 (DOOM 예시)
  • 뒤로 가려면: 이전 스냅샷으로 이동 후 목표 지점 직전까지 재실행
  • “뒤로 가기”는 착시 효과 - 실제로는 앞으로 재실행

스냅샷 최적화:

  • Copy-on-Write(COW) 방식 사용 (MMU 활용, fork/clone 이용)
  • 메모리 효율적: 변경된 페이지만 복사
  • 시간 효율적: 대부분의 메모리는 공유

적응형(Adaptive) 스냅샷 간격:

  • 처음엔 고정 간격(예: 2초마다)으로 스냅샷 생성
  • “흥미로운 이벤트” 감지 시 동적으로 조정
    • Segmentation fault
    • Breakpoint
    • Watchpoint
  • 사용자가 자주 탐색하는 구간에 더 많은 스냅샷 생성
  • 스냅샷 간격을 벗어나면 재생성 (시간이 걸릴 수 있음)

4. 비결정성(Non-determinism) 처리

컴퓨터는 기본적으로 결정론적이지만, 실제로는 그렇지 않다. 비결정적 입력 소스들을 캡처해야 재실행이 가능하다.

비결정적 입력 소스

Linux/Unix 시스템에서:

  1. 시스템 콜(System Calls): 모든 시스템 콜 캡처
  2. 스레드 스위칭(Thread Switches): 스레드 간 상호작용
  3. 공유 메모리(Shared Memory): 프로세스 간 통신, mmap 등

이 세 가지만 캡처하면 Linux 프로그램의 모든 비결정성을 처리 가능.

이벤트 로그(Event Log)

녹화 중 모든 비결정적 이벤트를 로그에 기록:

  • 시스템 콜 인자와 반환 값
  • 스레드 스위칭 시점
  • 공유 메모리 변경 사항

재실행 시 이 로그를 재생하여 동일한 실행 경로 보장.

5. 녹화 수준(Recording Granularity) 선택

프로세스 레벨 vs VM/언어 레벨

네이티브 바이너리(C/C++/Rust):

  • 프로세스 레벨 녹화가 적합
  • 커널과 프로세스 간 경계에서 캡처

관리형 언어(Java/JVM):

  • JVM 자체를 녹화하면 Java 프로그램 디버깅 가능
  • 하지만 JVM 내부 복잡도 증가

WebAssembly:

  • WASM 런타임 레벨에서 녹화 가능

구현 기법: ptrace와 JIT

강연자의 UDB(Undo Debugger)는 ptrace를 사용하여 모든 시스템 콜을 인터셉트:

# 기본 사용
udb record ./my_program
udb replay recording.undo

장점:

  • 소스 코드 불필요
  • 디버그 심볼 불필요 (있으면 더 좋음)
  • 재컴파일 불필요

RR(Mozilla의 Record and Replay)와 비교:

  • RR: 오픈소스, ptrace 기반, 유사한 접근법
  • UDB: 상용 제품, 더 많은 기능과 최적화

6. 멀티스레드 처리

멀티스레드 디버깅의 어려움

멀티스레드 프로그램은 Time Travel Debugging에서 가장 어려운 케이스:

  • 스레드 간 타이밍 이슈
  • 레이스 컨디션(Race Condition)
  • 비결정적 실행 순서

UDB의 해결책

모든 스레드를 동기화하여 이동:

  • 뒤로/앞으로 이동 시 모든 스레드가 함께 이동
  • 스레드 간 상대적 타이밍 유지

시연 예시 (DOOM):

  • 6개의 스레드 존재
  • 모든 스레드를 lockstep으로 이동
  • 스레드 스위칭을 이벤트 로그에 기록하여 재현

성능 특성

녹화 오버헤드:

  • 단일 스레드: 약 50% 느림 (2배 시간)
  • 멀티스레드: 5~10배까지 느려질 수 있음
  • Sanitizer(ASan, TSan)와 유사한 오버헤드

메모리 오버헤드:

  • 일반적으로 2배 메모리 필요
  • COW 스냅샷으로 효율성 향상

실용성:

  • 항상 켜두기엔 부담
  • 재현 어려운 버그나 플래키(flaky) 테스트 디버깅에 적합
  • 테스트 실행 시 녹화 활성화하여 실패 시 분석

7. 멀티프로세스 상관관계(Multiprocess Correlation)

MPC(Multiprocess Correlation) 기능

여러 프로세스가 소켓이나 공유 메모리로 통신하는 시스템 디버깅:

시나리오:

  • 프로세스 A와 B가 소켓으로 통신
  • 프로세스 B가 데이터를 받음 (recv)
  • 질문: 이 데이터는 어디서 왔는가?

해결:

  • 대응하는 send 호출을 프로세스 A에서 찾음
  • 시간을 역추적하여 송신 시점으로 이동
  • 크로스 프로세스 데이터 흐름 추적

구현 방식

소켓 통신:

  • 모든 I/O 모니터링
  • 바이트 오프셋으로 송수신 매칭
  • 예: 소켓에서 8192번째 바이트를 받았다면, 8192번째 바이트를 보낸 send 찾기

공유 메모리:

  • 녹화 시점에 접근 로깅
  • 더 복잡한 상관관계 알고리즘 필요

녹화 후 상관관계:

  • 실시간으로 동기화하지 않음
  • 사후 분석으로 연결점 찾기

8. 공유 메모리 최적화: 하이브리드 접근법

공유 메모리는 Time Travel Debugging에서 가장 까다로운 부분이다. 강연자는 이를 해결하기 위한 독창적인 하이브리드 기법을 소개한다.

두 가지 순진한(Naive) 접근법

접근법 1: 소프트웨어 체크

// 모든 메모리 접근 시 체크 코드 삽입
if (is_shared_memory(address) && has_changed(address)) {
    log_event(address, old_value, new_value);
}
value = *address;
  • 장점: 공유 메모리 접근 시에만 오버헤드
  • 단점: 모든 메모리 접근(전체 명령어의 약 25%)에 수십 개의 명령어 추가
  • 결과: 너무 느림

접근법 2: MMU 활용 (Memory Protection)

mprotect(shared_memory, size, PROT_NONE);  // 공유 메모리를 보호
  • 장점: 공유되지 않은 메모리는 전속력으로 실행
  • 단점: 공유 메모리 접근 시 페이지 폴트 발생 (약 백만 사이클 소요)
  • 결과: 공유 메모리 사용이 많으면 매우 느림

하이브리드 해결책: “Both”

핵심 아이디어: 두 방법을 동적으로 전환

  1. 초기 상태: 모든 공유 메모리를 PROT_NONE으로 보호
  2. 첫 접근 시: 페이지 폴트 발생
  3. 폴트 처리:
    • 해당 명령어가 공유 메모리에 접근함을 학습
    • JIT로 해당 명령어만 재컴파일하여 소프트웨어 체크 추가
  4. 이후: 소프트웨어 체크로 빠르게 처리 (폴트 없이)

메모리 맵 구조

Original Shared Memory Location: [Protected - PROT_NONE] (Emulated Shared Map)
                                         ↓ (on fault)
Actually Shared Memory:         [Mapped elsewhere] (Really Shared Memory)
Third Copy:                     [For change detection]

작동 흐름:

  1. 프로그램이 원래 공유 메모리 위치 접근 시도
  2. 폴트 발생 (첫 접근만)
  3. 실제 공유 메모리는 다른 곳에 매핑
  4. 재컴파일된 코드는 실제 공유 메모리 접근
  5. Third Copy와 비교하여 변경 여부 확인
  6. 변경 시에만 이벤트 로그에 기록

성능 특성:

  • 공유 메모리: 약간 느림 (소프트웨어 체크 오버헤드)
  • 비공유 메모리: 전속력 (대부분의 경우)
  • 최적의 균형점 달성

영감의 원천:

  • 강연자가 자전거를 타다가 떠올린 아이디어
  • 이전의 좋은 아이디어들: 샤워 중(Time Travel Debugging 기본 개념), 화장실에서(박사 논문 아이디어)
  • “스마트폰 때문에 화장실에서 좋은 아이디어가 안 나온다”는 농담

9. AI 기반 자동 디버깅: Agentic Debugging

LLM의 디버깅 한계

현재 LLM의 강점:

  • 코드 생성에 뛰어남
  • Claude Code, Cursor 등 도구들이 코드 작성에 특화

디버깅의 약점:

  • 얕은 문제(50줄 Python에 한 줄 버그)는 잘 찾음
  • 실제 복잡한 코드베이스에서는 추측에 의존
  • “증상에서 가장 흔한 버그 유형”을 제시 (예: Off-by-one error)
  • 환각(Hallucination) 문제: 실제 실행 기록 없이 추측

MCP(Model Context Protocol) 서버 구현

기본 개념: LLM에게 녹화된 실행 기록 접근 권한 부여

udb replay recording.undo
extend explain  # MCP 서버 다운로드
explain "Find all the times zombies were killed"

작동 방식:

  1. LLM이 가설(hypothesis) 제시
  2. 다른 에이전트가 실제 녹화 기록에서 검증
  3. 검증 실패 시 피드백 제공
  4. LLM이 다시 시도
  5. 반복 루프

에이전트 간 대화

흥미로운 패턴:

  • 두 에이전트가 서로 대화하며 문제 해결
  • “Excellent try, you’re almost there!”
  • “I think you’re missing something in the memory”
  • “Keep going, try harder”
  • LLM의 sycophantic(아첨하는) 특성이 에이전트 간에도 나타남

환각 방지(Hallucination Antidote)

핵심 가치:

  • LLM: “Off-by-one error일 것 같습니다”
  • 시스템: “녹화 기록에서 off-by-one error를 찾아보세요”
  • LLM: “찾을 수 없습니다”
  • 시스템: “다시 시도하세요”

→ 추측이 아닌 실제 증거 기반 디버깅

성능 특성

속도:

  • 숙련된 개발자가 수동으로 하는 것만큼 빠르거나 약간 느림
  • 하지만 개발자는 다른 일을 할 수 있음

실용성:

  • 복잡한 버그를 백그라운드에서 분석
  • 개발자는 다른 작업 수행 또는 휴식
  • “사랑하는 사람과 시간을 보내거나 맥주를 마실 수 있다”

적용 분야:

  • 재현 어려운 버그
  • 플래키 테스트
  • 레거시 코드베이스
  • 멀티스레드 레이스 컨디션

10. 실전 활용 시나리오

최적화 빌드 디버깅

문제: variable optimized out 에러

(gdb) print myvar
$1 = <optimized out>

Time Travel 해결책:

  • 몇 명령어 뒤로 이동
  • 변수가 레지스터에 살아있는 시점 찾기
  • 여전히 완전한 해결책은 아니지만, 전통적 디버거보다 나음

멀웨어 분석

사용 사례:

  • 소스 코드 없는 악성코드 분석
  • 실행 기록 녹화 후 역분석
  • 공격 벡터 추적
  • “이 프로그램이 실제로 뭘 하는가?” 분석

배포 환경 디버깅

시나리오:

  • 프로덕션에 디버그 심볼 포함하고 싶지 않음
  • IP 보호 또는 배포 효율성 이유

해결:

  1. 배포 환경에서 최적화 빌드 녹화 (디버그 심볼 없이)
  2. 녹화 파일을 개발 환경으로 가져옴
  3. 개발 환경에서 디버그 심볼 매칭
  4. 전체 디버깅 정보와 함께 분석

파일 시스템 상호작용

질문: “녹화 중 파일을 삭제했는데, 되감기 하면 파일이 다시 나타나나요?”

답변: 모든 I/O 캡처

  • 파일 읽기/쓰기 내용 로그에 저장
  • 재실행 시 실제 파일 시스템 대신 로그에서 재생
  • 프로그램 입장에서는 파일이 존재하는 것처럼 보임

11. 기술적 세부사항

JIT(Just-In-Time) Binary Translation

UDB는 바이너리를 실시간으로 번역:

  • 원본 명령어를 계측(instrumentation)된 버전으로 변환
  • 시스템 콜, 메모리 접근 등 인터셉트
  • 공유 메모리 접근 시 동적으로 재컴파일

녹화 파일 크기

DOOM 예시: 400 megabytes

  • 작지 않지만 감당 가능
  • 전체 게임플레이 캡처
  • 압축 가능

일반적 크기:

  • 실행 시간과 I/O 양에 비례
  • 이벤트 로그 + 스냅샷
  • 대부분 기가바이트 이하

명령어 세트 지원

지원: x86, x86-64, ARM 미지원: Windows (아직)

  • Windows API 복잡성
  • 시스템 콜 인터셉션 어려움
  • 가능은 하지만 투자 대비 효과 낮음

소스 코드 vs 바이너리

디버그 심볼 없이도 작동:

udb record /bin/ls  # 시스템 표준 ls 명령
# 녹화 성공

하지만:

  • 소스 코드 없으면 활용도 제한
  • 변수명, 함수명 확인 불가
  • 특정 소스 라인으로 이동 불가

실용적 사용:

  • 보통 자신의 코드 디버깅 (소스 있음)
  • 서드파티 라이브러리 일부만 소스 없음
  • “어려운 문제를 쉽게, 불가능한 문제를 매우 어렵게”

12. 결론: Time Travel Debugging의 미래

패러다임 전환

전통적 디버깅: 문제 재현 → 브레이크포인트 → 단계 실행 → 반복 Time Travel 디버깅: 한 번 녹화 → 자유로운 시간 탐색 → 빠른 근본 원인 분석

AI와의 결합

단기 미래:

  • LLM이 녹화 기록을 자동 분석
  • 개발자 대신 초기 트리아지(triage) 수행
  • 근본 원인에 대한 가설 제시

장기 비전:

  • 완전 자동화된 버그 분석
  • 백그라운드 디버깅
  • 개발자는 더 창의적인 작업에 집중

실용적 권장사항

언제 사용할까:

  • ✅ 재현 어려운 버그
  • ✅ 플래키 테스트
  • ✅ 멀티스레드 레이스 컨디션
  • ✅ 복잡한 데이터 흐름 추적
  • ✅ 프로덕션 문제 사후 분석

제약사항 인지:

  • 성능 오버헤드 (2~10배)
  • 메모리 오버헤드 (약 2배)
  • 항상 켜두기엔 부담

최적 활용법:

  • CI/CD 파이프라인의 테스트 실행 시 녹화
  • 실패한 테스트만 분석
  • 중요한 버그 재현 시 즉시 녹화

커뮤니티와 도구

오픈소스 대안:

  • RR (Mozilla): 무료, 기본 기능 제공
  • UDB (Undo): 상용, 고급 기능과 지원

학습 리소스:

  • 두 도구 모두 유사한 개념
  • RR로 시작하여 개념 학습
  • 필요시 UDB로 전환

핵심 기술 요약

Time Travel Debugging 구현의 3대 기둥

  1. 스냅샷 + 재실행: 과거 상태를 효율적으로 재구성
  2. 비결정성 캡처: 시스템 콜, 스레드, 공유 메모리 이벤트 로깅
  3. 하이브리드 최적화: MMU + 소프트웨어 체크로 성능 균형

디버깅 효율성 공식

전통적 디버깅 시간 = 재현 시간 × 가설 수 × 재컴파일 횟수
Time Travel 디버깅 시간 = 녹화 시간 + 탐색 시간

→ 비결정적 버그에서 Time Travel이 압도적으로 유리

AI 통합의 핵심 가치

환각 방지 = 실행 기록 검증

추측이 아닌 증거 기반 디버깅으로, LLM의 약점을 보완하고 강점을 극대화한다.