web-dev-qa-db-fra.com

La meilleure façon d'appeler gdb depuis le programme pour imprimer sa trace de pile?

Utiliser une fonction comme celle-ci:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void print_trace() {
    char pid_buf[30];
    sprintf(pid_buf, "--pid=%d", getpid());
    char name_buf[512];
    name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;
    int child_pid = fork();
    if (!child_pid) {           
        dup2(2,1); // redirect output to stderr
        fprintf(stdout,"stack trace for %s pid=%s\n",name_buf,pid_buf);
        execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL);
        abort(); /* If gdb failed to start */
    } else {
        waitpid(child_pid,NULL,0);
    }
}

Je vois les détails de print_trace dans la sortie.

Quelles sont les autres façons de procéder?

57
Vi.

Vous avez mentionné dans mon autre réponse (maintenant supprimée) que vous souhaitez également voir les numéros de ligne. Je ne sais pas comment faire cela lorsque j'appelle gdb depuis l'intérieur de votre application.

Mais je vais partager avec vous quelques façons d'imprimer une trace de pile simple avec les noms de fonction et leurs numéros de ligne respectifs sans utiliser gdb . La plupart d'entre eux provenaient d'un très bien article de Linux Journal :

  • Méthode n ° 1:

La première méthode consiste à le diffuser avec des messages d'impression et de journal afin de localiser le chemin d'exécution. Dans un programme complexe, cette option peut devenir lourde et fastidieuse même si, à l'aide de certaines macros spécifiques à GCC, elle peut être un peu simplifiée. Considérez, par exemple, une macro de débogage telle que:

 #define TRACE_MSG fprintf(stderr, __FUNCTION__     \
                          "() [%s:%d] here I am\n", \
                          __FILE__, __LINE__)

Vous pouvez propager cette macro rapidement tout au long de votre programme en la coupant et en la collant. Lorsque vous n'en avez plus besoin, désactivez-le simplement en le définissant sur no-op.

  • Méthode n ° 2: (Cela ne dit rien sur les numéros de ligne, mais je le fais sur la méthode 4)

Cependant, une meilleure façon d'obtenir une trace de pile est d'utiliser certaines des fonctions de support spécifiques fournies par glibc. La clé est backtrace (), qui parcourt les trames de pile du point d'appel au début du programme et fournit un tableau d'adresses de retour. Vous pouvez ensuite mapper chaque adresse sur le corps d'une fonction particulière de votre code en consultant le fichier objet avec la commande nm. Ou, vous pouvez le faire d'une manière plus simple - utilisez backtrace_symbols (). Cette fonction transforme une liste d'adresses de retour, telle que renvoyée par backtrace (), en une liste de chaînes, chacune contenant le nom de fonction décalé dans la fonction et l'adresse de retour. La liste des chaînes est allouée à partir de votre espace de tas (comme si vous appeliez malloc ()), vous devez donc la libérer () dès que vous en avez terminé.

Je vous encourage à le lire car la page contient code source exemples. Pour convertir une adresse en nom de fonction, vous devez compiler votre application avec l'option - rdynamic .

  • Méthode n ° 3: (Une meilleure façon de faire la méthode 2)

Une application encore plus utile pour cette technique consiste à placer une trace de pile dans un gestionnaire de signaux et à faire en sorte que ce dernier intercepte tous les "mauvais" signaux que votre programme peut recevoir (SIGSEGV, SIGBUS, SIGILL, SIGFPE et similaires). De cette façon, si votre programme plante malheureusement et que vous ne l'exécutiez pas avec un débogueur, vous pouvez obtenir une trace de pile et savoir où l'erreur s'est produite. Cette technique peut également être utilisée pour comprendre où votre programme est en boucle au cas où il cesserait de répondre

Une implémentation de cette technique est disponible ici .

  • Méthode n ° 4:

Une petite amélioration que j'ai faite sur la méthode # 3 pour imprimer les numéros de ligne. Cela pourrait être copié pour fonctionner également avec la méthode # 2.

Fondamentalement, je suivi un conseil qui utilise addr2line pour

convertir les adresses en noms de fichiers et numéros de ligne.

Le code source ci-dessous imprime les numéros de ligne pour toutes les fonctions locales. Si une fonction d'une autre bibliothèque est appelée, vous pourriez voir quelques ??:0 au lieu des noms de fichiers.

#include <stdio.h>
#include <signal.h>
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>

