Topleaked: Ein Tool zum Auffinden von Speicherlecks

Bild

Die Geschichte begann, wie so oft, damit, dass einer der Dienste auf dem Server ausfiel. Genauer gesagt wurde der Prozess abgebrochen, indem auf übermäßige Speichernutzung überwacht wurde. Der Bestand hätte mehrfach sein müssen, was bedeutet, dass wir einen Speicherverlust haben.
Es gibt einen vollständigen Speicherauszug mit Debugging-Informationen, es gibt Protokolle, die jedoch nicht reproduziert werden können. Entweder ist das Leck wahnsinnig langsam oder das Szenario hängt vom Wetter auf dem Mars ab. Kurz gesagt, ein weiterer Fehler, der nicht durch Tests reproduziert wird, sondern in freier Wildbahn gefunden wird. Es bleibt der einzige wirkliche Hinweis - ein Speicherauszug.


Idee


Der ursprüngliche Dienst wurde in C ++ und Perl geschrieben, obwohl dies keine besondere Rolle spielt. Alles, was im Folgenden beschrieben wird, gilt für fast jede Sprache.


Ausgehend von der Behauptung des Problems bestand unser Prozess darin, in ein paar Hundert Megabyte RAM zu passen, und wurde für mehr als 6 Gigabyte abgeschlossen. Der größte Teil des Prozessspeichers besteht aus verlorenen Objekten und deren Daten. Es muss nur herausgefunden werden, welche Arten von Objekten sich am meisten im Speicher befanden. Natürlich gibt es keine Liste von Objekten mit Typinformationen im Dump. Das Verfolgen von Beziehungen und das Erstellen eines Diagramms wie bei Garbage Collectors ist praktisch unmöglich. Aber wir müssen diesen binären Hash nicht verstehen, sondern berechnen, welche Objekte mehr sind. Objekte nicht-trivialer Klassen haben einen Zeiger auf eine Tabelle virtueller Methoden, und alle Objekte derselben Klasse haben denselben Zeiger. Wie oft befindet sich ein Zeiger auf eine vtbl-Klasse im Speicher? Es wurden so viele Objekte dieser Klasse erstellt.


Neben vtbl gibt es noch weitere häufig vorkommende Sequenzen: Konstanten, die Felder initialisieren, HTTP-Header in Zeichenfolgenfragmenten und Zeiger auf Funktionen.
Wenn Sie das Glück haben, einen Zeiger zu finden, können Sie mit gdb nachvollziehen, auf was dieser verweist (es sei denn, es gibt natürlich Debug-Zeichen). Bei Daten können Sie versuchen, sie anzuzeigen und zu verstehen, wo diese verwendet werden. Mit Blick auf die Zukunft stelle ich fest, dass es sowohl das als auch das andere passiert, und aus einem Fragment einer Zeile ist es durchaus möglich zu verstehen, was dieser Teil des Protokolls ist und wo es notwendig ist, weiter zu graben.


Die Idee wurde ausspioniert und die erste Implementierung wurde frech aus dem Stackoverflow kopiert. https://stackoverflow.com/questions/7439170/is-there-a-way-to-find-leaked-memory-using-a-core-file


hexdump core.10639 | awk '{printf "%s%s%s%s\n%s%s%s%s\n", $5,$4,$3,$2,$9,$8,$7,$6}' | sort | uniq -c | sort -nr | head 

Das Skript arbeitete ungefähr 15 Minuten auf unserem Dump, gab ein paar Zeilen zurück und ... nichts. Kein einziger Zeiger, nichts Nützliches.


Aussortiert


Stackoverflow-gesteuerte Entwicklung hat seine Nachteile. Sie können das Skript nicht einfach kopieren und hoffen, dass alles funktioniert. In diesem speziellen Skript fällt sofort eine Art Neuanordnung von Bytes auf. Es stellt sich auch die Frage, warum Permutationen um 4 sind. Sie müssen kein Superspezialist sein, um zu verstehen, dass solche Permutationen von der Plattform abhängen: Bit- und Byte-Reihenfolge.


Um genau zu verstehen, wie es aussieht, müssen Sie das Dateiformat des Speicherauszugs, LITTLE- und BIG-Endian, verstehen, oder Sie können einfach die Bytes in den gefundenen Teilen auf verschiedene Arten neu anordnen und gdb angeben. Oh wunder In direkter Reihenfolge sieht das GDB-Byte das Zeichen und sagt, dass es ein Zeiger auf eine Funktion ist!


