2010-01-15 12 views
6

Attualmente eseguo il debug di un crashlog. L'arresto anomalo si verifica perché il puntatore vtable di un oggetto (C++ -) è 0x1, mentre il resto dell'oggetto sembra essere ok per quanto posso dire dal crashlog.In quali circostanze un puntatore vtable può essere nullo (o 0x1)?

Il programma si arresta in modo anomalo quando tenta di chiamare un metodo virtuale.

La mia domanda: in quali circostanze un puntatore vtable può diventare nullo? L'operatore elimina imposta il puntatore vtable su null?

Ciò si verifica su OS X utilizzando gcc 4.0.1 (Apple Inc. build 5493).

+0

Può chiarire se il puntatore a funzione nel vtable è 0 o se è il questo puntatore che è nullo? O è un puntatore nell'oggetto che punta al vtable corrente? –

+0

Il puntatore vtable è 1. L'arresto anomalo si verifica durante il tentativo di eseguire 'call * 0x1c (% eax)' (in at & t syntax) e il valore di eax è 1 (non zero come ho dichiarato in modo errato). – Tobias

+0

Eseguilo in gdb, imposta un punto di riferimento all'indirizzo, guarda cosa lo sta scrivendo. –

risposta

7

Potrebbe essere un calpestio di memoria, qualcosa che si scrive su quello vtable per errore. C'è una quantità quasi infinita di modi per "raggiungere" questo in C++. Un buffer overflow, per esempio.

7

Qualsiasi tipo di comportamento non definito si può portare a questa situazione. Ad esempio:

  • Errori nell'aritmetica del puntatore o altri che fanno scrivere il programma nella memoria non valida.
  • Variabili non inizializzate, valori non validi ...
  • Il trattamento di una matrice in modo polimorfico potrebbe causare un effetto secondario.
  • Tentativo di utilizzare un oggetto dopo l'eliminazione.

Vedere anche le domande What’s the worst example of undefined behaviour actually possible? e What are all the common undefined behaviour that a C++ programmer should know about?.

La soluzione migliore è utilizzare un limite e un controllo della memoria, come supporto per il debugging.

1

Questo dipende totalmente dall'implementazione. Tuttavia sarebbe abbastanza sicuro assumere che dopo l'eliminazione qualche altra operazione possa impostare lo spazio di memoria su null.

Altre possibilità includono sovrascrittura della memoria da parte di alcuni puntatore sciolti - in realtà nel mio caso è quasi sempre presente ...

Detto questo, non si dovrebbe mai cercare di utilizzare un oggetto dopo eliminazione.

+1

eliminare certamente può farlo, ma non sono a conoscenza di alcuna implementazione in cui si fa –

+0

@Neil: le implementazioni non sono il mio campo. Ho cambiato la risposta a qualcosa di "più vero" ... –

3

La mia prima ipotesi potrebbe essere che il codice sia memset() 'un oggetto di classe.

5

Un caso molto comune: si cerca di chiamare un metodo virtuale puro dal costruttore ...

Costruttori

struct Interface 
{ 
    Interface(); 
    virtual void logInit() const = 0; 
}; 

struct Concrete: Interface() 
{ 
    virtual void logInit() const { std::cout << "Concrete" << std::endl; } 
}; 

Ora, supponiamo che il seguito dell'attuazione Interface()

Interface::Interface() {} 

Quindi tutto va bene:

Concrete myConcrete; 
myConcrete.pure(); // outputs "Concrete" 

È così doloroso chiamare puro dopo il costruttore, sarebbe meglio ridimensionare il codice giusto?

Interface::Interface() { this->logInit(); } // DON'T DO THAT, REALLY ;) 

Quindi possiamo farlo in una riga !!

Concrete myConcrete; // CRASHES VIOLENTLY 

Perché?

Perché l'oggetto è costruito dal basso verso l'alto. Diamo un'occhiata.

istruzioni per costruire una classe Concrete (circa)

  1. allocare memoria sufficiente (ovviamente), e la memoria sufficiente per la (1 puntatore a funzione _vtable troppo per funzione virtuale, di solito nell'ordine in cui sono dichiarati , partendo dalla base più a sinistra)

  2. chiamata Concrete costruttore (il codice non si vede)

    a> chiamata Interface costruttore, che inizializzare il _vtabl e con i suoi puntatori

    b> Chiama corpo Interface costruttori (si scrive così)

    c> sovrascrivono puntatori nella _vtable per quei metodi di override Calcestruzzo

    d> Chiamata corpo Concrete del costruttore (hai scritto che)

Quindi qual è il problema? Beh, guardate b> e c> ordine;)

Quando si chiama un metodo di virtual dall'interno di un costruttore, non fa quello che stai sperando. Va a _vtable per cercare il puntatore, ma lo _vtable non è ancora completamente inizializzato. Così, per tutto ciò che conta, l'effetto di:

D() { this->call(); } 

è infatti:

D() { this->D::call(); } 

Quando si chiama un metodo virtuale all'interno di un costruttore, non è necessario il tipo dinamico completo dell'oggetto in fase di costruzione, si ha il tipo statico dell'attuale Costruttore richiamato.

Nel mio Interface/Concrete esempio, significa Interface tipo, e il metodo è virtuale pura, in modo che il _vtable non detiene un vero e proprio puntatore (0x0 o 0x01 per esempio, se il compilatore è abbastanza amichevole per i valori di debug di impostazione ai aiutarti lì).

distruttori

Incidentalmente, esaminiamo il caso Destructor;)

struct Interface { ~Interface(); virtual void logClose() const = 0; } 
Interface::~Interface() { this->logClose(); } 

struct Concrete { ~Concrete(); virtual void logClose() const; char* m_data; } 

Concrete::~Concrete() { delete[] m_data; } // It's all about being clean 
void Concrete::logClose() 
{ 
    std::cout << "Concrete refering to " << m_data << std::endl; 
} 

Allora, cosa succede a distruzione? Bene, la _vtable funziona bene e viene invocato il tipo di runtime reale ... cosa significa qui, tuttavia, è un comportamento indefinito, perché chissà cosa è successo a m_data dopo che è stato cancellato e prima che fosse invocato il distruttore Interface?Io non;)

Conclusione

Mai e poi mai chiamare metodi virtuali dall'interno costruttori o distruttori.

Se non è che, si è lasciato con una corruzione della memoria, dura fortuna;)

+0

Grazie mille per la tua risposta dettagliata. Nel mio caso, questo non è il caso, sfortunatamente. – Tobias

Problemi correlati