OVH Cloud OVH Cloud

Bug indétectable via printf() / cout

148 réponses
Avatar
AG
Bonjour =E0 tous,

Je travaille dans une =E9quipe de personnes pour lesquels le d=E9bugger
n'est pas l'outil qui tombe sous le sens lorsqu'il s'agit de trouver
les bugs des outils qu'ils ont d=E9velopp=E9.

Afin de les sensibiliser, je recherche un exemple de bug difficile
(voir impossible) a d=E9tecter via des printf()/cout qui n'afficheraient
que les contenus des variables. (Je me rends bien compte qu'on doit
pouvoir tout d=E9bugger avec printf()/cout, mais il faut parfois
afficher les adresses des variable (en plus de leur valeur), voir
parfois la m=E9moire =E0 certains endroits.)

Je voudrais construire un exemple simple (notions de C++ pas trop
compliqu=E9es) et court (un seul fichier, 100 lignes max) pour qu'on
puisse l'=E9tudier en un quart d'heure, mais le d=E9bugger en un temps
presque infini sans d=E9bugger.

Il est possible que l'exemple, puisqu'il y a un bug, d=E9pende de la
plateforme mais c'est un peu in=E9vitable.

L'id=E9al serait que le plantage ait lieu bien apr=E8s le bug...=E7a rajout=
e
du piment. Bref, vous voyez l'id=E9e quoi...

J'avais plusieurs pistes d'exploitation de bug:

Piste 1:
Boucle for d=E9croissante sur un entier non sign=E9 : for(size_t i =3D N;
0<=3D i; i--)

Piste 2:
comparaison de double : double x; ... ; x =3D=3D 0.0

Piste 3:
retour de malloc() non test=E9

Piste 4:
caract=E8re de fin de ligne non test=E9 (\0)

Mais jusque l=E0, je crains qu'il ne soit trop facile de d=E9tecter le
bug

J'ai donc ensuite pens=E9 aux r=E9f=E9rences. Mais ce code est encore un pe=
u
trop voyant. Dans le main(), on peut facilement se demander pourquoi
f1 et f2 sont des r=E9f=E9rences, ce qui met directement la puce =E0
l'oreille. Peut =EAtre trouvez vous ce code bien trop compliqu=E9, ou
auriez vous en t=EAte un mani=E8re de "l'am=E9liorer" :-)

A bon entendeur salut.

AG.




#include <iostream>

using namespace std;

#define BUFFER_LENGTH 10

template<int N, class T>
class fifo_circular
{
private:
T * position;
size_t pos_offset;
T buffer[N];

public:
fifo_circular() { pos_offset =3D N-1; position =3D buffer + pos_offset;
for(int i=3D0;i<N;i++) buffer[i]=3DT(0);};

T step(T &input)
{
*position =3D input;

if(pos_offset>0) // ici il y aurait peut =EAtre moyen de glisser le
bug de la piste 1
pos_offset--;
else
pos_offset =3D N-1;

position =3D buffer + pos_offset;
return *position;
};

template<int M, class U> friend ostream & operator<<(ostream & o,
const fifo_circular<M,U> &f);
};

template<int N, class T>
fifo_circular<N,T> & Init(T & value)
{
fifo_circular<N,T> & f =3D fifo_circular<N,T>(); // h=E9 h=E9 h=E9

for(size_t i =3D 0; i < N; i++)
f.step(value);
return f;
}

template<int M, class U>
ostream & operator<<(ostream & o, const fifo_circular<M,U> &f)
{
for(size_t i =3D 0; i < M; i++)
o << f.buffer[i] << " ";
return o;
}

int main(int argc, char * argv[])
{
int a =3D 1;
fifo_circular<5,int> & f1 =3D Init<5,int>(a); // ici, c'est un peu trop
voyant. On se demande pourquoi f1 est d=E9clar=E9 en tant que
r=E9f=E9rence...
a =3D 2;
fifo_circular<5,int> & f2 =3D Init<5,int>(a);

cout << "fifo f1: " << f1 << "\n";
cout << "fifo f2: " << f2 << "\n";

return 0;
}

10 réponses

Avatar
James Kanze
On Dec 25, 11:20 pm, BOUCNIAUX Benjamin
hosting.com> wrote:
Le Mon, 30 Nov 2009 13:27:49 +0000, Marc Boyer a écrit :
> Le 30-11-2009, AG a écrit :
>> Je travaille dans une équipe de personnes pour lesquels le
>> débugger n'est pas l'outil qui tombe sous le sens lorsqu'il
>> s'agit de trouver les bugs des outils qu'ils ont développé.



> Mais est-ce grave ? J'avoue ne quasiment jamais me servir de
> débugger. Je travaille surtout en précondition/invariant
> (assert) + tests unitaires, et éventuellement valgrind quand
> je soupsonne une corruption mémoire



> Et une fois le bug identifié, à grand coups de cerr +
> assert.



> D'autant que souvent, le bug n'apparait pas en mode débug ;-)



Pour ma part, je ne pense pas qu'une technique soit mieux
qu'une autre. Le tout, c'est d'avoir la capacité à trouver
son bug, quelque soit la manière. Partant de ce postulat,
pourquoi ne pas simplement utiliser l'équivalent de ces
différentes manières?



Je m'explique : quand je développe, je base toutes mes
gestions d'erreur sur des exceptions, y compris les controles
appliqués (valeurs acceptées, pointeurs non NULL, retours de
syscalls corrects, etc.).



