Operatorüberladung new, delete - falsche Adressen?



  • Decimad schrieb:

    Ich hab das jetzt nicht total durchdacht, aber grundsätzlich würde ich sagen, stellt das doch kein problem dar.

    Das Problem ist aber eben, dass ich nicht PODs und Klassentypen unterscheiden kann (zumindest wüsste ich nicht wie). Bei PODs ist die Adresse für einzelne und zusammenhängende Speicherblöcke die gleiche.

    Dein Code kann so auch nicht funktionieren, da delete immer zuerst den Destruktor des Objekts aufruft und erst anschliessend operator delete . Wenn der Zeiger nicht direkt auf das Objekt zeigt, kommt es zu einer Zugriffsverletzung.



  • Ja, aber wie willst du denn mit dem operator delete verhindern, dass das Objekt falsch zerstört wird vom Compiler vorher?



  • Decimad schrieb:

    Warum auch nicht?

    Nexus schrieb:

    Dein Code kann so auch nicht funktionieren, da delete immer zuerst den Destruktor des Objekts aufruft und erst anschliessend operator delete . Wenn der Zeiger nicht direkt auf das Objekt zeigt, kommt es zu einer Zugriffsverletzung.



  • Also von Nachricht zu Nachricht willst du was anderes. In der ersten Nachricht wunderst du dich, warum die Zeiger unterschiedlich sind. In der 2. Message klingt es so, als hättest du es in der 1. schon gewusst, warum. In der dritten willst du auf einmal etwas verhindern, was mit operator delete nicht verhindern kannst (selbst wenn du wüsstest ob array POD oder nicht), weil es schon geschehen ist?



  • Decimad schrieb:

    Also von Nachricht zu Nachricht willst du was anderes. In der ersten Nachricht wunderst du dich, warum die Zeiger unterschiedlich sind. In der 2. Message klingt es so, als hättest du es in der 1. schon gewusst, warum.

    Nicht wirklich. In der ersten stellte ich fest, dass die Adressen bei delete und delete[] bei Klassentypen unterschiedlich seien, und fragte nach einem Lösungsansatz. Im zweiten Post wollte ich dich auf die Verwendung von verschiedenen Speicheroperatoren (z.B. new und delete[] ) aufmerksam machen, da du anscheinend meine Frage falsch verstanden hattest.

    Decimad schrieb:

    In der dritten willst du auf einmal etwas verhindern, was mit operator delete nicht verhindern kannst (selbst wenn du wüsstest ob array POD oder nicht), weil es schon geschehen ist?

    Hm, jetzt weiss ich nicht mehr, was du genau damit meinst. Der Fehler in deinem Code besteht darin, dass das Argument des delete -Operators (also ptr in der unten stehenden Codezeile) ein Zeiger auf ein Objekt sein muss, dessen Destruktor dann aufgerufen wird. Bei dir ist das nicht der Fall, weil zuerst noch vier Byte int kommen. Dann wird der Destruktor natürlich nicht richtig bzw. gar nicht aufgerufen und bei dem Versuch entsteht eine Zugriffsverletzung.

    delete ptr;
    

    Verstehst du mein Problem? Es gibt keinen trivialen Ansatz, der sowohl für PODs und Klassentypen funktioniert, da diese unterschiedlich gehandhabt werden.



  • Also, ich habe jetzt weiter darüber nachgedacht. Mein nächster Vorschlag wäre, zusätzlich zu dem Identifier noch ein integer davorzupacken, der eine 1 enthält. Dann zerstört delete[] bei nicht POD's wenigstens nur ein Objekt, und das sollte man ja mindestens mit new ohne [] erstellt haben. Dann weißt du außerdem, dass sowohl die parameter, die an beide delete-varianten geschickt werden immer innerhalb von blöcken liegen, die du mit new oder new[] alloziert hast (mehr als 4 Byte wird der Compiler ja nie abziehen, aber die hast du ja eh draufaddiert) kannst also in deiner Liste von alloziert Blöcken nachschauen)
    Den Array- oder nicht Array-Identifier sollte man sich dann ja sowieso sparen (Weil man vom Zeiger nicht direkt schließen kann, wo der steht), das kommt einfach in die Liste der allozierten Blöcke. Ansonsten müsstest du den Identifier ja sogar zweimal davorpacken, damit du sicher in einem der beiden Integer im Speicher landest.



  • So, ich habe jetzt die ultimative Version geschrieben, die für alle Fälle funktioniert.
    Wenn du delete[] auf ein durch ein nicht-[] new erstellten (POD oder nicht) Zeiger aufrufst, dann wird das einzige Objekt korrekt zerstört (falls halt nicht POD) und anschließend auch noch ein Fehler ausgegeben! Wenn du delete auf einen mit new[] erstellten POD aufrufst, dann funzt es, wenn es ein nicht-POD war, dann wird halt nur ein Objekt zerstört (das erste im Array), aber immerhin noch eine Fehlermeldung ausgegeben.

    Grüße,
    Michael

    const int noarray = 5;
    const int isarray = 11;
    
    void* operator new(size_t Size)
    {
        unsigned int* p = (unsigned int*)malloc(Size+12);
        p[0] = noarray;
        p[1] = noarray | 0xF00;
        p[2] = 1;
    
        std::cerr << "new      " << p << "  size: " << Size << "  returned: " << p+3 << std::endl;
        return reinterpret_cast<void*>(p+3);
    }
    
    void* operator new[](size_t Size)
    {
        unsigned int* p = (unsigned int*)malloc(Size+12);
        p[0] = isarray;
        p[1] = isarray | 0xF00;
        p[2] = 1;
    
        std::cerr << "new[]    " << p << "  size: " << Size << "  returned: " << p+3 << std::endl;
        return reinterpret_cast<void*>(p+3);
    }
    
    void operator delete(void* p)
    {
        unsigned int* p_ = reinterpret_cast<unsigned int*>(p);
        p_-=3;
        if( (*p_&0xFF) == isarray ) {		
            std::cerr << "ERRROORRRR!!!" << std::endl;
        }
        if( *p_&0xF00 ) p_-=1;
    
        std::cerr << "delete called with   " << p << "  freeing: " << p_ << std::endl;
        free(reinterpret_cast<void*>(p_));
    }
    
    void operator delete[](void* p)
    {   
        unsigned int* p_ = reinterpret_cast<unsigned int*>(p);
        p_-=2;
        if( (*p_&0xFF) == noarray ) {
            std::cerr << "ERRROORRRR!!!" << std::endl;
    
        }
        if( *p_ & 0xF00 ) p_ -= 1;
    
        std::cerr << "delete[] called with " << p << "  freeing: " << p_ <<  std::endl;
        free(reinterpret_cast<void*>(p_));
    }
    


  • Danke für deine Bemühungen.

    Ich hätte aber noch einige Fragen zu deinem Code: Was machst du genau mit der binären Logik ( operator& und operator| )? Und die 5 und 11 sind einfach willkürliche Zahlen? Und was ist mit den Hexadezimalwerten? Auch sonst versteh ich noch nicht ganz alles, ich versuchs mal zu interpretieren.

    Allerdings scheint mir das sehr viel Aufwand... Ist das reinterpret_cast en hier eigentlich überall einem definierten Verhalten? Man müsste einfach statt 4 sizeof(int) einsetzen, dann sollte es auch portabel sein... Oder man machts gleich mit char .



  • Die 5 und 11 sind völlig willkürlich gewählt (aber der folgende Code geht davon aus, dass sie unterschiedlich und kleiner als 256 sind).
    Die binäre Logik dient nur dazu, dass ich mir sparen konnte noch zusätzlich zwei konstanten zu definieren. Sie dient hier dazu, zu unterscheiden, ob man beim ersten oder zweiten Typfeld ist. Ja, in den mallocs sollte man 3*sizeof(unsigned int) benutzen anstatt 12. Das reinterpret_cast ist hier im Prinzip das gleiche, wie wenn du das Ergebnis von malloc (void*) in einen bestimmten Zeigertyp umwandelst. Der Code an sich ist definiert. Aber er funktioniert natürlich nur unter der Annahme, das der Compiler seine Verwaltung von dynamischen Feldern eines nicht-POD's so handhabt, wie du es beobachtet hast.



  • Decimad schrieb:

    Die binäre Logik dient nur dazu, dass ich mir sparen konnte noch zusätzlich zwei konstanten zu definieren. Sie dient hier dazu, zu unterscheiden, ob man beim ersten oder zweiten Typfeld ist.

    Ich hab mir eben auch überlegt, das mit 3 char s zu machen - der Wertebereich würde ja längstens reichen. Aber wieso unterteilst du die einzelnen int s noch, wenn du sowieso drei belegst?

    Decimad schrieb:

    Aber er funktioniert natürlich nur unter der Annahme, das der Compiler seine Verwaltung von dynamischen Feldern eines nicht-POD's so handhabt, wie du es beobachtet hast.

    Hm, ein guter Einwand. Weiss diesbezüglich jemand mehr?

    Schlussendlich scheint es mir ein bisschen viel Aufwand zu sein (wovon Teile möglicherweise gar nicht immer definiertes Verhalten sind) - ansonsten lass ich das einfach meine andere Abfrage erledigen (ob der Zeiger, der freigegeben wurde, gültig ist). Dann erhält man zwar eine weniger spezifische Fehlermeldung, aber da die Verwechslung von delete und delete[] wohl nicht sehr häufig vorkommt... 😉

    Naja, vorläufig werde ich es trotzdem mit deinem Ansatz versuchen und schauen, ob er sich bewährt. Vielen Dank nochmals. 🙂



  • Wie meinst du das mit dem Unterteilen?
    Bezüglich des gewählten Typs. Die 1 muss meiner Meinung nach sowieso in einem int stehen (halt weil delete[] darauf baut), drum machts doch nur mehr Aufwand, den Rest dann noch in char's zu packen.



  • Decimad schrieb:

    Wie meinst du das mit dem Unterteilen?

    Ich verstehe deinen Ansatz grundsätzlich nicht ganz.

    Wenn Speicher allokiert wird, forderst du doch zusätzlich noch Speicher für 3 int s vor dem "normalen" Speicherbereich an. Dann überprüfst du beim Freigeben, ob an den Stellen der vorher allokierten int -Speicherbereiche das Richtige steht (5 bei new , 11 bei new[] ).

    Jetzt sagst du, du brauchst die binäre Logik, um herauszufinden, in welchem Typfeld du bist. Das verstehe ich nicht ganz...

    Decimad schrieb:

    Bezüglich des gewählten Typs. Die 1 muss meiner Meinung nach sowieso in einem int stehen (halt weil delete[] darauf baut), drum machts doch nur mehr Aufwand, den Rest dann noch in char's zu packen.

    Sorry, welche Funktion hat die 1 in der dritten 4-Byte-Speicherzelle? Einfach als Puffer, da delete[] bei Klassentypen beginnt, vor dem tatsächlichen Speicherbereich zu löschen?



  • Die 1 dient dazu, dass delete[] nicht wild rumzerstört, wenn du ihm ein durch new erstellten Zeiger übergibst (Ansonsten kämst du oftmals gar nicht mehr in deinen selbstdefinierten operator delete[], weil das zerstören schon Speicherzugriffsverletzungen erzeugt hat).
    Ich brauche 2 Typfelder, weil ich ja nicht weiß, welchen Zeiger ich in operator delete[] oder delete bekomme (Der Zeiger liegt ja je nach auftretendem Fall 4 byte davor oder nicht). In beiden steht im unteren byte der Typ des Blocks und im zweiten der beiden Typfelder steht zudem im 2. Byte noch, dass es das 2. Byte ist, damit man sicher einen Zeiger für free() basteln kann.



  • Okay, ich habe nun einmal einen eigenen Ansatz versucht, bei dem man mit weniger auskommt. Habe ich etwas Wichtiges vergessen? Denn die Fehlererkennung funktioniert so.

    Ich allokiere jetzt nur zwei int -Felder vor dem eigentlichen Speicherbereich, auf die ich in den Freigabefunktionen mit ptr[-2] und ptr[-1] bei den Freigabefunktionen zugreife. Da immer zuerst auf ptr[-1] geprüft wird (und dieser Speicherbereich in jedem Falle reserviert wurde), sollte der Zugriff auf Speicher, der einem nicht gehört, vermieden werden.

    const int NoArray = 42;
    const int IsArray = 13;
    const int Buffer = 1;
    
    void* operator new(size_t Size)
    {	
    	int* ptr = reinterpret_cast<int*>(malloc(Size + 2*sizeof(int)));
    
    	ptr[0] = NoArray;
    	ptr[1] = Buffer;
    
    	return reinterpret_cast<void*>(ptr + 2);
    }
    
    void* operator new[](size_t Size)
    {
    	int* ptr = reinterpret_cast<int*>(malloc(Size + 2*sizeof(int)));
    
    	ptr[0] = IsArray;
    	ptr[1] = Buffer;
    
    	return reinterpret_cast<void*>(ptr + 2);
    }
    
    void operator delete(void* Pointer)
    {
    	int* ptr = reinterpret_cast<int*>(Pointer);
    
    	if (ptr[-1] != Buffer || ptr[-2] != NoArray)
    		std::cerr << "/!\\ Fehler!" << std::endl;
    }
    
    void operator delete[](void* Pointer)
    {
    	int* ptr = reinterpret_cast<int*>(Pointer);
    
    	if (ptr[-1] != Buffer || ptr[-2] != IsArray)
    		std::cerr << "/!\\ Fehler!" << std::endl;
    }
    

    Einige Dinge verstehe ich trotzdem noch nicht:

    • Wieso muss Buffer genau 1 sein, damit es zu keiner Zugriffsverletzung kommt? Du hast ja auch gesagt, dass dort sowieso eine 1 stehen muss, weil sie für delete[] benötigt wird...
    • Warum funktioniert das nur mit int (bei char oder short gibt es Zugriffsverletzungen)? Ist das so, weil bei mir int gleich gross wie ein Zeiger ist? Müsste man demnach auch das allgemein halten, wenn man Portabilität gewährleisten will?
    • Gibt es noch sonstige Stellen, die gefährlich sein könnten (undefiniertes Verhalten etc.)?


  • Jau, die 1 muss da stehen und ein int sein, weil ja bevor operator delete[] aufgerufen wird, der "compiler" ein int zurückspringt, sich die Anzahl der Elemente rausliest und dann in der Schleife die Destruktoren der Objekte aufruft und anschließend deinen operator delete[] aufruft. Wenn du da 2 in operator new nur 2 chars vorpacken würdest, würde er schonmal 2 bytest aus unalloziertem Speicher lesen. Wenn es 2 shorts wären, würde er beide zu einem int zusammenfassen und diesen integer (der dann ja schon einen ziemlich großen Wert darstellen könnte, weil du ja dann gedacht buffer[0]<<16+buffer[1] da stehen hättest) als Feldanzahl interpretieren (Und dann x Millionen mal einen Destruktor aufrufen, bzw. schon ziemlich am Anfang abschmieren). Die 1 simuliert ja sozusagen ein Feld mit einem Element.



  • Ah, jetzt! In dem Feld vorher steht die Anzahl der Elemente... 💡

    Aber das könnte wahrscheinlich auch von Compiler zu Compiler variieren... Hm, ich glaube, dafür gibts wohl keine wirklich standardkonforme Lösung.
    Und es wird doch einfach sizeof(void*) zurückgegangen, und nicht sizeof(int) , oder? Dass das oft das Gleiche ist, ist ja eher Zufall...

    Ansonsten, ist mein Code ungefähr brauchbar? Oder wieso ist deiner um einiges komplizierter? 😉



  • Na klar, das ganze Konzept, dass der Compiler den Arraycount in einem int vor dem Array speichert ist natürlich compilerabhängig. Wobei es wohl kaum einen gibt, der das nicht so implementiert. Fraglich wäre nur, ob das in x64-Code dann 64-bit Integer sind... Wobei das ja wiederum eigentlich mit sizeof(int) schon geregelt ist.
    Dein Code ist hauptsächlich deshalb schöner, weil du meinen genommen hast und ihn schöner gemacht hast 🙂 Ich hatte zuerst die 2 Felder-Idee wegen der 4-byte-Geschichte und dann nachträglich die Idee mit der 1 noch davor und das dann da reingefrickelt.
    Woher weißt du, das der Compiler um sizeof(void*) vorspringt und nicht um sizeof(int)? Ich dachte mir halt, da steht also eine Zahl vor, die 4 byte groß ist, also ist es ein int 🙂 Alles Annahmen... 🙂



  • Decimad schrieb:

    Dein Code ist hauptsächlich deshalb schöner, weil du meinen genommen hast und ihn schöner gemacht hast 🙂

    Sollte kein Vorwurf sein, nur dank dir hab ich diesen Code ja so hingekriegt. 😉

    Nur die Geschichten mit der binären Logik und dem dritten Feld haben mich von Anfang an verwirrt, deshalb habe ich jetzt versucht, ohne sie auszukommen.

    Decimad schrieb:

    Woher weißt du, das der Compiler um sizeof(void*) vorspringt und nicht um sizeof(int)? Ich dachte mir halt, da steht also eine Zahl vor, die 4 byte groß ist, also ist es ein int 🙂 Alles Annahmen... 🙂

    Nun ja, ich habe mir vorgestellt, dass er halt gerade eine Zelle im Arbeitsspeicher zurückgeht, und eine Adresse benötigt halt sizeof(void*) Bytes (Grösse eines Zeigers). Aber meistens sollte das sowieso gleich der Grösse von int sein... Aber ja, ist halt auch so eine Annahme. So wirklich portabel bringt man das sowieso kaum hin...

    Vielen Dank für deine Hilfe, du hast mich echt weitergebracht! 👍


Anmelden zum Antworten