In unserem Fall war es ein Zeiger auf eine der Lese- und Schreibfunktionen in openssl-Puffern. Zur Anpassung der Ein- und Ausgabe wird der OOP-Systemansatz verwendet - eine Struktur mit einer Reihe von Zeigern auf Funktionen, die eine Art Schnittstelle oder vielmehr vtbl ist. Diese Strukturen mit Zeigern waren wahnsinnig viele. Ein genauer Blick auf den Code, der für das Festlegen dieser Strukturen und das Erstellen von Puffern verantwortlich ist, ermöglichte es uns, den Fehler schnell zu finden. Wie sich herausstellte, gab es an der Kreuzung von C ++ und C keine RAII-Objekte, und im Fehlerfall ließ eine vorzeitige Rückkehr keine Chance, Ressourcen freizugeben. Niemand hat vermutet, dass der Dienst rechtzeitig mit falschen SSL-Handshakes geladen wird, also haben sie ihn verpasst. Interessant ist auch, wie man 6 Gigabyte falschen SSL-Handshakes wählt, aber wie man sagt, ist dies eine ganz andere Geschichte. Das Problem ist gelöst.


topleaked


Das Skript hat sich als nützlich erwiesen, weist jedoch bei häufiger Verwendung schwerwiegende Nachteile auf: Es ist sehr langsam, plattformabhängig. Später stellt sich heraus, dass Dump-Dateien auch unterschiedliche Offsets aufweisen und die Ergebnisse schwer zu interpretieren sind. Die Aufgabe, in einem Binärdump zu graben, passt nicht gut zu bash, daher habe ich die Programmiersprache in D geändert. Die Wahl der Sprache ist eigentlich auf den egoistischen Wunsch zurückzuführen, in Ihrer Lieblingssprache zu schreiben. Nun, die Rationalisierung der Wahl ist folgende: Geschwindigkeit und Speicherverbrauch sind entscheidend, daher benötigen Sie eine muttersprachliche kompilierte Sprache und es ist banal, D schneller als C oder C ++ zu schreiben. Später im Code wird es deutlich sichtbar sein. So war das Projekt geboren.


Installation


Da es keine binären Assemblys gibt, müssen Sie das Projekt auf die eine oder andere Weise aus dem Quellcode zusammenstellen. Dazu benötigen Sie den Compiler D. Es gibt drei Möglichkeiten: dmd ist der Referenz-Compiler, ldc basiert auf llvm und gdc, die ab Version 9 in gcc enthalten sind. Sie müssen also möglicherweise nichts installieren, wenn Sie den neuesten gcc haben. Wenn du installierst, dann empfehle ich ldc, da es besser optimiert. Alle drei sind auf der offiziellen Website zu finden .
Der Dub Package Manager wird mit dem Compiler mitgeliefert. Topleaked wird mit einem Befehl installiert:


 dub fetch topleaked 

In Zukunft werden wir den Befehl verwenden, um Folgendes zu starten:


 dub run topleaked -brelease-nobounds -- <filename> [<options>...] 

Um den Dub-Lauf und das Brelease-Nobounds-Compiler-Argument nicht zu wiederholen, können Sie die Quellen vom Github herunterladen und die ausführbare Datei sammeln:


 dub build -brelease-nobounds 

Im Stammverzeichnis des Projektordners erscheint topleaked.


Verwenden Sie


Nehmen wir ein einfaches C ++ - Programm mit einem Speicherverlust.


 #include <iostream> #include <assert.h> #include <unistd.h> class A { size_t val = 12345678910; virtual ~A(){} }; int main() { for (size_t i =0; i < 1000000; i++) { new A(); } std::cout << getpid() << std::endl; sleep(200); } 

Wir beenden es mit kill -6, dann bekommen wir einen Speicherauszug. Jetzt können Sie topleaked ausführen und die Ergebnisse anzeigen

 ./toleaked -n10 leak.core 

Die Option -n gibt die Größe des benötigten Top an. In der Regel sind Werte zwischen 10 und 200 sinnvoll, je nachdem, wie viel „Müll“ vorhanden ist. Das Standardausgabeformat ist eine Zeile für Zeile in lesbarer Form.


 0x0000000000000000 : 1050347 0x0000000000000021 : 1000003 0x00000002dfdc1c3e : 1000000 0x0000558087922d90 : 1000000 0x0000000000000002 : 198 0x0000000000000001 : 180 0x00007f4247c6a000 : 164 0x0000000000000008 : 160 0x00007f4247c5c438 : 153 0xffffffffffffffff : 141 

