2011-01-25 3 views
12

잘못된 함수를 호출하는 것으로 보이는 일부 C++ 코드 (다른 사람이 작성한 코드)가 있습니다. 상황은 다음과 같습니다.C++는 객체의 완전히 잘못된 (가상) 메소드를 호출합니다.

UTF8InputStreamFromBuffer* cstream = foo(); 
wstring fn = L"foo"; 
DocumentReader* reader; 

if (a_condition_true_for_some_files_false_for_others) { 
    reader = (DocumentReader*) _new GoodDocumentReader(); 
} else { 
    reader = (DocumentReader*) _new BadDocumentReader(); 
} 

// the crash happens inside the following call 
// when a BadDocumentReader is used 
doc = reader->readDocument(*cstream, fn); 

조건이 true 인 파일은 정상적으로 처리됩니다. 그것이 틀린 충돌 인 것들. DocumentReader의 클래스 계층 구조는 다음과 같습니다 : 다음

class GenericDocumentReader { 
    virtual Document* readDocument(InputStream &strm, const wchar_t * filename) = 0; 
} 

class DocumentReader : public GenericDocumentReader { 
    virtual Document* readDocument(InputStream &strm, const wchar_t * filename) { 
     // some stuff 
    } 
}; 

class GoodDocumentReader : public DocumentReader { 
    Document* readDocument(InputStream & strm, const wchar_t * filename); 
} 

class BadDocumentReader : public DocumentReader { 
    virtual Document* readDocument(InputStream &stream, const wchar_t * filename); 
    virtual Document* readDocument(const LocatedString *source, const wchar_t * filename); 
    virtual Document* readDocument(const LocatedString *source, const wchar_t * filename, Symbol inputType); 
} 

도 관련이 :

class UTF8InputStreamFromBuffer : public wistringstream { 
    // foo 
}; 
typedef std::basic_istream<wchar_t> InputStream; 

비주얼 C에서 실행 ++ 디버거, 그것은 BadDocumentReader에 readDocument 호출하지

를 호출하는 것을 보여줍니다
readDocument(InputStream&, const wchar_t*) 

아니라

readDocument(const LocatedString* source, const wchar_t *, Symbol) 

이것은 모든 readDocuments에 cout 문을 고수함으로써 확인됩니다. 콜 후 소스 인자는 물론 쓰레기로 가득 차서 곧 충돌을 일으킨다. LocatedString은 InputStream에서 하나의 인자를 갖는 암시 적 생성자를 가졌지 만, cout을 검사하면 호출되지 않는 것으로 나타납니다. 어떤 아이디어가 이것을 설명 할 수 있습니까?

: 다른 가능한 관련 세부 정보 : DocumentReader 클래스가 호출 코드와 다른 라이브러리에 있습니다. 또한 모든 코드를 완전히 다시 작성 했으므로 문제가 남아 있습니다.

편집 2 : 나는 비주얼 C++ 2008

편집 3을 사용하고 있습니다 : 나도 같은 행동은 "최소한의 컴파일 가능한 예"를 제작하려고했으나 문제를 복제 할 수 없습니다.

편집 4는 : 빌리 ONeal의 제안에서

, 나는 BadDocumentReader 헤더에 readDocument 방법의 순서를 변경했습니다. 물론, 순서를 변경하면 함수가 호출되는 순서가 변경됩니다. 이것은 내가 vtable에 색인을 붙이는 것과 관련하여 이상한 일이 있다는 의심을 확인하는 것으로 보이지만 그 원인을 정확히 알지 못합니다.

편집 5 : 다음은 함수 호출하기 전에 몇 줄의 분해이다 :

00559728 mov   edx,dword ptr [reader] 
0055972E mov   eax,dword ptr [edx] 
00559730 mov   ecx,dword ptr [reader] 
00559736 mov   edx,dword ptr [eax] 
00559738 call  edx 

나는 많은 어셈블리를 모르겠지만, 독자 변수 포인터를 역 참조 것처럼 그것은 나에게 보인다. 메모리의이 부분에 저장된 첫 번째 것은 vtable에 대한 포인터 여야하므로 eax로 다시 참조됩니다. 그런 다음 을 먼저 것으로 edx의 vtable에 넣고 호출합니다. 서로 다른 순서의 메소드로 재 컴파일하면 이것을 변경하지 않는 것 같습니다. 항상 vtable에서 첫 번째 것을 호출하려고합니다. (나는 이것에 대해 완전히 오해 할 수 있었고, 조립에 대한 지식이 전혀 없었을 것입니다 ...)

당신의 도움에 감사드립니다.

편집 6 : 편집 : 문제가 발견되어 모든 사람의 시간을 낭비하는 것에 사과드립니다.문제는 GoodDocumentReader가 DocumentReader의 서브 클래스로 선언되었지만 실제로는 그렇지 않다는 것이 었습니다. C 스타일의 캐스트는 컴파일러 오류를 억제하고있었습니다 (@sellibitze를 들었어 야합니다. 답글로 의견을 제출하려면 올바른 것으로 표시 할 것입니다). 까다로운 점은 누군가가 GoodDocumentReader에 두 개의 가상 함수를 추가하여 더 이상 올바른 함수를 호출하지 않을 때까지 코드가 몇 달 동안 순수 사고로 작동했기 때문이라는 것입니다.

+0

'DefaultDocumentReader'란 무엇입니까? –

+2

문제를 보여주는 가능한 최소화 가능한 예제로 잘라내시겠습니까? –

+0

올리 : 죄송합니다. "BadDocumentReader"여야합니다. 게시물을 업데이트했습니다. –

답변

2