En somme, tu considères qu'une erreur de frappe de la part d'un
utilisateur s'assimile à une violation d'un invariant dans le
code.

Pour chaque exception levée/reçue, j'ajoute une trace (via une
macro, désactivable donc) qui résulte en une stack trace
complète dans les logs. Ca fait un peu java, mais c'est très
clair. Au bout de la stack trace (cad au niveau le plus haut
de la gestion d'erreur, qui n'est pas forcément main() ),
j'affiche le message d'erreur qui est remonté ; une manière
élégante de voir d'où est venu le bug, et en plus d'où
venaient les appels à cette fonctionnalité. Et cela permet
également d'implémenter des préconditions/invariants.



Personnellement, je trouve l'assert un peu brutal, et le
printf trop limitatif :)



L'assert est brutal pour une raison : il ne sert que dans le cas
des erreurs de programmation. L'assert ne peut pas déclencher si
ton raisonement sur le code était correct. C'est donc que tu ne
sais plus dans quel état se trouve le programme.

Dans ce cas-là, chaque instruction que tu exécutes est une
risque. Il faut en exécuter les moins possibles. Et donc, ne pas
exécuter les destructeurs. (En général. Comme pour tout, il y a
des trade-offs, et il y a des cas particuliers où la règle
générale ne s'applique pas.)

Pourquoi complètement stopper un programme lorsque quelque
chose ne se passe pas bien?



Si ce qui ne se passe pas bien, c'est qu'on ne sait plus l'état
du programme, ni ce qu'il fait, il faut bien l'arrêter le plus
vite possible, avant qu'il ne fasse quelque chose de vraiment
nuisible.

Ajoutez à cela une sauvegarde des données d'entrée lors de la gesti on
d'erreur, et une compilation optimisée avec les symboles de debug dans
des fichiers séparés.
Lors d'un problème en production, il suffit généralement de regarde r la
stacktrace pour voir ce qui s'est produit, et comment corriger le
problème.



Exactement. Et c'est ce que te donne l'assert (au moins sous
Unix, mais je crois qu'on peut en avoir pareil sous Windows).
Avec un log des entrées, évidemment, pour pouvoir réproduire
l'erreur.

J'accorde le fait qu'il sera toujours nécessaire d'arriver à
reproduire le bug avec un test unitaire,



Disons qu'il ne faut pas faire la moindre modification dans les
sources du programme avant de pouvoir déclencher l'erreur dans
un programme de test. Qui en suite fera partie de tes tests de
regression.

mais ce n'est pas toujours le cas (si une
frame de niveau supérieur est corrompue, il se peut que le programme
"lache" lors d'appels un peu plus bas, ou après plusieurs retours ...



Oui, mais quelque chose à corrompu la frame de niveau supérieur.
C'est ça l'erreur qu'il faut chercher, et c'est pour ça qu'il
faut un log des entrées, pour pouvoir récréer l'état qui a
déclenché l'erreur.

Sans compter que quand une stack est foirée, printf ne fait
pas toujours son boulot comme il faut.



Exactement. Et quand la stack est corrompue, rémonter une
exception ne march pas comme il faut.

[..,]
Ce que je remarque en me relisant, c'est que pour du petit
bug, la trace peut suffir, mais je ne pense pas dans tous les
cas. Pour du "bon" bug (SIGSEGV/SIGPIPE, problèmes de synchros
entre threads, freeze, etc.), le débugueur apporte un confort
inégalé,



C'est justement ce genre de problèmes où le deboggueur ne marche
pas. Parce qu'il change les temps, et donc l'exécution du
programme.

--
James Kanze
Avatar
BOUCNIAUX Benjamin
Le Sat, 26 Dec 2009 04:11:13 -0800, James Kanze a écrit :

On Dec 25, 11:20 pm, BOUCNIAUX Benjamin hosting.com>
wrote:
Le Mon, 30 Nov 2009 13:27:49 +0000, Marc Boyer a écrit :
> Le 30-11-2009, AG a écrit :
>> Je travaille dans une équipe de personnes pour lesquels le débugger
>> n'est pas l'outil qui tombe sous le sens lorsqu'il s'agit de trouver
>> les bugs des outils qu'ils ont développé.



> Mais est-ce grave ? J'avoue ne quasiment jamais me servir de
> débugger. Je travaille surtout en précondition/invariant (assert) +
> tests unitaires, et éventuellement valgrind quand je soupsonne une
> corruption mémoire



> Et une fois le bug identifié, à grand coups de cerr + assert.



> D'autant que souvent, le bug n'apparait pas en mode débug ;-)



Pour ma part, je ne pense pas qu'une technique soit mieux qu'une autre.
Le tout, c'est d'avoir la capacité à trouver son bug, quelque soit la
manière. Partant de ce postulat, pourquoi ne pas simplement utiliser
l'équivalent de ces différentes manières?



Je m'explique : quand je développe, je base toutes mes gestions
d'erreur sur des exceptions, y compris les controles appliqués (valeurs
acceptées, pointeurs non NULL, retours de syscalls corrects, etc.).



En somme, tu considères qu'une erreur de frappe de la part d'un
utilisateur s'assimile à une violation d'un invariant dans le code.




J'explique juste que je ne fais pas d'assert.
Que tu lances une exception, fasse un assert ou retourne un code
d'erreur, voire fasse un exit(), tu agis face à une erreur, peu importe
laquelle.

