Question

Je me souviens quand j'étais dans un certain cours de programmation C, un enseignant, une fois suggéré que j'utilise printf pour assister à l'exécution d'un programme que je tentais de débogage. Ce programme a eu une erreur de segmentation avec une cause que je ne me souviens pas en ce moment. J'ai suivi ses conseils et la faute de segmentation ont disparu. Heureusement, un intelligent TA m'a dit de déboguer au lieu d'utiliser printfs. Dans ce cas, il est une chose utile à faire.

Alors, aujourd'hui je voulais montrer à quelqu'un que l'utilisation printf pourrait cacher un bug, mais je ne peux pas trouver ce vieux code qui avait ce bug bizarre (fonctionnalité? Hmmm).

Question: un de vous rencontré ce comportement aussi bien? Comment pourrais-je reproduire quelque chose comme ça?

Modifier

Je vois que ma part de question mon avis à oriente « à l'aide printf est faux ». Je ne dis pas exactement cela et je ne aime pas prendre opinions extrêmes, donc je suis d'édition un peu la question. Je suis d'accord que printf est un bon outil, mais je voulais juste recréer un cas où printfs faire disparaître une erreur de segmentation et, par conséquent, prouver qu'il faut être prudent.

Était-ce utile?

La solution

Il y a des cas lors de l'ajout printf appelle modifie le comportement du code, mais il y a aussi des cas où le débogage fait la même chose. L'exemple le plus important est le débogage du code multithread, où l'arrêt de l'exécution d'un thread peut modifier le comportement du programme, donc le bug que vous cherchez peut ne pas se produire.

Donc, en utilisant des déclarations de printf n'ont des raisons valables. Que ce soit pour le débogage ou printf devrait être décidée au cas par cas. Notez que les deux ne sont pas de toute façon exclusive - vous peut code de débogage, même si elle contient des appels printf: -)

Autres conseils

Vous auriez beaucoup de mal à me convaincre de ne pas utiliser la journalisation (et printf dans cette situation est un avait hoc sous forme d'exploitation forestière) à déboguer. Il est évident que pour déboguer un accident, les premières choses est d'obtenir un Purifier Backtrace et de l'utilisation ou un outil similaire, mais si la cause n'est pas évidente exploitation forestière est de loin l'un des meilleurs outils que vous pouvez utiliser. Un débogueur permet de vous concentrer sur les détails, l'exploitation forestière vous donner une image plus grande. Les deux sont utiles.

On dirait que vous avez affaire à un Heisenbug .

Je ne pense pas qu'il y ait quoi que ce soit en soi « mal » avec l'utilisation de printf comme outil de débogage. Mais oui, comme tout autre outil, il a ses défauts, et oui, il y a eu plus d'un occaision où l'ajout de printf a créé un Heisenbug. Cependant, j'ai aussi eu apparaissent comme bug logiciel inhabituel à la suite de la mise en page de mémoire changements introduits par un débogueur, auquel cas printf a prouvé une valeur inestimable dans le suivi des étapes qui conduisent à l'accident.

à mon humble avis développeur Chaque compte encore ici et là sur les impressions. Nous venons d'apprendre à les appeler « des journaux détaillés ».

Plus précisément, le principal problème que je l'ai vu est que les gens traitent printfs comme ils sont invincibles. Par exemple, il est pas rare en Java pour voir quelque chose comme

System.out.println("The value of z is " + z + " while " + obj.someMethod().someOtherMethod());

Cela tombe bien, sauf que z a été effectivement impliqué dans la méthode, mais cet autre objet n'a pas été, et il n'y a pour vous assurer de ne pas faire une exception à l'expression obj.

Une autre chose que les impressions font est qu'ils introduisent des retards. Je l'ai vu code avec des conditions de course parfois « se fixe » lorsque les impressions sont introduites. Je ne serais pas surpris si certaines utilisations de code qui.

Je me souviens une fois essayer de déboguer un programme sur le Macintosh (vers 1991) où le code de nettoyage généré pour un cadre de pile du compilateur entre 32K et 64K était erronée parce qu'il a utilisé un ajout d'adresse de 16 bits plutôt qu'un 32 bits (une quantité de 16 bits ajoutée à un registre d'adresse sera de connexion étendue sur le 68000). La séquence était quelque chose comme:

  copy stack pointer to some register
  push some other registers on stack
  subtract about 40960 from stack pointer
  do some stuff which leaves saved stack-pointer register alone
  add -8192 (signed interpretation of 0xA000) to stack pointer
  pop registers
  reload stack pointer from that other register

