double free wegen const std::string

Nun gut, wenn mir wieder mal die GCC Fehlermeldung

double free or corruption

um die Ohren fliegt, dann weiß ich in Zukunft, wie ich reagieren soll.

… aber dieses Wochenende hat mir jene Nachricht sprichwörtlich versaut!


Gleich die Lösung am Anfang für alle, denen das auch schon passiert ist:

  • Voraussetzung:
    • Wir arbeiten mit GCC unter Linux
    • Wir nutzen eine statische Bibliothek (*.a) mit einigen Konstanten.
    • Die statische Bibliothek wird in eine dynamische Bibliothek (*.so) eingebunden.
    • Die statische Bibliothek wird außerdem in ein Programm eingebunden, das die dynamische Bibliothek ebenso lädt und nutzt.
    • Am Ende des Programms kommt es zur Fehlermeldung double free or corruption, die manchmal das Beenden verhindert und/oder das Programm einfrieren lässt.
  • Lösung:
    • Alle globalen konstanten Text-Variablen des Typs const std::string durch char const* const ersetzen
    • Eventuell auch alle anderen globalen Objekte, die im Konstruktor oder Destruktor Code allokieren und freigeben, durch “flache” Alternativen ersetzen
    • Oder: Globale Objekte nicht frei herumliegen lassen, sondern als statische Objekte (static type_t const instance;) in eine Funktion packen, die eine Referenz auf ein solches “verstecktes” globales Objekt zurückgeben.

Und hier die ganze Geschichte

Ich arbeite an Kommunikationsplugins, die je nach Projekt unterschiedliche Daten transportieren sollen. Doch alle Projekte nutzen das gleiche Container-Format.

Damit kommt der Container-Code in eine statische Lib und jedes Projektimplementierung wird eine DLL (Windows) oder SO (Linux), die auf der statischen Lib aufbaut und die projektabhängigen Spezialitäten anfügt.

Und das funktionierte “eigentlich” ganz gut so.

Um die Features zu testen wird (wie so oft) ein Unit-Test für jedes Projekt angelegt, der alle Funktionen ausführen und abprüfen soll.

Und eben diese Unit-Tests weigerten sich plötzlich nach einigen Codeupdates korrekt zu terminieren. Die Tests liefen alle, doch beim Beenden kam es zur gefürchteten Nachricht double free or corruption, was der Buildserver gar nicht gern sehen wollte.

Das Ganze war ein reines Linux/GCC Problem, denn unter Windows mit MSVC gab es solche Exit-Crashes nicht.

Mit Hilfe der Address-Sanitizer Funktionen wurde die Fehlermeldung zwar etwas konkreter, nämlich dass es sich wohl um globale Variablen handelte, die vor main erzeugt und nach main zerstört wird und dass zum ersten Erzeugen eine private Methode von std::string im Einsatz war …
aber aus der Speicheradresse konnte ich überhaupt keine Rückschlüsse auf den wahren Übeltäter ziehen.

Obwohl ich es eigentlich nicht erwartet hatte (oder nicht wahr haben wollte), wendete sich mein Verdacht mehr und mehr auf die statische Bibliothek zu.

Ich bin ein bekennender Fan von statischen Bibliotheken und würde gerne viele dynamische Libs durch statische ersetzen.
Doch mir ist wohl bewusst, dass gerade beim Mischen von statischen und dynamische Code, besonders bei C++ Objekten, Fehler auftreten, die schwer zu finden sind.

Und so fand ich bei einer Suche im Netz den Beitrag Global variable in static library - double free or corruption error wo schon vor 10 Jahren gemeldet wurde, dass der GCC gerne mal Symbole aus zwei Quellen in einen Speicherbereich zusammenlegt.

Wenn also meine statische Container-Bibliothek einmal in die SO und einmal direkt ins Programm geladen wird, so würde ich mir erwarten, dass die globalen Objekt zweimal vorhanden sind.
Doch der GCC Linker erkennt den gemeinsamen Ursprung und legt sie zusammen. Leider gilt das aber nicht für Konstruktoren und Destruktoren.

Und deshalb werden die globalen Pseudokonstanten zweimal konstruiert (was “nur” ein Memory Leak wäre), aber sie werden auch am Ende zweimal zerstört, wobei der erste operator delete gut geht und der zweite dann mit kaputten Zeigern und falschen Speicherbereichen konfrontiert wird.

Ende gut, alles gut

Also habe ich einfach mal schnell alle Zeilen des Typs

1const std::string SomeClass::some_global_constant("Hello World");

durch

1char const* const SomeClass::some_global_constant("Hello World");

ersetzt.

Denn “die guten alten C-Strings” liegen flach im Datensegment und brauchen keine Kon- und Destruktoren. Zweimal freigeben gibt es hier einfach nicht.

Und schon waren die Unit-Tests und Linux wieder lauffähig.
… und zumindest der letzte Teil des Wochenendes ist gerettet.

Das einzig ungute an der ganze Sache ist:
Weil ich durch das GATE Projekt aktuell viel C-Syntax (auch in C++) einsetze, bekomme ich (zu Recht) im Code-Review zu hören:

Ersetze bitte deine char const* const durch std::string const.

Und das wäre ja auch die “schönere” Variante für C++ … aber manchmal sind die “guten alten C Strukturen” eben doch die stabilisierenden Elemente … vor allem, wenn man statische und dynamische Bibliotheken mixt.