Pour chaque exception levée/reçue, j'ajoute une trace (via une macro,
désactivable donc) qui résulte en une stack trace complète dans les
logs. Ca fait un peu java, mais c'est très clair. Au bout de la stack
trace (cad au niveau le plus haut de la gestion d'erreur, qui n'est pas
forcément main() ), j'affiche le message d'erreur qui est remonté ; une
manière élégante de voir d'où est venu le bug, et en plus d'où venaient
les appels à cette fonctionnalité. Et cela permet également
d'implémenter des préconditions/invariants.



Personnellement, je trouve l'assert un peu brutal, et le printf trop
limitatif :)



L'assert est brutal pour une raison : il ne sert que dans le cas des
erreurs de programmation. L'assert ne peut pas déclencher si ton
raisonement sur le code était correct. C'est donc que tu ne sais plus
dans quel état se trouve le programme.

Dans ce cas-là, chaque instruction que tu exécutes est une risque. Il
faut en exécuter les moins possibles. Et donc, ne pas exécuter les
destructeurs. (En général. Comme pour tout, il y a des trade-offs, et il
y a des cas particuliers où la règle générale ne s'applique pas.)




Et dans ce cas, on se retrouve avec des programmes sans aucune robustesse.
Je veux bien que ce soit le cas sur des systèmes embarqués, mais la
plupart des applications sur lesquelles j'ai travaillé ne doivent pas
s'arreter comme ça.
Je préfère un programme qui va remonter une erreur et retourner dans un
état initial tout seul, qu'un programme qui va s'arreter tout seul.
En développant correctement, il est difficile d'avoir un composant
"instable". Soit on l'a dans son état stable, soit on ne l'a pas du tout
(la construction a échouée, ou lors qu'il émet des erreurs, le composant
de plus haut niveau le réinitialise, et si rien n'est possible, à ce
moment là, OK, on envisage une résolution plus drastique.
La philosophie "j'ai une erreur, je coupe" était valable du temps des
codes assembleur, voire à la rigueur en C.
En C++, il y a quand meme assez de nouvelles fonctionnalites permettant
de faire de l'application robuste. Et c'est d'ailleurs un critère
important en termes de qualité logicielle.

Dans le cas de problèmes conséquents et qui ne feraient pas planter
l'appli (par exemple un stack overflow qui aurait simplement corrompu des
données sans écraser esp, eip, ebp etc.), on peut considérer que
l'application est dans un état instable.
Hors, si l'assert est capable de déterminer que la donnée est corrompue,
une autre gestion d'erreur en est capable aussi.
Si c'est possible, pourquoi ne pas placer tout le code accédant à ces
données dans un try, avec une précond. "mes données ne sont pas
corrompues".
Si les données sont corrompues, le code ne sera pas exécuté, et une
erreur sera enregistrée, sans faire dérouler d'instructions à risque.
Cela permettra surtout de continuer d'autres traitements.
Encore une fois, ça dépend de la manière de coder.

Pourquoi complètement stopper un programme lorsque quelque chose ne se
passe pas bien?



Si ce qui ne se passe pas bien, c'est qu'on ne sait plus l'état du
programme, ni ce qu'il fait, il faut bien l'arrêter le plus vite
possible, avant qu'il ne fasse quelque chose de vraiment nuisible.




La encore, c'est discutable: tu veux ouvrir une socket, mais le sd est
négatif, et tu fais un assert. Résultat, ton appli a coupé en disant "Je
ne peux pas ouvrir la connexion", et a perdu les données à envoyer.
Le fait de lancer une exception permettra plus de souplesse par
l'exécution du code "catch", notamment la possibilité de sauvegarder les
données à envoyer pour une émission ultérieure, et ce après un certain
nombre de tentatives de reconnexion.
Maintenant, je suis d'accord sur le fait que certaines erreurs seront
trop critiques pour etre rattrapables. Cela dit, quel est le pourcentage
de cas de ce genre d'erreur?
Si les preconditions sont bien implémentées, l'erreur ne se produira meme
pas pendant le traitement, mais avant. Et je pense sincerement qu'il vaut
mieux qu'un composant attrape une exception et dise par exemple
"Traitement impossible, en attente du suivant", plutot que "Traitement
impossible, arret".

Ajoutez à cela une sauvegarde des données d'entrée lors de la gestion
d'erreur, et une compilation optimisée avec les symboles de debug dans
des fichiers séparés.
Lors d'un problème en production, il suffit généralement de regarder la
stacktrace pour voir ce qui s'est produit, et comment corriger le
problème.



Exactement. Et c'est ce que te donne l'assert (au moins sous Unix, mais
je crois qu'on peut en avoir pareil sous Windows). Avec un log des
entrées, évidemment, pour pouvoir réproduire l'erreur.




Grumpf, il me semblait qu'assert affichait une trace pour la seule frame
d'où il a été lancé, mais je dois me tromper ...
Je te parle plus de stack trace du genre:

2009/10/11-16:10:58 ; WARNING : Exception received in void
tModuleLoader<tObjectType>::OpenLib() [with tObjectType = Basic*] at ../
include/module_loader.h:332
2009/10/11-16:10:58 ; WARNING : Exception received in tObjectType
tModuleLoader<tObjectType>::oCreateObject(tParam, std::basic_string<char,
std::char_traits<char>, std::allocator<char> >) [with tParam = char,
tObjectType = Basic*] at ../include/module_loader.h:129
2009/10/11-16:10:58 ; ERROR : FATAL: Fatal exception received in int main
(int, char**) at module_loader.cpp:69 message on next line:
Exception thrown: Can't load the module A: A: cannot open shared object
file: No such file or directory

Ca tombe bien, cet exemple fait l'équivalent d'un assert, puisque le
programme finit par s'arreter. Tout dépend de l'importance de la
fonctionnalité ...
OK, il n'y a pas de probleme assez gros pour empecher les exceptions de
remonter, mais je le repete, je pense que cet exemple fait partie de 90%
des erreurs produites.

J'accorde le fait qu'il sera toujours nécessaire d'arriver à reproduire
le bug avec un test unitaire,



Disons qu'il ne faut pas faire la moindre modification dans les sources
du programme avant de pouvoir déclencher l'erreur dans un programme de
test. Qui en suite fera partie de tes tests de regression.



Ca va de soi.


mais ce n'est pas toujours le cas (si une frame de niveau supérieur est
corrompue, il se peut que le programme "lache" lors d'appels un peu
plus bas, ou après plusieurs retours ...



Oui, mais quelque chose à corrompu la frame de niveau supérieur. C'est
ça l'erreur qu'il faut chercher, et c'est pour ça qu'il faut un log des
entrées, pour pouvoir récréer l'état qui a déclenché l'erreur.




Dans ces cas là, en plus du log il faut surtout un dump du process, ce
qui sera exploitable avec un debugueur, pas avec du printf.

Sans compter que quand une stack est foirée, printf ne fait pas
toujours son boulot comme il faut.



Exactement. Et quand la stack est corrompue, rémonter une exception ne
march pas comme il faut.




Tout a fait. De toute facon, quand une stack est corrompue, il vaut mieux
ne pas attendre grand chose, et utiliser le débugueur.

[..,]
Ce que je remarque en me relisant, c'est que pour du petit bug, la
trace peut suffir, mais je ne pense pas dans tous les cas. Pour du
"bon" bug (SIGSEGV/SIGPIPE, problèmes de synchros entre threads,
freeze, etc.), le débugueur apporte un confort inégalé,



C'est justement ce genre de problèmes où le deboggueur ne marche pas.
Parce qu'il change les temps, et donc l'exécution du programme.



Sauf que le débugueur a la possibilité de s'attacher, et donc de tracer
ce qu'il se passe une fois que le problème a ou a eu lieu.
Avec les fichiers de débug séparés, on lance le meme binaire qu'en
production, et donc on se rapproche au maximum des conditions.
De toute facon, si tu fais tes tests hors production (ce qui est
normalement le cas), tu changes ton environnement, et donc
potentiellement les temps d'exécution avec.

Pour ma part, j'ai déjà repris des applications multithreadées faites par
d'autres <développeurs> (et complètement buguées), sans débugueur ça
n'aurait pas été possible. Tous les bugs corrigés faisaient parti des
catégories citées ci-dessus.

Cordialement,

Benjamin BOUCNIAUX.
Avatar
BOUCNIAUX Benjamin
Le Sat, 26 Dec 2009 03:36:06 +0100, Fabien LE LEZ a écrit :

On Fri, 25 Dec 2009 17:20:55 -0600, BOUCNIAUX Benjamin
:

quand je développe, je base toutes mes gestions d'erreur sur des
exceptions



Attention, quand tu lances une exception, pas mal de destructeurs sont
appelés. Par conséquent, lancer une exception présuppose que le
programme est dans un état assez stable pour ce faire. Si tu t'aperçois
qu'un de tes objets est dans un état incohérent, il vaut mieux arrêter
tout de suite le programme, sans appeler le destructeur de cet objet.



Tout dépend l'importance de l'incohérence ; un objet bien conçu peut
avoir comme pire incohérence de la donnée invalide, et devient donc
inutilisable (au sens qu'il ne fait que remonter des erreurs, pas qu'il
se gaufre lamentablement :) ). S'il est plus corrompu que ça et se
retrouve dans un état complètement instable, c'est qu'il a été codé avec
les pieds, et qu'il faut le retravailler voire le réécrire.
Si cet objet est absolument nécessaire dans l'application, il faudra la
stopper, je l'accorde.
S'il s'agit d'un objet concernant une fonctionnalité moindre, il pourra
etre plus judicieux de stopper simplement le traitement concerné avec un
message d'erreur, et reprendre la suite des traitements indépendants de
ce dernier.
En dehors de problèmes de stack, je ne vois pas trop quelles instabilités
peuvent faire planter les destructeurs, s'ils contiennent les bons
controles bien sur.

Et en plus, cette manière de stopper violemment un programme fonctionnera
"bien" sous Unix. Sous winchiotte, en deux semaines, la station n'aura
plus de mémoire :p
Avatar
Fabien LE LEZ
On Sat, 26 Dec 2009 08:00:56 -0600, BOUCNIAUX Benjamin
:

Et en plus, cette manière de stopper violemment un programme fonctionnera
"bien" sous Unix. Sous winchiotte, en deux semaines, la station n'aura
plus de mémoire



J'imagine que tu parles là de Windows 3.1, voire peut-être des
bricolages qu'étaient Windows 95, 98 et ME.
Heureusement, tous ceux-là font partie du passé. Windows NT (NT4,
2000, XP, etc.) sont de vrais OS (même si l'interface utilisateur est
parfois bâclée).
Avatar
Fabien LE LEZ
On Sat, 26 Dec 2009 07:48:39 -0600, BOUCNIAUX Benjamin
:

J'explique juste que je ne fais pas d'assert.
Que tu lances une exception, fasse un assert ou retourne un code
d'erreur, voire fasse un exit(), tu agis face à une erreur, peu importe
laquelle.



Il y a quand même deux types d'erreurs très différents :

- Une erreur de données en entrée. Ton programme doit (au mieux) se
démerder pour en informer l'utilisateur. Pas de problème, les
exceptions sont adéquates.

- Une erreur de programmation. Ton programme ne fonctionne pas, parce
que tu as oublié un détail quelconque. Essayer de continuer à tourner
n'a aucun sens (d'autant que la pile est peut-être corrompue) ; mieux
vaut s'arrêter tout de suite.

