2010-12-03 42 views
5

J'ai divisé la question en plus petits:Comment gdb reconstruit stacktrace pour C++?

  1. Quel genre de différents algorithmes GDB est capable d'utiliser pour reconstruire stacktraces?
  2. Comment chaque algorithme de reconstruction de chemin de pile fonctionne-t-il à un niveau élevé? Avantages et inconvénients?
  3. Quel type de compilateur de méta-informations doit fournir dans le programme pour que chaque algorithme de reconstruction stacktrace fonctionne?
  4. Et aussi les commutateurs de compilateur g ++ correspondants qui activent/désactivent un algorithme particulier?
+1

http://en.wikipedia.org/wiki/Call_stack, le compilateur ajoute des décalages de code de mappage d'informations de débogage aux lignes source. –

+1

Cet article wikipedia ne va pas dans les détails sur d'autres algorithmes que simple "stocker RBP dans la pile lors de l'appel d'une autre fonction". Par exemple, cet algorithme ne fonctionnera plus si -fomit-frame-pointer est défini au moment de la compilation. Qu'en est-il du descripteur de déroulement de pile? Comment est-ce que l'on reconstruit stacktrace? Y a-t-il d'autres algorithmes? –

+1

Avec '-fomit-frame-pointer' GDB est caca. –

Répondre

7

En parlant pseudocode, vous pouvez appeler la pile « un tableau de cadres de pile emballés » cadre tous, où la pile est une structure de données de taille variable, vous pouvez exprimer comme:

template struct stackframe<N> { 
    uintptr_t contents[N]; 
#ifndef OMIT_FRAME_POINTER 
    struct stackframe<> *nextfp; 
#endif 
    void *retaddr; 
}; 

Le problème est que tous les la fonction a un <N> différent - les tailles d'armature varient.

Le compilateur connaît les tailles d'image et si la création d'informations de débogage les émet généralement. Tout ce que le débogueur doit ensuite faire est de localiser le dernier compteur de programme, rechercher la fonction dans la table de symboles, puis utiliser ce nom pour rechercher la taille de la barre dans les informations de débogage. Ajoutez cela au stackpointer et vous arrivez au début de l'image suivante.

Si vous utilisez cette méthode, vous n'avez pas besoin de tringlage d'image, et le backtracing fonctionnera correctement même si vous utilisez . D'un autre côté, si vous avez une liaison d'image, alors l'itération de la pile suit juste une liste chaînée - parce que chaque framepointer d'une nouvelle stackframe est initialisé par le code prologue de la fonction pour pointer vers le précédent.

Si vous n'avez ni informations sur la taille d'image, ni framepointers, mais toujours une table de symboles, vous pouvez également effectuer un backtracing par un peu de reverse engineering pour calculer les framesizes à partir du binaire actuel. Commencez avec le compteur de programme, recherchez la fonction à laquelle il appartient dans la table de symboles, puis démontez la fonction depuis le début. Isolez toutes les opérations entre le début de la fonction et le compteur de programme qui modifient réellement le stackpointer (écrivez n'importe quoi dans la pile et/ou allouez l'espace de pile). Cela calcule la taille d'image pour la fonction courante, donc soustrayez-le du stackpointer, et vous devriez (sur la plupart des architectures) trouver le dernier mot écrit dans la pile avant que la fonction ne soit entrée - qui est généralement l'adresse de retour dans l'appelant. Réitérer si nécessaire. Enfin, vous pouvez effectuer une analyse heuristique du contenu de la pile - isoler tous les mots de la pile qui se trouvent dans des segments mappés de manière exécutable de l'espace d'adressage du processus (et donc des offsets de fonction, adresses de retour) jouez un jeu de simulation en levant la mémoire, en désassemblant l'instruction et en vérifiant si c'est vraiment une instruction d'appel de tri, si c'est le cas, si cela s'appelle vraiment le suivant et si vous pouvez construire une séquence d'appel ininterrompue. Cela fonctionne à un degré même si le binaire est complètement dépouillé (bien que tout ce que vous pourriez obtenir dans ce cas est une liste d'adresses de retour). Je ne pense pas que GDB utilise cette technique, mais certains débogueurs de bas niveau intégrés le font. Sur x86, en raison des différentes longueurs d'instructions, cela est terriblement difficile à faire parce que vous ne pouvez pas facilement "reculer" dans un flux d'instructions, mais sur RISC, où les longueurs d'instructions sont fixes, par ex. sur ARM, c'est beaucoup plus simple.

Il existe des trous qui font parfois tomber des implémentations simples ou même complexes/exhaustives de ces algorithmes, comme des fonctions récursives, du code inline, etc.Le code source gdb peut vous donner quelques idées:

http://sourceware.org/cgi-bin/cvsweb.cgi/src/gdb/frame.c?rev=1.287&content-type=text/x-cvsweb-markup&cvsroot=src

GDB utilise une variété de ces techniques.

+0

Je dois dire que rien de tout cela n'est particulièrement spécifique à C++. Les spécificités C++ entrent en jeu lors du déroulement des piles d'exceptions, mais ce n'est pas le cas du débogueur (si vous plantez dans un 'throw()' vous n'avez pas encore déroulé la pile des exceptions, et si vous plantez dans un 'catch {}' alors vous l'avez fait, si vous plantez dans le processus de déroulement, alors vous avez un bug de compilateur ... et le débogage est laissé aux vrais dieux ...). –

+0

Je viens de trouver une autre source qui suggère que .eh_frame (spécifique à C++) pourrait être utilisé dans GDB pour rembobiner la pile: http://sourceware.org/gdb/papers/unwind.html –