Application Verifier로 만드는 Windows 이상 경로 테스트 기반
· 업데이트: · Go Komura · Windows 개발, 장애 조사, 산업용 카메라, Application Verifier, 이상 경로 테스트, 핸들 누수
수정 이력(9건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635111)
- 추가한 그림의 Mermaid 소스 들여쓰기를 기사 안 규칙에 맞추고, 리뷰에서 지적된 그림 표현을 본문 서술과 맞게 조정했습니다. 본문 문장은 바꾸지 않았습니다.
- 리뷰 지적에 따라, 오늘 추가한 그림 중 너비가 너무 컸던 것을 세로 구성으로 고치고, 일부 그림과 캡션 표현을 본문 서술에 맞게 바로잡았습니다. 본문 문장은 바꾸지 않았습니다.
- 본문 흐름·구조를 그림으로도 따라갈 수 있도록 Mermaid 그림을 20개 추가했습니다(본문 500~750자당 그림 1개의 규칙에 맞춘 것입니다). 기존 그림 2개에는 캡션을 넣었습니다. 본문 문장은 바꾸지 않았습니다.
- 기사 맨 앞에 '이 기사의 지식 맵' 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참고하십시오.
- 구해서 이 머신에서 켜기까지의 절을 새로 만들었습니다(Windows SDK 포함, GUI와 커맨드라인이 같은 레지스트리 설정을 쓸 뿐이라는 관계, Administrators 그룹 필수, 실행 중 프로세스에 나중에 켤 수 없음, 로그 위치). 함께 `/faults` 인자 `20000`이 '2만 번에 1번'이 아니라 2%임을 출처와 함께 명시했습니다. verifier stop이 무엇인지, WinDbg의 `-xd` 의미, DLL을 좁히는 방법도 추가했습니다. 실은 출력 예는 공식 문서의 것이며 당사 환경의 것이 아님을 본문에 명시했습니다.
- 본문 속 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635110)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「Application Verifier로 만드는 Windows 이상 경로 테스트 기반」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635110 https://comcomponent.com/ko/blog/application-verifier-abnormal-test-foundation-part2/
- DOI(최신 버전)
- 10.5281/zenodo.21635110
- DOI(이 버전)
- 10.5281/zenodo.22217420
Application Verifier는 Windows의 네이티브 코드나 Win32 경계에서 일어나는 이상을 앞당겨 드러내고 싶을 때 유력한 도구입니다. 특히 핸들 이상, 힙 파괴, 저자원 상태의 failure path를 테스트하고 싶은 장면에서는, 정상 경로 시험만으로는 보이지 않는 문제를 꽤 일찍 밖으로 끌어낼 수 있습니다.
이전 글 산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편에서는, 장시간 가동 뒤에 크래시하는 제어 앱을 조사한 결과 원인이 핸들 누수였던 사례를 정리했습니다. 다만 로그만 강화해서는 아직 절반입니다. 정말로 필요한 것은, 앞으로도 만약 예상 밖의 프로그래밍 실수로 메모리 누수나 핸들 누수, 도중 실패, 해제 누락이 일어나도 ‘무엇이 일어났는지 알 수 있는’ 상태가 되어 있는지를 미리 시험할 수 있는 것입니다.
그래서 쓴 것이 Application Verifier입니다. Windows의 네이티브 코드나 Win32 경계에서 동작하는 처리에 대해, 실행 시에 체크나 fault injection을 넣을 수 있는 도구입니다. 실무에서 특히 편리한 점은, 정말로 머신의 메모리를 다 쓰지 않아도, 메모리 부족이나 자원 부족처럼 깨지는 방식을 앞당겨 일으킬 수 있는 점입니다.
이번 글에서는 Application Verifier란 무엇인지, 어떤 일을 할 수 있는지, 그것을 어떻게 이상 경로 테스트 기반에 넣을지를, 산업용 카메라 제어 앱의 문맥으로 정리합니다.
목차
- 먼저 결론 (한 마디로)
- Application Verifier란 무엇인가
- 2.1. 한 마디로 말하면 무엇인가
- 2.2. 어떤 장면에서 효과가 있는가
- 2.3. 무엇이 좋은가
- 2.4. 구해서 이 머신에서 켜기까지
- Application Verifier로 무엇을 할 수 있는가
- 3.1. Basics: Handles / Heaps / Locks / Memory / TLS 등
- 3.2. Low Resource Simulation: 메모리 부족·자원 부족을 앞당기기
- 3.3. Page Heap과 debugger
- 3.4.
!avrf/!htrace/ 로그
- 이번에 왜 도입했는가
- 4.1. 목적은 ‘버그를 찾는’ 것만이 아니다
- 4.2. 메모리 부족처럼 보이는 현상을 일으키기
- 4.3. 핸들 이상이 났을 때 추적할 수 있는지 확인하기
- 메모리 부족이나 자원 부족 같은 현상을 어떻게 일으키는가
- 5.1. Low Resource Simulation의 접근
- 5.2. 무엇을 실패시킬 수 있는가
- 5.3. 실무에서의 적용
- 핸들 이상을 어떻게 보는가
- 6.1.
Handles체크 - 6.2.
!htrace로 open / close 스택 보기 - 6.3. 자체 로그와 어떻게 함께 쓰는가
- 6.1.
- 이상 경로 테스트 기반 만드는 법
- 7.1. 실행 단위를 harness로 모으기
- 7.2. 테스트 메뉴를 나누기
- 7.3. 수집할 것
- 7.4. 합격 조건
- 7.5. 주의점
- 대략적인 용도 구분
- 정리
- 참고 자료
이 글의 지식 맵
Application Verifier는 Windows의 user-mode 앱에 대해 OS API 이용과 자원 관리를 실행 시에 감시하는 런타임 검증 도구로, invalid handle 사용이나 heap corruption을 verifier stop으로 검출합니다. Low Resource Simulation이라는 fault injection으로 다양한 API 호출을 확률적으로 실패시키면, 실제 메모리 부족을 일으키지 않고도 저자원 상황의 failure path를 앞당겨 밟게 할 수 있습니다. Handles 체크를 활성화하면 자동으로 handle tracing이 작동해, WinDbg의 !htrace로 핸들의 이력을, !avrf로 설정과 stop 상태를 확인할 수 있습니다. 대상 EXE에는 실행 전 설정이 필요해 나중에 덧붙일 수 없고, 장시간 가동하는 앱의 핸들 누수 조사를 Application Verifier에만 맡기는 것은 현실적이지 않으므로, 전용 harness EXE와 자체 로그를 조합하는 것이 실무적인 운용이 됩니다.
flowchart LR
accTitle: Application Verifier를 통한 비정상 경로 테스트
accDescr: Application Verifier가 fault injection에 의한 Low Resource Simulation과 handle tracing을 통해 invalid handle 사용이나 heap corruption을 verifier stop으로 검출할 수 있는 것, WinDbg의 !avrf와 !htrace로 원인을 추적할 수 있는 것, 전용 harness EXE에 대해 활성화하는 운용과 장시간 가동하는 앱의 핸들 누수 조사에는 자체 로그와의 병행이 필요하다는 것을 보여주는 그림.
application_verifier["Application Verifier"]
abnormal_path_testing["비정상 경로 테스트"]
fault_injection["폴트 인젝션(fault injection)"]
low_resource_simulation["Low Resource Simulation"]
handle_tracing["핸들 트레이싱(handle tracing)"]
windbg["WinDbg"]
invalid_handle["잘못된 핸들 사용(invalid handle)"]
heap_corruption["힙 손상(heap corruption)"]
page_heap["페이지 힙(page heap)"]
verifier_stop["verifier stop"]
avrf_command["!avrf(WinDbg 확장 명령)"]
harness_exe_pattern["harness EXE 패턴"]
handle_leak["핸들 누수"]
abnormal_path_testing -.->|"이용한다"| application_verifier
application_verifier -->|"이용한다"| fault_injection
low_resource_simulation -->|"이용한다"| fault_injection
application_verifier -->|"이용한다"| low_resource_simulation
application_verifier -.->|"이용한다"| handle_tracing
handle_tracing -->|"에서 확인할 수 있다"| windbg
invalid_handle -->|"에서 확인할 수 있다"| application_verifier
heap_corruption -->|"에서 확인할 수 있다"| application_verifier
heap_corruption -->|"에서 확인할 수 있다"| page_heap
verifier_stop -->|"에서 확인할 수 있다"| avrf_command
invalid_handle -->|"원인이 될 수 있다"| verifier_stop
heap_corruption -->|"원인이 될 수 있다"| verifier_stop
harness_exe_pattern -->|"권장되는 대응"| application_verifier
application_verifier -->|"사용은 비권장"| handle_leak
avrf_command -.->|"전제로 한다"| windbg
low_resource_simulation -->|"에서 구성할 수 있다"| application_verifier
harness_exe_pattern -->|"권장되는 대응"| abnormal_path_testing
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 17건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
1. 먼저 결론 (한 마디로)
- Application Verifier는 Windows의 언매니지드 / 네이티브 경계에서 일어나는 오용을 실행 시에 찾기 쉽게 하는 도구입니다
- 편리한 점은 ‘버그를 찾는’ 것만이 아니라, 평소에는 잘 나오지 않는 이상 경로를 앞당겨 일으킬 수 있는 점입니다
Handles에서는 invalid handle 검출,Heaps에서는 힙 파괴의 표면화,Low Resource Simulation에서는 메모리 부족이나 자원 부족처럼 보이는 상황의 fault injection이 됩니다- 장시간 상주하는 EXE의 leak 조사를 Application Verifier에만 맡기는 것은 적절한 방법이 아니고,
Handle Count나 resource lifecycle의 자체 로그와 함께 쓰는 것이 현실적입니다 - 이상 경로 테스트 기반에서는 정상 경로의 verifier run과 fault injection run을 나눠 돌리는 편이 읽기 쉽습니다
- DLL을 시험하고 싶은 경우에도, Application Verifier를 켜는 대상은 그 DLL을 실제로 돌리는 테스트용 EXE입니다
요컨대 Application Verifier는 Windows의 native / Win32 주변에 있는 “지저분한 버그”를 표면으로 끌어내는 도구입니다. 특히 장치 제어 앱처럼 native SDK, P/Invoke, Win32 API가 흔히 섞이는 세계에서는 궁합이 꽤 좋습니다.
flowchart TB
accTitle: Application Verifier의 두 가지 역할
accDescr: Application Verifier는 네이티브 경계의 오용 검출과, 평소에는 잘 나오지 않는 이상 경로를 앞당기는 두 가지 역할을 가지며, invalid handle이나 힙 파괴 검출과 메모리 부족처럼 보이는 상황의 fault injection을 할 수 있다.
av["Application Verifier"] --> detect["네이티브 경계의 오용을 검출"]
av --> inject["잘 나오지 않는 이상 경로를 앞당김"]
detect --> d1["invalid handle이나 힙 파괴"]
inject --> d2["메모리 부족처럼 보이는 상황의 주입"]
그림 1: Application Verifier의 역할은 ‘오용 검출’과 ‘이상 경로를 앞당기기’ 두 기둥입니다.
2. Application Verifier란 무엇인가
2.1. 한 마디로 말하면 무엇인가
Application Verifier는 Windows의 user-mode 앱에 대한 런타임 검증 도구입니다. 실행 중인 앱의 OS API 이용과 자원 취급을 감시해, 의심스러운 사용을 검출하거나 의도적으로 실패를 주입할 수 있습니다.
‘정적 분석’이나 ‘단위 테스트’와 달리, 실제로 그 코드 패스를 지났을 때 어떻게 깨지는지를 보는 도구입니다. 그래서 평소 기능 테스트에서는 보이지 않는 failure path를 드러내는 데 적합합니다.
flowchart LR
A[테스트 harness] --> B[제어 앱 / SDK 래퍼]
B --> C[Application Verifier]
C --> D[Win32 API / native DLL / OS 자원]
C --> E[verifier stop]
C --> F[debugger output]
C --> G[AppVerifier logs]
B --> H[자체 structured log]
그림 2: 테스트 harness로 돌리는 제어 앱을 Application Verifier가 감시하고, verifier stop·debugger 출력·로그로 관측 결과를 남깁니다.
2.2. 어떤 장면에서 효과가 있는가
특히 효과가 나기 쉬운 것은 다음 같은 장면입니다.
- native DLL이나 카메라 SDK를 호출한다
- P/Invoke나 COM을 넘나든다
- handle, heap, lock, virtual memory를 직접 또는 간접으로 많이 쓴다
- 보통의 정상 경로에서는 거의 크래시하지 않지만, 이상 경로에서만 수명 관리가 무너질 것 같다
- ‘크래시한다’보다 ‘가끔 이상한 실패를 반환한다’가 먼저 나온다
반대로, 순수 managed 세계만의 object graph를 따라가기 위한 도구는 아닙니다. 그래서 C# 앱이어도 native SDK나 Win32 경계가 두껍다면 꽤 효과가 있지만, 순수 managed heap leak을 이것 하나로 전부 본다는 이야기는 아닙니다.
flowchart TB
accTitle: Application Verifier가 맞는 장면의 구분
accDescr: native DLL이나 P/Invoke, Win32 경계가 두꺼운 앱에는 맞지만, 순수 managed 세계만의 object graph를 따라가는 도구는 아니라는 구분을 나타낸다.
q{"어느 층의 문제인가"}
q -->|"native SDK나 Win32 경계"| yes["Application Verifier가 맞다"]
q -->|"순수 managed의 object graph"| no["대상 밖〔다른 도구로 본다〕"]
그림 3: 맞는지의 갈림은 native / Win32 경계의 두께이며, 순수 managed 세계만 보는 도구는 아닙니다.
2.3. 무엇이 좋은가
실무에서 좋은 점은 대체로 다음 세 가지입니다.
- 네이티브 경계의 오용을 일찍 멈출 수 있다
- invalid handle
- heap corruption
- lock misuse
- virtual memory API misuse 등
- 저자원 상태에서만 나오는 깨짐을 앞당길 수 있다
malloc에 해당하는 할당이 가끔 실패한다CreateEvent나CreateFile이 가끔 실패한다VirtualAlloc이 실패한다
- debugger와 맞추면 추적하기 쉽다
!avrf!htrace!heap -p -a- verifier stop 로그
장치 제어 앱에서 곤란한 것은 ‘이상 경로에서 무엇이 일어났는지 모른다’는 점입니다. Application Verifier는 그 “모름”을 줄이는 데 꽤 효과가 있습니다.
flowchart TB
accTitle: 실무에서 좋은 세 가지
accDescr: 네이티브 경계의 오용을 일찍 멈출 수 있고, 저자원 상태에서만 나오는 깨짐을 앞당길 수 있으며, debugger와 맞추면 추적하기 쉽다는 세 가지 이점을 나타낸다.
av["Application Verifier"] --> b1["오용을 일찍 멈출 수 있다"]
av --> b2["깨짐을 앞당길 수 있다"]
av --> b3["debugger로 추적하기 쉽다"]
b3 -.-> t["avrf나 htrace 같은 확장"]
그림 4: 실무에서의 이점은 조기 검출·이상 경로를 앞당기기·debugger 연동 세 가지로 모입니다.
2.4. 구해서 이 머신에서 켜기까지
먼저 도구를 갖추는 쪽을 정리해 둡니다. 여기가 빠지면 이후 이야기가 전부 탁상공론이 됩니다.
Application Verifier는 Windows SDK에 포함됩니다. Windows만으로는 들어 있지 않으므로, SDK 설치 프로그램을 실행하고 기능 선택 화면에서 ‘Application Verifier’에 체크해 주세요. 실행 파일 이름은 appverif.exe입니다.
쓰는 전제는 세 가지입니다.
- 실행하는 사용자가 그 머신의 Administrators 그룹 멤버일 것
- ARM64EC는 지원 대상이 아닐 것
- 검증 대상은 언매니지드(네이티브) 코드일 것
GUI와 커맨드라인의 관계는 다음처럼 이해하면 헷갈리지 않습니다.
| 무엇을 하는가 | |
|---|---|
GUI(appverif.exe) |
대상 EXE 이름과 켤 테스트 조합을 레지스트리에 쓴다 |
커맨드라인(appverif -enable ...) |
완전히 같은 레지스트리 설정을 명령으로 쓴다 |
| 실행 시 | 대상 EXE가 시작할 때 그 설정을 보고 verifier DLL이 로드되며, Win32 API 훅이 들어간다 |
즉 어느 쪽을 써도 하는 일은 같습니다. 손으로 하는 첫 설정은 GUI, CI나 스크립트는 커맨드라인이라는 구분입니다.
flowchart TB
accTitle: GUI와 커맨드라인의 관계
accDescr: GUI도 커맨드라인도 같은 레지스트리 설정을 쓸 뿐이며, 대상 EXE가 시작할 때 그 설정을 보고 verifier DLL이 로드되고 Win32 API 훅이 들어간다.
gui["GUI〔appverif.exe〕"] --> reg["레지스트리에 설정을 쓴다"]
cli["커맨드라인"] --> reg
reg --> boot["대상 EXE 시작 시에 참조"]
boot --> hook["verifier DLL 로드와 훅"]
그림 5: GUI도 커맨드라인도 같은 레지스트리 설정을 쓸 뿐이며, 훅이 들어가는 것은 대상 EXE 시작 시입니다.
GUI 조작은, 화면 왼쪽 Applications 칸에서 오른쪽 클릭해 ‘Add Application’으로 대상 EXE를 추가하고, 화면 오른쪽 Tests 칸에서 Basics 등에 체크한 뒤 ‘Save’를 누르는 흐름입니다. 해제는 같은 Applications 칸에서 오른쪽 클릭해 ‘Delete Application’을 고르고 ‘Save’합니다.
여기서 나오는 중요한 제약은 두 가지입니다.
- 돌아가는 중인 프로세스에는 나중에 켤 수 없습니다. 훅은 DLL 로드 시에 들어가므로, 설정한 뒤에 시작하는 순서가 됩니다.
- 설정은 명시적으로 지울 때까지 남습니다. ‘한 번만 시험한 셈’으로 방치하면, 그 머신에서는 그 EXE가 항상 verifier 아래에서 시작합니다.
참고로 검출 시 로그는 기본으로 %USERPROFILE%\AppVerifierLogs에 바이너리 형식으로 저장되며, GUI나 커맨드라인으로 XML로 변환해 집계할 수 있습니다.
flowchart TB
accTitle: 켜는 순서와 설정이 남는 방식
accDescr: 훅은 DLL 로드 시에 들어가므로 실행 중인 프로세스에는 나중에 붙일 수 없고, 설정한 뒤에 시작하는 순서가 되며, 설정은 명시적으로 지울 때까지 남는다.
set["설정을 쓴다"] --> launch["대상 EXE를 시작한다"]
launch --> on["verifier 아래에서 동작"]
on --> keep["설정은 지울 때까지 남는다"]
keep -.-> warn["방치하면 항상 verifier 아래에서 시작"]
running["실행 중인 프로세스"] -.-> ng["나중에 켜는 것은 불가"]
그림 6: ‘설정한 뒤에 시작’ 순서는 바꿀 수 없고, 설정은 명시적으로 지울 때까지 그 머신에 남습니다.
3. Application Verifier로 무엇을 할 수 있는가
3.1. Basics: Handles / Heaps / Locks / Memory / TLS 등
Application Verifier의 기본 세트는 Basics입니다.
여기에 실무에서 자주 쓰는 체크가 모여 있습니다.
| 레이어 | 무엇을 보는가 | 이번 문맥에서의 쓰임 |
|---|---|---|
Handles |
invalid handle 사용 | close된 / 깨진 handle을 밟지 않았는지 |
Heaps |
heap corruption | native SDK 경계의 버퍼 파괴나 use-after-free를 드러내기 |
Leak |
DLL unload 시점에 해제되지 않은 자원 | 짧은 수명 harness 테스트나 unload를 포함하는 경우의 확인 |
Locks / SRWLock |
lock 오용 | reconnect와 shutdown의 경합 확인 |
Memory |
VirtualAlloc / MapViewOfFile 등의 오용 |
큰 버퍼나 공유 메모리 주변의 이상 확인 |
TLS |
Thread Local Storage API 오용 | 스레드 경계가 복잡한 native 코드의 보험 |
Threadpool |
threadpool API나 worker state의 정합성 | callback이나 비동기 처리가 많은 경우의 보조 |
포인트는 ‘크래시 뒤에 읽으면 안다’가 아니라 ‘의심스러운 사용을 그 자리에서 멈춘다’는 점입니다. 장시간 가동형 장애에서는 이 앞당김이 꽤 효과가 있습니다.
flowchart TB
accTitle: Basics로 앞당겨 검출하는 방식
accDescr: 크래시 뒤에 로그를 읽고 추측하는 것이 아니라, 의심스러운 사용을 그 자리에서 멈춰 장시간 가동형 장애를 앞당겨 드러낸다.
use["의심스러운 API 사용"] --> basics["Basics 체크 군"]
basics --> stop["그 자리에서 멈춘다"]
stop --> early["문제를 앞당겨 드러냄"]
use -.-> later["기존에는 크래시 뒤에만 읽음"]
그림 7: Basics의 가치는 ‘크래시 뒤에 읽기’를 ‘그 자리에서 멈추기’로 바꾸는 것입니다.
3.2. Low Resource Simulation: 메모리 부족·자원 부족을 앞당기기
실무에서 꽤 편리한 곳이 여기입니다. 정말로 RAM을 다 쓰지 않아도, 메모리 부족이나 자원 부족에 가까운 현상을 일으킬 수 있기 때문입니다.
원리는 단순합니다.
- 어떤 API 호출을
- 일정 확률로
- 일부러 실패시킨다
이것으로 평소에는 거의 지나지 않는 error path를 지나게 할 수 있습니다.
구체적으로는 이런 현상을 의도적으로 일으키기 쉬워집니다.
HeapAlloc이나VirtualAlloc이 실패한다CreateFile이 실패한다CreateEvent가 실패한다MapViewOfFile이 실패한다SysAllocString같은 OLE/COM 계열 할당이 실패한다
정말로 메모리 부족을 만들려다 머신 전체를 괴롭히는 것보다, 이쪽이 훨씬 다루기 쉽습니다. 게다가 특정 DLL만 골라 fault injection을 넣을 수도 있습니다. 장치 제어 앱처럼 자체 래퍼와 vendor SDK가 섞인 구성에서는 꽤 실무에 맞습니다.
flowchart TB
accTitle: Low Resource Simulation의 구조
accDescr: 어떤 종류의 API 호출을 일정 확률로 일부러 실패시켜, 평소에는 거의 지나지 않는 error path를 의도적으로 지나게 할 수 있으며, 대상을 특정 DLL로 좁힐 수도 있다.
call["API 호출"] --> judge{"일정 확률에 걸렸는가?"}
judge -->|"예"| fail["일부러 실패를 반환"]
judge -->|"아니요"| ok["평소대로 처리"]
fail --> path["평소 지나지 않는 error path로"]
path -.-> dll["특정 DLL만으로 좁히는 것도 가능"]
그림 8: Low Resource Simulation의 정체는 API 호출을 일정 확률로 실패시키는 fault injection입니다.
3.3. Page Heap과 debugger
힙 파괴를 보려면 Heaps와 page heap 조합이 강합니다.
특히 full page heap은 guard page를 써서 깨진 순간에 멈추기 쉬운 점이 이점입니다.
다만 이것은 꽤 무겁습니다. 장시간 전수 시험이라기보다, 재현이 가까운 시나리오로 좁혀 debugger 아래에서 돌리는 편이 쓰기 쉽습니다.
그래서 운용으로는 이런 구분이 현실적입니다.
- 먼저
Basics로 넓게 건다 - 힙이 의심스러워지면 full page heap을 쓴다
- 너무 무거우면 light page heap으로 내린다
- 프로덕션에 가까운 장시간 시험은 자체 로그 중심으로 본다
결국 AppVerifier는 만능 지팡이가 아니라, 장면마다 날을 갈아 끼우는 공구입니다.
flowchart TB
accTitle: page heap 용도 구분의 흐름
accDescr: 먼저 Basics로 넓게 걸고, 힙이 의심스러우면 full page heap으로 깨진 순간에 멈추며, 너무 무거우면 light page heap으로 내리고, 프로덕션에 가까운 장시간 시험은 자체 로그 중심으로 본다.
s1["Basics로 넓게 건다"] --> s2{"힙이 의심스러운가?"}
s2 -->|"예"| s3["full page heap으로 멈춘다"]
s2 -->|"아니요"| s7["장시간 시험은 자체 로그 중심"]
s3 --> s4{"너무 무거운가?"}
s4 -->|"예"| s5["light page heap으로 내린다"]
s4 -->|"아니요"| s6["debugger 아래에서 좁은 범위 재현"]
그림 9: page heap은 항상 쓰는 도구가 아니라, Basics로 넓게 건 뒤 장면마다 날을 갈아 끼웁니다.
3.4. !avrf / !htrace / 로그
먼저 말 하나를 정리합니다. 여기까지 여러 번 나온 verifier stop은 Application Verifier가 ‘이 사용은 이상하다’고 판단했을 때 내는 검출 이벤트입니다. 단순한 로그 한 줄이 아니라, 디버거 아래에서 돌리고 있으면 그 자리에서 브레이크하는 점이 특징입니다. stop에는 번호가 붙어 VERIFIER STOP 00000300처럼 표시됩니다. 그대로 계속할 수 있는 stop과, 계속할 수 없는(프로세스를 끝내는 수밖에 없는) stop이 있습니다.
Application Verifier는 stop을 내고 끝이 아닙니다. debugger 확장과 로그가 있어, 무엇이 일어났는지를 따라가기 쉬워집니다.
!avrf- 현재 verifier 설정이나 지금 일어난 stop을 본다
!htrace- handle의 open / close / invalid reference 스택을 본다
!heap -p -a- page heap과 맞춰 깨진 힙 블록을 따라간다
- AppVerifier 로그
- stop이 났을 때의 로그를 남길 수 있다
특히 Handles를 켜 두면 handle tracing이 자동으로 켜지는 점이 고맙습니다.
이것으로 ‘이 handle을 어디서 열고 어디서 닫았는지’를 나중에 따라가기 쉬워집니다.
flowchart TB
accTitle: verifier stop에서 조사까지의 흐름
accDescr: 이상한 사용을 검출하면 번호가 붙은 verifier stop이 나오고, 디버거 아래라면 그 자리에서 브레이크하며, avrf로 설정과 stop을, htrace로 핸들 이력을 확인할 수 있다.
bad["이상한 사용을 검출"] --> stop["verifier stop〔번호 있음〕"]
stop --> brk["디버거 아래라면 브레이크"]
brk --> avrf["avrf로 설정과 stop을 확인"]
brk --> ht["htrace로 handle 이력을 확인"]
stop -.-> cont["계속할 수 있는 stop과 없는 stop"]
그림 10: verifier stop은 단순한 로그 한 줄이 아니라, 디버거 아래에서는 그 자리에서 브레이크해 조사의 출발점이 됩니다.
4. 이번에 왜 도입했는가
4.1. 목적은 ‘버그를 찾는’ 것만이 아니다
이번 목적은 단순히 ‘AppVerifier로 버그 하나를 찾는’ 것이 아니었습니다. 더 실무적으로 말하면, 확인하고 싶었던 것은 다음입니다.
- 나중에 또 다른 failure path에서 자원 누수가 났을 때
- 로그에 문맥이 제대로 남는가
- debugger 정보와 맞춰 끝까지 따라갈 수 있는가
- ‘무엇이 일어났는지 불명’인 상태가 되지 않는가
즉 검출기로서만이 아니라, 관측 기반의 테스트로서 썼습니다.
4.2. 메모리 부족처럼 보이는 현상을 일으키기
평소 개발 머신에서 정말로 메모리 부족을 일으키기는 꽤 번거롭습니다. 게다가 머신 전체가 불안정해지면, 이번에는 테스트 자체가 잡음투성이가 됩니다.
그래서 Low Resource Simulation을 써서, 메모리 부족이나 자원 부족에서 날 법한 failure path를 골라 밟히게 하는 쪽으로 갔습니다.
이것으로 이런 질문에 답하기 쉬워집니다.
CreateEvent가 실패하면 로그에cameraId와phase가 남는가- 어중간한 초기화 뒤에 clean up은 제대로 도는가
VirtualAlloc이 실패하면 재시도에서 깨지지 않는가- save path의
CreateFile실패 시 핸들이 돌아오는가
강조하고 싶은 것은, 이상을 일으키는 것 자체가 목적이 아니라, 이상 시에 깨지는 방식을 읽을 수 있는 것이 목적이라는 점입니다.
flowchart TB
accTitle: fault injection으로 확인하는 관측 기반
accDescr: Low Resource Simulation으로 실패를 골라 밟히게 하고, 로그에 문맥이 남는지, 뒷정리가 도는지, 재시도에서 깨지지 않는지를 확인한다. 목적은 이상을 일으키는 것이 아니라 깨지는 방식을 읽을 수 있는 것이다.
inject["실패를 골라 밟히게 한다"] --> q1["로그에 문맥이 남는가"]
inject --> q2["clean up은 도는가"]
inject --> q3["재시도에서 깨지지 않는가"]
q1 --> goal["깨지는 방식을 읽을 수 있는 상태"]
q2 --> goal
q3 --> goal
그림 11: fault injection의 목적은 이상을 일으키는 것이 아니라, 이상 시의 깨짐을 읽을 수 있는지를 확인하는 것입니다.
4.3. 핸들 이상이 났을 때 추적할 수 있는지 확인하기
이전 글에 나온 핸들 누수도 그렇지만, 핸들 주변은 마지막에 크래시한 곳과 진짜 원인이 어긋나기 쉽습니다.
그래서 확인하고 싶었던 것은 이런 점입니다.
- invalid handle stop이 났을 때
!htrace로 open / close를 따라갈 수 있는가 - 자체 로그의
resourceId/sessionId/phase와 이어지는가 - 실패 뒤에 handle count가 돌아오는가
- harness를 짧은 수명 프로세스로 했을 때 leak 차이를 보기 쉬운가
여기까지 보이면, 단순한 ‘버그가 났습니다’에서 ‘어느 책임에서 수명 관리가 무너졌는지’까지 갈 수 있게 됩니다.
flowchart TB
accTitle: 핸들 이상 시에 추적할 수 있는지의 확인
accDescr: invalid handle stop이 났을 때 htrace로 open과 close를 따라갈 수 있는지, 자체 로그의 문맥과 이어지는지, handle count가 돌아오는지를 확인하고, 수명 관리가 무너진 책임의 특정까지 도달한다.
stop["invalid handle stop"] --> c1["htrace로 open과 close를 따라간다"]
stop --> c2["자체 로그의 문맥과 잇는다"]
stop --> c3["handle count 회복을 확인"]
c1 --> goal["수명 관리가 무너진 책임의 특정"]
c2 --> goal
c3 --> goal
그림 12: 핸들 이상에서는 ‘버그가 났다’에서 멈추지 않고, 어느 책임에서 수명 관리가 무너졌는지까지 따라갈 수 있는지를 확인합니다.
5. 메모리 부족이나 자원 부족 같은 현상을 어떻게 일으키는가
5.1. Low Resource Simulation의 접근
Low Resource Simulation은 이른바 fault injection입니다. 저자원 환경을 진짜처럼 재현한다기보다, 저자원 상태에서 일어나는 대표적인 API 실패를 인공적으로 섞는 이미지입니다.
그래서 쓰임은 꽤 분명합니다.
- failure path의 뒷정리 확인
- retry / reconnect의 단단함 확인
- 도중 성공·도중 실패가 섞인 초기화 확인
- ‘평소에는 안 나는 실패’여도 로그가 남는지 확인
여기에서의 요령은 처음부터 무엇이든 실패시키지 않는 것입니다. 갑자기 전부 켜면 로그가 폭발해 ‘무엇을 보고 있는지’가 안 보이게 됩니다.
flowchart TB
accTitle: fault injection을 좁히는 방법
accDescr: 처음부터 무엇이든 실패시키면 로그가 폭발해 무엇을 보고 있는지 모르게 되므로, 보고 싶은 failure path에 가까운 실패부터 좁혀 켠다.
all["처음부터 전부 실패시킨다"] --> noise["로그가 폭발해 읽을 수 없다"]
narrow["보고 싶은 실패만 켠다"] --> clear["무엇을 보고 있는지 분명하다"]
그림 13: fault injection의 요령은 전부 켜지 않는 것이며, 보고 싶은 failure path에 가까운 실패부터 좁혀 켭니다.
5.2. 무엇을 실패시킬 수 있는가
Low Resource Simulation에서는 대표적으로 다음 종류의 API를 확률적으로 실패시킬 수 있습니다.
| 종류 | 예 | 장치 제어 앱에서의 예 |
|---|---|---|
Heap_Alloc |
힙 할당 | 임시 버퍼, 이미지 메타데이터, SDK 래퍼 내부 할당 |
Virtual_Alloc |
가상 메모리 할당 | 큰 프레임 버퍼, 링 버퍼 |
File |
CreateFile 등 |
저장 경로나 로그 파일 open |
Event |
CreateEvent 등 |
frame ready 알림, stop/reconnect 동기 |
MapView |
CreateMapView 등 |
공유 메모리나 memory mapped file |
Ole_Alloc |
SysAllocString 등 |
COM / OLE 경계 |
Wait |
WaitForXXX 계열 |
동기 대기 실패 주변 |
Registry |
레지스트리 접근 | 설정 읽기쓰기나 드라이버 주변 설정 |
실무에서는 전부를 동시에 켜기보다, 이번에 보고 싶은 failure path에 가까운 것부터 좁혀 켜는 것이 핵심입니다.
5.3. 실무에서의 적용
커맨드라인 이미지로는 예를 들어 다음처럼 됩니다.
appverif /verify CameraHarness.exe
appverif /verify CameraHarness.exe /faults
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 virtual_alloc=20000 file=20000 event=20000
appverif -query lowres -for CameraHarness.exe
복사해 붙여도 의도를 못 읽으면 의미가 없으므로, 한 줄씩 무엇을 하는지를 적어 둡니다.
| 명령 | 무엇을 하는가 |
|---|---|
appverif /verify CameraHarness.exe |
CameraHarness.exe에 대해 Basics 테스트 군을 켠다 |
appverif /verify CameraHarness.exe /faults |
위에 더해 fault injection을 켠다. 다만 대상은 OLE_ALLOC과 HEAP_ALLOC만 |
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 ... |
lowres(Low Resource Simulation)를 켜고, 실패시킬 API 종류와 확률을 개별 지정한다 |
appverif -query lowres -for CameraHarness.exe |
지금 무엇이 어떤 확률로 설정되어 있는지를 표시한다 |
appverif /n CameraHarness.exe |
그 EXE의 설정을 삭제한다(-disable * -for나 -delete settings -for와 같은 목적) |
인자 읽기도 잡아 둡니다.
- 확률은 100만분율입니다. 지정할 수 있는 것은 0 ~ 1,000,000의 정수이며,
20000은20000 / 1,000,000즉 2%입니다. ‘2만 번에 1번’이 아닙니다. Microsoft 문서도 레지스트리와 파일 API를 2%로 실패시키는 예로-with registry=20000 file=20000을 들고 있습니다. /faults뒤에는 확률·유예 시간·DLL 이름을 나열할 수 있습니다. 형식은/faults [확률 [유예밀리초 [DLL ...]]]입니다. 확률을 생략하면 5%, 유예 시간을 생략하면 500밀리초가 됩니다. 유예 시간은 ‘프로세스 시작부터 이 시간은 fault를 넣지 않는다’는 뜻이며, 시작 처리 자체가 실패해 아무것도 시험하지 못하게 되는 것을 막기 위한 것입니다./n은 해제입니다.n은 ‘no verifier’ 정도의 뜻이라고 생각하면 됩니다. 켜 둔 채로 두지 않기 위한, 짝이 되는 명령입니다.
-query lowres 출력은 대체로 이런 형태로 돌아옵니다. 설정한 확률이 들어 있는지, 노리지 않은 종류까지 열려 있지 않은지를 여기서 확인할 수 있습니다.
Settings for CameraHarness.exe:
Test [lowres] enabled.
Include = *
Exclude =
TimeOut = 2000 (0x7D0)
WAIT = 0 (0x0)
HEAP_ALLOC = 20000 (0x4E20)
VIRTUAL_ALLOC = 0 (0x0)
REGISTRY = 0 (0x0)
FILE = 20000 (0x4E20)
EVENT = 20000 (0x4E20)
MAP_VIEW = 0 (0x0)
OLE_ALLOC = 0 (0x0)
STACKS = false
Include와 Exclude가 대상 모듈 좁히기, TimeOut이 시작 직후에 fault를 넣지 않는 시간입니다. 기본값은 설정을 넣는 방법에 따라 달라지므로, 짐작하지 말고 이 출력으로 확인하는 것이 확실합니다.
진행 순서는 이런 식입니다.
- 먼저
Basics만으로 정상 경로를 돌린다 - 다음에
Low Resource Simulation을 더해 fault injection 있음으로 돌린다 - 필요하면
file이나event등, 보고 싶은 실패만 확률을 준다 - 특정 DLL만 노리고 싶으면 그 DLL로 좁혀 넣는다
/faults 단축은 편리하지만, 이것만이면 OLE_ALLOC과 HEAP_ALLOC 중심입니다.
CreateFile이나 CreateEvent의 failure path를 보고 싶으면 -enable lowres -with file=... event=...까지 쓰는 편이 확실합니다.
장치 제어 앱에서는 앱 전체에 fault를 뿌리는 것보다, camera wrapper나 save path DLL로 좁히는 편이 읽기 쉬운 경우가 많습니다.
flowchart TB
accTitle: fault injection을 거는 순서
accDescr: 먼저 Basics만으로 정상 경로를 돌리고, 다음에 Low Resource Simulation을 더해 fault injection 있음으로 돌리며, 보고 싶은 실패만 확률을 주고, 필요하면 특정 DLL로 좁힌다는 단계적 적용.
s1["Basics만으로 정상 경로"] --> s2["Low Resource를 더해 돌린다"]
s2 --> s3["보고 싶은 실패만 확률 지정"]
s3 --> s4["특정 DLL로 좁혀 넣는다"]
그림 14: 처음부터 조준하지 않고, Basics의 정상 경로부터 단계적으로 fault injection을 좁혀 갑니다.
그 ‘DLL로 좁히기’의 구체적인 쓰는 법도 둡니다. /faults의 세 번째 이후 인자가 대상 모듈 지정입니다.
appverif /verify CameraHarness.exe /faults 50000 1000 CameraSdkWrapper.dll
이것으로 CameraHarness.exe를 시작했을 때, 시작부터 1000밀리초가 지난 뒤, CameraSdkWrapper.dll에서 시작된 조작만을 5%(50000 / 1,000,000)확률로 실패시킵니다. 모듈 이름은 확장자까지 쓰고 경로는 붙이지 않습니다. .dll뿐 아니라 .ocx처럼 로드되는 모듈도 지정할 수 있습니다.
좁히기가 먹혔는지는 appverif -query lowres -for CameraHarness.exe의 Include와 Exclude 줄에서 확인할 수 있습니다. 여기가 * 그대로면 아직 프로세스 전체가 대상입니다.
flowchart TB
accTitle: DLL로 좁힌 fault injection의 동작
accDescr: faults 인자로 확률과 유예 시간과 대상 모듈을 지정하면, 시작부터 유예 시간이 지난 뒤 지정 DLL에서 시작된 조작만 지정 확률로 실패하고, query의 Include와 Exclude로 좁히기를 확인할 수 있다.
arg["확률과 유예 시간과 DLL 이름을 지정"] --> grace["시작부터 유예 시간은 주입하지 않는다"]
grace --> target["지정 DLL에서 시작된 조작만 실패"]
target --> check["query의 Include와 Exclude로 확인"]
그림 15: DLL로 좁힌 fault injection은 유예 시간이 지난 뒤, 지정 모듈에서 시작된 조작만 실패시킵니다.
디버거 아래에서 돌리고 있다면 도중에 범위를 바꿀 수도 있습니다.
!avrf -trg dll CameraSdkWrapper.dll
!avrf -skp dll VendorSdk.dll
-trg가 ‘여기를 노린다’, -skp가 ‘여기는 건너뛴다’입니다. !avrf -flt로 현재 fault injection 설정을 확인하거나, !avrf -flt stacks 10으로 최근에 주입된 실패의 스택을 볼 수도 있습니다.
예를 들어 이런 시나리오를 만들 수 있습니다.
- reconnect 시작 직후의
CreateEvent실패 - 저장 시작 시의
CreateFile실패 - 임시 버퍼 할당 실패
- COM 변환의
SysAllocString실패 - 대기 API의 실패 경로 확인
이 부근은 평소 정상 경로 테스트만으로는 거의 밟히지 않습니다. 그래서 의도적으로 밟히게 할 가치가 있습니다.
6. 핸들 이상을 어떻게 보는가
6.1. Handles 체크
핸들 주변에서는 먼저 Handles를 씁니다.
이것으로 invalid handle 사용을 검출하기 쉬워집니다.
전형적으로 맞는 것은 이런 사고입니다.
- close된 handle을 다시 쓴다
- 깨진 handle 값을 넘긴다
- 도중 실패로 초기화되지 않은 handle을 쓴다
- lifetime이 무너져 다른 스레드에서 건드린다
장시간 가동에서 보면 ‘가끔 이상한 에러가 난다’ 정도여도, verifier 아래에서는 그 자리에서 멈춰 주는 일이 있습니다. 이 앞당김은 꽤 도움이 됩니다.
flowchart TB
accTitle: Handles 체크가 맞는 사고
accDescr: close된 핸들 재사용, 깨진 핸들 값, 도중 실패로 미초기화 핸들, 수명 붕괴로 다른 스레드에서의 오용 같은 사고를, verifier 아래에서는 그 자리에서 멈출 수 있다.
a1["close된 것의 재사용"] --> stop["그 자리에서 verifier stop"]
a2["깨진 handle 값"] --> stop
a3["미초기화 handle"] --> stop
a4["수명 붕괴의 오용"] --> stop
그림 16: 장시간 가동에서는 ‘가끔 이상한 에러’로 넘어가는 사고를, Handles 체크는 그 자리에서 멈춥니다.
6.2. !htrace로 open / close 스택 보기
Handles가 고마운 점은 handle tracing과 궁합이 좋은 점입니다.
여기부터는 디버거를 쓰므로, 먼저 WinDbg를 넣어 둡니다. Debugging Tools for Windows로 배포되며, Application Verifier와 같은 Windows SDK 설치 프로그램에서 넣을 수 있습니다.
windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC
첫 줄의 옵션은 주문이 아닙니다. Application Verifier가 검출 시에 던지는 예외는 세 종류입니다.
| 옵션 | 예외 | 언제 나오는가 |
|---|---|---|
av |
액세스 위반(0xC0000005) |
heap의 버퍼 초과를 검출했을 때 |
ch |
무효 핸들(0xC0000008) |
invalid handle 사용을 검출했을 때 |
sov |
스택 오버플로(0xC00000FD) |
초기 스택이 부족하다고 판단했을 때 |
그리고 -xd는 그 예외를 second chance에서 잡는다는 지정입니다. first chance는 Application Verifier 자신이 처리해 stop 정보를 조립하므로, 디버거가 먼저 끼어들면 곤란하다는 사정입니다. 이미 시작한 디버거에서 설정한다면 sxd av, sxd ch, sxd sov를 치는 것과 같습니다.
!htrace로 보고 싶은 것은 대체로 이 부근입니다.
- 그 handle이 어디서 open되었는가
- 어디서 close되었는가
- invalid handle로 참조되었는가
- 예상보다 많이 open이 쌓이지 않았는가
실제 보이는 모습도 실어 둡니다. 다음은 공식 문서에 실린 출력 예이며, 당사 환경의 것은 아니지만 형태는 그대로입니다.
invalid handle을 밟으면 먼저 이렇게 나옵니다.
Invalid handle - code c0000008 (first chance)
===================================================
VERIFIER STOP 00000300: pid 0x558: invalid handle exception for current stack trace
C0000008 : Exception code.
0012FBF8 : Exception record. Use .exr to display it.
0012FC0C : Context record. Use .cxr to display it.
00000000 :
===================================================
This verifier stop is continuable.
After debugging it use 'go' to continue.
===================================================
이어서 !avrf를 치면, 지금 무엇이 켜져 있고 어떤 stop이 났는지가 나옵니다. 마지막 한 줄이 요약입니다.
0:000> !avrf
Global flags: 00000100
Application verifier global flag is set.
Application verifier settings (00000004):
- no heap checking enabled!
- handle checks
Page heap is not active for this process.
Current stop 00000300 : c0000008 0012fbf8 0012fc0c 00000000 .
Using an invalid handle (either closed or simply bad).
그 핸들의 이력을 !htrace로 보면, OPEN / CLOSE / BAD REFERENCE가 각각 스택과 함께 늘어섭니다.
0:000> !htrace 7DC
--------------------------------------
Handle 0x7DC - BAD REFERENCE:
0x801902BE: ntoskrnl!NtSetEvent+0x6C
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - CLOSE:
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - OPEN:
0x77DE265C: KERNEL32!CreateEventA+0x66
0x010011A0: badhandle!main+0x20
--------------------------------------
읽는 법은 단순합니다. CLOSE 뒤에 BAD REFERENCE가 오면, 닫은 핸들을 다시 쓰고 있다는 뜻입니다. OPEN 스택을 보면 그 핸들을 만든 곳도 알 수 있습니다.
핸들 누수나 handle misuse가 까다로운 이유는, 마지막에 넘어진 API가 진짜 원인이 아니기 때문입니다.
!htrace가 있으면 그 handle의 이력을 꽤 구체적으로 따라갈 수 있습니다.
flowchart TB
accTitle: htrace로 핸들 이력을 읽는 법
accDescr: htrace는 핸들의 OPEN, CLOSE, BAD REFERENCE를 각각 스택과 함께 늘어놓고, CLOSE 뒤에 BAD REFERENCE가 오면 닫은 핸들 재사용임을 알 수 있으며, OPEN 스택으로 만든 곳도 알 수 있다.
open["OPEN〔만든 곳〕"] --> close["CLOSE〔닫은 곳〕"]
close --> bad["BAD REFERENCE"]
bad --> mean["닫은 handle의 재사용으로 판명"]
open -.-> stack["각 기록에 스택이 붙는다"]
그림 17: htrace 읽는 법은 단순하며, CLOSE 뒤에 BAD REFERENCE가 늘어서 있으면 닫은 핸들의 재사용입니다.
6.3. 자체 로그와 어떻게 함께 쓰는가
그렇다고 해도 Application Verifier만으로는 부족합니다. 특히 장시간 상주하는 EXE의 leak 조사를 이것만으로 하는 것은 꽤 힘듭니다.
그래서 실무에서는 다음을 함께 씁니다.
- 정기
Handle Count sessionIdresourceIdphase- create/open과 close/dispose의 lifecycle log
- verifier stop 시의 dump와 debugger 출력
이것으로 예를 들어 다음처럼 따라갑니다.
- heartbeat로
Handle Count의 기울기가 이상하다고 안다 - lifecycle log로
Create는 있는데Close가 없는 resource를 좁힌다 - verifier run으로 invalid handle이나 misuse를 앞당겨 낸다
!htrace로 open / close stack을 본다
이 조합으로 훨씬 따라가기 쉬워집니다.
flowchart TB
accTitle: 자체 로그와 verifier를 맞추는 절차
accDescr: heartbeat로 Handle Count 기울기를 알아차리고, lifecycle log로 Close가 없는 자원을 좁히며, verifier run으로 오용을 앞당겨 내고, htrace로 open과 close 스택을 보는 순서로 따라간다.
s1["Handle Count 기울기를 알아차린다"] --> s2["lifecycle log로 자원을 좁힌다"]
s2 --> s3["verifier run으로 오용을 앞당긴다"]
s3 --> s4["htrace로 스택을 본다"]
그림 18: 기울기 검출은 자체 로그, 오용 검출은 verifier라는 분업을 이 순서로 잇습니다.
7. 이상 경로 테스트 기반 만드는 법
7.1. 실행 단위를 harness로 모으기
Application Verifier는 돌아가는 중인 프로세스에 나중에 켤 수 없습니다. 설정한 뒤에 시작합니다.
게다가 설정은 명시적으로 지울 때까지 남습니다. 그래서 실무에서는 프로덕션 앱 본체보다 테스트용 harness EXE에 두는 편이 다루기 쉽습니다.
예를 들어 이런 구성입니다.
flowchart LR
A[Scenario Runner] --> B[CameraHarness.exe]
B --> C[CameraSdkWrapper.dll]
C --> D[Vendor SDK]
B --> E[Structured Log]
B --> F[Dump / Debugger]
그림 19: 시나리오당 프로세스 하나로 돌리는 harness 구성. verifier 대상은 DLL이 아니라, 그것을 돌리는 harness EXE입니다.
이라면,
- 시나리오당 프로세스 하나로 돌릴 수 있다
- leak 차이를 보기 쉽다
- AppVerifier 설정의 ON/OFF를 바꾸기 쉽다
- DLL을 시험하는 경우에도 EXE 쪽에서 다룰 수 있다
는 이점이 있습니다.
명령 이미지는 이렇습니다.
appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe
/verify가 Basics 켜기, /n이 설정 삭제입니다(5.3의 표도 참고하세요).
켜기는 시작 전, 해제는 명시적으로, 입니다.
이 부근을 harness 전제로 돌리면 설정 사고도 줄이기 쉽습니다.
7.2. 테스트 메뉴를 나누기
이상 경로 테스트 기반에서는 전부를 한 번에 하지 않는 편이 좋습니다. 대체로 다음 세 갈래로 나누면 읽기 쉽습니다.
- 정상 경로 + Basics
- 아무것도 실패를 주입하지 않는다
- verifier stop이 나오지 않는 것을 확인한다
- fault injection 계열
Low Resource Simulationevent/file/heap_alloc/virtual_alloc등을 골라 실패시킨다
- heap 깊게 파는 계열
Heaps- full page heap
- debugger 아래에서 좁은 범위에서 재현한다
여기를 나누면, ‘평소 사용에서 깨져 있는가’와 ‘저자원 상태에서만 깨지는가’가 섞이기 어렵습니다.
특히 fault injection 유무에서는 지나는 code path가 꽤 바뀝니다. 그래서 fault 없는 run과 fault 있는 run은 둘 다 돌리는 편이 좋습니다.
flowchart TB
accTitle: 세 갈래로 나눈 테스트 메뉴
accDescr: 아무것도 주입하지 않는 정상 경로 플러스 Basics, Low Resource Simulation으로 골라 실패시키는 fault injection 계열, full page heap을 debugger 아래에서 돌리는 heap 깊게 파는 계열의 세 갈래로 테스트를 나눈다.
menu["이상 경로 테스트 돌리는 법"] --> m1["정상 경로와 Basics"]
menu --> m2["fault injection 계열"]
menu --> m3["heap 깊게 파는 계열"]
m1 -.-> p1["stop이 나오지 않는 것을 확인"]
m2 -.-> p2["노린 실패를 주입한다"]
m3 -.-> p3["debugger 아래에서 좁은 범위 재현"]
그림 20: 전부를 한 번에 하지 않고 세 갈래 메뉴로 나누면, 어디서 깨져 있는지가 섞이지 않습니다.
7.3. 수집할 것
최소한 이것만은 남겨 두고 싶은 곳입니다.
| 종류 | 필요한 것 |
|---|---|
| 앱 로그 | cameraId, sessionId, phase, handleCount, error code |
| process 상태 | Handle Count, Private Bytes, Thread Count |
| debugger 정보 | !avrf, !htrace, 필요에 따라 !heap -p -a |
| dump | verifier stop 시, 또는 이상 종료 시 |
| AppVerifier 로그 | stop 기록, 필요하면 XML로 바꿔 집계 |
필요하면 AppVerifier 쪽 로그도 XML로 바꿔 집계할 수 있습니다. 다만 그것만 봐도 원인 조사가 끝나지 않는 경우가 많으므로, 자체 로그와 나란히 읽는 전제가 실무에 맞습니다.
로그가 많은 것 자체가 잘난 일은 아닙니다. 나중에 인과가 이어지는 것이 중요합니다.
7.4. 합격 조건
합격 조건도 ‘크래시하지 않았다’만으로는 약합니다. 이번 문맥에서는 적어도 다음은 필요했습니다.
- 정상 경로 + Basics에서 verifier stop이 나오지 않는다
- fault injection이 있어도, 예상한 실패는 로그에 남는다
- 어중간하게 초기화된 자원이 제대로 치워진다
- reconnect / retry 뒤에
Handle Count가 baseline 근처로 돌아온다 - verifier stop이 났을 때
sessionId/phase/ stack으로 따라갈 수 있다 - ‘무엇이 일어났는지 모른다’는 실패가 되지 않는다
여기서 중요한 것은, 깨지지 않는 것과 깨졌을 때 따라갈 수 있는 것을 나눠 평가하는 것입니다.
flowchart TB
accTitle: 합격 조건의 두 축
accDescr: 합격 조건을, 정상 경로에서 verifier stop이 나오지 않고 자원이 치워진다는 깨지지 않는 축과, 예상한 실패가 로그에 남아 문맥과 스택으로 따라갈 수 있다는 깨졌을 때 따라갈 수 있는 축으로 나눠 평가한다.
pass["합격 조건"] --> a["깨지지 않는 것"]
pass --> b["깨졌을 때 따라갈 수 있는 것"]
a --> a1["stop이 나오지 않는다"]
a --> a2["자원이 치워진다"]
b --> b1["실패가 로그에 남는다"]
b --> b2["스택으로 따라갈 수 있다"]
그림 21: ‘크래시하지 않았다’만으로는 약하고, 깨지지 않는 것과 따라갈 수 있는 것을 다른 축으로 평가합니다.
7.5. 주의점
Application Verifier는 꽤 편리하지만 마법은 아닙니다.
- 실제로 지나지 않은 코드 패스는 검증되지 않는다
- full page heap은 무겁다
- third-party SDK 쪽에서 stop이 나오기도 한다
- fault injection 있음/없음으로 지나는 코드 패스는 꽤 다르다
- 순수 managed heap leak 조사를 이것 하나로 하는 도구는 아니다
그래서 위치는 이렇습니다.
- 장시간의 기울기는 자체 로그와 counters
- native 경계의 오용은 Application Verifier
- 이상 시 인과 복원은 structured log + dump + debugger
이 분업이 가장 실무에 맞습니다.
flowchart TB
accTitle: 조사 분업의 전체 모습
accDescr: 장시간 기울기는 자체 로그와 counters로, native 경계의 오용은 Application Verifier로, 이상 시 인과 복원은 structured log와 dump와 debugger로 본다는 분업을 나타낸다.
q1["장시간의 기울기"] --> t1["자체 로그와 counters"]
q2["native 경계의 오용"] --> t2["Application Verifier"]
q3["이상 시 인과 복원"] --> t3["로그와 dump와 debugger"]
그림 22: Application Verifier는 만능 지팡이가 아니라, 보고 싶은 것마다 도구를 나눕니다.
8. 대략적인 용도 구분
- invalid handle이나 double close가 의심스럽다
Handles+!htrace
- heap corruption / use-after-free가 의심스럽다
Heaps+ full page heap +!heap -p -a
- 메모리 부족이나 자원 부족처럼 보이는 현상을 일으키고 싶다
Low Resource Simulation
- 장시간 가동에서 서서히 깨진다
- 먼저 자체의
Handle Count/Private Bytes/ lifecycle log
- 먼저 자체의
- DLL을 시험하고 싶다
- 그 DLL을 호출하는 harness EXE에 대해 Application Verifier를 켠다
처음부터 전부 켜면 대체로 로그의 안개가 됩니다. 보고 싶은 failure path에 가까운 날부터 거는 편이 훨씬 알기 쉽습니다.
9. 정리
Application Verifier의 위치는 Windows의 native / Win32 경계의 runtime verifier입니다. Handles / Heaps / Locks / Memory / TLS / Low Resource Simulation 등을 써서, 평소에는 잘 나오지 않는 failure path를 앞당겨 밟히게 할 수 있습니다.
이번 문맥에서 효과가 있던 것은, 핸들 이상이 났을 때 !htrace로 따라가기 쉬운 점, 메모리 부족이나 자원 부족처럼 보이는 현상을 머신 전체를 깨지 않고 일으킬 수 있는 점, 그리고 그때 자체 로그가 정말 도움이 되는지를 확인할 수 있었던 점이었습니다.
실무에서의 돌리는 법으로는, 정상 경로 + Basics와 fault injection 계열을 나누고, harness EXE를 마련해 짧은 수명 프로세스로 시나리오를 돌립니다. 그 위에 자체 로그, dump, debugger 정보와 맞추고, 장시간 leak의 기울기 자체는 자체 counters로 본다는 분담이 됩니다.
Application Verifier는 ‘거의 안 나오는 이상’을 “우연히 기다리는” 것이 아니라, “이쪽에서 마중 나가는” 도구입니다.
장치 제어 앱에서는 깨지지 않는 것도 중요하지만, 깨졌을 때 무엇이 일어났는지를 설명할 수 있는 것이 그만큼 중요합니다. 그 의미에서 꽤 실무에 맞는 도구라고 생각합니다.
이전 글: 산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편
10. 참고 자료
- 이전 글: 산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편
- Application Verifier - Overview
- Application Verifier - Testing Applications
- Application Verifier - Tests within Application Verifier
- Application Verifier - Debugging Application Verifier Stops
- Application Verifier - Features
- !htrace (WinDbg)
- !avrf (WinDbg)
- Download Debugging Tools for Windows
- Windows SDK 다운로드
- GetProcessHandleCount 함수 (processthreadsapi.h)
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
산업용 카메라 장기 가동 크래시 조사 - 핸들 누수편
Windows 앱이 장시간 가동 뒤에 갑자기 크래시할 때의 보는 법을, 산업용 카메라 제어 앱 사례로 핸들 누수를 찾는 방법과 로그 설계 관점에서 정리합니다.
TCP 재전송으로 산업용 카메라 통신이 멈추는 원인 분리
TCP 재전송으로 산업용 카메라 통신이 수 초 정지할 때의 원인 분리 방법을 패킷 로스, RTO, RFC1323 타임스탬프, Wireshark 확인 포인트와 함께 정리합니다.
절전에서 재개하면 깨지는 앱 ── 전원 이벤트의 구조와 재개에 강한 업무 앱을 만드는 방법
노트북을 열었더니 업무 앱의 통신이 끊어져 있었다――원인은 절전을 전제하지 않은 설계입니다. WM_POWERBROADCAST에 의한 알림의 흐름, Modern Standby의 동작, 끊김·재연결 설계, 절전 억제와 조사 명령까지를 1차 정보로 설...
「응답 없음」의 정체 ── Windows가 앱을 「멈췄다」고 판단하는 구조와, 멈추지 않는 설계
Windows의 「응답 없음」은 창이 5초 동안 메시지를 꺼내지 않으면 OS가 판단해 고스트 창으로 교체하는 구조입니다. 판단의 내부 동작부터 멈추는 전형적인 원인, UI 스레드에서 무거운 처리를 빼내는 설계, hang 조사 절차까지 설명합니다.
WPR/WPA 실무 ── 「PC 전체가 느리다」를 시스템 전체에서 조사하는 성능 조사 입문
「PC 전체가 느리다」「부팅이 느리다」처럼 작업 관리자로는 따라갈 수 없는 성능 문제는 OS 전체 ETW 트레이스를 모아 읽는 WPR/WPA로 조사합니다. wpr.exe 수집 절차부터 WPA에서 CPU·대기·디스크 I/O를 읽는 법까지 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
장애 조사 & 장기 가동 장애
간헐적 장애, 통신 진단, 장기 가동 크래시, 실패 경로 테스트 기반을 정리한 토픽 페이지입니다.
관련 사례
실제 정리와 개선 진행 방식이 가까운 사례 페이지입니다.
Application Verifier로 실패 경로 테스트 기반을 만든 사례
이후 장애 조사를 쉽게 하기 위해 먼저 실패 경로 테스트 기반을 구축한 사례 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
장애 조사 & 원인 분석
Application Verifier와 이상 경로 테스트 기반은, 장애 재현과 원인 특정을 진행하는 버그 조사·원인 분석의 핵심 주제입니다.
기술 상담 & 설계 리뷰
이상 경로 테스트나 관측점을 설계에 어디까지 넣을지 정리하고 싶을 때는, 기술 상담·설계 리뷰로 검토할 수 있습니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Application Verifier란 무엇인가요?
- Windows의 user-mode 앱에 대한 런타임 검증 도구입니다. 실행 중인 앱의 OS API 이용과 자원 취급을 감시해 invalid handle 사용이나 heap corruption 같은 의심스러운 사용을 검출하거나, 의도적으로 실패를 주입할 수 있습니다. 정적 분석이나 단위 테스트와 달리, 실제로 그 코드 패스를 지났을 때 어떻게 깨지는지를 보는 도구이므로, 평소 기능 테스트에서는 보이지 않는 failure path를 드러내는 데 적합합니다.
- Application Verifier로 메모리 부족을 재현할 수 있나요?
- Low Resource Simulation을 쓰면, 정말로 머신의 RAM을 다 쓰지 않아도 메모리 부족이나 자원 부족에 가까운 현상을 앞당겨 일으킬 수 있습니다. 원리는 fault injection이며, HeapAlloc, VirtualAlloc, CreateFile, CreateEvent 같은 API 호출을 일정 확률로 일부러 실패시킵니다. 특정 DLL만 골라 실패를 주입할 수도 있어, 자체 래퍼와 vendor SDK가 섞인 구성에서도 다루기 쉽습니다. 다만 처음부터 무엇이든 실패시키면 로그를 읽기 어려워지므로, 보고 싶은 failure path에 가까운 것부터 좁혀 켜는 것이 요령입니다.
- 핸들 누수 조사에 Application Verifier를 쓸 수 있나요?
- Handles 체크를 켜면 close된 핸들을 다시 쓰는 식의 invalid handle 사용을 검출할 수 있고, handle tracing도 자동으로 켜지므로 !htrace로 그 핸들의 open / close 스택을 따라갈 수 있습니다. 다만 장시간 상주하는 EXE의 leak 조사를 Application Verifier에만 맡기는 것은 현실적이지 않습니다. 정기적인 Handle Count 기록이나 resource lifecycle의 자체 로그와 함께 쓰고, 기울기 검출은 자체 로그, 오용 검출은 verifier로 나누는 편이 실무에 맞습니다.
- Application Verifier를 DLL 테스트에 쓰려면 어떻게 하면 되나요?
- Application Verifier를 켜는 대상은 그 DLL을 실제로 돌리는 테스트용 EXE입니다. 실행 중인 프로세스에 나중에 켤 수는 없고, 설정한 뒤에 시작해야 합니다. 설정은 명시적으로 지울 때까지 남으므로, 프로덕션 앱 본체보다 테스트용 harness EXE에 두는 편이 다루기 쉽습니다. 시나리오당 프로세스 하나로 돌리면 leak 차이도 보기 쉽고, 설정의 ON/OFF도 바꾸기 쉬워집니다.