La encore, c'est discutable: tu veux ouvrir une socket, mais le sd est
négatif, et tu fais un assert.



Ben justement, là c'est une question de données incorrectes (l'OS
t'envoie une information "socket invalide").
Avatar
BOUCNIAUX Benjamin
Le Sat, 26 Dec 2009 15:17:08 +0100, Fabien LE LEZ a écrit :

On Sat, 26 Dec 2009 08:00:56 -0600, BOUCNIAUX Benjamin
:

Et en plus, cette manière de stopper violemment un programme
fonctionnera "bien" sous Unix. Sous winchiotte, en deux semaines, la
station n'aura plus de mémoire



J'imagine que tu parles là de Windows 3.1, voire peut-être des
bricolages qu'étaient Windows 95, 98 et ME. Heureusement, tous ceux-là
font partie du passé. Windows NT (NT4, 2000, XP, etc.) sont de vrais OS
(même si l'interface utilisateur est parfois bâclée).



Euh pas forcément, sous XP je suis régulièrement amené à rebooter mon
poste par manque de ressources, et ce meme après avoir fermé toutes les
applications (qui sont le plus souvent des produits MS ou des sessions
X) :]
Enfin ce débat n'est pas le but de ce thread.
Avatar
Fabien LE LEZ
On Sat, 26 Dec 2009 08:27:34 -0600, BOUCNIAUX Benjamin
:

