온라인 게임에서사례로 살펴보는디버깅KGC 2009박일리니지2 서버팀parkpd.egloos.com
저를 소개합니다박일2006 년 초 리니지2 서버팀으로 이동The Chaotic Chronicle – Oath of Blood (Chronicle 5) 부터 credit 에 제 이름을 볼 수 있습니다블로그 : http://parkpd.egloos.com
경고:발표에서 사용된 사례들은대부분 실제사례를근거로 하여재구성된 것들입니다.
왜 디버깅인가?
버그?충돌과 응답없음, 성능과 측정 가능성의 저하부정확한 결과, 보안 취약점, 일관성 없는 사용자 인터페이스요구 사항 불충족
디버깅을 잘 하는 방법 배우기나와 다른 사람의 문제 해결 과정을 관찰내성법“문제가 무엇이었나” 보다,“어떻게 문제를 찾았나” 가 더 중요의사보다 유리한 점프로세스를 여러 번 다양하게 죽여 볼 수 있다웃기는 점디버깅이 오래 걸릴 수록, 고마워한다
버그 찾기버그 재현버그 분석해결책 결정 및 예방
버그 찾기dump 로 Crash 난 위치 확인최근에 고친 파일부터 diff좋은 diff 툴, 일일빌드이상현상이 생긴 시간대 근처 에러로그 확인내가 고친 코드를 먼저 의심다른 사람과 얘기해보자(Bug Talk)디버깅은 근성이다도저히 모르겠다 싶을 때 1시간만 더 보자어셈블리는 항상 가장 마지막에서버라면 실제 서버를 터미널 서비스로 들어가 작업관리자를 보자
Everybody liesfalse positive(거짓양성)예전 바이너리를 쓰고 있다던가false negative(거짓음성)QA 가 고쳐지지도 않은 걸 고쳐졌다고 하거나플라시보 효과예전부터 있던 코드도 100% 믿지 말자
// Dump 보기 실습typedefBOOL(WINAPI*MINIDUMPWRITEDUMP)(HANDLEhProcess,...);LONGTopExceptionFilter(LPEXCEPTION_POINTERS pExp){LONGretval=EXCEPTION_CONTINUE_SEARCH;HMODULEhDll=NULL;hDll=::LoadLibrary(_T("DBGHELP.DLL"));if(hDll){MINIDUMPWRITEDUMPpDump=::GetProcAddress(hDll,"MiniDumpWriteDump");if(pDump){HANDLEhFile=::CreateFile(_T("Crash.dmp"),..);if(INVALID_HANDLE_VALUE!=hFile){_MINIDUMP_EXCEPTION_INFORMATIONExInfo;ExInfo.ThreadId=::GetCurrentThreadId();ExInfo.ExceptionPointers=pExp;ExInfo.ClientPointers=NULL;BOOLbOK=pDump(GetCurrentProcess(),GetCurrentProcessId(),hFile,MiniDumpNormal,&ExInfo,NULL,NULL);if(bOK){tcout<<" dump to Crash.dmp"<<endl;retval=EXCEPTION_EXECUTE_HANDLER;}::CloseHandle(hFile);}}::FreeLibrary(hDll);}returnretval;}int_tmain(intargc,_TCHAR*argv[]){SetUnhandledExceptionFilter(TopExceptionFilter);TCHAR*p=NULL;p[1]=_T('M');return0;}
minidump를 출력하기.dmp파일을 만드는 것 보다 장점은?이메일로 전송Label, 빌드별 바이너리, pdb를 SVC 에 저장라이브용 바이너리 만든 후, 이메일로 label 공유
Base Line
Base Line지금 상태는 정상인가?평소 CPU 는 몇 % 정도였나?의도대로 최적화가 되었나?
Performance Countersvoid start(constchar*pszCounter){PdhOpenQuery(NULL,0,&hQuery);PdhValidatePath(pszCounter);PdhAddCounter(hQuery,pszCounter,0,&hCounter);}longcurrent()const{PdhCollectQueryData(hQuery);PDH_FMT_COUNTERVALUEvalue;PdhGetFormattedCounterValue(hCounter,PDH_FMT_LONG,NULL,&value);returnvalue.longValue;}int_tmain(intargc,_TCHAR*argv[]){Perfmoncpu("cpu.csv");Perfmonmem("mem.csv");cpu.start("\\Processor(_Total)\\% Processor Time");mem.start("\\Memory\\Pool Nonpaged Bytes");for(inti=0;i<3;++i){Sleep(1000);printf("CPU load: %d%, Nonpaged bytes: %d KB\n",cpu.current(),mem.current()/1024);}}CPUload:99,Nonpagedbytes:22408KBCPUload:0,Nonpagedbytes:22500KBCPUload:3,Nonpagedbytes:22400KB
Performance Countersbase line 만들기Counter 를 log 나 DB 에 주기적으로 저장counter 예시SQL Server: SQL Statistics: Batch Requests/sec초당 요청 받은 SQL 배치 요청 수SQL Server: Buffer Manager: Buffer Cache Hit Ratio90 이상이어야 함SQL Server: Locks: Lock Waits/sec잠금대기요청수Process: Page Faults/sec프로세스가 Cache Hit하지 않은 페이지수
사례 – 특정 머신Tomcat 서버가 죽었음JVM의 다운으로 인해서 발생되었으며죽은 시점은 GC 과정 중이었음Log4j를 통한 로그 모니터링 결과에는 죽기 전 후 특이 사항 발견되지 않음
JVM 에러로그# An unexpected error has been detected by HotSpot Virtual Machine:#  Internal Error (50532D41524B33574545502445434F5241544F520E4350500024), pid=27607, tid=1828866976# Java VM: Java HotSpot(TM) Server VM (1.5.0_12-b04 mixed mode)---------------  T H R E A D  ---------------Current thread (0x080ffe48):  VMThread [id=27611]Stack: [0x6cfa4000,0x6d025000),  sp=0x6d023ae0,  free space=510kNative frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)V  [libjvm.so+0x512633]V  [libjvm.so+0x1c862c]V  [libjvm.so+0x51a3f0]V  [libjvm.so+0x438228]C  [libpthread.so.0+0x53cc]VM_Operation (0x66f6ae40): parallel gc failed allocation,mode: safepoint, requested by thread 0x6bbcf7d0HeapPSYoungGen      total 17216K, used 696K [0xaa370000, 0xabc90000, 0xb1530000)eden space 16512K, 0% used [0xaa370000,0xaa370000,0xab390000)  from space 704K, 98% used [0xab3d0000,0xab47e1d0,0xab480000)  to   space 4608K, 0% used [0xab810000,0xab810000,0xabc90000)PSOldGen        total 629440K, used 627294K [0x71530000, 0x97be0000, 0xaa370000)  object space 629440K, 99% used [0x71530000,0x979c7b48,0x97be0000)PSPermGen       total 37888K, used 37271K [0x6d530000, 0x6fa30000, 0x71530000)  object space 37888K, 98% used [0x6d530000,0x6f995ed0,0x6fa30000)
다행인지 불행인지동일한 문제가 터짐과거의 이력을 조사해보니 동일한 장비에서 같은 문제가 여러 번 있었음
하드웨어에 빨간 불이 들어오고 있었음사례 – 기계는 믿을 수 있는가특정 머신, CPU, 그래픽카드, NIC
사례 – 언제 문제가 발생했나?특정 시각? 특정 요일? CPU 가 튀는 주기는?게임 서버가 새벽 2시만 되면 죽었던 이유는?Windows Update 가 새벽 2시에 실행바이러스 update 창이 200 개 이상 오픈같은 port 를 bind 계속하면 port 차단서버관리툴이 실행될 때마다 특정 port 를 bind 하게 설계일본 IDC 에서 꼭 설치하게 하는 패킷 필터링 시스템의 DDOS, Garbage Attack 탐지기가 해당 port 를 차단얼마만에 죽는가특정 tick 에 죽는 잘못된 코드와 memory leakPrintf대신 사용한 로그 파일이 엄청나게 커짐Memory leak은 아니지만, 너무 많은 메모리 할당2G 는 절대 큰 용량이 아님결국 일본 찾아가서 해결
버그 찾기버그 재현버그 분석해결책 결정 및 예방
무조건 재현! 재현!현상은 원인이 있기 때문에 나타난다. 정상적인 재현이 불가능하면, 억지로라도 재현해라.재현 할 때는 BP 걸어놓고 한 step 씩 이동하면서 확인재현할 때는 문제가 되는 현상을 데이터까지 똑같게택배로 컴퓨터를 아예 받아와라. 고친 게 하나도 없어도, 환경만 바뀌면 버그 발생 가능설정 파일, 하드웨어, OS 버전, 보안 프로그램 동작 여부버그 report 를 그대로 믿지 마라 QA 에서도 아는 선 안에서만 리포트를 쓴다QA 분들에게 내 개발서버로 접속해서 재현해 달라고 부탁해외 지사에서 보낸 버그 리포트는 특히 심하다
사례 - 경매라이브 서버가 아닌 테스트 서버, 그 중 한 npc에서만 발생재현도안 되어서, 별 문제 없겠지 생각하고 무시업데이트 후에 전 라이브 서버에서 문제 발생 이유 : 라이브 서버에서는 처음에 개발자가 직접 seed 값을 넣어줘서 경매에 문제 없었지만, 테스트 서버의 경우 개발자가 여러 npc중 한 npc만 까먹고 seed 값을 넣어주지 않았기 때문에 계속 문제가 있었음처음부터 문제가 있었는데도, QA 에서는 ‘잘 되다가 저번 주부터 문제가 생겼음’ 이라고 보고했고, 개발팀도 QA 만 믿고특이한 사례보고라고 생각함대규모 업데이트를 위해 모든 seed 값을 초기화했더니, 테스트 서버와 동일한 문제가 모든 npc에서 발생결론증상이 있다면 무시하지 말자[사례보고] 라는 제목 때문에 선입관 생김
사례보고를무시하면한 방에훅 간다
사례 - OpenMP수 십대의 개발 서버 중 한 서버에서만 에러 발생알고 보니 특정 머신(하퍼타운)에서만 에러 발생. 이유는?제대로 MT 를 지원하는 머신에서 테스트물리 core 가 여러 대인 CPU 머신 사용하자하퍼타운을 쓰는 다른 개발자는 설정에서 omp사용을 빼 놨기 때문에 에러나는 줄 몰랐음
사례 – 공성과 결자해지voidLoadData(){// do somethingCCastlec(nCastleNum);// dead code???return;}
사례 – 공성과 결자해지voidLoadData(){// do somethingCCastlec(nCastleNum);// dead code? NO!!!return;}classCCastle{public:CCastle(intnId){g_CastleDB.Register(nId, this);}};
사례 – 토요일에만 실패하는단위테스트요일 index 문제GetLocalTimewDayOfWeek : The day of the week. Sunday = 0, Monday = 1해외 : 특정 요일에만 실패
버그 찾기버그 재현버그 분석해결책 결정 및 예방
보여주기만 해도버그를찾을 수 있다
Visualizer in Visual Studio 2005
Visualizer in C++autoexp.dat으로 watch 창 변경C:\Program Files\Microsoft Visual Studio 8\Common7\Packages\Debugger\autoexp.dat[AutoExpand]CMyData =head=<m_Head> m_Tail=<m_Tail,x> name=<m_Name,su>classCMyData{public:CMyData(inth,intt):m_Head(h),m_Tail(t){}intm_Head;charm_Name[1024];intm_Tail;};int_tmain(intargc,_TCHAR*argv[]){typedefvector<CMyData*>DataList;DataListdata;for(inti=0;i<10;++i){data.push_back(newCMyData(i,i+1));}return0;}
Debug Windows 1{[function],[source],[module]} 뒤에location, variable_name, expression특정 함수의 static local 변수 보기void Test() { static intsLocalNum = 0; }{Test,,}sLocalNum특정 dll의 전역변수 : {,,foobar.dll}g_pMyStructclassCTest{public:staticCTest&Inst(){staticCTests;returns;}voidCheckValidate(){OutputDebugString(L"T\n");m_Test.push_back(1);}std::vector<int>m_Test;};int_tmain(){CTest::Inst().CheckValidate();return0;}
Debug Windows 2Pseudo Register@eax : 리턴값@err : GetLastError@HANDLES현재 프로세스의 핸들 갯수@ebp : 지역 변수 시작 지점@esp : 최상위 스택
사례 – BotvoidCPlayer::OnItemExchange(intnItemCount1,BYTE*pData1,intnItemCount2,BYTE*pData2) {for(inti=0;i<nItemCount1;++i){// do something}for(inti=0;i<nItemCount2;++i){// do something}}
사례 – hp 회복 문제classCPlayer{public:CPlayer():m_HP(1.0){}voidOnRegenTick();doublem_HP;doublem_Buff[3];};voidCPlayer::OnRegenTick(double delta){m_HP=m_HP * 1,05+(0.5*delta)+(10.0*0.3)/(52+m_Buff[1])+(m_HP/m_Buff[2]*3.0)+2;}
사례 - MSVC2005 최적화 오류classVec{intx,y,z;Vec&operator=(constVec&v){if(this!=&v){x=v.x;y=v.y;z=v.z;}return*this;}};Vecv1,v2;v1=v2;/Ot 컴파일러 옵션으로 빌드한 프로그램을 실행할 때 일부 "if" 문은 실행 안 될 수 있음Visual Studio 2005 sp1 에서 수정
BUG?(x != x) : true(x == x) : false(y > x) : false(y < x) : false
BUG?(x != x) : true(x == x) : false(y > x) : false(y < x) : falseNaN(Not a Number)double answer = sqrt(-1.0);
// http://msdn.microsoft.com/en-us/library/w22adx1s%28VS.80%29.aspxclassCData{public:enum{HALF_NUM=0x7fffffff,MAX_NUM=0xffffffff};CData():m_dPoint(1.0){m_dData[0]=1.3;m_dData[1]=1.5;m_nData[0]=HALF_NUM;m_nData[1]=MAX_NUM;}voidGetBonus(intnBonusIndex){m_dPoint*=m_dData[nBonusIndex];}doublem_dPoint;doublem_dData[2];unsignedlongm_nData[2];};int_tmain(intargc,_TCHAR*argv[]){CDataa;a.GetBonus(2);wcout<<L"isnan : "<<_isnan(a.m_dPoint)<<L'\n';wcout<<"(1.0 < a.m_dPoint) : "<<(1.0<a.m_dPoint)<<L'\n';wcout<<"(1.0 >= a.m_dPoint) : "<<(1.0>=a.m_dPoint)<<L'\n';return0;}// outputisnan:1(1.0<a.m_dPoint):0(1.0>=a.m_dPoint):0
사례 – 메모리 침범classCTest{public:conststaticintMAX_DATA=10;intm_Data[MAX_DATA];vector<int>m_Nums;};int_tmain(intargc,_TCHAR*argv[]){CTestt;t.m_Nums.push_back(1);ZeroMemory(t.m_Data,sizeof(int)*CTest::MAX_DATA+10);t.m_Nums.push_back(1);// Crashreturn0;}#include <boost/array.hpp>int_tmain(intargc,_TCHAR*argv[]){intcArray[256];boost::array<int,5>aTest={1,2,3,4 ,5};boost::array<int,256>aArray;cArray[256]=0;// not always dieaArray[256]=0;// always die// Assertion failed: i < N && "out of range", file , line 91return0;}
사례 – goto변수 선언문을 건너뛰기#define goto WhatTheHellint_tmain(intargc,_TCHAR*argv[]){gotoJumpToHere;intnTestValue;JumpToHere:nTestValue=4;wcout<<nTestValue;return0;}
사례 - random// ………………………..intGetRand1(intnMin,intnMax){intnOffset=rand()%(nMax-nMin);returnnMin+nOffset;}doubleGetRand2(doubledMin,doubledMax){return((double)rand()/(double)RAND_MAX)*(dMax-dMin);}
사례 - random// RAND_MAX : 32767intGetRand1(intnMin,intnMax){intnOffset=rand()%(nMax-nMin);returnnMin+nOffset;}doubleGetRand2(doubledMin,doubledMax){return((double)rand()/(double)RAND_MAX)*(dMax-dMin);}0 ~ 2767 확률이 2768 ~ 9999 보다 1/32767 높다.if (Rand(1000000) <= 1) // 0.0001% 과if (Rand(100000) <= 1)  // 0.001% 의차이는rand 를 RAND_MAX 번 호출했을 때 1이 나올 확률은 1/32767 = 0.00305% > (0.001% 과 0.0001%)0.001% 와 0.003% 의 차이가 없음
예제 - std::sortstructData{Data(intn):m_Num(n){}intm_Num;};boolIsLessThan(Data*a,Data*b){returna->m_Num>b->m_Num;}typedefvector<Data*>DataList;voidtest(DataList& d){for(inti=0;i<10;++i)d.push_back(newData(i));sort(d.begin(),d.end(),IsLessThan);
예제 - std::sortsort(m_data.begin() …)하는 도중에 다른 thread 에서 Num 값을 바꾸면 무한 루프가 발생할 수 있음structData{Data(intn):m_Num(n){}intm_Num;};boolIsLessThan(Data*a,Data*b){returna->m_Num>b->m_Num;}typedefvector<Data*>DataList;voidtest(DataList& d){for(inti=0;i<10;++i)d.push_back(newData(i));sort(d.begin(),d.end(),IsLessThan);
Symbol Server
Symbol Server
Symbol Server
버그 찾기버그 재현버그 분석해결책 결정 및 예방
좋은 코드 작성하기voidTest1(CTest*p){// do something}voidTest2(CTest&t){// do something}
voidTest1(CTest*p){if(p){Test1_1(p);}else{// do something}}voidTest1_1(CTest*p){if(p){// do something}}voidTest2(CTest&t){Test2_1(t);}voidTest2_1(CTest&t){// do something}
Example of bugs repartition Open source codeSource:  Coverity White Paper
Example of bugs repartition Open source code버그의 비용Source:  Coverity White Paper
진짜 해결책펫, 소환수 자동 소환09.09.09 라이브 업데이트펫인벤 아이템을 언제 주인에게 옮겨줄 것인가?진짜 옮겨야 하나?
에러 로그를 적절하게 남긴다잘남겨야 한다copy & paste 하면서 에러 로그도 똑같이 남기면 어디에서 생긴 에러인지 알 수 없다DDiba!__FILE__, __LINE__캐릭터 이름, 아이템 아이디(해외 에러 사례)너무 많이 남기면느려진다, 정보 노이즈사례 : [NO_ERROR] 로그너무 조금 남기면필요한 정보를 찾을 수 없다
버그 미리 찾기_ASSERT !!정기적인 코드 리뷰코드리뷰 별 거 아님정적 분석툴과CI 연동Code Analysis (자료 추가할 것)pc-lint단위테스트Magic bit 사용int를 int64 로 바꾸면서 magic bit 끼워넣기(특정 bit 가 1 이 아니면 crash)
Code Analysis Buffer Overrun 경고, 배열 인덱스 체크 기능
 포인터에 대한 사용 주의 경고, 중복된 변수 선언
 묵시적 Type Casting, sprintf 의 인자 개수 체크
단 x64 는지원하지 않으므로 편법을 좀 써야 함longlData[10];sValue.Format("%d",lData[10]);warningC6201:Index'10'isoutofvalidindexrange'0'to'9'forpossiblystackallocatedbuffer'lData¡¯voidCTest::DrawData(CDC*pDC){ASSERT(pDC);pDC->FillSolidRect(rect,RGB(255,0,0));}CRectrcTmp;for(...){CRectrcTmp;}
단위 테스트활용법
단위테스트단위테스트로 버그 재현 후 해결책을 적용해 버그가 사라지는 걸 확인가장 많이 물어보는 질문무엇을 단위테스트로 만들 것인가?버그가 발생했던 문제부터 단위테스트로 재현하라가장 쉬운 것 부터 해라테스트를 만들기 쉽게 해라
Memory Leak Detector 1structCItem{CItem(){g_ItemCount++;}~CItem(){g_ItemCount--;}};structFixtureBase{FixtureBase(){g_ItemCount=0;}virtual~FixtureBase(){_ASSERT(0==g_ItemCount);}};structFixtureTest:publicFixtureBase{FixtureTest(){m_pPlayer=CPlayer::Create();m_pItem=CItem::Create();}~FixtureTest(){CItem::Delete(m_pItem);CPlayer::Delete(m_pPlayer);}CPlayer*m_pPlayer;CItem*m_pItem;};TEST_FIXTURE(FixtureTest,UseItem){m_pPlayer->Use(m_pItem);}
Memory Leak Detector 2#define pbData(pblock) ((unsigned char *)((_CrtMemBlockHeader *)pblock + 1))#define pHdr(pbData) (((_CrtMemBlockHeader *)pbData)-1)FILE*g_hfileLog=NULL;intAllocHook(intnAllocType,void*pvData,size_tnSize,intnBlockUse,longlRequest,constunsignedchar*szFileName,intnLine){staticsize_tsizeAlloc=0;_CrtMemBlockHeader*pHead;if(nBlockUse==_CRT_BLOCK)// alloced by c libreturntrue;switch(nAllocType){case_HOOK_ALLOC:sizeAlloc+=nSize;fprintf(g_hfileLog,"ALLOC\t%d\n",sizeAlloc);break;case_HOOK_REALLOC:break;case_HOOK_FREE:pHead=pHdr(pvData);sizeAlloc-=pHead->nDataSize;fprintf(g_hfileLog,"FREE\t%d\n",sizeAlloc);break;}returntrue;}int_tmain(intargc,_TCHAR*argv[]){g_hfileLog=fopen("log.txt","w+");fprintf(g_hfileLog,"Start\n");_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);_CrtSetAllocHook(AllocHook);fclose(g_hfileLog);return0;}#include <crtdbg.h>#define nNoMansLandSize 4typedefstruct_CrtMemBlockHeader{struct_CrtMemBlockHeader*pBlockHeaderNext;struct_CrtMemBlockHeader*pBlockHeaderPrev;char*szFileName;intnLine;#ifdef _WIN64/* These items are reversed on Win64 to eliminate gaps       * in the struct and ensure that sizeof(struct)%16 == 0, * so 16-byte alignment is maintained in the debug heap.       */intnBlockUse;size_tnDataSize;#else  /* _WIN64 */size_tnDataSize;intnBlockUse;#endif/* _WIN64 */longlRequest;unsignedchargap[nNoMansLandSize];// followed by:// unsigned char           data[nDataSize];// unsigned char           anotherGap[nNoMansLandSize]; }_CrtMemBlockHeader;
Memory Leak DetectorMemory Pool 대신 new, delete 를 쓰게 만드는 flag 를 하나 둘 것이러면 gflag 나 UMDH 로 버그 찾기도 훨씬 쉽다
간단한 Mock 만들기classCTest{protected:intm_Test;voidTest(){}};classCMockTest:publicCTest{public:usingCTest::m_Test;// 부모 클래스의 멤버를 public 으로 쓰겠다.usingCTest::Test;};int_tmain(intargc,_TCHAR*argv[]){CTesta;//a.m_Test = 1; // protected 멤버 변수 접근할 수 없음.//a.Test();	// protected 멤버 함수 접근할 수 없음.CMockTest*pMockTest=(CMockTest*)(&a);pMockTest->m_Test=1;// CMockTest로 강제 캐스팅하면 접근할 수 있음.pMockTest->Test();// a 가 CMockTest객체가 아니어도 이렇게 쓸 수 있다는 점에 주의return0;}
NewTest SUITE특수한 조건에서의 함수 테스트에 Break Point 를 걸고 싶을 때RunAllTests(… “NewTest”);RunAllTests(… “DefaultSuite”);TEST_FIXTURE(FixtureTest,UseItem){m_pPlayer->Use(m_pItem);}SUITE(NewTest){	TEST_FIXTURE(FixtureTest,UseSpecialItem){m_pPlayer->Use(m_pItem);	}}
개발자측정에는쓰지 말 것
결론버그 찾는 법을 수련하자어떻게든 재현하자작은 버그도 무시하지 말자쓰고 있는 툴을 최대한 활용하자해결책을 최소 2 개 이상 생각해보자버그를 미리 막을 방법을 만들어보자단위테스트를 활용하자
Q/A
그 외 사례들
사례classCConnector{public:~CConnector(){Close();}voidOpen(){/*...*/}voidClose(){/*...*/}vector<int>m_TestData;// new added member};classCVideo:publicCObj{public:CVideo(intnId):m_nId(nId){}intm_nId;};int_tmain(intargc,_TCHAR*argv[]){// create CComicBook and do something{CConnectorc;CVideo*p1=CVideoManager::CreateVideo(1);if(p1){wcout<<p1->m_nId;p1->Release();}}} 2-3 시간마다 한 번씩 Crash
 Crash 위치 :CConnector 소멸자 -> 멤버변수 vector m_TestData 소멸자
 재현은안 되나 꾸준히 발생
 원인을 찾지 못해, 가능한 원인을 하나씩 제거하기로 결정
새로 추가된 m_TestData 제거
 Crash 는 없어진 듯 했으나1주일 후 다시 Crash 발생
 CVideo 의 멤버변수값이 이상하게 변경되는 다른 현상도 발생
CVideo 과 관련 있을까?버그 찾기 과정이전 업데이트 code 와 전체 code 를 diff추가된 코드 중에서 의심가는 부분이 있긴 한데, 코드가 몇 천줄이 넘었다// create CComicBook and do something 주석 발견이 코드는 CVideo 를 생성하는 코드인데?copy & paste 를 한 원본 코드를 찾은 후, 문제가 있는 코드와 diff대부분의 코드가 거의 비슷한데, 한 라인에서 리턴값을 smartptr 에 저장하지 않고, raw pointer 에 저장하고 있었다
typedefCSmartPtr<CVideo>CVideoSP;classCVideoManager{staticCVideoSPCreateVideo(intnId){returnnewCVideo(nId);}};int_tmain(intargc,_TCHAR*argv[]){CVideo* p1=CVideoManager::CreateVideo(1);return0;}classCObj{public:CObj():m_nRef(0){}virtual~CObj(){}intAddRef(){return++m_nRef;}boolRelease(){--m_nRef;if(0==m_nRef){//return_to_pool(this); // reason 3deletethis;returntrue;}elseif(m_nRef<0){//_ASSERT_EXPR(0, L"over release!");                            // reason 4}returnfalse;}staticvoid*operatornew(size_tsize){//void *p=allocate_from_pool(size);void*p=malloc(size);returnp;}staticvoidoperatordelete(void*p){//_ASSERT_EXPR(0, L"don't delete CObj");free(p);}intm_nRef;};template<typenameT>classCSmartPtr{public:typedefCSmartPtr<T>this_type;CSmartPtr():m_p(NULL){}//explicit CSmartPtr(T* p) : m_p(p) {}CSmartPtr(T*p):m_p(p){}// reason 1~CSmartPtr(){if(m_p){m_p->Release();}}T&operator*()const{return*m_p;}T*operator->()const{returnm_p;}operatorT*(){// reason 2returnm_p;}private:T*m_p;};
사례int_tmain(intargc,_TCHAR*argv[]){intnInput=0;constfloatfail=1.0f;while(1){wcin>>nInput;for(inti=0;i<5;++i){DWORDt=GetTickCount();DWORDnRet=(nInput==0)?fail:t;wcout<<nRet<<L'\t'<<t<<L'\n';Sleep((rand()%10)+20);}}return0;}
사례int_tmain(intargc,_TCHAR*argv[]){intnInput=0;constfloatfail=1.0f;while(1){wcin>>nInput;for(inti=0;i<5;++i){DWORDt=GetTickCount();DWORDnRet=(nInput==0)?fail:t;wcout<<nRet<<L'\t'<<t<<L'\n';Sleep((rand()%10)+20);}}return0;}426468224   426468234426468256   426468250426468288   426468281426468320   426468312426468352   426468343
사례 – 설정 파일분명 그 파일을 고쳤어요!설정 파일을 암호화하고, 개발팀에서 관리
사례 : 해외 설정Country Code : tailand
사례 : 해외 설정Country Code : tailand> Country Code : thailand
팀 디버깅왜 서버가 뜨는 도중에 죽을까?각자 disassemble, 코드 히스토리 비교, 로그 비교원인 : 운영팀에서 DB 에 직접 데이터를 insert 하는 바람에 특정 데이터의 갯수가 max 값을 넘어버려서, pass by index 문제 발생DBA 가 발견교훈 : 디버깅할 때는 모든 가정을 버리자각자 전문분야를 동원해 문제를 바라보면, 같은 문제를 다양한 시각에서 바라볼 수 있다업무를 돌아가면서 맡기
사례 – DBupdate pc_data set c_value = 127delete from event_data실제 크기 데이터로 테스트 해 보자

온라인 게임에서 사례로 살펴보는 디버깅

  • 1.
    온라인 게임에서사례로 살펴보는디버깅KGC2009박일리니지2 서버팀parkpd.egloos.com
  • 2.
    저를 소개합니다박일2006 년초 리니지2 서버팀으로 이동The Chaotic Chronicle – Oath of Blood (Chronicle 5) 부터 credit 에 제 이름을 볼 수 있습니다블로그 : http://parkpd.egloos.com
  • 3.
    경고:발표에서 사용된 사례들은대부분실제사례를근거로 하여재구성된 것들입니다.
  • 4.
  • 6.
    버그?충돌과 응답없음, 성능과측정 가능성의 저하부정확한 결과, 보안 취약점, 일관성 없는 사용자 인터페이스요구 사항 불충족
  • 7.
    디버깅을 잘 하는방법 배우기나와 다른 사람의 문제 해결 과정을 관찰내성법“문제가 무엇이었나” 보다,“어떻게 문제를 찾았나” 가 더 중요의사보다 유리한 점프로세스를 여러 번 다양하게 죽여 볼 수 있다웃기는 점디버깅이 오래 걸릴 수록, 고마워한다
  • 8.
    버그 찾기버그 재현버그분석해결책 결정 및 예방
  • 9.
    버그 찾기dump 로Crash 난 위치 확인최근에 고친 파일부터 diff좋은 diff 툴, 일일빌드이상현상이 생긴 시간대 근처 에러로그 확인내가 고친 코드를 먼저 의심다른 사람과 얘기해보자(Bug Talk)디버깅은 근성이다도저히 모르겠다 싶을 때 1시간만 더 보자어셈블리는 항상 가장 마지막에서버라면 실제 서버를 터미널 서비스로 들어가 작업관리자를 보자
  • 10.
    Everybody liesfalse positive(거짓양성)예전바이너리를 쓰고 있다던가false negative(거짓음성)QA 가 고쳐지지도 않은 걸 고쳐졌다고 하거나플라시보 효과예전부터 있던 코드도 100% 믿지 말자
  • 11.
    // Dump 보기실습typedefBOOL(WINAPI*MINIDUMPWRITEDUMP)(HANDLEhProcess,...);LONGTopExceptionFilter(LPEXCEPTION_POINTERS pExp){LONGretval=EXCEPTION_CONTINUE_SEARCH;HMODULEhDll=NULL;hDll=::LoadLibrary(_T("DBGHELP.DLL"));if(hDll){MINIDUMPWRITEDUMPpDump=::GetProcAddress(hDll,"MiniDumpWriteDump");if(pDump){HANDLEhFile=::CreateFile(_T("Crash.dmp"),..);if(INVALID_HANDLE_VALUE!=hFile){_MINIDUMP_EXCEPTION_INFORMATIONExInfo;ExInfo.ThreadId=::GetCurrentThreadId();ExInfo.ExceptionPointers=pExp;ExInfo.ClientPointers=NULL;BOOLbOK=pDump(GetCurrentProcess(),GetCurrentProcessId(),hFile,MiniDumpNormal,&ExInfo,NULL,NULL);if(bOK){tcout<<" dump to Crash.dmp"<<endl;retval=EXCEPTION_EXECUTE_HANDLER;}::CloseHandle(hFile);}}::FreeLibrary(hDll);}returnretval;}int_tmain(intargc,_TCHAR*argv[]){SetUnhandledExceptionFilter(TopExceptionFilter);TCHAR*p=NULL;p[1]=_T('M');return0;}
  • 13.
    minidump를 출력하기.dmp파일을 만드는것 보다 장점은?이메일로 전송Label, 빌드별 바이너리, pdb를 SVC 에 저장라이브용 바이너리 만든 후, 이메일로 label 공유
  • 14.
  • 15.
    Base Line지금 상태는정상인가?평소 CPU 는 몇 % 정도였나?의도대로 최적화가 되었나?
  • 16.
  • 17.
    Performance Countersbase line만들기Counter 를 log 나 DB 에 주기적으로 저장counter 예시SQL Server: SQL Statistics: Batch Requests/sec초당 요청 받은 SQL 배치 요청 수SQL Server: Buffer Manager: Buffer Cache Hit Ratio90 이상이어야 함SQL Server: Locks: Lock Waits/sec잠금대기요청수Process: Page Faults/sec프로세스가 Cache Hit하지 않은 페이지수
  • 18.
    사례 – 특정머신Tomcat 서버가 죽었음JVM의 다운으로 인해서 발생되었으며죽은 시점은 GC 과정 중이었음Log4j를 통한 로그 모니터링 결과에는 죽기 전 후 특이 사항 발견되지 않음
  • 19.
    JVM 에러로그# Anunexpected error has been detected by HotSpot Virtual Machine:# Internal Error (50532D41524B33574545502445434F5241544F520E4350500024), pid=27607, tid=1828866976# Java VM: Java HotSpot(TM) Server VM (1.5.0_12-b04 mixed mode)--------------- T H R E A D ---------------Current thread (0x080ffe48): VMThread [id=27611]Stack: [0x6cfa4000,0x6d025000), sp=0x6d023ae0, free space=510kNative frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)V [libjvm.so+0x512633]V [libjvm.so+0x1c862c]V [libjvm.so+0x51a3f0]V [libjvm.so+0x438228]C [libpthread.so.0+0x53cc]VM_Operation (0x66f6ae40): parallel gc failed allocation,mode: safepoint, requested by thread 0x6bbcf7d0HeapPSYoungGen total 17216K, used 696K [0xaa370000, 0xabc90000, 0xb1530000)eden space 16512K, 0% used [0xaa370000,0xaa370000,0xab390000) from space 704K, 98% used [0xab3d0000,0xab47e1d0,0xab480000) to space 4608K, 0% used [0xab810000,0xab810000,0xabc90000)PSOldGen total 629440K, used 627294K [0x71530000, 0x97be0000, 0xaa370000) object space 629440K, 99% used [0x71530000,0x979c7b48,0x97be0000)PSPermGen total 37888K, used 37271K [0x6d530000, 0x6fa30000, 0x71530000) object space 37888K, 98% used [0x6d530000,0x6f995ed0,0x6fa30000)
  • 20.
    다행인지 불행인지동일한 문제가터짐과거의 이력을 조사해보니 동일한 장비에서 같은 문제가 여러 번 있었음
  • 21.
    하드웨어에 빨간 불이들어오고 있었음사례 – 기계는 믿을 수 있는가특정 머신, CPU, 그래픽카드, NIC
  • 22.
    사례 – 언제문제가 발생했나?특정 시각? 특정 요일? CPU 가 튀는 주기는?게임 서버가 새벽 2시만 되면 죽었던 이유는?Windows Update 가 새벽 2시에 실행바이러스 update 창이 200 개 이상 오픈같은 port 를 bind 계속하면 port 차단서버관리툴이 실행될 때마다 특정 port 를 bind 하게 설계일본 IDC 에서 꼭 설치하게 하는 패킷 필터링 시스템의 DDOS, Garbage Attack 탐지기가 해당 port 를 차단얼마만에 죽는가특정 tick 에 죽는 잘못된 코드와 memory leakPrintf대신 사용한 로그 파일이 엄청나게 커짐Memory leak은 아니지만, 너무 많은 메모리 할당2G 는 절대 큰 용량이 아님결국 일본 찾아가서 해결
  • 23.
    버그 찾기버그 재현버그분석해결책 결정 및 예방
  • 24.
    무조건 재현! 재현!현상은원인이 있기 때문에 나타난다. 정상적인 재현이 불가능하면, 억지로라도 재현해라.재현 할 때는 BP 걸어놓고 한 step 씩 이동하면서 확인재현할 때는 문제가 되는 현상을 데이터까지 똑같게택배로 컴퓨터를 아예 받아와라. 고친 게 하나도 없어도, 환경만 바뀌면 버그 발생 가능설정 파일, 하드웨어, OS 버전, 보안 프로그램 동작 여부버그 report 를 그대로 믿지 마라 QA 에서도 아는 선 안에서만 리포트를 쓴다QA 분들에게 내 개발서버로 접속해서 재현해 달라고 부탁해외 지사에서 보낸 버그 리포트는 특히 심하다
  • 25.
    사례 - 경매라이브서버가 아닌 테스트 서버, 그 중 한 npc에서만 발생재현도안 되어서, 별 문제 없겠지 생각하고 무시업데이트 후에 전 라이브 서버에서 문제 발생 이유 : 라이브 서버에서는 처음에 개발자가 직접 seed 값을 넣어줘서 경매에 문제 없었지만, 테스트 서버의 경우 개발자가 여러 npc중 한 npc만 까먹고 seed 값을 넣어주지 않았기 때문에 계속 문제가 있었음처음부터 문제가 있었는데도, QA 에서는 ‘잘 되다가 저번 주부터 문제가 생겼음’ 이라고 보고했고, 개발팀도 QA 만 믿고특이한 사례보고라고 생각함대규모 업데이트를 위해 모든 seed 값을 초기화했더니, 테스트 서버와 동일한 문제가 모든 npc에서 발생결론증상이 있다면 무시하지 말자[사례보고] 라는 제목 때문에 선입관 생김
  • 26.
  • 27.
    사례 - OpenMP수십대의 개발 서버 중 한 서버에서만 에러 발생알고 보니 특정 머신(하퍼타운)에서만 에러 발생. 이유는?제대로 MT 를 지원하는 머신에서 테스트물리 core 가 여러 대인 CPU 머신 사용하자하퍼타운을 쓰는 다른 개발자는 설정에서 omp사용을 빼 놨기 때문에 에러나는 줄 몰랐음
  • 28.
    사례 – 공성과결자해지voidLoadData(){// do somethingCCastlec(nCastleNum);// dead code???return;}
  • 29.
    사례 – 공성과결자해지voidLoadData(){// do somethingCCastlec(nCastleNum);// dead code? NO!!!return;}classCCastle{public:CCastle(intnId){g_CastleDB.Register(nId, this);}};
  • 30.
    사례 – 토요일에만실패하는단위테스트요일 index 문제GetLocalTimewDayOfWeek : The day of the week. Sunday = 0, Monday = 1해외 : 특정 요일에만 실패
  • 31.
    버그 찾기버그 재현버그분석해결책 결정 및 예방
  • 32.
  • 33.
  • 34.
    Visualizer in C++autoexp.dat으로watch 창 변경C:\Program Files\Microsoft Visual Studio 8\Common7\Packages\Debugger\autoexp.dat[AutoExpand]CMyData =head=<m_Head> m_Tail=<m_Tail,x> name=<m_Name,su>classCMyData{public:CMyData(inth,intt):m_Head(h),m_Tail(t){}intm_Head;charm_Name[1024];intm_Tail;};int_tmain(intargc,_TCHAR*argv[]){typedefvector<CMyData*>DataList;DataListdata;for(inti=0;i<10;++i){data.push_back(newCMyData(i,i+1));}return0;}
  • 35.
    Debug Windows 1{[function],[source],[module]}뒤에location, variable_name, expression특정 함수의 static local 변수 보기void Test() { static intsLocalNum = 0; }{Test,,}sLocalNum특정 dll의 전역변수 : {,,foobar.dll}g_pMyStructclassCTest{public:staticCTest&Inst(){staticCTests;returns;}voidCheckValidate(){OutputDebugString(L"T\n");m_Test.push_back(1);}std::vector<int>m_Test;};int_tmain(){CTest::Inst().CheckValidate();return0;}
  • 36.
    Debug Windows 2PseudoRegister@eax : 리턴값@err : GetLastError@HANDLES현재 프로세스의 핸들 갯수@ebp : 지역 변수 시작 지점@esp : 최상위 스택
  • 38.
    사례 – BotvoidCPlayer::OnItemExchange(intnItemCount1,BYTE*pData1,intnItemCount2,BYTE*pData2){for(inti=0;i<nItemCount1;++i){// do something}for(inti=0;i<nItemCount2;++i){// do something}}
  • 39.
    사례 – hp회복 문제classCPlayer{public:CPlayer():m_HP(1.0){}voidOnRegenTick();doublem_HP;doublem_Buff[3];};voidCPlayer::OnRegenTick(double delta){m_HP=m_HP * 1,05+(0.5*delta)+(10.0*0.3)/(52+m_Buff[1])+(m_HP/m_Buff[2]*3.0)+2;}
  • 40.
    사례 - MSVC2005최적화 오류classVec{intx,y,z;Vec&operator=(constVec&v){if(this!=&v){x=v.x;y=v.y;z=v.z;}return*this;}};Vecv1,v2;v1=v2;/Ot 컴파일러 옵션으로 빌드한 프로그램을 실행할 때 일부 "if" 문은 실행 안 될 수 있음Visual Studio 2005 sp1 에서 수정
  • 41.
    BUG?(x != x): true(x == x) : false(y > x) : false(y < x) : false
  • 42.
    BUG?(x != x): true(x == x) : false(y > x) : false(y < x) : falseNaN(Not a Number)double answer = sqrt(-1.0);
  • 43.
    // http://msdn.microsoft.com/en-us/library/w22adx1s%28VS.80%29.aspxclassCData{public:enum{HALF_NUM=0x7fffffff,MAX_NUM=0xffffffff};CData():m_dPoint(1.0){m_dData[0]=1.3;m_dData[1]=1.5;m_nData[0]=HALF_NUM;m_nData[1]=MAX_NUM;}voidGetBonus(intnBonusIndex){m_dPoint*=m_dData[nBonusIndex];}doublem_dPoint;doublem_dData[2];unsignedlongm_nData[2];};int_tmain(intargc,_TCHAR*argv[]){CDataa;a.GetBonus(2);wcout<<L"isnan :"<<_isnan(a.m_dPoint)<<L'\n';wcout<<"(1.0 < a.m_dPoint) : "<<(1.0<a.m_dPoint)<<L'\n';wcout<<"(1.0 >= a.m_dPoint) : "<<(1.0>=a.m_dPoint)<<L'\n';return0;}// outputisnan:1(1.0<a.m_dPoint):0(1.0>=a.m_dPoint):0
  • 44.
    사례 – 메모리침범classCTest{public:conststaticintMAX_DATA=10;intm_Data[MAX_DATA];vector<int>m_Nums;};int_tmain(intargc,_TCHAR*argv[]){CTestt;t.m_Nums.push_back(1);ZeroMemory(t.m_Data,sizeof(int)*CTest::MAX_DATA+10);t.m_Nums.push_back(1);// Crashreturn0;}#include <boost/array.hpp>int_tmain(intargc,_TCHAR*argv[]){intcArray[256];boost::array<int,5>aTest={1,2,3,4 ,5};boost::array<int,256>aArray;cArray[256]=0;// not always dieaArray[256]=0;// always die// Assertion failed: i < N && "out of range", file , line 91return0;}
  • 45.
    사례 – goto변수선언문을 건너뛰기#define goto WhatTheHellint_tmain(intargc,_TCHAR*argv[]){gotoJumpToHere;intnTestValue;JumpToHere:nTestValue=4;wcout<<nTestValue;return0;}
  • 46.
    사례 - random//………………………..intGetRand1(intnMin,intnMax){intnOffset=rand()%(nMax-nMin);returnnMin+nOffset;}doubleGetRand2(doubledMin,doubledMax){return((double)rand()/(double)RAND_MAX)*(dMax-dMin);}
  • 47.
    사례 - random//RAND_MAX : 32767intGetRand1(intnMin,intnMax){intnOffset=rand()%(nMax-nMin);returnnMin+nOffset;}doubleGetRand2(doubledMin,doubledMax){return((double)rand()/(double)RAND_MAX)*(dMax-dMin);}0 ~ 2767 확률이 2768 ~ 9999 보다 1/32767 높다.if (Rand(1000000) <= 1) // 0.0001% 과if (Rand(100000) <= 1) // 0.001% 의차이는rand 를 RAND_MAX 번 호출했을 때 1이 나올 확률은 1/32767 = 0.00305% > (0.001% 과 0.0001%)0.001% 와 0.003% 의 차이가 없음
  • 48.
  • 49.
    예제 - std::sortsort(m_data.begin()…)하는 도중에 다른 thread 에서 Num 값을 바꾸면 무한 루프가 발생할 수 있음structData{Data(intn):m_Num(n){}intm_Num;};boolIsLessThan(Data*a,Data*b){returna->m_Num>b->m_Num;}typedefvector<Data*>DataList;voidtest(DataList& d){for(inti=0;i<10;++i)d.push_back(newData(i));sort(d.begin(),d.end(),IsLessThan);
  • 50.
  • 51.
  • 52.
  • 53.
    버그 찾기버그 재현버그분석해결책 결정 및 예방
  • 54.
    좋은 코드 작성하기voidTest1(CTest*p){//do something}voidTest2(CTest&t){// do something}
  • 55.
    voidTest1(CTest*p){if(p){Test1_1(p);}else{// do something}}voidTest1_1(CTest*p){if(p){//do something}}voidTest2(CTest&t){Test2_1(t);}voidTest2_1(CTest&t){// do something}
  • 56.
    Example of bugsrepartition Open source codeSource: Coverity White Paper
  • 57.
    Example of bugsrepartition Open source code버그의 비용Source: Coverity White Paper
  • 58.
    진짜 해결책펫, 소환수자동 소환09.09.09 라이브 업데이트펫인벤 아이템을 언제 주인에게 옮겨줄 것인가?진짜 옮겨야 하나?
  • 59.
    에러 로그를 적절하게남긴다잘남겨야 한다copy & paste 하면서 에러 로그도 똑같이 남기면 어디에서 생긴 에러인지 알 수 없다DDiba!__FILE__, __LINE__캐릭터 이름, 아이템 아이디(해외 에러 사례)너무 많이 남기면느려진다, 정보 노이즈사례 : [NO_ERROR] 로그너무 조금 남기면필요한 정보를 찾을 수 없다
  • 60.
    버그 미리 찾기_ASSERT!!정기적인 코드 리뷰코드리뷰 별 거 아님정적 분석툴과CI 연동Code Analysis (자료 추가할 것)pc-lint단위테스트Magic bit 사용int를 int64 로 바꾸면서 magic bit 끼워넣기(특정 bit 가 1 이 아니면 crash)
  • 61.
    Code Analysis BufferOverrun 경고, 배열 인덱스 체크 기능
  • 62.
    포인터에 대한사용 주의 경고, 중복된 변수 선언
  • 63.
    묵시적 TypeCasting, sprintf 의 인자 개수 체크
  • 64.
    단 x64 는지원하지않으므로 편법을 좀 써야 함longlData[10];sValue.Format("%d",lData[10]);warningC6201:Index'10'isoutofvalidindexrange'0'to'9'forpossiblystackallocatedbuffer'lData¡¯voidCTest::DrawData(CDC*pDC){ASSERT(pDC);pDC->FillSolidRect(rect,RGB(255,0,0));}CRectrcTmp;for(...){CRectrcTmp;}
  • 65.
  • 66.
    단위테스트단위테스트로 버그 재현후 해결책을 적용해 버그가 사라지는 걸 확인가장 많이 물어보는 질문무엇을 단위테스트로 만들 것인가?버그가 발생했던 문제부터 단위테스트로 재현하라가장 쉬운 것 부터 해라테스트를 만들기 쉽게 해라
  • 67.
    Memory Leak Detector1structCItem{CItem(){g_ItemCount++;}~CItem(){g_ItemCount--;}};structFixtureBase{FixtureBase(){g_ItemCount=0;}virtual~FixtureBase(){_ASSERT(0==g_ItemCount);}};structFixtureTest:publicFixtureBase{FixtureTest(){m_pPlayer=CPlayer::Create();m_pItem=CItem::Create();}~FixtureTest(){CItem::Delete(m_pItem);CPlayer::Delete(m_pPlayer);}CPlayer*m_pPlayer;CItem*m_pItem;};TEST_FIXTURE(FixtureTest,UseItem){m_pPlayer->Use(m_pItem);}
  • 68.
    Memory Leak Detector2#define pbData(pblock) ((unsigned char *)((_CrtMemBlockHeader *)pblock + 1))#define pHdr(pbData) (((_CrtMemBlockHeader *)pbData)-1)FILE*g_hfileLog=NULL;intAllocHook(intnAllocType,void*pvData,size_tnSize,intnBlockUse,longlRequest,constunsignedchar*szFileName,intnLine){staticsize_tsizeAlloc=0;_CrtMemBlockHeader*pHead;if(nBlockUse==_CRT_BLOCK)// alloced by c libreturntrue;switch(nAllocType){case_HOOK_ALLOC:sizeAlloc+=nSize;fprintf(g_hfileLog,"ALLOC\t%d\n",sizeAlloc);break;case_HOOK_REALLOC:break;case_HOOK_FREE:pHead=pHdr(pvData);sizeAlloc-=pHead->nDataSize;fprintf(g_hfileLog,"FREE\t%d\n",sizeAlloc);break;}returntrue;}int_tmain(intargc,_TCHAR*argv[]){g_hfileLog=fopen("log.txt","w+");fprintf(g_hfileLog,"Start\n");_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);_CrtSetAllocHook(AllocHook);fclose(g_hfileLog);return0;}#include <crtdbg.h>#define nNoMansLandSize 4typedefstruct_CrtMemBlockHeader{struct_CrtMemBlockHeader*pBlockHeaderNext;struct_CrtMemBlockHeader*pBlockHeaderPrev;char*szFileName;intnLine;#ifdef _WIN64/* These items are reversed on Win64 to eliminate gaps * in the struct and ensure that sizeof(struct)%16 == 0, * so 16-byte alignment is maintained in the debug heap. */intnBlockUse;size_tnDataSize;#else /* _WIN64 */size_tnDataSize;intnBlockUse;#endif/* _WIN64 */longlRequest;unsignedchargap[nNoMansLandSize];// followed by:// unsigned char data[nDataSize];// unsigned char anotherGap[nNoMansLandSize]; }_CrtMemBlockHeader;
  • 69.
    Memory Leak DetectorMemoryPool 대신 new, delete 를 쓰게 만드는 flag 를 하나 둘 것이러면 gflag 나 UMDH 로 버그 찾기도 훨씬 쉽다
  • 70.
    간단한 Mock 만들기classCTest{protected:intm_Test;voidTest(){}};classCMockTest:publicCTest{public:usingCTest::m_Test;//부모 클래스의 멤버를 public 으로 쓰겠다.usingCTest::Test;};int_tmain(intargc,_TCHAR*argv[]){CTesta;//a.m_Test = 1; // protected 멤버 변수 접근할 수 없음.//a.Test(); // protected 멤버 함수 접근할 수 없음.CMockTest*pMockTest=(CMockTest*)(&a);pMockTest->m_Test=1;// CMockTest로 강제 캐스팅하면 접근할 수 있음.pMockTest->Test();// a 가 CMockTest객체가 아니어도 이렇게 쓸 수 있다는 점에 주의return0;}
  • 71.
    NewTest SUITE특수한 조건에서의함수 테스트에 Break Point 를 걸고 싶을 때RunAllTests(… “NewTest”);RunAllTests(… “DefaultSuite”);TEST_FIXTURE(FixtureTest,UseItem){m_pPlayer->Use(m_pItem);}SUITE(NewTest){ TEST_FIXTURE(FixtureTest,UseSpecialItem){m_pPlayer->Use(m_pItem); }}
  • 72.
  • 73.
    결론버그 찾는 법을수련하자어떻게든 재현하자작은 버그도 무시하지 말자쓰고 있는 툴을 최대한 활용하자해결책을 최소 2 개 이상 생각해보자버그를 미리 막을 방법을 만들어보자단위테스트를 활용하자
  • 74.
  • 75.
  • 76.
    사례classCConnector{public:~CConnector(){Close();}voidOpen(){/*...*/}voidClose(){/*...*/}vector<int>m_TestData;// new addedmember};classCVideo:publicCObj{public:CVideo(intnId):m_nId(nId){}intm_nId;};int_tmain(intargc,_TCHAR*argv[]){// create CComicBook and do something{CConnectorc;CVideo*p1=CVideoManager::CreateVideo(1);if(p1){wcout<<p1->m_nId;p1->Release();}}} 2-3 시간마다 한 번씩 Crash
  • 77.
    Crash 위치:CConnector 소멸자 -> 멤버변수 vector m_TestData 소멸자
  • 78.
    재현은안 되나꾸준히 발생
  • 79.
    원인을 찾지못해, 가능한 원인을 하나씩 제거하기로 결정
  • 80.
  • 81.
    Crash 는없어진 듯 했으나1주일 후 다시 Crash 발생
  • 82.
    CVideo 의멤버변수값이 이상하게 변경되는 다른 현상도 발생
  • 83.
    CVideo 과 관련있을까?버그 찾기 과정이전 업데이트 code 와 전체 code 를 diff추가된 코드 중에서 의심가는 부분이 있긴 한데, 코드가 몇 천줄이 넘었다// create CComicBook and do something 주석 발견이 코드는 CVideo 를 생성하는 코드인데?copy & paste 를 한 원본 코드를 찾은 후, 문제가 있는 코드와 diff대부분의 코드가 거의 비슷한데, 한 라인에서 리턴값을 smartptr 에 저장하지 않고, raw pointer 에 저장하고 있었다
  • 84.
    typedefCSmartPtr<CVideo>CVideoSP;classCVideoManager{staticCVideoSPCreateVideo(intnId){returnnewCVideo(nId);}};int_tmain(intargc,_TCHAR*argv[]){CVideo* p1=CVideoManager::CreateVideo(1);return0;}classCObj{public:CObj():m_nRef(0){}virtual~CObj(){}intAddRef(){return++m_nRef;}boolRelease(){--m_nRef;if(0==m_nRef){//return_to_pool(this); //reason 3deletethis;returntrue;}elseif(m_nRef<0){//_ASSERT_EXPR(0, L"over release!"); // reason 4}returnfalse;}staticvoid*operatornew(size_tsize){//void *p=allocate_from_pool(size);void*p=malloc(size);returnp;}staticvoidoperatordelete(void*p){//_ASSERT_EXPR(0, L"don't delete CObj");free(p);}intm_nRef;};template<typenameT>classCSmartPtr{public:typedefCSmartPtr<T>this_type;CSmartPtr():m_p(NULL){}//explicit CSmartPtr(T* p) : m_p(p) {}CSmartPtr(T*p):m_p(p){}// reason 1~CSmartPtr(){if(m_p){m_p->Release();}}T&operator*()const{return*m_p;}T*operator->()const{returnm_p;}operatorT*(){// reason 2returnm_p;}private:T*m_p;};
  • 85.
  • 86.
  • 87.
    사례 – 설정파일분명 그 파일을 고쳤어요!설정 파일을 암호화하고, 개발팀에서 관리
  • 88.
    사례 : 해외설정Country Code : tailand
  • 89.
    사례 : 해외설정Country Code : tailand> Country Code : thailand
  • 90.
    팀 디버깅왜 서버가뜨는 도중에 죽을까?각자 disassemble, 코드 히스토리 비교, 로그 비교원인 : 운영팀에서 DB 에 직접 데이터를 insert 하는 바람에 특정 데이터의 갯수가 max 값을 넘어버려서, pass by index 문제 발생DBA 가 발견교훈 : 디버깅할 때는 모든 가정을 버리자각자 전문분야를 동원해 문제를 바라보면, 같은 문제를 다양한 시각에서 바라볼 수 있다업무를 돌아가면서 맡기
  • 91.
    사례 – DBupdatepc_data set c_value = 127delete from event_data실제 크기 데이터로 테스트 해 보자