Topleaked: un outil pour détecter les fuites de mémoire

image

L'histoire, comme cela arrive souvent, a commencé avec le fait que l'un des services sur le serveur est tombé. Plus précisément, le processus a été tué en surveillant l'utilisation excessive de la mémoire. Le stock aurait dû être multiple, ce qui signifie que nous avons une fuite de mémoire.
Il y a un vidage de mémoire complet avec des informations de débogage, il y a des journaux, mais ne peuvent pas être reproduits. Soit la fuite est incroyablement lente, soit le scénario dépend de la météo sur Mars. En un mot, un autre bug qui n'est pas reproduit par les tests, mais qui se retrouve à l'état sauvage. Il reste le seul véritable indice - un vidage de mémoire.


Idée


Le service d'origine a été écrit en C ++ et Perl, bien que cela ne joue pas un rôle spécial. Tout ce qui est décrit ci-dessous s'applique à presque toutes les langues.


Notre processus à partir de l'énoncé du problème devait tenir dans quelques centaines de mégaoctets de RAM, et a été achevé pour dépasser 6 gigaoctets. Ainsi, la majeure partie de la mémoire de processus est constituée d'objets ayant fui et de leurs données. Il suffit de savoir quels types d'objets étaient le plus en mémoire. Bien sûr, il n'y a pas de liste d'objets avec des informations de type dans le vidage. Il est pratiquement impossible de suivre les relations et de créer un graphique comme le font les ramasse-miettes. Mais nous n'avons pas besoin de comprendre ce hachage binaire, mais de calculer quels objets sont plus. Les objets des classes non triviales ont un pointeur sur une table de méthodes virtuelles, et tous les objets de la même classe ont le même pointeur. Combien de fois un pointeur vers une classe vtbl est trouvé en mémoire - autant d'objets de cette classe ont été créés.


