GEMS 4권 디버깅의 과학
5단계 디버깅 공정
단계 1 : 문제를 일관되게 재현한다.
버그가 나오는 환경과 상황을 구체적으로 알수 있는 것이 중요하다.
그래야지 버그를 재현할 수 있기 때문에
단계 2 : 단서 수집
단서는 가능한 원인들 중 가망이 없는 것들을 제거하는 역할을 한다.
버그와 관계없는 코드들을 않보게 하는 것이 중요하다?
단계 3 : 오류 지적
두가지 방법이 있다.
1. 버그에 원인에 대한 가설을 만들고, 그 가설을 증명 또는 반증하는 것
2. 분할정복
실패의 지점을 식별하고, 거기서부터 입력들을 통해 오류의 근원으로 역추적해 들어가는 것
단계 4 : 문제 고치기
버그는 버그가 있는 코드를 작성한 프로그래머가 고치는게 좋지만 그럴 수 없을 경우에는 작성한 프로그래머에게 정확히 설명을 듣고 맥락을 확인한 후에 해야한다.
단계 5 : 해결책의 검사
이 버그 정말로 수정되었는지 ? 이 버그로 인해 다른 버그가 생겨나지는 않았는지? 확인해야한다.
디버깅 요령들
- 가정을 의심한다.
- 상호작용과 간섭을 최소화한다.
- 무작위성을 최소화한다.
- 복잡한 계산을 여러단계로 나눈다.
수많은 계산이 한줄에 있다면 디버깅하기 힘들다.
- 경계 조건들을 점검한다.
마지막 값이나 처음값이 제대로 들어가는지 제대로 도달하는지 확인하자
- 병렬 계산을 분할한다.
스레드나 프로세스 간의 경쟁 조건이 의심스럽다면 코드를 직렬화 해서 버그를 찾아보는 것도 방법이다.
- 최근 변경된 코드를 점검한다.
- 버그를 다른 사람에게 설명한다.
다른사람에게 설명하다보면 모순이 되는 부분을 발견할 때가 있다.
- 동료와 함께 디버깅한다.
사람마다 디버깅하는 방법이 다르므로 그 다른방법으로 찾아지는 경우가 있다.
- 문제에서 잠시 벗어나라
너무 오래잡고있으면 시야가 좁아질 수도 있다.
- 외부의 도움을 요청한다.
어려운 디버깅 시나리오와 패턴들
버그가 릴리즈 빌드에서만 나타나고 디버그 빌드에서는 나타나지 않는다.
이 경우 변수를 제대로 초기화하지 않았거나 최적화된 코드에 버그가 있는 것이다.
후자에 경우에는 디버그 빌드에서 한번에 하나씩 최적화 옵션을 켜나가면서 새로 빌드하고 점검하는 것이다.
릴리즈 빌드에도 디버그 기호들을 포함시킬 수 있다.
무관해 보이는 뭔가를 수정했을 때 버그가 사라진다.
타이밍 문제이거나 메모리 덮어쓰지 문제일 가능성이 있다.
고쳐진다고 하더라도 디버깅이 끝난것이 아니므로 제대로 확인해야한다.
정말로 간헐적인 문제들
문제가 발생하였을 때 최대한 많은 정보를 기록해 두는 것이다.
문제가 언제 일어날지 모르므로 나타났을 때 최대한 많은것을 알아내자
이때 알아낸 자료들과 발생하지 않았을때의 자료와 비교해서 문제를 알아낸다.
도저히 이해할 수 없는 행동
캐시 플러싱(cache flushing) 수준을 증가시켜서 시스템을 동기화 한다.
(캐시 플러싱이 낮은 수준부터 높은수준으로의 순서임)
* 재시도
* 재빌드
* 재부팅
* 재설치
내부 컴파일러 오류
1. 완전한 재빌드를 수행한다.
2. 컴퓨터를 다시 부팅하고 완전한 재빌드를 수행한다.
3. 컴파일러가 최신 버전인지 점검한다.
4. 사용하는 라이브러리들의 버전을 점검한다.
5. 같은 코드가 다른 컴퓨터에서는 컴파일되는지 본다.
자신의 코드가 문제가 아닌 것 같다면
항상 자신의 코드를 의심해야 한다.
내부 시스템의 이해
정말로 어려운 버그를 찾기 위해서는 내부적인 시스템을 이해할 필요가 있다.
게임 프로그래머가 알아야 할 내부적인 사항은 무엇이 있을까?
* 컴파일러가 코드를 구현하는 방식을 알아야 한다.
상속, 가상함수 호출, 호출 규약, 예외 등이 어떻게 구현되는지 알고 있어야한다. 컴파일러가 메모리를 할당하고 메모리 정렬을 다루는 방법도 알 필요가 있다.
* 하드웨어 세부사항을 알아야 한다.
예를 들면 특정 하드웨어의 캐싱문제들, 주소 정렬 제약조건들, 엔디안 방식, 스택 크기, 형식의 크기들(int , log, bool 등) 등등.
* 어셈블리의 작동방식과 어셈블리 코드 읽는 법을 알아야 한다.
그러면 최적화된 빌드를 디버깅 할 때, 특히 디버거가 원래의 소스 코드를 제대로 보여주지 못할 때 많은 도움이 된다.
내부 시스템을 이애하고 작동 방식과 규칙들을 상세히 알아둘 것.
디버깅 보조를 위한 기반 추가
게임 플레이 도중 게임 변수들을 변경
실행시점에서 게임 변수들을 변경할 수 있으면 디버깅과 버그 재현이 매우 쉬워진다.
시각적인 AI 진단
텍스트와 선을 이용해서 시각적으로 AI를 확인한다.
기록기능
로그를 개별적으로 만들어 두어서 실패 원인을 추적하기 쉽게하자.
게임플레이 기록/재생 기능
버그 재현의 궁극적인 수단은 플레이어 입력을 기록하고 재생하는 것이다.
즉 특정 초기상태와 특정 플레이어 입력은 항상 동일한 결과를 내야한다.
메모리 할당의 추적
모든 할당에 대해 완전한 스택 추적을 수행할 수 있는 메모리 할당자를 갖출 것.
폭주 시 최대한 많은 정보를 출력
팀 전체를 교육
팀원들이 버그가 나타났을 때 무시하지않고 관련정보를 모으는 방법을 숙지하도록 해야하낟.
버그 방지
컴파일러의 경고 수준을 가장 높은 수준으로 설정하고, 경고를 오류로 취급하게 한다.
경고들을 최대한 교정하고, 나머지는 #pragma로 꺼버릴 것. 종종 자동적인 형변환이나 기타 경고 수준문제들이 미묘한 버그를 만들어낸다.
게임이 여러 컴파일러들에서 컴파일되게 한다.
여러 컴파일러, 플랫폼에서 작동되게 코드를 작성하다보면 코드가 견고해진다.
그리고 버그를 찾을 때 어느 플랫폼, 컴파일러에만 국한된 버그가 있을 수 있다.
독자적인 메모리 관리자를 작성한다.
콘솔게임에는 필수적인다. PC개발에 경우엔 반드시는 아니다. VC++의 메모리 시스템이 상당히 강력하고, SmartHeap같은 도구도 존재하기 때문이다.
단언문을 이용해서 가정들을 확인한다.
인수들에 대한 가정을 확인하기 위한 단언문을 추가한다. assert 매크로를 좀더 강력한 형태로 확장하는 것도 좋다.
변수를 항상 선언과 함께 초기화한다.
변수를 선언할 때 어떤 의미 있는 값을 배정할 수 없는 상황이라면 어떤 특별한 값을 배정한다.
루프와 if 문의 본문을 항상 중괄호로 감싼다.
명시적으로 감싸주는 것이 가독성이 좋다.
구분하기 힘든 변수이름들을 피한다.
동일한 코드가 여러 장소에 존재하지 않도록 한다.
코드를 변경해야 할 때 한곳만 변경하고 다른 곳은 변경하지 않을 가능성이 있기 때문이다.
마법의(하드코딩된) 수를 피한다.
코드에 숫자를 직접 사용하면 그 수치의 의미와 중요도를 쉽게 까먹을 수 있다.
마법의 수를 사용했다면 상수나 매크로를 이용해서 의미있는 이름을 붙여야 할 것이다.
테스팅할 때 코드 포괄도를 확인한다.
어떤 하나의 코드 조각을 작성했다면, 그것이 모든 분기에서 정확히 실행되는지 점검해야한다. 특정 분기에서 그 부분이 전혀 실행되지 않는 다면 버그가 들어 있을 가능성이 있다.
이런 문제는 빨리 발견할수록 좋다.