덤프 파일을 통한 사후 디버깅 실용 테크닉 ndc2012
TRANSCRIPT
덤프 파일을 통한 사후 디버깅
실용 테크닉
김이선
veblush@[nexon|gmail]
BNB 프로그래머
카트라이더 리드 프로그래머
버블파이터 프로토타입
리드 프로그래머 에버플래닛
리드 프로그래머 던전엔파이터
테크니컬 디렉터 GTR
프로그래머
게임 프로그래밍 11년차
배포후 디버깅
배포전 디버깅
개발은 곧 디버깅
배포후 디버깅
배포전 디버깅
배포후 디버깅
개발은 곧 디버깅
도입
특정 상황에서 프로세스의 상태 를
디버깅을 목적으로 파일로 남긴 것
덤프 파일
Exception
System
Thread
Module
Memory
…
프로세스 상태
Full- Dump?
아니오
예
6 KB
4804 KB
덤프 파일의 정보를 보기
>dat i A.dmp
TheadId: 1956 Code: 80000003 Flags: 00000000 Record: 00000000 Address: 004011cf
Processor/CPU Arch: 0 Level: 6 Revision: 5894 VendorId: Intel Version: 00010676 Feature: bfebfbff Extended: 6d960e89 OS Version: 6.1 Build: 7601 Platform: 2 CSD: SP1
Thread(id:1956) SCount: 0 PClass: 32 Priority: 0 Stack: 1244892 EIP: 004011cf EBP: 0012ff40 Thread(id:2910) …
Modules(0) Name: Tests.exe Base: 00400000 Size: 00006000 CheckSum: 00006722 Stamp: 4f926cc4 CV_GUID: 244b5515- CV_AGE: 43 CV_Path: Tests.pdb Modules(1) Name: ntdll.dll Base: 777e0000 Size: 0013c000 CheckSum: 00141016 Stamp: 4ec49b60 CV_GUID: 93d2cd7- CV_AGE: 2 CV_Path: ntdll.pdb …
addr: 0012fedc, size: 00000124 addr: 0040114f, size: 00000100 addr: 0040114f, size: 00000100 addr: 00501000, size: 00010000 addr: 00520000, size: 00021000 …
Exception System Module Memory
Thread
VS IDE 가 온라인 디버깅 처럼 보여줌
미니덤프를 남기고 수집해서
VS IDE 로 디버깅하는 시스템은 필수
편하다!
그런데?
병원에서 디버깅하는 내용을 다룬 드라마
까다로운 경우의 사후 디버깅을
해결하기 위해 했던 작업 소개
덤프 파일과 PDB 연결 여러 개의 덤프 파일 분석
콜스택
힙 메모리
덤프 파일과 PDB 파일의 키
소스
EXE
PDB
컴파일러 링커
K
K
EXE K
DMP
Minidump
K
PDB Path (UTF-8)
Age
덤프 파일에 맞는 PDB 파일 찾기
K
K K
K K
L L
M M
Symbol Server
M
?
심볼 서버를 사용하고 있지 않다면
바로 도입하는 것을 추천!
어? 잘 되잖아?
뭐가 문제?
실행파일 보안 솔루션! Themida, ASProtect, …
K K
EXE K
DMP
Minidump
! EXE
K
보안
EXE K
DMP
Minidump
! EXE
K
보안
Name: Tests.exe Base: 00400000 Size: 00173000 CheckSum: 000b1076 Stamp: 4f936fe3 CV_GUID: CV_AGE: CV_Path:
Name: Tests.exe Base: 00400000 Size: 00005000 CheckSum: 0000fdf1 Stamp: 4f936fe3 CV_GUID: 152589ac- CV_AGE: 1 CV_Path: Tests.pdb
이를 해결하기 위해
DMP 파일에 원본 EXE 키를 연결하자!
DMP !
EXE K K
EXE K
DMP !
EXE K
DMP !
DMP K
dat
수집
K
DMP → EXE, PDB 연결을 만들어줌
>dat l -m A.exe A.dmp
수집된 DMP 가 어떤 EXE 랑 연결되는지?
클라이언트 버전 or 빌드 타임을 사용!
EXE K
DMP !
?
EXE K
DMP !
EXE K
DMP !
DMP K
dat
수집
K
T T T
T T
T EXE L T’
EXE M T”
클라이언트 덤프 수집 단계에
올바른 PDB 연결 단계를 꼭 넣자!
덤프 파일과 PDB 연결
여러 개의 덤프 파일 분석 콜스택
힙 메모리
적합한 기준!
게임 내용에 맞춰
문제 시점의 맵, 스킬, 장비, …
대부분 역할을 잘 해줌
그런데…
그런데 외부 모듈이나
절대 발생할 수 없는 로직에서 문제가
나타난다면?
혹은 에러를 내는 양상이 매번 바뀐다면?
클라이언트의 경우 실행 환경이 통제되지 않는데
H/W 결함
Driver 결함
바이러스
악성코드
악성유저
외부 요인인지 아닌지
잘 판단하는 것이 중요!
EIP 차트 OS CPU GPU
EIP 차트 OS CPU GPU
환경 편재에 주목!
외부 요인을 추정할 수 있다!
OS
CPU
GPU
윈도우 9X : OS + 드라이버 불안 OS Heap 차이, ?
CPU 자체 보다는 메인 보드인 경우가 더 많음
그래픽 카드 드라이버!!!
모듈 외부 프로그램에 의한 영향
메신저, 보안 솔루션 등의 DLL 이 게임 프로세스에 연결됨.
악성 프로그램 DLL 도 마찬가지
CASE STUDY
[던전엔 파이터]
2011 년 상반기
Visual Studio 2003 → 2008
Migration
개발 배포 괜찮나?
2003 2008
재배포
2003
아싸!
아니요
예 2008
덤프, 클레임
VS2008 빌드 버전: 유저의 클레임
“채널을 선택하면 클라이언트가 종료됩니다”
VS2008 빌드 버전: 덤프 리포트
HeapAlloc, HeapFree 수행 중 크래시가 늘어남
HeapAlloc, HeapFree 중에 크래시가 나는 것은
십중팔구 힙 깨짐
힙 깨짐에 중점을 두고
코드 리뷰 + 트랩 코드 패치
해당 문제를 유발 하는 코드가 없음!
악성 프로그램이 문제의 원인!
문제의 덤프 파일들로부터 모듈 편재 확인
악성DLL 악성DLL
힙깨짐
크래시
덤프 보고의 환경 편재를 잘 살펴서
외부 요인을 잡아내자!
덤프 파일과 PDB 연결
여러 개의 덤프 파일 분석
콜스택 힙 메모리
콜스택은?
쓰레드의 현재 함수 호출 상태를 보여줌
Visual Studio 는
어떻게 콜스택을 보여줄까?
400h int Func(int x, int y) { 403h int a = x * y; 40Bh return a; 411h } 420h void Test() { 423h Func(2, 5); 435h }
ADDR VALUE DESC
104h 10 a
108h 118h EBP
10Ch 435h EIP
110h 2
114h 5
118h 12Ch EBP
11Ch 470h EIP
코드 스택
STACK
…
EBP
EIP
…
EBP
EIP
…
EBP
EIP
…
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
EIP:010E2E13
EBP:0020FA58
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
EIP:010E2E13
EBP:0020FA58
콜스택 구성에 필요한 것:
스택 메모리, EBP, EIP
그 중에 하나만이라도 깨지면?
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
EBP:0020FA5c
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
EBP:002BFE64
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
EBP:002BFE64
보통의 콜 스택 재구성은
스택, EBP 가 정상일 때만 동작!
스택, EBP 깨졌을 수도 있을 때는
다른 방법을 사용해보자!
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
1. 스택 메모리에서
코드 주소 추리기
Func2
Func1
Test4
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
2. 스택 메모리에서
스택 주소 영역 추리기
ADDR VALUE
0020fa58 0020fa64
0020fa5c 010e2e29
0020fa60 004b2ff8
0020fa64 0020fa74
0020fa68 010e2e4d
0020fa6c 00000005
0020fa70 004b2ff8
0020fa74 0020fa88
0020fa78 010e2e6d
0020fa7c 00000002
0020fa80 00000005
3. 콜 스택 추정 +Func2
+Func1
+Test4
예외 쓰레드 콜스택 강제 재구성
>dat c A.dmp
함수 재구성에 프로그래머의 해석이 필요
하지만 스택 파괴에도 최대한 정보를 제공!
< 팁 >
함수 스택에 디버그 변수 저장하기
struct Item {
int id;
int count;
};
int GetCount(const vector<Item>& items) {
int count = 0;
for (i in items) {
VERIFY(i->count > 0);
count += i->count;
}
return count;
}
int GetCount(const vector<Item>& items) {
int count = 0;
for (i in items) {
int item_id = item.id;
int item_count = item.count;
VERIFY(i->count > 0);
count += i->count;
}
return count;
}
그냥 Trace 쓰지 왜? 간단히 덤프 파일로 볼 수 있다면 더 간편해서!
재귀함수 등 콜 스택의 여러 변수를 봐야 한다!
#define DEBUG_VAR(T, var) \
volatile T& var = *(T*)_alloca(sizeof(T));
int GetCount(const vector<Item>& items) {
DEBUG_VAR(int, item_id);
DEBUG_VAR(int, item_count);
int count = 0;
for (i in items) {
item_id = item.id;
item_count = item.count;
VERIFY(i->count > 0);
count += i->count;
}
return count;
}
DEBUG_VAR(int, item_id);
DEBUG_VAR(int, item_count);
스택 할당은 비용이 거의 없으므로
간편하게 써보자! (다만 스택 오버플로우는 조심)
덤프 파일과 PDB 연결
여러 개의 덤프 파일 분석
콜스택
힙 메모리
덤프에 힙 메모리를 넣으면
디버깅에 많은 도움을 줌
class Monster
: public Object
{
int id;
int ai_state;
int more_over;
virtual ~Monster();
};
몬스터
void Move(Monster* m)
{
if (m->ai_state == 0)
m->ai_state = 1;
verify(Route(m));
}
사용
일반
힙포함
< 1MB
프로세스 메모리 용량
용량이 크지만 디버깅이 무척 편함
개발버전 클라이언트, 서버, 툴은 적극 사용!
배포 클라이언트의 경우 선택적으로 사용
풀 덤프만 있으면
이제 이 버그는 제겁니다!
void OnAttack(Monster* m, Player* p)
{
SetAttackTarget(m, p);
log(“M:%d – P:%d”, m->id, p->id);
verify(p->valid());
}
사용
M:1500 – P:5
M:1901 – P:5
로그
모든 객체에 쉽게 접근할 수 없다. 콜스택 지역 변수에 남았거나,
전역변수 (싱글턴 등) 로부터!
덤프 파일에 힙 메모리 내용이 다 있으니
어떻게 해볼 수 있지 않을까?
1500 이 나오는 메모리 주소를 찾아
Watch 로 살펴보자!
(Monster*)(0x043BD14-4)
메모리를 다 뒤져서 전부 찾아서 Watch!
덤프 메모리에서 32bit 1500 모두 찾기
>dat m –f i1500 A.dmp
무식하지만 작동하는 방법!
좀 더 나아가 보자!
잘못된 주소 올바른 주소
메모리에 있는 객체를 찾아보자?
vftbl
id
ai_state
more_over
…
&LOC
&::~dtor
…
signature
offset
cdOffset
pTypeDesc
pClassDesc
vftbl
data
name[0]
…
Object vftbl COL Type
메모리에서 모든 객체를 찾아
주소를 출력한다.
Monster* 타입?
vftbl 이 있는 객체
Monster 의 vfptr: 0x4102c4
메모리의 모든 Monster 객체를 출력
>dat o –t Monster A.dmp
찾은 Monster 객체에 대해
1500 숫자를 가지고 있는 객체만 출력
sizeof(T) 안에서
Monster 의 vfptr: 0x4102c4
메모리의 모든 Monster 객체 중
32bit 1500 을 가지고 있는 것들 출력
>dat o –t Monster –s 16 –f i1500 A.dmp
힙 메모리에서 객체 찾기 응용
class Player
: public Object
{
string name;
int ip_addr;
int cid;
virtual ~Player();
};
플레이어
class string
{
int len;
char buf[16];
char* ptr;
};
문자열
길이가 15 이하면 여기에
16 이상이면 동적할당
힙 메모리에서
이름이 test_player_9000 인
Player 객체 찾기
길이가 16 이니 string 이 문자열 포인터를 가리킴. 먼저 문자열 주소를 찾자.
문자열 주소 0x00391840 을 포함하고 있는 Player 객체를 찾자.
길이가 16 이니 string 이 문자열 포인터를 가리킴. 먼저 문자열 주소를 찾자.
문자열 주소 0x00391840 을 포함하고 있는 Player 객체를 찾자.
이제 모든 객체를 찾을 수 있어요! (vftbl 은 있어야 겠지만…)
정리
덤프는 프로그래머의 친구!
좀 더 많이 사용해보자!
덤프 분석툴도
틈틈이 만들어 보자!
>dat
감사합니다!
http://code.google.com/p/dump-analysis-tool/