Euh pas forcément, sous XP je suis régulièrement amené à rebooter mon
poste par manque de ressources, et ce meme après avoir fermé toutes les
applications



De quelles ressources parles-tu ?
Une fois qu'un processus est tué[*], il n'occupe plus ni mémoire, ni
processeur. Les fichiers et sockets sont également libérés.
À moins que tu aies un service dysfonctionnel ?


[*] (Bien évidemment, la présence ou non d'une fenêtre à l'écran n'est
qu'un piètre indicateur de l'état d'un processus.)
Avatar
BOUCNIAUX Benjamin
Le Sat, 26 Dec 2009 15:21:58 +0100, Fabien LE LEZ a écrit :

On Sat, 26 Dec 2009 07:48:39 -0600, BOUCNIAUX Benjamin
:

J'explique juste que je ne fais pas d'assert. Que tu lances une
exception, fasse un assert ou retourne un code d'erreur, voire fasse un
exit(), tu agis face à une erreur, peu importe laquelle.



Il y a quand même deux types d'erreurs très différents :

- Une erreur de données en entrée. Ton programme doit (au mieux) se
démerder pour en informer l'utilisateur. Pas de problème, les exceptions
sont adéquates.

- Une erreur de programmation. Ton programme ne fonctionne pas, parce
que tu as oublié un détail quelconque. Essayer de continuer à tourner
n'a aucun sens (d'autant que la pile est peut-être corrompue) ; mieux
vaut s'arrêter tout de suite.




Parfaitement d'accord, l'implémentation de la gestion dépend du type
d'erreur... Ce n'est pas ce que je cherchais à expliquer ici.
Néanmoins, dans ce que tu dis, quelque chose me turlupine :
Dans le cas d'un problème de stack corrompue, je vois deux cas possibles:

- Soit certains registres stockés dans la pile ont été corrompus, à ce
moment là je ne vois pas trop comment tu pourras t'en rendre compte avec
un assert (tu ne vas pas tester la différence entre bp et sp à chaque
entrée de fonction, ou le fait qu'ils n'aient pas été modifiés avant de
retourner, n'est-ce pas) ?
Cela dit, faire du stack overflow en C++, j'avoue qu'il faut le faire. Il
y a maintenant une floppée d'objets à disposition qui permettent de
limiter ces risques. Autant s'en servir.
Dans ce cas, de toute façon, souvent ip se fait bananer, le programme
segfault, et on sort son gdb magique.
Donc la détection de stack "trop" corrompue, c'est généralement l'OS qui
s'en charge, pas le programme.

- Soit seulement des données sont corrompues sans avoir modifié les
registres, et à ce moment le moyen de détecter cette erreur est de
vérifier les valeurs des variables stockées dans la pile. Ce qui revient
au final, à tester de la valeur, et donc revient au type d'erreur N°1 que
tu exposes. Ce qui reviendrait à faire de l'assert sur certains tests de
valeurs, qui ne seront pas *obligatoirement* dus à un problème de stack,
et donc déclencher de l'assert dans ton cas N°1.

La encore, c'est discutable: tu veux ouvrir une socket, mais le sd est
négatif, et tu fais un assert.



Ben justement, là c'est une question de données incorrectes (l'OS
t'envoie une information "socket invalide").



Donc par rapport à ce que tu dis précédemment, cela ne nécessitera pas
d'assert, mais une remontée d'erreur.
Dans ce cas, nous sommes d'accord.
Avatar
BOUCNIAUX Benjamin
Le Sat, 26 Dec 2009 15:54:18 +0100, Fabien LE LEZ a écrit :

On Sat, 26 Dec 2009 08:27:34 -0600, BOUCNIAUX Benjamin
:

Euh pas forcément, sous XP je suis régulièrement amené à rebooter mon
poste par manque de ressources, et ce meme après avoir fermé toutes les
applications



De quelles ressources parles-tu ?
Une fois qu'un processus est tué[*], il n'occupe plus ni mémoire, ni
processeur. Les fichiers et sockets sont également libérés. À moins que
tu aies un service dysfonctionnel ?


[*] (Bien évidemment, la présence ou non d'une fenêtre à l'écran n'est
qu'un piètre indicateur de l'état d'un processus.)



Je parle de RAM.

En pratique, je constate que malgré un kill massif sur la plupart des
process qui me semblent inutiles, la mémoire n'est pas libérée
correctement. J'entends par là que si je remet mon système avec les meme
processus que quand il a démarré, il y a une grande quantité de mémoire
qui reste allouée: au boot, je consomme moins de 100Mo de RAM. Au bout
d'une ou deux semaines, je passe sans problème au Go, meme si je ferme
les applications (au sens qu'il n'y a plus de process dans le taskmgr).
Etant donné que je ne me sers de mon poste de travail que pour de
l'administratif (les devs sont fait sur des serveurs attitrés), je
n'utilise que Outlook, Word, et un navigateur, il s'agit de produits
fournis par la meme société que l'OS, rien de "maison" ou d'exotique sur
le poste. Après, je n'ai pas vu leur code source :) (Ca me refait penser
au code source de W2K qui avait tourné, et qui présentait clairement des
bugs de type bof ou memory leak, connus par les dev, mais pas corrigés).
Donc ce n'est pas parce que c'est la branche NT que l'OS gère
parfaitement la mémoire.
Avec seulement ces programmes, je ne récupère jamais toute la mémoire qui
a été allouée.
Chez moi, je ne lance pas assez Windows, ni le laisse tourner assez
longtemps pour en tirer une conclusion.

Mais honnetement, avec un XP, tu ne t'es jamais retrouvé sans process
particulièr (ie. apres avoir tout killé car tu manques de ressources), et
avec un pc de 2Go de RAM qui swap?
Tu fais alors parti du petit ensemble de windowsien que je connais pour
qui tout roule. Pour ma part, je suis toujours emmerdé avec sur ce genre
de détails, et je constate assez régulièrement ne pas etre le seul dans
ce cas.
Avatar
James Kanze
On Dec 26, 1:48 pm, BOUCNIAUX Benjamin
wrote:
Le Sat, 26 Dec 2009 04:11:13 -0800, James Kanze a écrit :



> On Dec 25, 11:20 pm, BOUCNIAUX Benjamin hosting.com >
> wrote:
>> Le Mon, 30 Nov 2009 13:27:49 +0000, Marc Boyer a écrit :
>> > Le 30-11-2009, AG a écrit :
>> >> Je travaille dans une équipe de personnes pour lesquels
>> >> le débugger n'est pas l'outil qui tombe sous le sens
>> >> lorsqu'il s'agit de trouver les bugs des outils qu'ils
>> >> ont développé.



>> > Mais est-ce grave ? J'avoue ne quasiment jamais me servir
>> > de débugger. Je travaille surtout en
>> > précondition/invariant (assert) + tests unitaires, et
>> > éventuellement valgrind quand je soupsonne une corruption
>> > mémoire



>> > Et une fois le bug identifié, à grand coups de cerr +
>> > assert.



>> > D'autant que souvent, le bug n'apparait pas en mode débug ;-)



>> Pour ma part, je ne pense pas qu'une technique soit mieux
>> qu'une autre. Le tout, c'est d'avoir la capacité à trouver
>> son bug, quelque soit la manière. Partant de ce postulat,
>> pourquoi ne pas simplement utiliser l'équivalent de ces
>> différentes manières?



>> Je m'explique : quand je développe, je base toutes mes
>> gestions d'erreur sur des exceptions, y compris les
>> controles appliqués (valeurs acceptées, pointeurs non NULL,
>> retours de syscalls corrects, etc.).



> En somme, tu considères qu'une erreur de frappe de la part
> d'un utilisateur s'assimile à une violation d'un invariant
> dans le code.



J'explique juste que je ne fais pas d'assert.
Que tu lances une exception, fasse un assert ou retourne un
code d'erreur, voire fasse un exit(), tu agis face à une
erreur, peu importe laquelle.



Mon point était simplement qu'on entend beaucoup de choses
différentes sous le vocable « erreur ». Choses qui ont en
souvent une gravité différente. Certaines choses qui se produira
forcement, d'autres qui ne doivent se produira jamais. Certaines
qui ne sont pas grave pour le programme, d'autres qui implique
qu'il n'est plus capable de faire son travail. Je voulais être
certain que tu voulais dire que tu les loges toutes à la même
enseigne, malgré leurs grosses différences.

>> Pour chaque exception levée/reçue, j'ajoute une trace (via
>> une macro, désactivable donc) qui résulte en une stack
>> trace complète dans les logs. Ca fait un peu java, mais
>> c'est très clair. Au bout de la stack trace (cad au niveau
>> le plus haut de la gestion d'erreur, qui n'est pas
>> forcément main() ), j'affiche le message d'erreur qui est
>> remonté ; une manière élégante de voir d'où est venu le
>> bug, et en plus d'où venaient les appels à cette
>> fonctionnalité. Et cela permet également d'implémenter des
>> préconditions/invariants.