L'effet net est que tout allait bien sauf que les registres sauvegardés ont été corrompus, et l'un d'eux tenait une constante (l'adresse d'un tableau global). Si le compilateur optimise une variable à un registre au cours d'une section de code, il signale que dans le fichier des informations de débogage de sorte que le débogueur peut produire correctement il. Quand une constante est si optimisée, le compilateur ne semble pas tenir compte de ces informations, car il ne sera pas nécessaire. Je traqué les choses en faisant un « printf » de l'adresse du tableau et points d'arrêt pour que je puisse voir l'adresse avant et après le printf. Le débogueur a rapporté correctement l'adresse avant et après le printf, mais le printf la valeur erronée émis, donc je démonte le code et vu que printf poussait registre A3 sur la pile; registre de visualisation A3 avant le printf a montré qu'il avait une valeur assez différente de l'adresse du tableau (le printf a montré la valeur A3 effectivement lieu).

Je ne sais pas comment j'aurais jamais suivi que l'un vers le bas si je ne l'avais pas été en mesure d'utiliser à la fois le débogueur et ensemble printf (ou, d'ailleurs, si je ne l'avais pas compris 68000 code assembleur).

J'ai réussi à le faire. Je lisais des données depuis un fichier plat. Mon algorithme défectueux a comme suit:

  1. get longueur du fichier d'entrée en octets
  2. allouer un tableau de longueur variable de caractère pour servir de tampon
    • les fichiers sont petites, donc je ne suis pas inquiet au sujet de débordement pile, mais que sur les fichiers d'entrée de longueur nulle? oups!
  3. retourner un code d'erreur si la longueur du fichier d'entrée est 0

Je trouve que ma fonction serait sûrement jeter un défaut de seg - à moins qu'il y avait un endroit printf dans le corps de la fonction, dans ce cas, cela fonctionnerait exactement comme je voulais. Le correctif pour la faille seg était d'affecter la longueur du fichier plus un à l'étape 2.

Je viens d'avoir une expérience similaire. Voici mon problème spécifique, et la cause:

// Makes the first character of a word capital, and the rest small
// (Must be compiled with -std=c99)
void FixCap( char *word )
{
  *word = toupper( *word );
  for( int i=1 ; *(word+i) != '\n' ; ++i )
    *(word+i) = tolower( *(word+i) );
}

Le problème est lié à la condition de boucle - je « \ n » à la place du caractère nul, « \ 0 ». Maintenant, je ne sais pas exactement comment fonctionne printf, mais à partir de cette expérience, je devine qu'il utilise un emplacement de mémoire après mes variables comme espace temporaire / travail. Si un résultat de déclaration printf dans un « \ n » caractère en cours d'écriture à un endroit où après ma parole est stockée, la fonction FixCap sera en mesure d'arrêter à un moment donné. Si je retire le printf, il continue à boucle, la recherche d'un « \ n », mais jamais le trouver, jusqu'à ce qu'il segfaults.

Donc à la fin, la cause de mon problème est que, parfois, je tape « \ n » quand je veux dire « \ 0 ». C'est une erreur que je l'ai fait avant, et probablement celui que je vais faire à nouveau. Mais maintenant, je sais chercher.

Eh bien, peut-être que vous pourriez lui apprendre à utiliser gdb ou d'autres programmes de débogage? Dites-lui que si un bug disparaît grâce à un Juste « printf », il n'a pas vraiment disparaître et pourrait apparaître à nouveau celui-ci. Un bug devrait être fixé, pas ignoré.

