<스택의 쓰레기 값>
// [사양서]
// 기사는 생명력(hp), 공격력(attack)을 갖고 있으며
// 기본값은 Hp = 100, Attack = 10입니다.
// 원활한 게임 진행을 위해 기사를 2개 생성하고,
// 1번 기사는 기본값으로,
// 2번 기사는 Hp를 200으로 올려서 설정합니다.
// [Bug Report #1]
// 2번 기사의 정보가 사양서와 일치하지 않습니다.
// 2번 기사의 공격력이 엉뚱한 값(음수)로 되어 있습니다.
// - 공격력이 음수로 설정된 원인을 찾아주세요
// - 2번 기사의 공격력이 기본값(10)으로 설정되길 희망합니다.
https://velog.io/@starkshn/%EC%B4%88%EA%B8%B0%ED%99%94-%EB%A6%AC%EC%8A%A4%ED%8A%B8
현재 스택 메모리는 밀물 썰물처럼 불안정한 메모리 이다.
이전 녀석이 사용했던 값을 그대로 사용하고 막 그럼 => 계속 공유한다.
이전 입주민이 자신이 사용하던 공간을 정리를 안해놓음.
현재 인자를 하나를 받는 생성자에 들어와서 메모리에 this를 까보면
현재 cdcdcdcdcd.... 있는 값이 있는데
이것을 계산기로 두들겨보면은
4바이트 기준으로 -842150451 이라는 값이 들어가있다.
어차피 Knight의 멤버 변수의 크기가 4바이트 + 4바이트이기 때문..
그래서 _hp가 들어갈 자리에는 정상적으로 들어가고
초기화 해주지 않은 뒤에 cdcd라는 값을 k2의 _attack이 가르키게 되는 것이다.
실제로 현재 상태에서
_attack의 주소를 가보면 이런 값이 있다.
결론 : 이전 입주민이 자신이 사용하던 공간을 정리를 안해놓음.
내가 생각한거 일단 맞다.
그냥 생성자 선처리 영역에서 초기화 해주면됨.
<for문 조건식>
for문의 조건식이 잘못됬다.
i < KNIGHT_COUNT로 변경해주어야 하는데
현재 11번째에서 크래쉬가 나는 이유는
잘못된 메모리 접근으로 인한 크래쉬이다.
i가 0~9까지는 동적할당으로 만들어 주었는데
knights[10] 은 어디에있는지도 모르는 엉뚱한 값을 가르키는 주소이다.
그래서 엉뚱한 주소라도 있는 것이니 접근을 하도록 하겠다.
여기서 걸리는데 이럴경우 호출 스택을 보는것이 좋다.
유효하지 않은 값을 접근해서 건드리니까 크래쉬 난다.
<= KNIGHT_COUNT 이기때문에
유효하지 않은 메모리에 접근을 했던것이 원인이다.
<버그는 아니고 잔 실수>
IsDead 함수의 return 이 bool 이고 _hp == 0일 경우라면 true
아니라면 false이다 (즉 죽지 않았다.)
현재 AddHp의 에 들어와서 조사식을 보면은
this->_hp가 k1의 _hp이다.
값이 -100으로 변경된것은 당연함.
그런데
여기서 _hp == 0일때만 죽은것으로 해주었으니 걸릴 수가없다.
_hp == 0일 경우만 죽은 경우이니까
<= 0 OK?
<오버 플로우>
100만 x 100만 =
k1의 _hp주소의 자리는 4바이트 정수형이다.
이곳에 100만 + 100만 + 100만...을 하다보니
4바이트 정수형이 표현할 수 있는 범위를 벗어나 음수를 표현하게 된 것이다.
현재 _hp = 100
이렇게 더하고나면 백만에 100을 더한 값.
메모리봐도
이값은
100100이다.
unsigned int 최대 범위가 이렇다.
그런데 100만을 100만 넣으면
이러한 값인데
이게 밀리고 밀리다가
hp에 3,567,587,428 35억 얼마가 들어간것임.
현재 42억 얼마 까지 갔다가
4294000000 에서 1000000 더 더하니까 이제
이런값이 되어버림
이게 밀리고 밀리다가 양수인 얼마가 나오게 되는 것이다.
되게 간단한 문제인데
i가 8일때 부터 크래쉬가 걸려서 다른 방향으로 막 생각했던거 같다.
i가 8정도이면 8십만~9십만인데 unsigned가 아니더라도 아직 범위를 초과 하지 않았다고 생각해서 빙빙 돌아서 문제를 해결하기는 했다.
breakpoint 조건에다가
_hp < 0 인경우로 두고
보면은 -21억 몇이다.
int의 범위가 -21억~21억인데 그 문제가 아닌가? 유추할 수 있다.
"오버 플로우"가 발생해서 음수가 들어감.
정수 * 정수 곱할때 int범위 금방 초과할 수 있으니 이부분을 체크를 해주고 주의 해야한다!
< zero division >
여기서 딱거리는데
ratio가 int형이고 _maxHp도 int, _hp 도 int형이다.
int / int => 소수점 나누어 떨어짐.
0이 나온다. 그래서 if (ratio > 2) 에 걸릴일이 없는것이다.
사실은 이게 아니라 정수를 0으로 나눈 zero division발생해서 그냥 바로 크래쉬남.
if문으로 조건을 걸어주어서
이렇게 수정해주어도 됨.
글런데 k1의 _hp를 이런식으로 수정하였을 경우
percentage 가 0이 나오는데
정수 / 정수 => 소수점 버려지고 거기다가 * 100을해서 percentage가 양수가 나올줄 알았지만
0이 나온다.
이말인 즉슨 나누기 연산자가 우선순위가 높다?
연산자의 우선순위는 같은데 연속적으로 있기 때문에 결합 방향을 봐야한다.
1) 둘중하나를 float만들어서 연산한다.
2) 아니면 ()
이렇게.
그런데 또 주의 해야할게 정수 * 정수라 오버 플로우 주의 해야한다.
< Stack Overflow>
눈으로 봤을 때,
여기서 문제가 발생함.
뎀지 100 넣고
k1의 _hp확인하면 0이다. (예상대로)
그리고 현재 this의 정보를 보면
_hp = 0, _maxHp = 100. _attack = 100이다.
percentage 현재 0이다.
이대로 내려가면 damage = 200이고
이거 OnDamaged에서 재귀함수 무한정 호출함.
이거 걸어주면 k1이 IsDead라 공격안하고 종료함.
< vftable >, < memory leak >, < 소멸자 >, < 상속관계 >
이거 가상함수 테이블 문제임.
Player의 ~Player()에다가 가상함수를 안 붙여 주었기 때문에
현재 p가 Player이기 때문에
delete p를 할경우 Player의 ~Player()만을 호출 하기 때문에
Archer클래스의 ~Archer()의
이부분을 호출하지 못한다.
그래서 메모리가 해제되지가 않고
"메모리 누수" 현상이 계속 발생을 하는 것이다.
메모리 누수가 발생 -> 현재 p는 Player* 이다.
상속관계에서는 소멸자를 항상 virtual로 만들어야한다.
Archer의 소멸자를 호출을 못해서
_pet메모리 해제를 못해서 메모리 누수 방생.
이거는 좀 쉬운 문제이다.
< 동적 할당 >, < 스택 >, < 힙 >
delete 키워드는 동작할 당된 힙메머리 영역을 해제를 하는 것이지
스택메모리를 해제하는 것이 아니다.
또한
Pet pet의 영역은 switch문
while (true)
{
// 나중엔 궁수, 법사 등 추가 예정
Player* p = nullptr;
switch (rand() % 3)
{
case 0:
{
p = new Knight();
p->_hp = 100;
p->_attack = 100;
break;
}
case 1:
{
// 여기서 같이 만들어준다
// 이런 저런 펫 정보 추가될 예정
Pet pet;
// Archer를 만들 때 pet 정보도 넘겨준다
p = new Archer(&pet);
p->_hp = 100;
p->_attack = 100;
break;
}
case 2:
{
p = new Mage();
p->_hp = 100;
p->_attack = 100;
break;
}
}
delete p;
}
pet은 switch문 안에서만 유효한데
switch밖에 나오면 스택 메모리가 해제가 된다.
이것을 switch문을 나와서 delete p를 할때 Archer의 소멸자가
먼저 호출이 되면서 delete pet을 할려니까
없는 메모리를 해제한다 => 에러가 딱 걸리는 것이다.
현재 pet의 주소가 0x0095eaa4 이다.
좀 쉬운 문제
못 풀었다면 보는 눈을 키워라.
로컬 변수로 만들어 주었다(지역변수)
Pet pet이 생성자와 소멸자를 알아서 호출해 줄 것이다.
또한 유효한 범위가 case 1 {}이 안이다.
이거 ㅈㄴ 위함한 행동이다.
매개변수, 지역변수, return 주소 가 스택프레임에 잡히게 되는데
이게 반복해서 만들어짐.
또한 스택에 있는 메모리를 강제로 delete한다는 것임.(이상하다)
이렇게 바꾸든가 해야한다.
< double free >
일단 delete 두번해서 발생하는 문제임. double free
일단 Archer 동적할당으로 new Archer만드는 거 OK
Include Player.h도 했고 public:에 Pet클래스 전방선언도 했기 때문에
main에서는 Arcer클래스의 크기가 잡혀서 스택에 올라가기는 한다.
(또한 Archer의 pet의포인터를 받는 생성자 호출한다.
이것또한 동적할당하여 넘겨준 것이다.)
현재 Pet* 을받는 Archer의 생성자가 호출이 되면
선처리 영역에서 동적할당 하여 받은 pet을 넘겨주고
객체의 Pet* 포인터 멤버 포인터 변수에 선처리 영역에서 할당 받은애가 넘어가진다.
이까지는 OK.
이후
Knight클래스는 기본생성자를 통해서 만들어주고 멤버 변수의 값은 직접 셋팅해준다.
이후
int damage = knight->GetAttackDamage()함수를 보면은
virtual이 없기때문에 상속을 해준 Player의 구현부로 가게된다.
조건에 걸리는것이 없기 대문에 그대로 100return.
이후 archer->AddHp(-100); 넣어서 호출.
AddHp는 virtual로 구현되어있기 때문에 가상함수 테이블 주소를 찾아서
Archer클래스의 AddHp를 호출한다.
ㅇㅇ.
이 함수 안에서 Player의 AddHp를 호출 먼저함.
value가 100이기 때문에 => _hp = 0된다.
if문에 걸리지 않고 그대로 호출 종료
다시 Archer의 AddHp로 돌아온다.
이후 IsDead에 걸리기 때문에
delete _pet수행한다.
그러면 일단 28번째 줄까지 수행하고 나면 동적할당 받은
_pet을 delete한번 하였다.
archer만 delete하면 끝나는데
문제는
virtual ~Player()이기 때문에
delete하게되면 가상함수 테이블로 인해
Archer클래스 가상함수 테이블 주소를 찾아 소멸자를 호출하게된다.
이과정에서 응...?? 현재_pet은 nullptr이기 때문에
if문에 걸리지 않고 그냥 나옴...
그런데 지금 걸리고 에러가 뜸..
원인을 다시 보면은
일단 이까지 넘어오면은 delete _pet을 하는 것 까지는 ok이다.
"주소 자체"는 힙에 있는데 값이 dddd...로 밀려잇음 해제 했기 때문에
이후 가상함수테이블 주소에 따라 Archer의 소멸자 호출하러 왔다.
아하 씨발!
지금 _pet을 delete를 하고 nullptr로 안 밀어 주었기 때문에
"접근"자체는 가능하다!!
씨발 그래서 지금 해제되어 있는 메모리로 가서 다시 한번 해제를 할려고 하니까 에러가 남
두번 delete했던것이다.
그래서
이렇게 nullptr로 밀어주어야
가상함수 테이블에 따른 소멸자에 걸리지 않고
끝!
메모리를 까보아도 문제가 안보인다면
코드를 분석해라.
지금 소멸자에서 딱 걸리는데
AddHp에서
delete _pet을 한다는 말이 뭐냐하면은
_pet이라는 값에 nullptr이 들어간게 아니라
_pet이 차지하고있는 메모리 공간에가서 그 공간을 날려준 것이다.
Pet* _pet이라는 것은
포인터 이기 때믄에 값으로 주소를 가진다.
메모리에 올라간 주소값 | 변수명 | 값(데이터) |
---|---|---|
0x1000 | _pet | new Pet을 통해 동적할당하여 받은 주소 |
new Pet을 통해 실제 Pet데이터가 있는 곳을 가리키는게
_pet이 들고있는 값(데이터)이다.
그러니까 저 밑줄 친 부분이 뭐냐하면은
동적할당하여 받은 주소를 삭제를 하는 것이 아니라
동적할당하여 받은 주소로 갔을 때 있는 데이터를 delete로 날려준 것이다.(해제)
그렇기 때문에 _pet의 0x1000이라는 주소는 그대로 남아있는 것이다.
그렇다면
_pet은 이제 "유효하지 않은 메모리 주소"를 가르키는 상태인 것이다.
이렇게 사용안할 것이라면 이렇게 밀어주어야한다.
double free 이슈.
< RTTI > , < Use After Free >
virtual PrintInfo에 접근을 하면서 생기는 문제임.
죽었다면 knight를 delete해주고 nullptr로 밀어버린다음
AttackTarget에서 죽어서 nullptr이 되어버린 knight에게 접근을 못하도록 해야한다
만약 break없으면 i = 8일때 여기로 들어와서
_target은 nullptr이 아니라 여기로 들어와서
데미지를 깍고 Printinfo를 하게 되는 것이다.
아니면은
if로 분기처리.
PrintInfo의 부분에서
가상함수 테이블이 어떤애를 참조해야하는지를 본다음에
vftable을 이용해서 접근을 하는 것인데
RTTI 개념. 동적으로 어떤애를 선택해야하는 지를 알아야하는데
그 정보는 _target 이라는 객체의 첫번째 변수 offset에
어떤 테이블의 주소에 저장이 되어있다.
그래서 이녀석을 호출할려면
해당 가상함수 테이블에 접근을 한다음에
해당하는 테이블에 해당하는 함수 주소를 호출을 할텐데
그게 이미 밀린 0xdddddddd 이런 주소로 이동을 할려 시도를 하다가 발생한 이슈.