>> Personnellement, je trouve l'assert un peu brutal, et le
>> printf trop limitatif :)



> L'assert est brutal pour une raison : il ne sert que dans le
> cas des erreurs de programmation. L'assert ne peut pas
> déclencher si ton raisonement sur le code était correct.
> C'est donc que tu ne sais plus dans quel état se trouve le
> programme.



> Dans ce cas-là, chaque instruction que tu exécutes est une
> risque. Il faut en exécuter les moins possibles. Et donc, ne
> pas exécuter les destructeurs. (En général. Comme pour tout,
> il y a des trade-offs, et il y a des cas particuliers où la
> règle générale ne s'applique pas.)



Et dans ce cas, on se retrouve avec des programmes sans aucune
robustesse. Je veux bien que ce soit le cas sur des systèmes
embarqués, mais la plupart des applications sur lesquelles
j'ai travaillé ne doivent pas s'arreter comme ça.



Certes. Mais quand ils ne peuvent pas faire autrement. Ou quand
faire autrement risque de faire plus de dégats.

Je préfère un programme qui va remonter une erreur et
retourner dans un état initial tout seul, qu'un programme qui
va s'arreter tout seul.



Le problème, c'est que tu ne sais pas dans tel état il est. Tu
es donc loin d'être certain de pouvoir le remettre dans un état
initial.

En développant correctement, il est difficile d'avoir un
composant "instable". Soit on l'a dans son état stable, soit
on ne l'a pas du tout (la construction a échouée, ou lors
qu'il émet des erreurs, le composant de plus haut niveau le
réinitialise, et si rien n'est possible, à ce moment là, OK,
on envisage une résolution plus drastique. La philosophie
"j'ai une erreur, je coupe" était valable du temps des codes
assembleur, voire à la rigueur en C. En C++, il y a quand
meme assez de nouvelles fonctionnalites permettant de faire de
l'application robuste. Et c'est d'ailleurs un critère
important en termes de qualité logicielle.



En C++, comme en C, on n'est jamais à l'abri d'un pointeur
sauvage. En fait, justement à cause des fonctionnalités
supplémentaire de C++, par rapport à C, il y a davantage de
chances en C++ qu'un état incohérent d'un objet provient d'un
pointeur sauvage ailleurs, et non d'un problème logique autour
de l'objet même.

Dans le cas de problèmes conséquents et qui ne feraient pas
planter l'appli (par exemple un stack overflow qui aurait
simplement corrompu des données sans écraser esp, eip, ebp
etc.), on peut considérer que l'application est dans un état
instable.



Le problème de débordement de la pile est un autre problème, pas
forcément assimilable aux erreurs d'invarante ou des
préconditions. C'est une erreur en fait assez difficile de
gérer, dans la mesure que toutes les actions qu'on pourrait
vouloir faire (y compris abort) risque d'avoir aussi besoin de
la pile. Et que la structure de la pile risque d'être
incohérente au moment que ça se produit, ce qui veut dire que le
walkback d'une exception ne marcherait pas.

Hors, si l'assert est capable de déterminer que la donnée est
corrompue, une autre gestion d'erreur en est capable aussi.



Le problème n'est pas de déterminer que la donnée est corrompue.
Le problème, si on veut continuer, c'est de déterminer quelles
autres données sont peut-être aussi corrompues, et comment les
réparer. Ce qui veut dire déterminer pourquoi la donnée est
corrompue.

Si c'est possible, pourquoi ne pas placer tout le code
accédant à ces données dans un try, avec une précond. "mes
données ne sont pas corrompues". Si les données sont
corrompues, le code ne sera pas exécuté, et une erreur sera
enregistrée, sans faire dérouler d'instructions à risque.
Cela permettra surtout de continuer d'autres traitements.
Encore une fois, ça dépend de la manière de coder.



>> Pourquoi complètement stopper un programme lorsque quelque
>> chose ne se passe pas bien?



> Si ce qui ne se passe pas bien, c'est qu'on ne sait plus
> l'état du programme, ni ce qu'il fait, il faut bien
> l'arrêter le plus vite possible, avant qu'il ne fasse
> quelque chose de vraiment nuisible.



La encore, c'est discutable: tu veux ouvrir une socket, mais
le sd est négatif, et tu fais un assert.



Tu veux dire la valeur de retour de socket() ? Que socket()
renvoie -1, ce n'est pas une erreur de programmation, et ne doit
en aucun cas (ou au moins seulement dans des cas exceptionnels)
resulte en un appel à abort. Selon la logique du programme, une
exception serait souvent la meilleur solution. Ou un code de
retour -- tout dépend de la logique du programme. (Aussi,
évidemment, dans ce cas-là, un snapshot de la pile ne te sert à
rien.)

Résultat, ton appli a coupé en disant "Je ne peux pas ouvrir
la connexion", et a perdu les données à envoyer. Le fait de
lancer une exception permettra plus de souplesse par
l'exécution du code "catch", notamment la possibilité de
sauvegarder les données à envoyer pour une émission
ultérieure, et ce après un certain nombre de tentatives de
reconnexion.



Maintenant, je suis d'accord sur le fait que certaines erreurs
seront trop critiques pour etre rattrapables. Cela dit, quel
est le pourcentage de cas de ce genre d'erreur?



Je ne sais pas. Dans la pratique, je n'ai prèsque jamais vu un
assert qui se déclenche dans un code de production, une fois
passé tous les tests et toutes les revues. Si ce genre d'erreur
est fréquent, il y a un problème avec le processus de
développement.