Es ist von geringem Nutzen, außer dass wir die Zahl 0x2dfdc1c3e sehen können, die ebenfalls 12345678910 ist und millionenfach vorkommt. Das könnte schon reichen, aber ich will mehr. Um die Klassennamen von durchgesickerten Objekten anzuzeigen, können Sie das Ergebnis an gdb senden, indem Sie den Standardausgabestream einfach mit einer geöffneten Dump-Datei an gdb input umleiten. -ogdb - Option zum Ändern des Formats in verständliche gdb.


 $ ./topleaked -n10 -ogdb /home/core/leak.1002.core | gdb leak /home/core/leak.1002.core ...<   gdb  > #0 0x00007f424784e6f4 in __GI___nanosleep (requested_time=requested_time@entry=0x7ffcfffedb50, remaining=remaining@entry=0x7ffcfffedb50) at ../sysdeps/unix/sysv/linux/nanosleep.c:28 28 ../sysdeps/unix/sysv/linux/nanosleep.c: No such file or directory. (gdb) $1 = 1050347 (gdb) 0x0: Cannot access memory at address 0x0 (gdb) No symbol matches 0x0000000000000000. (gdb) $2 = 1000003 (gdb) 0x21: Cannot access memory at address 0x21 (gdb) No symbol matches 0x0000000000000021. (gdb) $3 = 1000000 (gdb) 0x2dfdc1c3e: Cannot access memory at address 0x2dfdc1c3e (gdb) No symbol matches 0x00000002dfdc1c3e. (gdb) $4 = 1000000 (gdb) 0x558087922d90 <_ZTV1A+16>: 0x87721bfa (gdb) vtable for A + 16 in section .data.rel.ro of /home/g.smorkalov/dlang/topleaked/leak (gdb) $5 = 198 (gdb) 0x2: Cannot access memory at address 0x2 (gdb) No symbol matches 0x0000000000000002. (gdb) $6 = 180 (gdb) 0x1: Cannot access memory at address 0x1 (gdb) No symbol matches 0x0000000000000001. (gdb) $7 = 164 (gdb) 0x7f4247c6a000: 0x47ae6000 (gdb) No symbol matches 0x00007f4247c6a000. (gdb) $8 = 160 (gdb) 0x8: Cannot access memory at address 0x8 (gdb) No symbol matches 0x0000000000000008. (gdb) $9 = 153 (gdb) 0x7f4247c5c438 <_ZTVN10__cxxabiv120__si_class_type_infoE+16>: 0x47b79660 (gdb) vtable for __cxxabiv1::__si_class_type_info + 16 in section .data.rel.ro of /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (gdb) $10 = 141 (gdb) 0xffffffffffffffff: Cannot access memory at address 0xffffffffffffffff (gdb) No symbol matches 0xffffffffffffffff. (gdb) quit 

Lesen ist nicht sehr einfach, aber möglich. Zeilen der Form $ 4 = 1.000.000 spiegeln die Position oben und die Anzahl der gefundenen Vorkommen wider. Unten sehen Sie die Ergebnisse der Ausführung von x und des Info-Symbols für den Wert. Hier sehen wir, dass vtable für A millionenfach vorkommt, was einer Million durchgesickerter Objekte der Klasse A entspricht.

Um einen Teil der Datei zu analysieren (wenn diese zu groß ist), werden die Optionen offset und limit hinzugefügt - beginnend mit wo und wie viele Bytes gelesen werden sollen.


Ergebnis


Das resultierende Dienstprogramm ist spürbar schneller als das Skript. Sie müssen noch warten, aber nicht auf der Skala einer Teewanderung, sondern ein paar Sekunden bevor die Spitze auf dem Bildschirm erscheint. Ich bin absolut sicher, dass der Algorithmus erheblich verbessert und umfangreiche Eingabe- und Ausgabeoperationen erheblich optimiert werden können. Aber das ist eine Frage der zukünftigen Entwicklung, jetzt funktioniert alles gut.