먼저 C- 캐스트를 제거하려고합니다.

  • 그것은 완전히 불필요있는 자료로 파생 된이 (가 안 비록) 그것은, 실제로, 그것은처럼 보이는

버그가 발생할 수 있습니다

  • 언어로 자연에서 캐스트 컴파일러 버그 ... 확실히 VS에서 첫 번째되지 않습니다.

    나는 불행하게도 GCC의 캐스트가 제대로 발생, 손에 VS 2008가 없습니다 :

    struct Base1 
    { 
        virtual void foo() {} 
    }; 
    
    struct Base2 
    { 
        virtual void bar() {} 
    }; 
    
    struct Derived: Base1, Base2 
    { 
    }; 
    
    int main(int argc, char* argv[]) 
    { 
        Derived d; 
        Base1* b1 = (Base1*) &d; 
        Base2* b2 = (Base2*) &d; 
    
        std::cout << "Derived: " << &d << ", Base1: " << b1 
               << ", Base2: " << b2 << "\n"; 
    
        return 0; 
    } 
    
    
    > Derived: 0x7ffff1377e00, Base1: 0x7ffff1377e00, Base2: 0x7ffff1377e08 
    
  • 11

    다른 소스 파일이 클래스의 vtable 레이아웃에서 일치하지 않기 때문에 이러한 현상이 발생합니다. 함수를 호출하는 코드는 readDocument(InputStream &, const wchar_t *)이 특정 오프셋에 있다고 생각하지만 실제 vtable은 다른 오프셋에 있습니다.

    일반적으로 vtable을 해당 클래스 나 상위 클래스에서 가상 메서드를 추가하거나 제거한 다음 하나의 원본 파일을 다시 컴파일하지만 다른 원본 파일은 다시 컴파일하지 않을 때 발생합니다. 그런 다음 호환되지 않는 객체 파일을 가져오고 링크하면 서로 붐을 일으 킵니다.

    이 문제를 해결하려면 라이브러리 코드와 라이브러리를 사용하는 코드 모두를 완전히 정리하고 다시 작성하십시오. 라이브러리에 대한 소스 코드가 없지만 클래스 정의가있는 헤더 파일이 있으면 옵션이 아닙니다. 이 경우 클래스 정의를 수정할 수 없습니다. 클래스 정의가 원래대로 되돌려 야하고 모든 코드를 다시 컴파일해야합니다.

    +0

    +1. .h 파일은 vtable을 포함하여 객체의 레이아웃을 정의합니다. 라이브러리를 다시 컴파일하지 않고 변경하면 모든 것이 엉망이됩니다. –

    +1

    이론적으로 소스가 다른 컴파일러, 다른 버전 또는 다른 설정으로 컴파일되는 경우이 문제가 발생합니다. 그러나 실제로 컴파일러/옵션은이 문제가 발생하기 위해서는 상당히 달라야합니다 (다시 컴파일이 필요하다는 것을 감지하지 못하면 빌드 시스템이 매우 나쁘지 않습니다.). 나는 포인터 값을 올바르게 이동하지 않는 C 스타일의 캐스트에 문제가 있다고 생각한다 (파생 클래스 'vtable에서 기본 클래스'vtable로 다시 이동). –

    +0

    @Mikael Persson : 클래스 레이아웃이 변경되었지만 사용 된 모든 소스 파일이 다시 컴파일되지 않았기 때문에 아주 비슷한 문제가 발생했습니다. 그 이유는'-I- -Isomedir'와 같은 컴파일러 플래그가 사용 되었기 때문에'-MM'으로 의존성이 생성되었을 때'somedir '의 헤더가 의존성으로 나열되지 않았기 때문입니다. –

    0

    어셈블리를 기반으로하면 바인딩이 동적이며 vtable의 첫 번째 항목에서 꽤 명확한 것처럼 보입니다. 문제는 가상 테이블입니다! C 스타일의 캐스트 대신 static_cast을 사용하는 것이 좋습니다 (물론이 경우에는 @VJo : dynamic_cast이 필요하지 않습니다!). 이 표준에서 포인터 BadDocumentReader* ptr은 캐스트 static_cast<DocumentReader*>(ptr)과 동일한 실제 값 (주소)을 가져야합니다. 이것은 BadDocumentReader이라는 vtable의 첫 번째 항목에 대한 호출을 바인드하고 기본 클래스의 vtable에 바인딩하지 않는 이유를 설명합니다. 그리고,이 경우에는 캐스팅이 필요하지 않습니다.

    asm과 실제로 동의하지 않지만 여전히 알아두면 좋은 한 가지 가능성. reader->readDocument을 호출하는 것과 동일한 범위에 BadDocumentReader을 작성하기 때문에 컴파일러는 약간 영리 해지고 동적으로 vtable에서 조회하지 않고도 호출을 해결할 수 있다고 결정합니다. 이는 리더 포인터의 "실제"유형이 실제로 BadDocumentReader이라는 것을 알고 있기 때문입니다. 따라서 vtable을 바이어스하고 정적으로 호출을 바인드합니다. 적어도 그것은 거의 동일한 상황에서 저에게 일어난 한 가지 가능성입니다. 그러나 asm을 기반으로, 나는 첫 번째 가능성이 당신의 경우에 발생하는 것이 확실하다.

    +0

    C 스타일의 캐스트는'static_cast'를 먼저 시도해야합니다. 그래서 나는 그것이 현재 진행되고 있다고 생각하지 않습니다. –

    0

    나는이 문제와 나를 위해 문제 나 클래스 멤버 변수에 저장 한 것이 었습니다 있었다. 그것을 포인터로 변경하고 new/delete를 사용하면 자식 클래스와 그 함수를 성공적으로 등록했습니다.

    관련 문제