Si les preconditions sont bien implémentées, l'erreur ne se
produira meme pas pendant le traitement, mais avant. Et je
pense sincerement qu'il vaut mieux qu'un composant attrape une
exception et dise par exemple "Traitement impossible, en
attente du suivant", plutot que "Traitement impossible,
arret".



Encore, ça dépend de l'erreur. Si l'erreur indique un état
« impossible » dans un programme correct, c'est une signe qu'on
ne sait pas ce que le programme est en train de faire. (Tu as
parlé d'un socket tout à l'heure. Une telle erreur pourrait bien
être le resultat d'un virus qui a infecté le programme.)

>> Ajoutez à cela une sauvegarde des données d'entrée lors de
>> la gestion d'erreur, et une compilation optimisée avec les
>> symboles de debug dans des fichiers séparés.
>> Lors d'un problème en production, il suffit généralement de
>> regarder la stacktrace pour voir ce qui s'est produit, et
>> comment corriger le problème.



> Exactement. Et c'est ce que te donne l'assert (au moins sous
> Unix, mais je crois qu'on peut en avoir pareil sous
> Windows). Avec un log des entrées, évidemment, pour pouvoir
> réproduire l'erreur.



Grumpf, il me semblait qu'assert affichait une trace pour la
seule frame d'où il a été lancé, mais je dois me tromper ...
Je te parle plus de stack trace du genre:

2009/10/11-16:10:58 ; WARNING : Exception received in void
tModuleLoader<tObjectType>::OpenLib() [with tObjectType = Basic*] at .. /
include/module_loader.h:332
2009/10/11-16:10:58 ; WARNING : Exception received in tObjectType
tModuleLoader<tObjectType>::oCreateObject(tParam, std::basic_string<char,
std::char_traits<char>, std::allocator<char> >) [with tParam = char,
tObjectType = Basic*] at ../include/module_loader.h:129
2009/10/11-16:10:58 ; ERROR : FATAL: Fatal exception received in int main
(int, char**) at module_loader.cpp:69 message on next line:
Exception thrown: Can't load the module A: A: cannot open shared object
file: No such file or directory



Encore, je ne suis sûr ici que pour Unix, mais sous Unix, un
assert te donne un core dump, et avec un core dump, le débogueur
te donne le stack trace. De tous les threads, si tu lui le
démandes gentillement. (Si je n'utilise pas beaucoup le
débogueur sur un programme live, je m'en sers volentiers pour
les post mortem.)

[...]
Dans ces cas là, en plus du log il faut surtout un dump du
process, ce qui sera exploitable avec un debugueur, pas avec
du printf.



Juste pour que les choses soient claires : ce n'est pas moi qui
a dit d'insérer des printf. S'il s'avère qu'on veut davantage
d'information lors de l'exécution (ce qui suppose qu'on sait
déjà réproduire l'erreur, et qu'on sait de quelles informations
on a besoin), je préconcise d'y ajouter des logs. Parce que si
tu en a eu besoin une fois, on peut supposer que tu en auras
besoin aussi à l'avenir.

>> Sans compter que quand une stack est foirée, printf ne fait
>> pas toujours son boulot comme il faut.



> Exactement. Et quand la stack est corrompue, rémonter une
> exception ne march pas comme il faut.



Tout a fait. De toute facon, quand une stack est corrompue, il
vaut mieux ne pas attendre grand chose, et utiliser le
débugueur.



Qui ne marche pas toujours non plus:-). Une fois que la pile est
corrompue, on a du travail sur les planches.

> [..,]
>> Ce que je remarque en me relisant, c'est que pour du petit
>> bug, la trace peut suffir, mais je ne pense pas dans tous
>> les cas. Pour du "bon" bug (SIGSEGV/SIGPIPE, problèmes de
>> synchros entre threads, freeze, etc.), le débugueur apporte
>> un confort inégalé,



> C'est justement ce genre de problèmes où le deboggueur ne
> marche pas. Parce qu'il change les temps, et donc
> l'exécution du programme.



Sauf que le débugueur a la possibilité de s'attacher, et donc de trac er
ce qu'il se passe une fois que le problème a ou a eu lieu.
Avec les fichiers de débug séparés, on lance le meme binaire qu'en
production, et donc on se rapproche au maximum des conditions.
De toute facon, si tu fais tes tests hors production (ce qui est
normalement le cas), tu changes ton environnement, et donc
potentiellement les temps d'exécution avec.



Aussi. De même que si tu actives un log qui est d'habitude
désactivé. Mais au moins sous les systèmes que je connais,
l'intervention du débogueur est bien plus lourd. Dans certaines
implémentations des watch point, le programme s'exécute en fait
en pas-à-pas, avec le débogueur qui intervient après chaque
instruction machine pour régarder la valeur. (Je crois que les
processeurs Intel ont des régistres spéciaux pour gerer les
watchpoint par hardware, et qu'un watchpoint n'est pas si cher
sur eux.)

Pour ma part, j'ai déjà repris des applications multithreadées
faites par d'autres <développeurs> (et complètement buguées),
sans débugueur ça n'aurait pas été possible. Tous les bugs
corrigés faisaient parti des catégories citées ci-dessus.



Effectivement, une des utilisations importantes d'un débogueur
sur un programme en cours d'exécution, c'est bien quand tu as
devant toi du code que tu n'as pas écrit, et qui n'est pas
documenté.

--
James Kanze