Bug Report 실습 (연습 문제)

Jaemyeong Lee·2024년 10월 30일

게임 서버1

목록 보기
63/220

실습 방식

  1. 증상을 먼저 재현한다.
  2. 가설을 세운다. (“null일 것 같다”, “인덱스 범위일 것 같다”)
  3. 브레이크포인트/Watch/호출 스택으로 가설을 검증한다.
  4. 수정 후 같은 입력으로 다시 실행하고 회귀 여부를 확인한다.

초기화 누락 (쓰레기값)

증상

  • 2번 기사 _attack이 음수/이상값으로 출력됨.

원인

  • 오버로드 생성자에서 _attack 초기화 누락.
Knight::Knight(int hp) : _hp(hp)
{
    // _attack 초기화 누락
}

수정

Knight::Knight(int hp) : _hp(hp), _attack(10) {}

디버깅 포인트

  • 디버그 빌드의 미초기화 패턴값(0xCD...)을 Watch/메모리 창에서 확인.
  • 생성자가 여러 개면 “모든 생성 경로”를 점검.

배열 인덱스 오버플로우

증상

  • 10개를 출력해야 하는 루프에서 마지막에 Access Violation.

원인

  • i <= KNIGHT_COUNT 오타로 knights[10] 접근.
for (int i = 0; i <= KNIGHT_COUNT; i++) // ❌
    knights[i]->PrintInfo();

수정

for (int i = 0; i < KNIGHT_COUNT; i++) // ✅
    knights[i]->PrintInfo();

디버깅 포인트

  • 호출 스택에서 크래시 직전 호출부 인덱스 값 확인.
  • 규칙 고정: “배열 크기 N -> 유효 인덱스 0..N-1”.

비교 로직 오류 (IsDead)

증상

  • HP가 -100인데도 “살았습니다” 출력.

원인

  • 죽음 판정을 _hp == 0으로만 검사.
bool Knight::IsDead() { return (_hp == 0); } // ❌

수정

bool Knight::IsDead() { return (_hp <= 0); } // ✅

디버깅 포인트

  • 경계값 테스트(0, -1, 1)를 항상 같이 돌린다.
  • HP 갱신 시 클램프(0~maxHp)도 함께 설계.

정수 오버플로우

증상

  • 힐을 반복했는데 오히려 HP가 음수로 변해 사망 처리됨.

원인

  • int 범위를 넘어선 누적 연산.
  • 특히 signed overflow는 C++에서 UB로 취급(래핑처럼 보여도 보장 아님).

수정

#include <algorithm>

void Knight::AddHp(int value)
{
    long long next = static_cast<long long>(_hp) + value;
    next = std::clamp(next, 0LL, static_cast<long long>(_maxHp));
    _hp = static_cast<int>(next);
}

디버깅 포인트

  • 조건부 중단점: _hp < 0로 걸어서 최초 깨지는 시점 확인.

0으로 나누기 + 정수 나눗셈

증상

  • GetAttackDamage()에서 예외 발생 또는 비율 계산 오동작.

원인

  • _hp == 0일 때 분모가 0.
  • _hp / _maxHp를 정수로 계산해 0이 되어 버림.

수정

int Knight::GetAttackDamage()
{
    int damage = _attack;
    if (_hp <= 0 || _maxHp <= 0)
        return damage;

    int percentage = (_hp * 100) / _maxHp; // 곱셈 먼저
    if (percentage <= 50)
        damage *= 2;

    return damage;
}

디버깅 포인트

  • 나눗셈 코드면 “분모 0 여부”를 먼저 Watch에 추가.
  • 정수 퍼센트 계산은 (x * 100) / y 습관화.

무한 재귀 -> 스택 오버플로우

증상

  • 반격 로직 실행 시 OnDamaged 호출이 끝없이 반복되어 stack overflow.

원인

  • OnDamaged() 내부에서 상대의 OnDamaged()를 다시 호출해 탈출 조건 없이 순환.
attacker->OnDamaged(this); // ❌

수정(예시)

void Knight::OnDamaged(Knight* attacker, bool canCounter)
{
    if (attacker == nullptr || IsDead())
        return;

    const int damage = attacker->GetAttackDamage();
    AddHp(-damage);

    if (canCounter && !IsDead() && damage > 0)
        attacker->OnDamaged(this, false); // 반격은 1회로 제한
}

디버깅 포인트

  • 호출 스택에 같은 함수가 반복되면 무한 재귀를 먼저 의심.

virtual 소멸자 누락 -> 메모리 누수

증상

  • Player* p = new Archer(); delete p; 반복 시 메모리 사용량 지속 증가.

원인

  • 부모 소멸자가 virtual이 아니어서 자식 소멸자가 호출되지 않음.

수정

class Player {
public:
    virtual ~Player() = default; // ✅
};

디버깅 포인트

  • “부모 포인터로 delete”가 보이면 virtual 소멸자부터 확인.

스택 vs 힙 혼동

증상

  • Pet pet;의 주소를 보관한 뒤 소멸자에서 delete _pet 시 크래시.

원인

  • 스택 객체 주소를 힙 소유 객체처럼 delete함.
Pet pet;               // 스택
p = new Archer(&pet);  // 주소 전달
// ...
delete _pet;           // ❌ 스택 메모리 delete

수정

p = new Archer(new Pet()); // 힙 객체를 넘기거나
// 더 나은 방법: unique_ptr<Pet> 사용

디버깅 포인트

  • 규칙: deletenew로 만든 메모리에만.

Double Free

증상

  • 궁수 사망 시점 + 소멸자에서 같은 Pet을 두 번 해제.

원인

  • delete 후 포인터 무효화 누락.
  • 해제 책임이 여러 지점에 분산됨.

수정

void Archer::AddHp(int value)
{
    Player::AddHp(value);
    if (IsDead() && _pet != nullptr) {
        delete _pet;
        _pet = nullptr; // ✅
    }
}

Archer::~Archer()
{
    delete _pet;        // nullptr이면 안전
    _pet = nullptr;
}

디버깅 포인트

  • delete 호출 지점을 grep/검색으로 모두 확인해 “단일 책임”으로 정리.

Use-After-Free (다중 참조)

증상

  • 화살 여러 개가 같은 Knight*를 타겟으로 가진 상태에서
    기사 삭제 후 다른 화살이 _target 접근하며 크래시.

원인

  • 한 객체를 여러 포인터가 참조하는데, 소유권/삭제 순서 규칙이 없음.

수정 방향

  • 참조자(화살) 작업이 끝난 후 삭제하거나,
  • 삭제 전에 모든 화살의 타겟을 무효화.
for (int i = 0; i < 10; i++)
    arrows[i]->AttackTarget();

for (int i = 0; i < 10; i++)
    arrows[i]->SetTarget(nullptr); // 또는 화살 먼저 파괴

delete knight;
knight = nullptr;

디버깅 포인트

  • UAF는 “삭제 시점”과 “크래시 시점”이 다를 수 있어 호출 스택만으로 부족할 때가 많음.
  • 객체 생명주기 로그(생성/삭제/소유자)를 함께 남겨야 추적이 빨라짐.

profile
李家네_공부방

0개의 댓글