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

8 réponses

Avatar
James Kanze
On Dec 26, 2:54 pm, BOUCNIAUX Benjamin
wrote:
Le Sat, 26 Dec 2009 15:21:58 +0100, Fabien LE LEZ a écrit :



[...]
Cela dit, faire du stack overflow en C++, j'avoue qu'il faut le faire.



C++ supporte la récursion. Et éviter la possibilité d'un stack
overflow dans le cas d'un parseur à descente récursive, ce n'est
pas évident. (Je l'ai fait dans certains serveurs, mais il faut
dire que je me suis servi du code non portable pour le faire.)

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

On Dec 26, 2:54 pm, BOUCNIAUX Benjamin
wrote:
Le Sat, 26 Dec 2009 15:21:58 +0100, Fabien LE LEZ a écrit :



[...]
Cela dit, faire du stack overflow en C++, j'avoue qu'il faut le faire.



C++ supporte la récursion. Et éviter la possibilité d'un stack overflow
dans le cas d'un parseur à descente récursive, ce n'est pas évident. (Je
l'ai fait dans certains serveurs, mais il faut dire que je me suis servi
du code non portable pour le faire.)



En effet, je pensais plus à des problèmes de buffer overflow. Pour la
récursion, je te rejoins amplement, ta précision a son importance.

Cordialement,

Benjamin.
Avatar
Fabien LE LEZ
On Sat, 26 Dec 2009 07:48:39 -0600, BOUCNIAUX Benjamin
:

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



Imagine le code suivant :

class Machin
{
public:
void f()
{
SOCKET s= OuvrirSocket();
if (!EstValide (s))
{
throw quelque_chose;
}
g (s);
}

private:
void g (SOCKET s)
{
assert (EstValide (s));
faire_quelque_chose;
}
};

Dans f(), j'ouvre un socket. Suivant l'état de l'OS, ça peut échouer,
auquel cas j'en informe l'appelant via une exception.

En revanche, g() s'attend à recevoir un socket valide -- c'est à
l'appelant de s'en assurer. Par conséquent, si s n'est pas valide, il
y a une erreur de programmation quelque part. Par exemple, j'ai appelé
g() depuis une autre fonction qui ne fait pas les tests nécessaires.
Avatar
BOUCNIAUX Benjamin
Le Sun, 27 Dec 2009 00:39:33 +0100, Fabien LE LEZ a écrit :


Dans f(), j'ouvre un socket. Suivant l'état de l'OS, ça peut échouer,
auquel cas j'en informe l'appelant via une exception.



Ok.


En revanche, g() s'attend à recevoir un socket valide -- c'est à
l'appelant de s'en assurer. Par conséquent, si s n'est pas valide, il y
a une erreur de programmation quelque part. Par exemple, j'ai appelé g()
depuis une autre fonction qui ne fait pas les tests nécessaires.



Et quel est l'avantage par rapport à une remontée d'exception avec
rigoureusement le meme test? D'autant que le premier test l'aura
déclenché si la socket n'est pas valide.
Le fait de faire remonter une exception ne placera pas le programme dans
un état instable ou indéterminable.
Avatar
Fabien LE LEZ
On Sun, 27 Dec 2009 05:42:18 -0600, BOUCNIAUX Benjamin
:

En revanche, g() s'attend à recevoir un socket valide [...]



Et quel est l'avantage par rapport à une remontée d'exception avec
rigoureusement le meme test?



D'autant que le premier test l'aura
déclenché si la socket n'est pas valide.



Ben justement, c'est là le truc : dans mon esprit, g() reçoit un
socket valide, puisque f() l'a bien vérifié. Si ce n'est pas le cas,
c'est que je me plante complètement sur le fonctionnement du
programme.

[...] ne placera pas le programme dans
un état instable ou indéterminable.



Justement, ici, si le assert() échoue, le programme est dans un état
indéterminé, puisque moi (le programmeur), je n'ai aucune idée de ce
qu'il fait effectivement.
Avatar
BOUCNIAUX Benjamin
Le Sun, 27 Dec 2009 13:16:42 +0100, Fabien LE LEZ a écrit :