En plus de vtbl, il existe d'autres séquences fréquentes: constantes qui initialisent les champs, en-têtes HTTP en fragments de chaîne, pointeurs vers les fonctions.
Si vous avez la chance de trouver un pointeur, alors nous pouvons utiliser gdb pour comprendre à quoi il pointe (à moins bien sûr qu'il y ait des caractères de débogage). Dans le cas des données, vous pouvez essayer de les consulter et de comprendre où elles sont utilisées. Pour l'avenir, je constate qu'il se produit à la fois cela et un autre et, à partir d'un fragment de ligne, il est tout à fait possible de comprendre ce que cette partie du protocole, et où il est nécessaire de creuser plus loin.


L'idée a été espionnée et la première implémentation a été impudemment copiée à partir de stackoverflow. 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 

Le script a fonctionné pendant environ 15 minutes sur notre vidage, a renvoyé un tas de lignes, et ... rien. Pas un seul pointeur, rien d'utile.


Trié


Le développement piloté par Stackoverflow a ses inconvénients. Vous ne pouvez pas simplement copier le script et espérer que tout fonctionnera. Dans ce script particulier, une sorte de réarrangement d'octets attire immédiatement l'attention. La question se pose également, pourquoi les permutations par 4. Vous n'avez pas besoin d'être un super-spécialiste pour comprendre que ces permutations dépendent de la plate-forme: ordre des témoins et des octets.


Pour comprendre exactement comment regarder, vous devez comprendre le format de fichier du vidage de mémoire, LITTLE- et BIG-endian, ou vous pouvez simplement réorganiser les octets dans les morceaux trouvés de différentes manières et donner à gdb. Oh miracle! Dans l'ordre direct, l'octet gdb voit le caractère et dit qu'il s'agit d'un pointeur vers une fonction!


Dans notre cas, il s'agissait d'un pointeur vers l'une des fonctions de lecture et d'écriture des tampons openssl. Pour personnaliser l'entrée et la sortie, on utilise l'approche système OOP - une structure avec un ensemble de pointeurs vers des fonctions, qui est une sorte d'interface ou plutôt vtbl. Ces structures avec des pointeurs se sont révélées incroyablement nombreuses. Un examen attentif du code responsable de la définition de ces structures et de la création de tampons nous a permis de trouver rapidement l'erreur. Il s'est avéré qu'à la jonction de C ++ et C il n'y avait pas d'objets RAII et en cas d'erreur, un retour précoce ne laissait pas la possibilité de libérer des ressources. Personne n'a deviné charger le service avec des poignées de main SSL incorrectes en temps opportun, alors ils l'ont manqué. Comment composer 6 gigaoctets de poignées de main SSL incorrectes est également intéressant, mais comme on dit, c'est une histoire complètement différente. Le problème est résolu.


topleaked


Le script s'est avéré utile, mais présente encore de sérieux inconvénients pour une utilisation fréquente: il est très lent, dépendant de la plate-forme, plus tard il s'avère que les fichiers de vidage sont également avec des décalages différents, il est difficile d'interpréter les résultats. La tâche de creuser dans un vidage binaire ne correspond pas bien à bash, j'ai donc changé le langage de programmation en D. Le choix du langage est en fait dû au désir égoïste d'écrire dans votre langue préférée. Eh bien, la rationalisation du choix est la suivante: la vitesse et la consommation de mémoire sont essentielles, vous avez donc besoin d'un langage natif compilé, et il est banal d'écrire D plus rapidement que C ou C ++. Plus tard dans le code, il sera clairement visible. Le projet topleaked est donc né.


L'installation


Il n'y a pas d'assemblages binaires, donc d'une manière ou d'une autre, vous devrez assembler le projet à partir de la source. Cela nécessitera le compilateur D. Il y a trois options: dmd - le compilateur de référence, ldc - basé sur llvm et gdc, inclus dans gcc, à partir de la version 9. Vous n'aurez donc peut-être pas besoin d'installer quoi que ce soit si vous avez la dernière version de gcc. Si vous installez, je recommande ldc, car il optimise mieux. Tous les trois peuvent être trouvés sur le site officiel .
Le gestionnaire de packages dub est fourni avec le compilateur. En l'utilisant, topleaked est installé avec une seule commande:


 dub fetch topleaked 

À l'avenir, nous utiliserons la commande pour démarrer:


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

Afin de ne pas répéter l'exécution dub et l'argument du compilateur brelease-nobounds, vous pouvez télécharger les sources depuis le github et collecter le fichier exécutable:


 dub build -brelease-nobounds 

À la racine du dossier du projet apparaîtra en haut.


Utiliser


Prenons un simple programme C ++ avec une fuite de mémoire.


 #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); } 

Nous le terminons par kill -6, puis nous obtenons un vidage de mémoire. Maintenant, vous pouvez exécuter topleaked et regarder les résultats

 ./toleaked -n10 leak.core 

L'option -n est la taille du haut dont nous avons besoin. En règle générale, les valeurs comprises entre 10 et 200 ont un sens, selon la quantité de «corbeille». Le format de sortie par défaut est un haut ligne par ligne sous une forme lisible par l'homme.


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

Il est de peu d'utilité, sauf que nous pouvons voir le nombre 0x2dfdc1c3e, qui est également 12345678910, qui se produit un million de fois. Cela pourrait déjà suffire, mais j'en veux plus. Afin de voir les noms de classe des objets ayant fait l'objet d'une fuite, vous pouvez envoyer le résultat à gdb en redirigeant simplement le flux de sortie standard vers l'entrée gdb avec un fichier de vidage ouvert. -ogdb - option changeant le format en gdb compréhensible.


 $ ./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 

La lecture n'est pas très simple, mais possible. Les lignes du formulaire 4 $ = 1 000 000 reflètent la position en haut et le nombre d'occurrences trouvées. Voici les résultats de l'exécution de x et du symbole info pour la valeur. Ici, nous pouvons voir que vtable pour A se produit un million de fois, ce qui correspond à un million d'objets divulgués de classe A.

Pour analyser une partie du fichier (s'il est trop volumineux), les options de décalage et de limite sont ajoutées - à partir de l'endroit et du nombre d'octets à lire.


Résultat


L'utilitaire résultant est sensiblement plus rapide que le script. Vous devez encore attendre, mais pas à l'échelle d'une randonnée pour le thé, mais quelques secondes avant que le sommet n'apparaisse à l'écran. Je suis absolument sûr que l'algorithme peut être considérablement amélioré et que les opérations lourdes d'entrée et de sortie peuvent être considérablement optimisées. Mais c'est une question de développement futur, maintenant tout fonctionne bien.


Grâce à l'option -ogdb et à la redirection dans gdb, nous obtenons immédiatement des noms et des valeurs, parfois même des numéros de ligne, si nous avons la chance d'accéder à la fonction.


La conséquence évidente, mais très inattendue, de la solution frontale était la multiplateforme. Oui, topleaked ne connaît pas l'ordre des octets, mais puisqu'il n'analyse pas le format de fichier, mais lit simplement le fichier octet par octet, il peut être utilisé sur Windows ou tout système avec n'importe quel format de vidage de mémoire. Il est seulement nécessaire que les données soient alignées dans le fichier.


Langue D


Je voudrais noter séparément l'expérience de développement d'un tel programme en D. La première version de travail a été écrite en quelques minutes. Je dois dire que jusqu'à présent, l'algorithme principal ne prend que trois lignes:


 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); 

Tout cela grâce aux plages paresseuses et à la présence d'algorithmes prêts à l'emploi sur eux dans la bibliothèque standard, tels que group et topN.


Plus tard, l'analyse des arguments de la ligne de commande, le formatage de la sortie et tout ce qui est verbeux, mais aussi écrit rapidement, a augmenté. À moins que la lecture du fichier ne se révèle étrange, hors du style général.


Dans la dernière version actuellement, l'indicateur --find est apparu pour la recherche habituelle d'une sous-chaîne, qui n'est pas du tout liée à la fréquence. En raison de cette bagatelle, le code a sensiblement augmenté en taille, mais avec de fortes chances, la fonctionnalité sera supprimée et le code reviendra à son état simple d'origine.


Au total, les coûts de main-d'œuvre sont comparables aux langages de script et bien meilleurs en termes de performances. Potentiellement, vous pouvez l'amener au maximum possible, car le même code en C et D fonctionnera de la même manière à la même vitesse.


Indications et contre-indications d'utilisation


  • Topleaked est nécessaire pour rechercher des fuites lorsqu'il n'y a qu'un vidage de la mémoire du processus en cours, mais il n'y a aucun moyen de le reproduire sous le désinfectant.
  • Ce n'est pas un autre valgrind et ne prétend pas être une analyse dynamique.
  • Une exception intéressante à la remarque précédente peut être des fuites temporaires. Autrement dit, la mémoire est libérée, mais trop tard (lorsque le serveur est arrêté, par exemple). Ensuite, vous pouvez supprimer le vidage au bon moment et analyser. Valgrind ou asan, travaillant à la fin du processus, peuvent faire pire.
  • Uniquement en mode 64 bits. La prise en charge des autres bits et de l'ordre des octets est reportée à l'avenir.

Problèmes connus


Pendant les tests, des fichiers de vidage ont été utilisés qui ont été reçus en envoyant un signal au processus. Avec de tels fichiers, tout fonctionne bien. Lorsqu'un vidage est supprimé, la commande gcore écrit certains autres en-têtes ELF et un décalage d'un nombre indéfini d'octets se produit. Autrement dit, les valeurs des pointeurs ne sont pas alignées sur 8 dans le fichier, donc des résultats sans signification sont obtenus. Pour la solution, l'option de décalage a été introduite - pour lire le fichier non pas en premier, mais décalé de octets de décalage (généralement 4).
Pour résoudre ce problème, je prévois d'ajouter la lecture du résultat de objdump -s depuis stdin. Eh bien, connectez-vous libelf et analysez-le vous-même, mais cela tuera «multiplateforme», et stdout est plus flexible et plus proche de la méthode unix.


Les références


Projet Github
Compilateurs D
Question d'origine sur stackoverflow

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


All Articles