1. 빌드 관리와 디버깅
(이 문서는 2010 년 사내 교육 문서로 작성한 것이므로 업로드 시점(2016 년)의 상황과 많은
차이가 있을 수 있음. – 유영천 , tw:@dgtman, http://megayuchi.wordpress.com)
유영천
빌드관리
코드작성부터 컴파일, 디버깅, 패키징, 배포까지의 과정을 빌드라고 합니다.
프로그램 소스와 데이타, 산출물 바이너리를 모두 포함합니다.
중간중간 만들어지는 결과물은 이후에 나올 결과물, 최종적으로 출시되는 제품에 영향을 미칩니다.
즉 오늘의 빌드는 출시, 혹은 서비스 결과물에 영향을 미칩니다.
이전에 언급한 바와 같이 버그를 줄이고 안정적인 소프트웨어를 만들기 위해서는 환경을 먼저 구축해야합니다.
그 환경구축의 첫 걸음이 빌드관리입니다.
빌드관리의 모범사례 - MS 의 Daily Build
MS 내부에서 , 또 소프트웨어업계에서 가장 모범적인 사례로 꼽히는 것이 MS Windows NT 개발사례입니다.
NT 팀은 개발시작 1 년정도 후부터 ‘일일빌드’와 , 자신들이 개발중인 미완성의 OS 를 이용해서 코드를 개발을
진행하는 ‘개밥먹기’를 실시했습니다.
버그투성이의 OS 와 엄격한 빌드 절차로 인해 프로그래머들은 엄청나게 괴로웠죠.
지금은 열배 이상 더 커졌겠지만, 88 년 당시의 NT 도 결코 작은 소프트가 아니었습니다.
1 억 5 천만 라인이나 되는 엄청난 코드였고 빌드에만 수시간이 걸렸습니다.
프로그래머들이 퇴근할때까지 코드를 체크인 하면 심야에 빌드를 실시하고 빌드가 실패하면 담당
프로그래머들을 회사로 소환하곤 했습니다. (새벽에 자고 있더라도)
빌드가 끝나면 QA 팀에서 새벽시간동안 200 여대의 PC 를 동원하여 스트레스 테스트를 실시합니다.
2. 다음날 아침에 테스트 결과가 보고되고, 테스트 결과를 참고하여 간부회의에서 그날 그날의 일정을 수정합니다.
이것이 빌드의 한 싸이클이고 NT 를 출시할때까지 매일매일 계속되었습니다.
Windows NT 1.0 에서 시작된 빌드번호는 Windows 2000 발매당시 2950 번이었고 지금 Windows 7 에 이르러서
7600 번에 도달해있습니다.
엄격한 매일매일의 빌드절차를 거쳐 오늘날의 Windows 가 있는 것입니다.
‘게임 소프트웨어에서 이같은 엄격함이 필요한가’라는 의문이 있을 수 있겠습니다만, 국내외의 유명 개발사들이
(국내는 NC 소프트 아이온팀) 이에 준하는 엄격한 빌드 프로세스를 유지하고 있습니다.
모두 알만한 진짜 잘만든 게임들은 다 그렇게 만듭니다.
우리회사, 우리팀에 정기빌드 프로세스의 적용
일단 개발 4 팀에선 매주 목요일 주간빌드를 실시하고 있습니다.
2008 년 2 월부터 지금까지 실시해오고 있으며 현재 빌드번호는 143 번입니다.
가능하면 모든 팀이 정기빌드 프로세스를 도입하는것이 좋다고 생각합니다.
Build 의 정의 및 원칙
1. 소스코드와 데이타를 취합하여 DLL 과 EXE 를 만들어냅니다.
2. 단순히 컴파일 후 얻은 바이너리가 아닌 내부에 배포되어 테스트를 거친 것만을 빌드로 간주합니다.
3. 컴파일뿐 아니라 DB 에 올라가는 설정 데이타, 기타 클라이언트의 데이타의 검증까지 포함합니다.
4. 사소한 수정이라도 버젼번호를 변경해야합니다.
5. 다음번 빌드까지 알려진 버그는 모두 수정해야한다. (도저히 해결 안될 경우 일단 기록해두고 계속 원인을
분석해나간다) 이것이 가장 지키기 어려운 항목일텐데 이것이 지켜지지 않으면 빌드 관리의 의미가 80%쯤은
사라집니다.
3. 빌드관리에서의 최소한의 필요사항
1.빌드머신
배포용 클라이언트와 서버를 빌드할 PC 를 준비합니다.
이 머신에 현재 프로젝트의 모든 소스코드와 빌드에 필요한 스크립트 파일등을 버젼별로 보관합니다.
인스톨러 포함, 배포용의 바이너리는 모두 이 머신에서 빌드됩니다. 다른 머신에서 빌드된 바이너리가 배포되는
일은 절대 있어선 안됩니다.
2.빌드 담당자
MS 와 같은 거대 소프트회사에서는 빌드조직이 따로 있습니다만, 우리 회사 규모에서는 숙련된 담당자 한명이면
충분합니다.
빌드 담당자는 매번 빌드(테스트 포함)를 책임집니다. 클라이언트, 서버, 맵툴등 관련 툴들까지 모두 빌드하며 이
중 하나라도 문제가 생기면 작업자를 찾아서 문제를 해결하도록 합니다.
소스코드나 데이타파일이 모두 갖춰져 있는지 확인합니다. 일부 소스나 데이타 파일이 없어서 빌드가 실패하는
경우 작업자에게 통보합니다.
매 빌드에서 debug/release 버젼이 모두 나와야합니다.
바이너리가 모두 빌드되면 아주 최소한의 작동테스트 – 로컬서버를 열어서 클라이언트 1 개를 접속시켜
작동하는지- 는 빌드 담당자가 합니다.
빠진 데이타, 컴파일되지 않는 소스 코드를 모두 확인하고 변경된 데이타에 대해서도 알고 있어야하기 때문에
프로그래밍툴, 그래픽툴 사용이 가능한 기획자가 빌드 담당을 하는것이 좋습니다.
3. 빌드 스크립트
한방에 프로젝트를 빌드할 수 있는 수단입니다. EO 프로젝트에서는 배치파일을 사용합니다.
개인적으로는 절대 없어선 안된다고까지 생각지는 않습니다. 빌드 담당자가 있다면 크게 상관은 없다고
생각합니다만 있으면 여러모로 편합니다.
다음은 EO 프로젝트에서의 빌드 스크립트 샘플입니다.
4. 4. 프로젝트 폴더구성
폴더 구성은 엄청나게 중요합니다. 신경쓰지 않으면 나중에 덤프를 관리하고 분석하는데 큰 어려움을 겪게
됩니다.
EO 프로젝트의 폴더 구성을 예로 간단히 설명해보겠습니다.
5. 프로젝트위 최상위 폴더는 MetalTomato 입니다. 스튜디오 이름입니다. 게임의 모든 부품과 각종 툴, 클라이언트
및 서버의 소스코드와 바이너리, 데이타들을 포함합니다.
6. MetalTomatoDLL 폴더
3D 엔진 DLL, 네트워크라이브러 DLL, 사운드라이브러리 DLL 등 게임에 필요한 모든 DLL 이 이 안에 들어갑니다.
MetalTomatoEO_Online 폴더
서버와 클라이언트의 소스 및 exe 바이너리가 들어있습니다. 유져의 pc 에 설치될때 게임클라이언트가 위치하는
폴더이기도 합니다.
MetalTomatosymbols 폴더
아아아주 중요합니다. 모든 DLL 과 exe 를 빌드할때 나오는 디버깅 정보 pdb 파일이 모두 이 안에 들어갑니다.
한개라도 빠지면 안됩니다. 디버깅에 꼭 필요한 파일은 실행바이너리(exe,dll)과 pdb 파일, 소스(cpp,h)파일인데 그
중에서 pdb 파일이 이 안에 들어갑니다. 엔진이든 게임 클라이언트든 툴이든 예외없습니다. 한개도 빠짐없이
들어있어야합니다. pdb 파일을 모아두는 이유는 매번 빌드버젼별로 보관을 용이하게 하고 디버깅시에 pdb 를
찾기 쉽도록 하기 위함입니다.
archive_build.bat 파일
한번 실행하면 날짜별로 현재 프로젝트(MetalTomato 폴더 기준)의 exe, dll, pdb, cpp, h, txt…등 기타 빌드와
디버깅에 필요한 파일들을 몽땅 압축해줍니다. 그래픽 데이타와 사운드 데이타는 포함하지 않습니다. 기본적으로
컴파일과 디버깅에 필요한 데이타만을 압축합니다.
매 빌드마다 자동, 또는 수동으로 이 배치파일을 실행해서 버젼별로 보관합니다.
압축파일들
Archive_build.bat 로 만들어낸 압축파일들입니다. 항시 보관하고 있어야합니다. 나중에 크래시 덤프를 분석할때
덤프와 맞는 버젼을 찾아서 압축을 풀고 디버깅에 사용할것입니다.
디버깅
1. 코드 작성단계의 디버깅
이러한 유형의 디버깅은 모든 프로그래머가 합니다. 굳이 설명이 필요없겠죠.
7. 2. 덤프파일을 이용한 디버깅
위에서 열거한 준비작업의 50%의 이유는 디버깅을 위함입니다.
프로그래머는 코드를 작성하며 IDE 에 통합되어있는 디버거를 계속해서 사용하지만, 이 상태에서 발견할 수 있는
버그는 전체 버그에서 아주 적은 수에 불과합니다.
상당 수의 버그는 디버거가 물려있지 않은 상태에서 발생하며, 그것도 release 버젼으로(optimiz 된) , 더 끔찍한
것은 개발자에게 아무 도움도 줄 생각이 없는 유져의 PC 에서 발생한다는 것입니다.
더 심각한 것은 서버의 버그입니다. 국내 서비스, 자체 퍼블리싱이라면 IDC 서버에 VC++을 깔아놓고 디버거로
돌릴수도 있겠지만 해외 퍼블리셔에게 서버소스를 제공할 수는 없는 노릇입니다. 이럴때 서버의 프로세서의
풀메모리덤프 파일이 있다면 디버깅에 엄청난 도움이 됩니다.
덤프파일 디버깅을 위한 필요사항
1. 클라이언트코드에 자동 덤프 저장 기능 및 덤프 전송 기능 추가.
클라이언트가 크래시하면 자동으로 덤프를 저장하고 덤프수집 서버로 파일을 전송하도록 합니다.
.dmp 파일 뿐 아니라 시스템 사양을 기록한 텍스트 파일도 저장하도록 합니다.
그래픽카드 종류 , 그래픽 드라이버 버젼, 사운드카드 유무, OS 버젼등은 디버깅에 큰 도움이 됩니다.
2. 덤프파일 수집서버
FTP 또는 http 를 이용해서 덤프파일을 수집하도록 서버를 설정합니다.
다음은 4 팀에서 사용하고 있는 덤프파일 수집 서버의 폴더입니다. 2 차알파테스트에서 유져로부터 날라온 142 번
빌드의 덤프파일들이 가득하군요.
txt 파일은 해당 PC 의 시스템 사양이 기록되어있습니다.
8. 덤프파일 네이밍
덤프파일 네이밍은 꽤 중요합니다. 이 덤프파일이 언제적 패치가 적용된 버젼인지 알아야합니다.
EO 프로젝트에서 사용하는 덤프파일의 네이밍입니다. 일단 참고용으로만 한번 봅시다.
DMP_139_00089FF21FC9_May262010_0526141241.dmp
9. DMP_139 -> 일단 139 번 빌드입니다. 지난 사내테스트 버젼이군요.
00089FF21FC9 -> MAC 어드레스입니다. PC 별로 가장 유니크하게 식별할 수 있는 값입니다.
May262010 -> 클라이언트 EXE 가 빌드된 날짜입니다. 이 날짜가 뒤죽박죽되지 않게 하기 위해서 빌드머신
하나로만 배포용 빌드를 만드는 것입니다. 2010 년 5 월 26 일입니다.
0526141241 -> 덤프파일이 생성된 시각, 더 정확히는 클라이언트가 크래시한 시각입니다. 5 월 26 일 14 시
12 분 41 초군요.
테스팅 그룹별 클라이언트 사용
빌드가 배포될때마다 팀 전원은 테스트에 참가해야합니다.
Q/A 팀이 있더라도 Q/A 팀에 넘기기 전에 팀 전원 테스트는 반드시 해야합니다. 왜냐하면 문제 발생시 작업자가
직접 확인하는 것이 가장 확실하기 때문입니다.
직군 혹은 테스팅 그룹별로 환경은 다음과 같이 구성합니다
1. 프로그래머 – VC++디버거와 소스코드로 게임실행
2. 기획자, 그래픽디자이너, 밀접한 QA 팀 – 인스톨러와 오토패치를 이용한 유져 배포판으로 직접실행하되 Full
Memory Dump 버젼으로 게임 실행
3. 유져 (일반유져 대상 외부테스트) , 밀접하지 않은 외부 QA 팀 – 스택과 컨텍스트 정보만을 저장하는
미니덤프 저장용으로 빌드한 최종 유져배포용 클라이언트.
덤프파일 분석
모든 준비가 끝났고 이제 테스트중 덤프가 날라오기 시작했습니다. 혹은 내부 테스트중 Full Memory
Dump 파일을 얻었습니다. 분석해서 버그를 찾습니다.
덤프를 분석하려면 크게 두 가지 툴을 사용할 수 있는데요.
10. Visual Studio
윈도우즈 프로그래머들의 밥숟가락이죠. DMP 파일 더블클릭하면 그냥 소스코드까지 보여줍니다. 쉽습니다.
그런데 이건 아주 아름다운 케이스고 실제 디버깅 케이스에서 그렇게 깔끔하게 정확한 소스를 보여주는 경우는
별로 없습니다.
pdb 파일에 기록된 최초 빌드 폴더를 기준으로 소스와 exe 및 심볼파일를 찾는데, 빌드가 진행될수록 예전 그
소스가 그 소스가 아니지요. 폴더 상태도 바뀌고 바이너리도 바뀌고 .pdb 도 바뀝니다.
Dmp 파일을 더블 클릭했더닌 아래와 같은 화면이 튀어나와 허탈함을 느껴본 경험이 있으신가요?
11. 이 정보로는 아무것도 할 수 없습니다. 심볼파일, 소스파일, exe 파일의 폴더 구성이 일치하지 않기 때문에
일어나는 현상입니다.
무식하게 피해가는 방법으로는 프로그래머의 현재 작업 폴더를 백업하고 덤프파일과 같은 버젼의 프로젝트
소스를 찾아서 현재의 프로젝트 폴더로 대체하는 방법이 있습니다.
12. 덤프파일이 수백 개 쌓여있고 버젼이 3 종류 이상이면 도저히 할 짓이 못됩니다.
뿐만 아니라 VS 디버거는 GUI 기반이다보니 한계가 있습니다. 프로세스의 상태를 그렇게까지 세밀하게 확인할 수
없습니다.
Windbg
Windbg 는 MS 에서 Debugging tools for windows 라는 이름으로 배포하고 있는 디버거입니다. 사용방법은 다소
불편하지만 실제로 Windows OS 를 만드는데도 사용될만큼 아주 강력합니다. 사실 MS 내부에서 OS 개발중에
필요해서 만들었다고 보는 것이 맞습니다. 드라이버나 바이러스백신등 커널모드 프로그래밍계에선 아주
오래전부터 익히 사용되어오던 툴입니다.
현재는 Windows SDK 를 다운받거나 Windows Driver Kit 을 다운받으면 최신버젼을 설치할 수 있습니다.
windbg 로 덤프파일 분석하기
이제 windbg 로 덤프 분석을 하는 요령을 설명하겠습니다.
실제 유져로부터 날라온 덤프파일을 가지고 설명하겠습니다.
파일명을보니 142 번 빌드로군요. 142 번빌드의 압축파일을 찾아서 E:dump_142폴더에 풀어둡니다.
edump_142metaltomato
이 폴더로 dmp 파일을 카피할 필요는 없습니다. 심볼 path, 소스 path, exe path 만 맞춰주면 됩니다.
처음 Windbg 를 실행하고 나면 무지 썰렁한 화면에 막막할 것입니다. 과감하게 dmp 파일을 드래그 앤
드롭하거나 open 으로 로드합니다.
13. 이 상태로는 VC++디버거와 별 차이가 없습니다. 심볼과 바이너리, 소스 파일의 폴더는 일치하지 않죠.
이제 path 를 맞춰줍니다.
14. 제일 먼저 windows symbol path 를 설정합니다. MS 에서 Windows 의 모든 시스템 DLL 및 exe path 를 얻을 수
있습니다. 과거에는 다운로드해야했지만 요새는 MS 에서 운영하는 심볼서버로부터 그때그때 필요한
심볼파일만을 다운로드 할 수 있습니다.
.symfx c:symbols
이 명령은 c:symbols 폴더를 windows symbol 저장소로 사용한다는 뜻입니다. C:symbols 에 일치하는
pdb 파일이 없다면 ms 로부터 자동 다운로드합니다.
이제 게임 프로젝트의 심볼 path 를 설정합니다.
.sympath+ e:dump_142metaltomatosymbols
+를 붙이면 현재 설정된 심볼 path 에 새로운 path 를 추가합니다. windows 심볼 path 를유지하면서
게임프로젝트의 심볼 path 를 추가하는 것입니다.
다음은 소스 path 를 설정합니다.
.srcpath e:dump_142metaltomato
하위디렉토리는 자동검색하기 때문에 엔진이나 게임클라이언트 폴더를 따로 구분해서 지정할 필요는 없습니다.
그리고 exepath 를 지정합니다.exepath 라고 하지만 DLL 도 포함합니다.
.exepath e:dump_142metaltomato
15. 이제 심볼정보를 로드합니다.
.reload –i
시간이 좀 걸릴 것입니다. 특히 처음 MS 로부터 windows 심볼들을 다운로드 한다면 시간이 꽤 걸립니다.
커맨드창에 busy 표시가 사라지고 나면 LM 명령어로 프로세스에 로드된 DLL, EXE 의 심볼 로드 상태를
확인합니다.
16. 이제 분석에 들어갑니다.
!analyze –v
이 명령어를 실행하면 windbg 가 친절하게 덤프를 분석해서 어떤 유형의 버그인지 알려주고 이후 입력해야할
명령어까지 알려줍니다.
17. 대충 훑어보니 NULL_POINTER_READ 라고 하는군요.
코드의 어느 라인인지 찾아봅시다. WINDBG 가 다음의 명령어를 입력하라고 합니다.
STACK_COMMAND: .ecxr ; ~~[e6c] ; .frame 0 ; ** Pseudo Context ** ; kb
18. 대충 크래시한 스레드의 컨텍스트로 옮겨가서 0 번 스택 프레임으로 설정하고 콜스택 상태를 출력한다.
그런뜻입니다. 자세한 내용은 WINDBG HELP 를 참고해주세요.
다음과 같은 화면이 나옵니다. 디스어셈블리창과 콜스택 창은 VIEW 메뉴에서 활성화시킵니다.
19. 소스코드가 나오는군요.
m_pScene->OnMouseMove(x,y,nFlags); 라인에서 크래시했습니다.
어셈블리 코드를 보면 ecx 레지터가 가리키는 어드레스의 첫 4 바이트를 읽어서 0x4C 옵셋을 더해서 call 하고
있습니다. 0x4C 는 10 진수 76 이고 현재 32 비트 프로세스이므로 포인터는 4 바이트, 즉 ecx 레지스터가 가리키는
메모리의 첫 4 바이트의 어드레스에서 19 번째 함수입니다. 따라서 virtual 로 선언된 19 번째 함수임으로 알 수
있죠.
소스코드를 찾아보니 OnMouseMove()함수가 CSceneClient 클래스에서 virtual 로 19 번째 선언된 함수가 맞군요.
ecx 레지스터가 0 이니까 m_pScene 이 NULL 입니다.
콜스택을 살펴봅시다. 게임의 초기화코드 도중에 발생한 크래시입니다. 아직 게임의 메시지 루프까지는 도달도
못했네요. 그런데 WndProc 로 진입했습니다.
뭔가 이상합니다. 윈도우 메지시를 디스패치한 함수는 winmain()함수에 써넣은 DispatchMessage() 함수가 아닙니다.
TFWAH.DLL 이란 처음 보는 모듈을 통해서 메시지펌핑이 이루어져 WndProc()로 진입했습니다. 즉 게임코드를 작성한
프로그래머 입장에서 코드의 흐름 자체는 다이렉트인풋 초기화 코드에서 멈춰있는 것입니다.
헤괴한 경우라고 생각할 수 있지만 게임의 초기화 코드 앞단에 Messagebox(NULL,…)과 같은 코드를 추가하면
이런 현상은 바로 확인할 수 있습니다.
윈도우 메시지 펌핑에 대한 자세한 설명은 생략하고 그 다음 단계로 넘어가겠습니다.
요약하면 게임에서 다이렉트 인풋코드를 초기화하던 중에 윈도우 메시지 펌핑이 이루어졌고 그로 인해 게임
초기화가 끝나기 않은 상태 즉 m_pScene 에 메모리가 할당되지 않은 상태에서 WM_MOUSEMOVE 메시지를
처리하기 위해 WndProc()로 진입한것입니다.
게임코드에서선 WM_MOUSEMOVE 메시지를 물론 처리하지만 아직 메모리도 할당되지 않은 시점이니
NULL 포이넡만 참조할 뿐이죠.
TFWAH.dll 은 먼저번에 LM 명령으로 확인했을때 프로세스에 로드되어 있었습니다. windows 기본 DLL 은 아닌데
수상합니다.
조사를 해봅시다. 구글 검색을 하니 다음과 같이 나오는군요.
TfWah.dll file information
The process File Description or PC Tools ThreatFire belongs to the software ThreatFire or PC Tools Internet
Security or Spyware Doctor by PC Tools.
Description: TfWah.dll is located in a subfolder of "C:Program Files". Known file sizes on Windows XP are
247,104 bytes (31% of all occurrence), 279,872 bytes, 255,264 bytes, 275,776 bytes, 398,608 bytes, 259,344
bytes, 451,856 bytes, 443,664 bytes, 455,952 bytes.
A .dll file (Dynamic Link Library) is a special type of Windows program containing functions that other programs
can call. This .dll file can be injected to all running processes and can change or manipulate their behavior. The
program has no visible window. It is a Verisign signed file. The file has a digital signature. There is no detailed
description of this service. It can change the behavior of other programs or manipulate other programs. It is not a
20. Windows system file. TfWah.dll is able to record inputs, manipulate other programs, monitor applications.
Therefore the technical security rating is 48% dangerous, however also read the users reviews.
라고 하네요. 스파이웨어 디텍팅 툴의 일부지만 돌아가는 원리는 스파이웨어랑 똑같네요. 이 녀석이
DLL 인젝션을 통해서 함께 로드되고 메시지 후킹을 하나봅니다.
이 경우는 얼마든지 일어날 수 있으므로 WndProc()함수를 수정해서 게임코드에서 Dispatch 하는 경우에만
WndProc()의 메시지 핸들러를 수행하고, 그 외의 경우는 DefWndProc()로 돌렸습니다.
완전히 같은 경우는 아니지만 게임 초기화 코드에서 MessageBox(NULL,”TEST”,”TEST”,MB_OK);라고 한줄을 넣어서
비슷한 상황을 재현했습니다. 궁금하신 분들은 직접 해보시기 바랍니다.
어쨌건 이렇게 클라이언트로부터 날라온 덤프를 분석했고 전체 덤프중 95%정도의 원인을 분석해서
처리했습니다.
풀메모리 덤프 힙크래시 버그분석(서버에서의실제 사례)
이번에는 약간 다른 유형의 버그를 확인해보겠습니다.
EO 프로젝트의 2 차알파테스트를 이틀 앞두고 발견했던 버그입니다.
IDC 에서 돌리던 x64 release 빌드의 게임서버를 종료할때 크래시했습니다.
IDC 서버에 VC++이 설치되어있었고 곧 디버거로 연결했습니다만, 소스코드를 보는 것으로는 분석이 불가능한
버그였습니다.
일단 차근차근 분석하기 위해서 VC++에서 덤프파일로 세이브하고 사무실의 개인 pc 로 가져와서 덤프파일을
분석했습니다.
기본적인 절차는 앞에 설명한것과 똑같습니다.
22. 대충 보니 힙오버런 아니면 언더런으로 힙메모리를 깨먹은것 같습니다.
free 함수의 소스까지는 볼 수 없습니다. vC++10.0 런타임 라이브러리에 포함되어있는 malloc(), free()함수인데
내용을 좀 더 확실히 보기 위해 c 런타임라이브러 소스 path 를 추가합니다.
.srcpath+ C:Program Files (x86)Microsoft Visual Studio 10.0VCcrt
다시 !analyze –v 명령을 사용합니다.
23. C – Runtime Library 의 힙메모리 해제 함수가 보이는군요. 해제할때 문제되는건 확실합니다.
24. 어셈블리코드를 보니 직접적으로는 Int 3(debug break)으로 인한 크래시지만, 힙블럭의 헤더가 손상되어서 해제할
수 없기 때문에 OS 의 힙매니져 코드에서 디버그 브레이크를 걸어준 것입니다. 따라서 원인은 힙 오버런 혹은 힙
언더런일 것입니다.
이 시점에서 완전 혀깨물고 죽고 싶은 마음이 들죠. 테스트는 이틀 뒤인데 소스 코드를 봐서는 원인이 뭔지
알수가 없습니다.
힙을 깨먹은 시점은 언제인지 알 수도 없고 어느 메모리가 깨졌는지도 모릅니다. 어떤 메모리를 억세스 하다가
깨먹었는지도 모릅니다.
포기하지 말고 Windbg 로 조사 들어갑니다.
!heap 명령을 사용해서 프로세스의 힙 상태를 봅니다.
26. Error address: 0000000036a629b0
Heap handle: 00000000025f0000
Error type heap_failure_block_not_busy (8)
0000000036a629b0 어드레스의 메모리를 해제하려고 했으나 헤더에 기록된 바로는 할당받은 상태가 아니기
때문에 해제도 불가능합니다. 할당 받은 메모리가 맞다면 앞 블럭의 메모리를 억세스하면서 이 힙블럭의 헤더를
덮어쓴걸로 추측할 수 있습니다.
기본적으로 하나의 프로세스는 여러개의 힙매니져를 사용합니다. 고맙게도 어느 힙매니져가 손상되었는지
나와있군요. 해당 힙매니져를 조사합니다.
!heap –a 00000000025f0000
28. 굉장히 길게 나올텐데 크래시한 어드레스 부근을 봅시다.
내용을 보면 할당된 어드레스, 힙에서의 범위 (start offset , end offset), 그리고 사이즈가 나옵니다. 힙메모리는
블럭들이 링크로 연결되어있으므로 앞블럭의 end 가 현재 블럭의 start 가 됩니다. 표시된 부분이 free()에 실패한
문제의 힙블럭입니다. 보면 start 부분과 사이즈가 이상합니다. 정상이 아니죠.
즉 앞 블럭의 메모리에 뭔가를 써넣다가 오버런 해서 0000000036a629b0 블럭의 헤더를 덮어썼을 가능성이
높습니다.
바로 앞 블럭의 메모리는
0000000036a429a0: 0d9d0 . 20010 [101] - busy (20000)
이와 같고 사이즈는 16 진수로 20000h, 131072 bytes 입니다.
이제 131072 bytes 사이즈로 할당받는 메모리의 포인터를 찾아봅시다.
다음의 함수로 CRT heap 할당함수(new, malloc)가 호출될때 훅을 걸 수 있습니다.
_CRT_ALLOC_HOOK _CrtSetAllocHook(_CRT_ALLOC_HOOK allocHook);
게임서버의 초기화 부분에 메모리 할당 후킹 함수를 설정합니다.
31. 콜스택을 따라가보면 pTransDesc 포인터에서 131072 bytes 크기로 메모리를 할당받고 있습니다. 다행히도 이
사이즈의 메모리를 할당받는 포인터는 이 놈이 유일하네요.
이 포인터로 할당받은 메모리를 오버런하여 뒷 블럭의 헤더를 덮어썼을 가능성이 엄청나게 높습니다.
이후는 간단합니다.
VS 의 Find 기능으로 pTransDesc 포인터를 참조하는 코드들을 다 찾아냈습니다.
범위를 이만큼 좁혀놓고 코드를 보니 사소한 코딩 실수를 찾아낼 수 있었습니다.
참 쉽죠?
WINDBG 명령어 요약
자주 쓰는 windbg 명령어를 몇 개 적어놓습니다. 자세한 내용은 windbg help 와 참고문헌로 적어놓은 서적들을
참고하시기 바랍니다.
(심볼 path 설정)
.symfix
.sympath
(소스 path 설정)
.srcpath
.exepath
(심볼로드)
.reload
(현대 로드된 모듈상태)
lm
(버그 분석)
!analyze
(현재 프로세스의 heap 상태)
!heap
(레지스터 상태)
R
스레드 상태 (프로세스의 모든 스레드 나열)
~
32. 스레드컨텍스트설정
~0s (0 번스레드)
~1s (1 번스레드)
~2s (2 번스레드)
현재 스레드 컨텍스트에서 크래시한 어드레스의 콜스택으로 설정
.ecxr
ip 레지스터가 가리키는 코드로 점프
.lsa eip 또는 lsa rip
프로그래머 그룹에서 기본적으로 실시해야하는 테스트 (서버 클라이언트 공통)
내부 테스트 혹은 QA로 넘어가기 전 프로그래밍 그룹에서 기본적으로 통과해야하는 테스트입니다.
아주 간단합니다.
1. 힙체크
초기화 코드에 _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
종료 코드에 _CrtCheckMemory(); 반드시 사용할것
종료시에 memory leak이발생하는지 여부, heap corruption이 발생하는지 여부
2. DX 체크
게임 클라이언트 시작 전에 DXSDK Control Pannel에서 debug모드로 설정, Break on Memory Leaks, Break on D3D9 Error 에
체크
33. 3. VC++디버거로 시작,종료시 리소스 누수 및 크래시 보고 없이 깔끔하게 종료되어야함.
4. 디버거를 연결하지 않고 release버젼의 exe로 실행, 종료시 크래시 보고 없어야함.
마치며
다들 알고 계신 내용일수도 있지만, 모르시는 분들도 많을것 같아서 시간을 들여 문서를 작성했습니다.
모쪼로 도움이 되었으면 좋겠습니다.
전사적으로 안정적인 소프트웨어 개발을 위한 환경이 구축되었기를 희망합니다.
34. 디버깅 관련된 많은 정보교류가 가능하다면 더더욱 좋겠구요.
참고문헌
실전 윈도우 디버깅(Advanced Windows Debugging) ,에이콘
WinDbg 로 쉽게 배우는 Windows Debugging ,에이콘