On Sun, 27 Dec 2009 05:42:18 -0600, BOUCNIAUX Benjamin
:

En revanche, g() s'attend à recevoir un socket valide [...]



Et quel est l'avantage par rapport à une remontée d'exception avec
rigoureusement le meme test?



D'autant que le premier test l'aura
déclenché si la socket n'est pas valide.



Ben justement, c'est là le truc : dans mon esprit, g() reçoit un socket
valide, puisque f() l'a bien vérifié. Si ce n'est pas le cas, c'est que
je me plante complètement sur le fonctionnement du programme.



Je suis d'accord la dessus.


[...] ne placera pas le programme dans un état instable ou
indéterminable.



Justement, ici, si le assert() échoue, le programme est dans un état
indéterminé, puisque moi (le programmeur), je n'ai aucune idée de ce
qu'il fait effectivement.



A ma sauce, voilà le code que j'aurais écris:

class Machin {
public:
void f(){
try {
// OuvrirSocket() peut retourner une exception en
// cas de probleme d'ouverture
SOCKET s = OuvrirSocket();
g(s);
} catch(...){
// Log ...
throw;
}
}


void g(SOCKET s){
if(!EstValide(s)){
// Log ...
throw CriticalException("La socket est invalide");
}

// Faire quelque chose
}
};

Le principe est toujours de passer une socket valide, sauf qu'une
exception pete jusqu'à l'appelant de f(), qui jugera de la criticité de
l'erreur (bien sur, il y a plusieurs types d'exceptions selon les
erreurs).
Avatar
Fabien LE LEZ
On Sun, 27 Dec 2009 06:41:28 -0600, BOUCNIAUX Benjamin
:

throw CriticalException("La socket est invalide");



Le message est incorrect.
Un message correct serait: "g() a reçu un paramètre invalide ; le
programme est donc bogué. L'arrêter immédiatement, corriger le bug,
recompiler."

Note par ailleurs que même si, pour une raison ou une autre,
l'assert() du C++ ne te plaît pas, mieux vaut définir le tien :

#define my_assert(x) { if (!(x)) Erreur (#x, __FILE__, __LINE__); }

Ainsi, tu peux écrire :

private:
void g (SOCKET s)
{
my_assert (EstValide (s));
//faire_quelque_chose;

En effet, une ligne assert() sert également de documentation : ici,
elle indique clairement que le code appelant est responsable de donner
à g() un socket valide.
Avatar
BOUCNIAUX Benjamin
Le Mon, 28 Dec 2009 03:02:18 +0100, Fabien LE LEZ a écrit :

On Sun, 27 Dec 2009 06:41:28 -0600, BOUCNIAUX Benjamin
:

throw CriticalException("La socket est invalide");



Le message est incorrect.
Un message correct serait: "g() a reçu un paramètre invalide ; le
programme est donc bogué. L'arrêter immédiatement, corriger le bug,
recompiler."



Oui enfin on se moque un peu du message dans ce cas, il s'agit d'un bete
exemple.
Le fait est que le composant aura la meme reaction, à savoir avertir le
programmeur, sauf que ce dernier recevra une exception au lieu de voir
son programme planter.
Faut pas chipoter non plus, hein!


Note par ailleurs que même si, pour une raison ou une autre, l'assert()
du C++ ne te plaît pas, mieux vaut définir le tien :

#define my_assert(x) { if (!(x)) Erreur (#x, __FILE__, __LINE__); }

Ainsi, tu peux écrire :

private:
void g (SOCKET s)
{
my_assert (EstValide (s));
//faire_quelque_chose;

En effet, une ligne assert() sert également de documentation : ici, elle
indique clairement que le code appelant est responsable de donner à g()
un socket valide.



Donc, un bloc tel que :

if(!EstValide(s)){
// Log ...
throw CriticalException("La socket est invalide");
}

ne le fait pas?
Encore une fois, faut pas chipoter :p
Le programmeur qui implémentera la fonction en donnant une mauvaise
socket s'en rendra très rapidement compte ...