Dank der Option -ogdb und der Umleitung in gdb erhalten wir sofort Namen und Werte, manchmal sogar Zeilennummern, wenn wir Glück haben, an die Funktion heranzukommen.


Die offensichtliche, aber sehr unerwartete Folge der Frontallösung war plattformübergreifend. Ja, topleaked kennt die Bytereihenfolge nicht, aber da es das Dateiformat nicht analysiert, sondern einfach die Datei byteweise liest, kann es unter Windows oder auf jedem System mit einem beliebigen Speicherauszugsformat verwendet werden. Es ist nur erforderlich, dass die Daten innerhalb der Datei ausgerichtet werden.


D Sprache


Ich möchte die Erfahrung mit der Entwicklung eines solchen Programms in D gesondert erwähnen. Die erste Arbeitsversion wurde in wenigen Minuten geschrieben. Ich muss sagen, dass der Hauptalgorithmus bisher nur drei Zeilen umfasst:


 auto all = input.sort; ValCount[] res = new ValCount[min(all.length, maxSize)]; return all.group.map!((p) => ValCount(p[0],p[1])) .topNCopy!"a.count>b.count"(res, Yes.sortOutput); 

Alles dank Lazy Ranges und dem Vorhandensein vorgefertigter Algorithmen in der Standardbibliothek, wie Group und TopN.


Später kamen das Parsen der Befehlszeilenargumente, das Formatieren der Ausgabe und alles, was wortreich, aber auch schnell geschrieben war, hinzu. Es sei denn, das Lesen der Datei stellte sich als ungewöhnlich heraus.


In der aktuellsten Version erschien das Flag --find für die übliche Suche nach einem Teilstring, der überhaupt nicht mit der Häufigkeit zusammenhängt. Aufgrund dieser Kleinigkeit hat der Code merklich an Größe zugenommen, aber mit hoher Wahrscheinlichkeit wird das Feature gelöscht und der Code kehrt zu seinem ursprünglichen einfachen Zustand zurück.


Insgesamt sind die Arbeitskosten vergleichbar mit Skriptsprachen und weisen eine viel bessere Leistung auf. Möglicherweise können Sie das Maximum erreichen, da derselbe Code in C und D bei gleicher Geschwindigkeit gleich funktioniert.


Indikationen und Kontraindikationen für die Verwendung


  • Topleaked wird benötigt, um nach Lecks zu suchen, wenn nur ein Speicherauszug des aktuellen Prozesses vorhanden ist, es jedoch keine Möglichkeit gibt, ihn unter dem Desinfektionsprogramm zu reproduzieren.
  • Dies ist kein weiterer Valgrind und erhebt keinen Anspruch auf dynamische Analyse.
  • Eine interessante Ausnahme zur vorherigen Bemerkung können vorübergehende Lecks sein. Das heißt, der Speicher wird freigegeben, aber zu spät (wenn der Server beispielsweise angehalten wird). Dann können Sie den Dump zum richtigen Zeitpunkt entfernen und analysieren. Valgrind oder Asan, die zu dem Zeitpunkt arbeiten, an dem der Prozess endet, können dies noch schlimmer machen.
  • Nur 64-Bit-Modus. Die Unterstützung für andere Bit- und Byte-Reihenfolgen wird für die Zukunft verschoben.

Bekannte Probleme


Während des Tests wurden Speicherauszugsdateien verwendet, die durch Senden eines Signals an den Prozess empfangen wurden. Mit solchen Dateien funktioniert alles gut. Wenn ein Speicherauszug entfernt wird, schreibt der Befehl gcore einige andere ELF-Header und es tritt ein Versatz um eine unbestimmte Anzahl von Bytes auf. Das heißt, die Werte der Zeiger werden in der Datei nicht an 8 ausgerichtet, sodass bedeutungslose Ergebnisse erzielt werden. Für die Lösung wurde die Offset-Option eingeführt - um die Datei nicht zuerst zu lesen, sondern um Offset-Bytes (normalerweise 4) zu verschieben.
Um dies zu lösen, plane ich, das Ergebnis von objdump -s aus stdin zu lesen. Nun, entweder verbinde dich mit libelf und analysiere es selbst, aber es wird "plattformübergreifend" töten, und stdout ist flexibler und näher am Unix-Weg.


Referenzen


Github-Projekt
Compiler D
Ursprüngliche Frage zum Stackoverflow

Source: https://habr.com/ru/post/de485102/


All Articles