VC 컴파일러 혹은 링커의 버그를 발견한 듯 싶습니다. |
올려짐: 2005-04-24 09:38
|
|
자세한 내용은
http://wminos.jaram...p/Project_PacMan#s-5
위키에 정리해놨습니다.
-----------------------
팩맨 게임 코어에 GUI시스템을 작성하던 중
실행타임 버그를 발견하게 되었습니다.
디버깅을 아무리 해도, 납득할 수 없는 현상이었는데.
소스에는 아무런 문제가 없어 보입니다.
dev-c++ 로는 에러없이 컴파일 되는군요.
vc7 과 vc8 beta 에서는 에러가 발생합니다.
몇시간을 디버깅 한 결과, 발생 원인을 알게 되었고..
스샷과 그 현상이 일어나는 최간소화된 소스를 제 위키에 올려놨습니다.
어떤 현상인가 간략하게 설명하면,
class A {};
class B { 반드시 A클래스 non-static 멤버함수포인터를 가짐; 반드시 가상함수가짐 };
class C : public B { int c; C() { c 변수 초기화를 해야함 } };
int main()
{
C * c = new C; // 힙에 할당해야 합니다(스택할당은 에러발생x)
delete c; // 에러 발생 ( 힙 메모리 해제하는 중 에러 발생 )
}
즉, 상위 클래스가 타클래스함수포인터를 가지고,
가상테이블을 가지고 있는 상속된 클래스 객체를 힙에 할당하면
그 객체의 변수의 값을 수정을 한적이 있으면, 객체를 메모리 해제시 에러가 발생한다는 것입니다.
vc 에서 소스를 컴파일 할 때 Ctrl+F5 로 해야 에러가 뜹니다.
이 때 디버그 모드로 들어가야 합니다.
디버그시 F5 로 에러 시점을 잡아내질 못합니다.
제 생각으로는 타클래스함수포인터와 가상테이블을 가진 상속된 클래스의 객체에 대해서
vc 가 올바른 변수의 주소를 인식하지 못하는 것 같습니다.
팩맨소스를 디버깅 해본 결과, 엉뚱한 변수에 값을 집어넣는 현상이 발생합니다.
정말 황당했습니다.
컴파일러 문제가 아니라, 혹시 free 함수의 문제라면,
operator delete 재정의로 해결을 볼 수도 있을 것 같군요.
이 것이 정말 VC 의 버그인지,
고수님들의 답변 부탁드립니다.
== 소스파일 ==
http://wminos.jaram...ect_PacMan/VCBug.zip
_________________
xcoder wminos | |
위로 |
|
|
zupet
가입: 2003년 5월 13일
올린 글: 2760
소속: EA Seoul Studio
|
에러 원인은 찾았는데.. |
올려짐: 2005-04-24 12:29
|
|
안녕하세요. 매크로 없는 메비~랍니다.
에러의 원인은 찾았는데 왜 그렇게 컴파일 되는지는 잘 모르겠군요. Listing 파일에서 문제를 찾을 수 있었는데 아래는 보기 좋도록 printf() 문으로 문제점을 찍는 코드를 추가했습니다. 에러가 발생하는 문제는 C.cpp 파일 내에서 C의 크기와 c 멤버변수의 위치를 잘못 파악하여 main.cpp 에서 할당한 메모리와 다른 위치에 데이터를 써넣기 때문에 Heap 이 깨지는 문제가 발생하고 있습니다. 초기화 할때 Heap 을 깨먹기 때문에 main의 첫줄의 printf("") 를 넣어주지 않으면 printf()가 new 다음 실행될때 에러가 나더군요.
실행시 scanf()에서 멈춤 결과 씀: |
C:24 32
C:0x003e0f60 0x003e0f78 24
main:1 8 12
main:0x0012fed4 0x003e0f68 8 |
일단.. 문제는 간단합니다. A의 크기는 0, B의 크기는 8, C의 크기가 4이므로 main.cpp 에서는 모두 정상적으로 보고 했습니다. 그런데 C.cpp 안에서는 자신의 크기가 24바이트라고 믿.고. 있습니다. 멋진넘입니다. 사실 남자라면 이래야... 쿨럭~ 어쨌거나 컴파일러가 뭔가 헷갈려서 이런 문제를 발생시키고 있습니다. 코드의 설명을 따르면 C.cpp가 컴파일 되는 시점에 A의 어떠한 정보도 알지 못한다는 이유로 B의 크기를 멋대로 늘려버렸다는 겁니다. 더불어 C의 크기도 B->C 로 가면서 불어난 크기가 24->32로 8바이트로 몸집도 커졌습니다.
코드: |
printf ( "C:%d %d\n" , sizeof (B), sizeof (C)); |
printf ( "C:0x%08x 0x%08x %d\n" , this , & this ->c, ( char *)& this ->c - ( char *) this ); |
|
코드: |
#include "A.h" // class C{}; 앞에 class A {}; 가 없으면 에러가 나지 않는다. |
printf ( "main:%d %d %d\n" , sizeof (A), sizeof (B), sizeof (C)); |
printf ( "main:0x%08x 0x%08x %d\n" , &c, &c->c, ( char *)&c->c - ( char *)c); |
| | |
위로 |
|
|
wminos
가입: 2005년 1월 27일
올린 글: 46
|
흠.. |
올려짐: 2005-04-24 13:25
|
|
VC 결과입니다.
C : sizeof(B)=24, sizeof(C)=32
main : sizeof(A)=1 sizeof(B)=8 sizeof(C)=12
C.cpp 소스에서 B, C 사이즈가
main.cpp 소스에서 B, C 사이즈와 다릅니다.
- 즉, 엄청난 위험의 소지를 갖고 있습니다.
-----------------------------------------------------
G++(DEVC++) 결과입니다.
C : sizeof(B)=12, sizeof(C)=16
main : sizeof(A)=1 sizeof(B)=12 sizeof(C)=16
C.cpp 소스에서 B, C 사이즈가
main.cpp 소스에서 B, C 사이즈와 같습니다.
------------------
main.cpp 에서
#include "A.h" 를 제거해봤습니다. 즉, A class 에 대한 자세한 정보를 알 수 없게되는거죠.
VC 결과입니다.
C : sizeof(B)=24, sizeof(C)=32
main : sizeof(A)=1 sizeof(B)=24 sizeof(C)=32
비록 크기는 같게 나와서 위험성은 줄어버렸지만, B 크기와 C 크기가 무척 비대합니다.
--------------------
또 확인해본 결과,
B 에 있는 가상함수가 없어도 크기가 다르게 나오네요
에러가 나오지 않는다고 해서 괜찮다고 볼 수가 없습니다.
이런 코드가 더 무서운 코드죠.
지금 제 VC6 상태가 안좋아서 확인을 못하겠네요.
누가 확인해주실뿐?
VC8 에서도 같은 버그가 남아있는데;
지금까지 몇년동안 수많은 개발자들이 발견못한 버그를
설마 제가 발견한 것이 되는것인가요?
그리 희귀하게 나올만한 코드는 아닌데 말이죠.
이 버그 때문에 VC 에 대해 그나마 있던 애정(?)이 식었습니다. ㅡㅡ;
패치가 있다.. 뭐 이런 답변이 있길 기대하면서..
만약 없다면 MS 에 신고하든지 해야겠습니다.
그래서 DEV-C++ 로 작업할까 생각했는데;
DEV-C++ 은 C 로는 적합한데, C++ 로는 가끔 엄청난 버벅임과
인텔리센스의 미숙함, 더중요한것은 함수설명태그가 화면에서
사라지지 않는 버그(1년이상 안고쳐지고있는) 때문에 못쓰겠습니다.
혹시 다른 좋은 IDE 혹은 VS IDE에서 VC컴파일러&링커 대체해서 쓸 수 있는
것이 있다면 추천부탁드립니다.
_________________
xcoder wminos | |
위로 |
|
|
wminos
가입: 2005년 1월 27일
올린 글: 46
|
이 문제에 대한 그나마 가장 나은 해결방법? |
올려짐: 2005-04-24 13:41
|
|
멤버함수포인터 크기
sizeof(c->fp);
를 출력해봤습니다.
main.cpp 에서 #include "A.h" 했을때
4바이트입니다. 정상적입니다.
include 하지 않았을 때,
16바이트입니다. 말도 안됩니다.
포인터는 무조건 4바이트이면 될텐데,
뭔가 컴파일러가 멤버함수포인터 선언구문을
다른 식으로 해석하는 것 같다는 생각이 드는군요.
A 클래스를 알던 모르던 아무런 상관이 없을 것 같은데 말이죠.
혹시 짐작되는 다른 해석의 여지를 알고 계신분 있으면 리플을..
결국, 이 것을 바탕으로 좋은 해결방법을 찾았습니다.
C.cpp 컴파일에 필요없는 #include "A.h" 을
했더니 함수포인터크기가 4바이트입니다.
팩맨에는 넣어보지 않았지만,
크기가 같으니, 일관적으로 돌아갈 것 같습니다.
모든 상속받은 클래스 .cpp 넣기는 어려우니,
간단하게 B.h 에 #include "A.h" 하면 자동으로 들어갈테니 그게 낫겠군요
마찬가지로 dev-c++ 은 다른 모든 포인터는 4바이트인데,
타클래스함수포인터는 8바이트입니다. 이해하진 못하겠지만, 일관적이므로 뭐 좋습니다.
결론은 이겁니다.
B 클래스 안에 A 클래스의 멤버함수를 가르키는 포인터를 선언할때,
class A; 만 해서는 에러가 납니다.
#include "A.h" 를 해야 VC 에서는 에러가 발생하지 않는 다는 것입이다.
_________________
xcoder wminos | |
위로 |
|
|
henjeon
가입: 2004년 5월 22일
올린 글: 36
소속: NEXON
|
|
올려짐: 2005-04-24 14:58
|
|
http://msdn.microso...sing_inheritance.asp
http://groups.googl...kella.com%26rnum%3D1
버그라고 보기에는 약간 무리가 있고,
C++에서 PTMF(pointer to member function)의 크기가 4,8,12,16 중 하나가 될 수 있기 때문인 것 같습니다.
정의되지 않은 클래스의 PTMF의 크기는 일단 안전하게 16으로 가게 되고,
정의된 클래스의 PTMF는 요놈이 가상함수냐, 다중상속되었느냐에 따라서 그 크기가 결정된다고 하네요.
저도 클래스 C의 생성자를 디스어셈해보니까 0을 EAX+18h에 넣는 것에서 뭔가 이상하다 싶어서 구글님에게 물어보니 저런 링크를 알려주시더군요.^^
오브젝트랑 함수의 포인터 크기는 같은 줄 알았는데 아닌가 봅니다. | |
위로 |
|
|
zupet
가입: 2003년 5월 13일
올린 글: 2760
소속: EA Seoul Studio
|
근질근질해서.. |
올려짐: 2005-04-24 15:05
|
|
안녕하세요. 매크로 없는 메비~랍니다.
근질근질해서 대충 써서 MSDN Forum 에 올려버렸습니다. 여기 글을 한번 써보고 싶었는데 잘된 것 같네요. M$ 애들이 과연 여기까지 올까.. 모르겠지만 MVP 들도 왔다갔다 할테니 뭔가 위에까지 보고가 올라겠죠. ^__^
http://forums.micro...ost.aspx?PostID=3555
p.s.써놓고 보니 제목이 틀렸다.. 흠.. 영문으로 '매크로 없는 메비~'라고 써볼까..
p.s.2.MSDN 포럼에는 편집 기능이 없군요. OTL.. 원래 에러 찾은분 이름이랑 GPG에서 찾았다고 추가하려 했더니.. 쿨럭~
zupet 가 2005-04-24 15:09에 수정함, 총 1 번 수정됨 | |
위로 |
|
|
비회원
손님
|
알려진 버그(?)입니다. |
올려짐: 2005-04-24 15:08
|
|
c.cpp에서 b의 사이즈가 16인것은 상속에 관련된 문제입니다. 그래서 pointer to member function을 제한하는 여러가지 방법을 제공하고 있습니다. c.cpp에서 a의 정의를 알수 없기 때문에, a의 멤버함수를 가르키는 사이즈를 결정할수가 없죠. 그래서 최대값인 16을 사용하는것 같습니다.
(이건 짐작입니다. 여기에 대해서 더 알아 내고 싶은 마음은 안들더군요. 멤버함수에 대한 포인터의 크기는 컴파일이 끝나서 최종적으로 상속관계를 모두 파악한 후에야 결정할수 있다 정도로 이해하고 있습니다.)
다음과 같은 방법을 동원해서 에러를 막을 수 있습니다.
1. 위에서 지적한바와 같이 a의 정의를 c.pp의 가시권에 두는 겁니다.
2. a가 단일상속된다는 것을 미리 알려주면 됩니다. _inheritance keyword와 #pragma pointers_to_members 를 사용하면 됩니다.
b.h에서 class a를 class _single_inheritance A;로 수정해서 문제를 해결할 수 있습니다.
c.cpp의 가시권에 #pragma prointers_to_members (full_generality, single_inheritance)와 같은 문장을 삽입함으로써 해결 할 수도 있습니다.
더 자세한 내용은 msdn을 파보시면 될것 같습니다. 이게 과연 버그인가 하는 문제에 대해서는 버그가 아니다에 한표를 던지고 싶네요.
.neuk | |
위로 |
|
|
wminos
가입: 2005년 1월 27일
올린 글: 46
|
버그가 아니라고 하기에는; |
올려짐: 2005-04-24 15:16
|
|
답변 감사드리구요 ^^
멤버함수포인터 크기가 다른건 가상 함수를 가르킬때 등
이유가있을거라고 짐작했습니다만;
확실하지 않게 생각했던 것들을 확신하게 해주는 답변을 얻은 것 같아 기쁩니다.
그리고 다른 해결방법이 존재하긴 하지만;
이 것은 명백한 문제라고 생각합니다.
사용자가 의도한 결과를 낼 수 없는데 컴파일을 허용하면,
버그가 아니라고 인정할 수 없겠군요.
정말 어쩔 수 없는 특별한 이유라도 있으면 이해할 수 있겠지만;
이런 경우 에러를 내주어야 하지 않을까요?
해당 클래스에 대해 완벽한 선언이 없으면,
혹은 #pragma 와 같은 설정이 없으면
멤버함수포인터 선언에 대해서 에러를 내도록 해야 좋은 것 아니겠습니까?
그리고 dev-c++ 에서 에러가 없는 것은 우연일 수도 있겠다는 생각이 드는군요.
_________________
xcoder wminos
wminos 가 2005-04-24 15:30에 수정함, 총 1 번 수정됨 | |
위로 |
|
|
zupet
가입: 2003년 5월 13일
올린 글: 2760
소속: EA Seoul Studio
|
Re: 알려진 버그(?)입니다. |
올려짐: 2005-04-24 15:18
|
|
비회원 씀: |
c.cpp에서 b의 사이즈가 16인것은 상속에 관련된 문제입니다. 그래서 pointer to member function을 제한하는 여러가지 방법을 제공하고 있습니다. c.cpp에서 a의 정의를 알수 없기 때문에, a의 멤버함수를 가르키는 사이즈를 결정할수가 없죠. 그래서 최대값인 16을 사용하는것 같습니다.
(이건 짐작입니다. 여기에 대해서 더 알아 내고 싶은 마음은 안들더군요. 멤버함수에 대한 포인터의 크기는 컴파일이 끝나서 최종적으로 상속관계를 모두 파악한 후에야 결정할수 있다 정도로 이해하고 있습니다.)
다음과 같은 방법을 동원해서 에러를 막을 수 있습니다.
1. 위에서 지적한바와 같이 a의 정의를 c.pp의 가시권에 두는 겁니다.
2. a가 단일상속된다는 것을 미리 알려주면 됩니다. _inheritance keyword와 #pragma pointers_to_members 를 사용하면 됩니다.
b.h에서 class a를 class _single_inheritance A;로 수정해서 문제를 해결할 수 있습니다.
c.cpp의 가시권에 #pragma prointers_to_members (full_generality, single_inheritance)와 같은 문장을 삽입함으로써 해결 할 수도 있습니다.
더 자세한 내용은 msdn을 파보시면 될것 같습니다. 이게 과연 버그인가 하는 문제에 대해서는 버그가 아니다에 한표를 던지고 싶네요.
.neuk |
알려진 '버그'는 아니라고 생각되는군요. 위의 경우는 아주 특수한 경우이고 선언 없이 클래스의 포인터를 사용하는 경우는 아주 흔히 사용하는 경우입니다. 저도 클래스간의 상관 관계를 줄이기 위해서 class MyClass; 식으로 선언만 하고 포인터를 주고 받는 코드를 작성하는 경우가 많이 있습니다. 대부분의 경우는 문제가 없었는데 이번은 상당히 특수한 경우고 몇가지를 삭제하거나 순서를 바꾸면 발생하지 않는 에러입니다. 뭣보다.. gcc 에선 잘 된다니까요. -_-a
저는 별로 신경 안쓰는 부분이지만 저희 프로젝트에서 fucntor 를 상당히 많이 쓰게 되어있기 때문에 위와 같은 문제가 언젠가는 발생할 수도 있고 이런 문제는 디버깅하기 너무 더.럽.습니다. 만약 상용화 이후 이런 문제가 랜덤하게 발생하면 정말정말정말 죽고 싶을 정도로 짜증이 날지 모르니 잡을 수 있을때 빨리 잡아두는게 좋겠죠. 뭣보다.. 이런 문제가 있으면 warning level 1 에서 출력을 해줘야 하는 문제라고 생각됩니다. 초기화 안한 변수는 워닝을 내주면서 이런 문제를 그냥 지나치는건 많이 곤란하니까요. 뭣보다.. 이왕 16바이트로 잡을꺼면 다같이 잡지 main.cpp은 안잡고 c.cpp에서만 잡는건 매우 곤란하죠. | |
위로 |
|
|
조프
가입: 2005년 2월 21일
올린 글: 115
|
|
올려짐: 2005-04-24 15:37
|
|
구글님의 도움을 받아서 찾아봤는데...
멤버 함수 포인터의 크기는 그 클래스를 컴파일하는 시점에서 알 수 있느냐 없느냐에 따라 달라집니다.
그 클래스의 함수 중에 virtual이 있냐 없냐에 따라 달라지는데,
Visual C++은 이 부분에서 엄한 최적화를 하고 있습니다.
클래스의 정체를 비밀로 하고, 그 클래스의 멤버 함수 포인터를 쓰고 싶다면
/vmg /vmv 옵션을 주고 컴파일하면 항상 16이 나오게 해주는 것 같습니다. 절대 에러가 안나온다고 써있네요.
MSDN에서 #pragma pointers_to_members 를 찾아서 링크를 따라가면서 읽어보시면 대강 설명이 나옵니다.
추가. 찾아서 올리는 사이 이미 답글이 올라왔네요. | |
위로 |
|
|
비회원
손님
|
Re: 알려진 버그(?)입니다. |
올려짐: 2005-04-24 15:48
|
|
zupet 씀: |
알려진 '버그'는 아니라고 생각되는군요. 위의 경우는 아주 특수한 경우이고 선언 없이 클래스의 포인터를 사용하는 경우는 아주 흔히 사용하는 경우입니다. |
선언 없이 포인터를 사용하는 경우는 아주 흔한 경우가 맞습니다만, 멤버함수 포인터를 다른 클래스의 멤버로 덜컥 넣는건 흔히 있는 일이 아니죠. 일반적으로 말하는 전방참조(forward declaration)와 이문제는 그다지 관계가 없어 보입니다.
전방 참조가 의존성을 줄여주는건 확실하지만, 이 경우처럼 최후까지 자신의 모습을 완전히 감추는 경우는 없습니다. 헤더파일에서 class MyClass;라고 사용했다고 하더라도 *.cpp파일에 반드시 include해줘야 할테니 말입니다.
g++나 dev-c++에서 멤버함수에 대한 포인터를 8바이트로 항상 동일하게 생성하는게 장점일수도 있겠지만, 전 그걸 4바이트로 줄여주는게 장점으로도 보이는데요.
위의 짧은 에러코드에서는 생략되었지만, B의 fp에 제대로된 값을 할당하기 위해서는 최소한 A의 모습을 알아야 합니다. 근데 그 위치가 B.cpp도 아니고 C.cpp도 아닌 x.cpp라면 그게 더 문제라고 생각하는데요.
걱정하시는 functor의 경우에 이러한 일이 발생할 확률은 0이라고 생각합니다.
ps. 상황에 따라 달라지는 멤버함수포인터 크기가 걱정이라면 /vmg 옵션을 사용하셔서 근본적으로 차단할수도 있습니다.
.neuk | |
위로 |
|
|
비회원
손님
|
|
올려짐: 2005-04-24 15:51
|
|
답글 쓰는 사이에 조프님께서 /vmg 옵션에 관한 내용을 적어 주셔서 반복되버렸군요. -_-
.neuk | |