void bt_sighandler(int sig, struct sigcontext ctx) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;

  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, ctx.cr2, ctx.eip);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *)ctx.eip;
  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:\n");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] #%d %s\n", i, messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i], p, messages[i]);
        //last parameter is the file name of the symbol
    system(syscom);
  }

  exit(0);
}


int func_a(int a, char b) {

  char *p = (char *)0xdeadbeef;

  a = a + b;
  *p = 10;  /* CRASH here!! */

  return 2*a;
}


int func_b() {

  int res, a = 5;

  res = 5 + func_a(a, 't');

  return res;
}


int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_handler = (void *)bt_sighandler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d\n", func_b());
}

Ce code doit être compilé comme: gcc sighandler.c -o sighandler -rdynamic

Le programme produit:

Got signal 11, faulty address is 0xdeadbeef, from 0x8048975
[bt] Execution path:
[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]
/home/karl/workspace/stacktrace/sighandler.c:44
[bt] #2 ./sighandler(func_b+0x20) [0x804899f]
/home/karl/workspace/stacktrace/sighandler.c:54
[bt] #3 ./sighandler(main+0x6c) [0x8048a16]
/home/karl/workspace/stacktrace/sighandler.c:74
[bt] #4 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]
??:0
[bt] #5 ./sighandler() [0x8048781]
??:0

Mise à jour du 28/04/2012 pour les versions récentes du noyau Linux, la signature sigaction ci-dessus est obsolète. De plus, je l'ai un peu amélioré en saisissant le nom de l'exécutable cette réponse . Voici une version à jour :

char* exe = 0;

int initialiseExecutableName() 
{
    char link[1024];
    exe = new char[1024];
    snprintf(link,sizeof link,"/proc/%d/exe",getpid());
    if(readlink(link,exe,sizeof link)==-1) {
        fprintf(stderr,"ERRORRRRR\n");
        exit(1);
    }
    printf("Executable name initialised: %s\n",exe);
}

const char* getExecutableName()
{
    if (exe == 0)
        initialiseExecutableName();
    return exe;
}

/* get REG_EIP from ucontext.h */
#define __USE_GNU
#include <ucontext.h>

void bt_sighandler(int sig, siginfo_t *info,
                   void *secret) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;
  ucontext_t *uc = (ucontext_t *)secret;

  /* Do something useful with siginfo_t */
  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, info->si_addr, 
           uc->uc_mcontext.gregs[REG_EIP]);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *) uc->uc_mcontext.gregs[REG_EIP];

  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:\n");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] %s\n", messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i] , p, messages[i] );
           //last parameter is the filename of the symbol
    system(syscom);

  }
  exit(0);
}

et initialiser comme ceci:

int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_sigaction = (void *)bt_sighandler;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = SA_RESTART | SA_SIGINFO;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d\n", func_b());

}
93
karlphillip

Si vous utilisez Linux, la bibliothèque C standard comprend une fonction appelée backtrace, qui remplit un tableau avec les adresses de retour des trames, et une autre fonction appelée backtrace_symbols, qui prendra les adresses de backtrace et recherchera les noms de fonction correspondants. Celles-ci sont documentées dans le manuel de la bibliothèque GNU C .

Ceux-ci ne montreront pas les valeurs d'argument, les lignes source et similaires, et ils s'appliquent uniquement au thread appelant. Cependant, ils devraient être beaucoup plus rapides (et peut-être moins feuilletés) que d'exécuter GDB de cette façon, donc ils ont leur place.

8
Jim Blandy

nobar publié ne réponse fantastique . En bref;

Vous voulez donc une fonction autonome qui imprime une trace de pile avec toutes les fonctionnalités qui gdb les traces de pile ont et cela ne met pas fin à votre application. La réponse est d'automatiser le lancement de gdb dans un mode non interactif pour effectuer uniquement les tâches que vous souhaitez.

Cela se fait en exécutant gdb dans un processus enfant, en utilisant fork () et en le scriptant pour afficher une trace de pile pendant que votre application attend qu'elle se termine. Cela peut être effectué sans utiliser de vidage de mémoire et sans abandonner l'application.

Je crois que c'est ce que vous cherchez, @Vi

6
karlphillip

abort() n'est-il pas plus simple?

De cette façon, si cela se produit sur le terrain, le client peut vous envoyer le fichier de base (je ne connais pas beaucoup d'utilisateurs qui sont suffisamment impliqués dans l'application my pour que je les oblige à le déboguer).

1
Motti