덤프 파일을 통한 사후 디버깅 실용 테크닉 ndc2012

Post on 10-Jun-2015

10.755 Views

Category:

Technology

9 Downloads

Preview:

Click to see full reader

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/

top related