Cela vous donnera une division par 0 lors du retrait de la ligne printf:

int a=10;
int b=0;
float c = 0.0;

int CalculateB()
{
  b=2;
  return b;
}
float CalculateC()
{
  return a*1.0/b;
}
void Process()
{
  printf("%d", CalculateB()); // without this, b remains 0
  c = CalculateC();
}

Quel serait le cas de débogage? Impression d'un tableau de char *[] avant d'appeler exec() juste pour voir comment il a été tokenisé -. Je pense que c'est une utilisation assez valable pour printf()

Cependant, si le format fourni à printf() est d'un coût et d'une complexité qu'il peut effectivement l'exécution du programme alter (vitesse, la plupart du temps), un débogueur peut être le moyen d'aller mieux. Là encore, débogueurs et profileurs viennent aussi à un coût. Ou bien on peut exposer les races qui pourraient ne pas faire surface en leur absence.

Tout dépend de ce que vous écrivez et le bug que vous poursuivez. Les outils disponibles sont débogueurs, printf() (regroupement des enregistreurs dans printf ainsi) et assertions profileurs.

est un tournevis mieux que d'autres types? Dépend de ce dont vous avez besoin. Remarque, je ne dis pas que les affirmations sont bonnes ou mauvaises. Ils sont juste un autre outil.

Une façon de traiter ce problème est de mettre en place un système de macros qui le rend facile à éteindre printfs w / o avoir à les supprimer dans votre code. J'utilise quelque chose comme ceci:

#define LOGMESSAGE(LEVEL, ...) logging_messagef(LEVEL, __FILE__, __LINE__, __FUNCTION__, __VA_ARGS__);

/* Generally speaking, user code should only use these macros.  They
 * are pithy. You can use them like a printf:
 *
 *    DBGMESSAGE("%f%% chance of fnords for the next %d days.", fnordProb, days);
 *
 * You don't need to put newlines in them; the logging functions will
 * do that when appropriate.
 */
#define FATALMESSAGE(...) LOGMESSAGE(LOG_FATAL, __VA_ARGS__);
#define EMERGMESSAGE(...) LOGMESSAGE(LOG_EMERG, __VA_ARGS__);
#define ALERTMESSAGE(...) LOGMESSAGE(LOG_ALERT, __VA_ARGS__);
#define CRITMESSAGE(...) LOGMESSAGE(LOG_CRIT, __VA_ARGS__);
#define ERRMESSAGE(...) LOGMESSAGE(LOG_ERR, __VA_ARGS__);
#define WARNMESSAGE(...) LOGMESSAGE(LOG_WARNING, __VA_ARGS__);
#define NOTICEMESSAGE(...) LOGMESSAGE(LOG_NOTICE, __VA_ARGS__);
#define INFOMESSAGE(...) LOGMESSAGE(LOG_INFO, __VA_ARGS__);
#define DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#if defined(PAINFULLY_VERBOSE)
#   define PV_DBGMESSAGE(...) LOGMESSAGE(LOG_DEBUG, __VA_ARGS__);
#else
#   define PV_DBGMESSAGE(...) ((void)0);
#endif

logging_messagef() est une fonction définie dans un fichier séparé .c. Utilisez les macros xmessage (...) dans votre code en fonction du but du message. La meilleure chose à propos de cette configuration est que cela fonctionne pour le débogage et la connexion en même temps, et la fonction logging_messagef() peut être modifiée pour faire plusieurs choses différentes (printf à stderr, dans un fichier journal, utilisez syslog ou une autre installation de journalisation du système, etc.), et les messages ci-dessous un certain niveau peuvent être ignorées dans logging_messagef() lorsque vous ne les avez pas besoin. PV_DBGMESSAGE() est pour les messages de débogage copieuses que vous aurez certainement envie de désactiver la production.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top