Upload
seungjae-lee
View
2.720
Download
4
Embed Size (px)
DESCRIPTION
Citation preview
사례로 배우는 디스어셈블리 디버깅
넥슨코리아 데브캣스튜디오 DD1실
이승재
발표자
이승재 : 프로그래머
카바티나 스토리 2007~2009
데스크탑 히어로즈 2010~2011.9
마비노기2 2011.10~2013
미공개 프로젝트 2014~
이 발표는
마비노기2 개발중에 실제로 발생했던 버그의 원인을 추적하는 과정을 재구성했습니다.
주제
디스어셈블리 디버깅
• 포인터가 있는 언어로 개발하는 한 피할 수 없다
• 아무나 할 수 없고, 누구도 하기 싫어하는 일
진짜 주제
어렵지 않다
멘붕하지 말자
2013년 9월
상황
여기서 크래시 0x00000010 읽다가 사망
재현조건: 던전을 플레이한다 (;;;)
상황
0x00000010을 왜 읽지???? 디버거로 sim을 보면 멀쩡한 값이 들어있다
상황
언제부터 이랬지? 하루? 이틀? 모르겠다 누가 뭘 바꿨지? 프로그래머 20명 이상…
재현조건이 명확하다면…
리비전 이분검색을 쓸 수 있다
리비전 이분검색?
- 버그 없는 리비전과 버그 있는 리비전을 알면 - 가운데 리비전을 받아서 버그가 재현되는지 본다 - 재현되면 앞쪽 절반, 재현안되면 뒤쪽 절반을 반복 두 명이 동시에 재현하면 삼분검색 가능!
그러나…
재현조건이 애매했다 • 보통 20분 정도면 재현되었지만 • 40분 동안 재현이 안 되어도 버그가 없다는 뜻은 아니다 마감 직전이면 인력을 동원해서 해결하겠지만… 무엇보다, 지루한 작업
가설: 다른 스레드가 덮어썼나?
해당 객체를 건드리는 모든 코드를 리뷰함: 값을 덮어쓰는 곳이 없었다. 멀티스레드 문제는 아니다. 디스어셈블리를 보자
나 어셈 볼 줄 모르는데…
인텔 어셈블리에 대해 알고 있던 것: • 함수 호출할 때 인자를 스택에 푸시한다 (역순) • 함수 호출할 때 레지스터를 스택에 푸시해서 보관한다 • 가상함수 호출 메커니즘 • mov는 복사, call은 함수호출 자세한 건 몰랐음
• 데이터 브레이크포인트라는 것이 있고, 코드에서 제어할 수 있다
마음가짐
재현만 되면 못 잡을 버그가 없다 어셈블리 그까짓거
어셈블리 그까짓거!!
디스어셈블리를 보자!
이게 다 무슨 뜻인가? eax는 정체가 뭔가?
← 여기에서, eax+0x0c를 읽다가 죽었다.
잠깐 가상함수 호출 절차
실제 멤버변수들
VTable 시작 주소
class X의 객체 가상함수 A 주소
가상함수 B 주소
가상함수 C 주소
가상함수 D 주소
가상함수 E 주소
가상함수 F 주소
class X의 VTable
Visual C++에서의
Machine code
X::가상함수 C 코드
디스어셈블리를 보자
edi에 sim이 저장되어 있다고 가정하는 코드다. 가상함수 테이블을 조회하다가 죽었구나!
↖vtbl 주소를 eax에 저장
← GameContext 함수 주소를 얻어서
←호출
디버거 Watch에 (GameSimulation*)edi 를 입력해보니 실로 멀쩡한 객체를 가리키는 값이 아니었다
edi는 함수 맨 앞에서 설정했고 (mov edi,dword ptr [ebp+8])
이후에 edi를 덮어쓰는 곳이 없다
그런데 edi가 이상한 값이 되어 있다…?
디스어셈블리를 보자
구글 검색 <edi push 책임> 해봄
호출되는 쪽에서 edi를 쓰기 전에 저장할 책임이 있다고 한다
edi 저장 책임은 누가 지나?
누가? 어떻게?
레지스터를 깨먹는 함수라니 듣도 보도 못했다
이 함수에는 58건의 call이 있다. 누굴까?
일일이 들여다보는 건 시간 낭비라고 판단
탐침을 박아보자
누군가가 edi를 깨먹고 있다
모든 함수호출 전에 edi를 저장하고
모든 함수호출 후의 edi와 비교해서
달라지면 브레이크포인트
탐침을 박아보자
여러가지 방법을 시도해봄
뭘 추가하면 레지스터 배치가 달라져서 탐침이 동작 않는 경우가 부지기수
“변수 선언 순서 바꾸니까 되는데요”가 바로 이런 경우
탐침을 박고 빌드한 뒤 어셈블리를 봐서 의도한 대로 동작하는지 확인해봐야 함
탐침을 박아보자
재현 ㄱㄱ
걸렸다!
← 여기에 걸렸다
↑이 코드를 수행하던 도중에 edi가 깨진 것이다
딱히 수상한 점이 없다
한 단계 더 들어가 보자
RegionService::PickingRegion
PickingFloor
여기 어딘가 범인이 있다
디스어셈블리를 보자
edi를 스택에 보관하고 리턴 전에 복구하네!
보관해놓은 동안 누군가 스택을 긁었겠다!
레지스터 긁는 걸 찾아내는 문제가
스택 긁는 걸 찾아내는 문제로 바뀌었다
PickingFloor 함수 첫머리
함수 끝부분
가설: edi가 저장된 위치 주변의 지역변수에 버퍼 오버런한 거 아닐까?
검증: 코드리뷰해 봤는데 딱히 그런 건 안 보임
새로운 가설
스택에 보관해둔 레지스터를 복구하기 전에 그 스택 영역을 변경하는 일은 일어나선 안 된다
데이터 브레이크포인트가 출동!
다시 탐침
데이터 브레이크포인트?
이 경우 스택의 변조를 감지해야 한다
VS에서 수동으로 매번 잡았다 풀었다 할 순 없다;;
코드에서 데이터 브레이크포인트 설정/해제가 가능
참고: http://dual5651.hacktizen.com/tt/entry/BreakBreak-BreakPoint
없어진 문서이지만 http://web.archive.org/ 를 통해 접근 가능
데이터 브레이크포인트?
edi가 push된 곳을 어떻게 알지?
구글: intel assembly stack top register → esp • edi를 push한 직후에
• 디버그 Watch로 *(int*)esp 를 찍어보니…
• edi 내용이 똑같이 저장되어 있다
• 됐다!
메모리 변경을 감지하자
재현 ㄱㄱ
근데 왜??
걸렸다!
ILocationComponent* logComp에
실제로는 다른 타입의 객체가 들어있다?
새로운 가설
locComp의 GetHandle()을 호출하려 했다
가상함수테이블의 첫번째 항목인 듯 (오프셋 0)
locComp가 실제론 PhysXCollisionActor를 가리키고 있었다면?
Phys…Actor에는 가상함수가 딱 하나 있다: 소멸자
그렇다고 치면
ILocationComponent의 GetHandle()을 부르려는 동작이 PhysXCollisionActor의 소멸자를 불렀다
그 과정에서 운좋게(?) 그 자리에서 크래시하지 않고 스택을 망쳐놓기만 했고
함수를 빠져나간 이후에 스택으로부터 복구한 레지스터에 쓰레기 값이 들어있어서 크래시한 것
문제 발생 경로
수사 종료 구체적으로 어떤 과정을 거쳐서 edi가 저장된 위치를 망가뜨린 건지는 더 파고들어 보면 알 수 있겠지만 이 정도에서 충분히 원인을 파악했다고 보고 디버깅을 마무리.
크래시 → 멘탈 붕괴 → 디스어셈블리를 보자 →
가상함수 호출하려다 죽었다 → 레지스터 edi가 깨져있네? →
다른 함수가 스택에 저장했던 걸 또다른 누군가 긁은 거다 →
데이터 브레이크포인트로 긁는 순간을 잡아내자 →
엉뚱한 객체의 함수를 호출했다 → 고기! 고기! 고기!
요약
• void* 는 위험하다.
• 어셈블리는 마법이 아니다.
• 디버깅은 가설 수립과 검증의 연속 가설이 없을 때는 시간낭비하기 쉬움
• 적절한 도구를 사용하면 도움이 됨 인라인 어셈블리, 데이터 브레이크포인트
정리
마지막으로
디버깅 멘탈을 키워주는 교재
• 모든 디버깅이 사후postmortem디버깅
• 단서는 비행기록과 잔해 조각 뿐
• 재현해볼 수 없다
항공사고수사대: 항공 사고 디버깅
• 기껏해야 MRI, CT, 혈액검사, 요추천자, 조직검사 정도
• 시시각각 상황이 변화하고
• 환자가 죽기 전에 고쳐야 한다
HOUSE M.D.: 인간 디버깅
항공 사고, 인간 디버깅에 비하면
소프트웨어 디버깅 따위 쉽다
끝