Ours & Hippy — le blog Ours & Hippy ourshippy@huoc.org tag:blog.huoc.org,2009:atom 2010-03-01T23:29:26+01:00 tag:blog.huoc.org,2009:bluestorm/3 Détection automatique de bugs 2009-09-04T22:12:55+02:00 2010-03-01T23:29:26+01:00 Quentin Pradet (Cygal) <div class="Edito"><p>L’<i>Ours &amp; Hippy crew</i> vous présente son second billet <i>feat. Cygal</i>. Sous le regard hippie de blueblue aux commandes des platines d’édition — que l’on reconnaît grâce au manque d’originalité de son titre frigido-sérieux — l’artiste nous a concocté en direct de derrière le studio un nouvel article… <br /> Qui nous parle du chant des insectes déviants, <br /> Leurs pesticides heuristiques, <br /> Et ses recettes hygiéniques, <br /> À inculquer absolument aux ptits enfants ! </p><p>—minh </p></div><p>Cette réaction traite d’un point qui va finir par ressembler à une obsession de ma part<sup>☃</sup> : la correction de bugs. Plus précisément, ce papier traite de la détection automatique de bugs. Le but est d’obtenir du programme assez d’informations pour pouvoir remarquer des comportements anormaux. </p><div class="Notes"><p>☃ : Alors que pas du tout, je suis encore à la recherche d’un fétiche. </p></div><p>C’est d’une certaine manière ce qu’apporte le typage à la compilation : les types apportent suffisament d’informations pour pouvoir détecter un certain nombre d’erreurs avant qu’elles n’apparaissent à l’exécution. L’analyse qui est faite ici est aussi statique, et se base sur un principe général qui peut s’appliquer à un nombre étonnament varié d’erreurs possibles. Les auteurs du papier ont ainsi détecté des centaines de bugs dans les noyaux Linux et BSD, principalement dans des drivers pas toujours maintenus. </p><p>Des langages comme OCaml sont capables <em>d’inférer</em> les types utilisés dans un programme, c’est-à-dire les deviner à partir du code. Le principe est ici le même, sauf qu’on essaie d’inférer du <em>sens</em> pour nous aider à trouver des erreurs. </p><h3>Présentation du papier </h3><p>Les exemples donnés ici sont directement tirés du papier : <a class="extern" href="http://www.stanford.edu/~engler/deviant-sosp-01.pdf">Bugs as deviant behavior (PDF, 16 pages)</a>. </p><p>Le principe est donc d’inférer le maximum d’informations possible dans chaque fonction, et d’essayer de détecter les contradictions présentes. L’existence d’une contradiction entre deux lignes montre qu’il y a forcément une erreur, même si le système ne peut pas forcément savoir de quelle ligne il s’agit. </p><p>Le montant d’informations qu’il est possible d’obtenir à partir d’un code source automatiquement est non seulement limité, mais on doit parfois se baser sur des informations <em>peut-être fausses</em>. C’est pourquoi j’ai séparé la présentation en deux courtes parties : </p><ul><li>une sur ce qui est appelé les « MUST beliefs » dans le papier (on est certain qu’il y a une erreur) ; </li><li>et une autre sur les « MAY beliefs » (il y a très probablement une erreur). </li></ul><h4>MUST beliefs </h4><p>Ainsi, prenons un exemple simple, tiré d’une version du noyau Linux aussi vieille que le papier. </p><pre><code>/* 2.4.7 drivers/char/mxser.c */ int mxser_write(struct tty_struct *tty, ...) { struct mxser_struct *info = tty-&gt;driver_data; unsigned long flags; if (!tty || !info-&gt;xmit_buf) return(0); ... </code></pre><p>Le bug ici vient du déréférencement du pointeur <code>tty</code> alors qu’il pourrait valoir <code>NULL</code>. C’est quelque chose qui peut arriver assez vite si l’on ni porte pas attention, par exemple pendant l’évolution d’un code source. Regardons comment notre programme fonctionne dans ce cas précis. </p><dl><dt><code>*info = tty-&gt;driver_data</code></dt><dd>Le pointeur <code>tty</code> est déréférencé, ce qui veut dire que le programmeur estime qu’il ne peut pas être nul. </dd><dt><code>if (!tty || !info-&gt;xmit_buf)</code></dt><dd>La nullité du pointeur <code>tty</code> est testée, ce qui veut dire que le programmeur estime qu’il peut être nul. </dd></dl><p>On a donc une contradiction : l’une des deux lignes pose problème, ce qui être un bug. Et en effet si le pointeur peut être nul, il est dangereux de le déréférencer. </p><p>En pratique, il y a beaucoup trop de faux positifs. Par exemple, les macros vérifient souvent que leur arguments ne sont pas nul, et une fois que leur expansion est faite, on ne fait pas la différence avec le code normal. Les auteurs ont donc modifié leur programme (basé sur GCC apparement) pour annoter les expansions de macros, et ne pas les traiter pendant l’algorithme. D’autres problèmes ont été traités pour pouvoir montrer de réelles erreurs, je vous laisse lire le papier si ça vous intéresse. </p><h4>MAY beliefs </h4><p>Dans l’exemple sur les pointeurs donné ci-dessus et quelques autres, le programme est capable de dire avec certitude qu’il y a un problème. Cependant, ce n’est souvent pas possible de l’affirmer avec certitude : les informations inférées lors de la lecture du code ne sont pas forcément exactes. Ce n’est pas une raison de les ignorer, mais il faut les traiter avec prudence. L’idée est alors de rassembler un maximum de cas d’utilisations similaires, et d’en déduire les cas érronés. Par exemple, si une fonction est utilisée 999 fois d’une certaine manière, et autrement ailleurs, on peut supposer que le cas seul est érroné. </p><p>Donnons encore une fois un exemple pour expliquer cette pratique. </p><pre><code>lock lk; // Lock int a, b; // Variables potentially // protected by l void foo() { lock(lk); // Enter critical section a = a + b; // MAY: a, b protected by l unlock(lk); // Exit critical section b = b + 1; // MUST: b not protected by l } void bar() { lock(lk); a = a + 1; // MAY: a protected by l unlock(lk); } void baz() { a = a + 1; // MAY: a protected by l unlock(lk); b = b - 1; // MUST: b not protected by l a = a / 5; // MUST: a not protected by l } </code></pre><p>Supposons que <code>lock()</code> soit un appel système qui existe, et qui bloque l’accès aux données du noyau.<sup>‽</sup> On suppose donc que toute variable modifiée entre deux <i>locks</i> <em>peut</em> être concernée par ce <i>lock</i>. Une fois que l’on sait à peu près quelles sont les chances estimées que la variable soit concernée par le <i>lock</i> <code>lk</code>, on peut dire à l’utilisateur à quel point une utilisation en dehors du <i>lock</i> peut être dangereuse. </p><div class="Notes"><p>‽ : Pour information, le <i>Big Kernel Lock</i> a été créé, puis est enlevé <a class="extern" href="http://lwn.net/Articles/86859/">petit</a> à <a class="extern" href="http://lwn.net/Articles/281938/">petit</a>. </p></div><p>Ici, la fonction <code>foo</code> nous permet de dire que <code>a</code> est sûrement concerné par le <i>lock</i> <code>lk</code>, et que <code>b</code> peut l’être (mais moins de chance, vu que l’accès ne se fait qu’en lecture, non ?). La fonction <code>bar</code> montre que <code>a</code> est la seule variable utilisée dans le <i>lock</i>, donc il commence à y avoir de grandes chances que ce soit effectivement <code>a</code> qui est protégé. Dans <code>baz</code>, <code>a</code> est utilisé en dehors d’un <i>lock</i>, donc c’est certainement un bug qu’il faut corriger ! </p><p>Cette manière d’essayer de deviner des informations plausibles pour ensuite essayer de détecter des erreurs a elle aussi de nombreuses applications décrites dans le papier. Certaines ne sont que des idées placées en plein milieu sans aucun rapport : d’ailleurs le style du papier laisse à désirer, le tout n’est pas très structuré, soyez avertis si vous comptez le lire (sachez aussi que je ne prétends pas faire mieux :). Une de ces idées consiste en la vérification de la cohérence d’une API proposée d’une version stable à une autre. Si on compare les croyances obtenues par notre système entre deux versions stables, elle doivent être exactement les mêmes. Sinon cela implique une possibilité de casser les applications utilisant l’API en question. Cela peut être aussi trouvé via l’utilisation de tests (potentiellement unitaires), mais c’est plus compliqué à mettre en place : un effort de la part du programmeur est impératif, et un test doit être exécuté, contrairement à l’analyse qui est ici statique. </p><p>N’hésitez pas à lire le papier si ces quelques lignes ne vous ont pas endormies : un certain nombre d’autres techniques intéressantes pour par exemple : </p><ul><li>inférer le maximum d’information d’un code (notamment avec le nom des fonctions) ; </li><li>vérifier que la gestion d’erreurs d’un programme est efficace ; </li><li>que les pointeurs sont utilisés correctement dans le kernel (différence entre <i>kernel pointer</i> et <i>user pointer</i>) ; </li><li>détecter des fuites mémoires ; </li><li>etc. </li></ul><h3>Enseignements à tirer ? </h3><p>Pour m’être amusé pendant tout un été à corriger des bugs, je commence à avoir une idée des pratiques à éviter ou au contraire à encourager pour éviter l’apparition de bugs. Le principe de base de ces pratiques est d’apporter le maximum d’informations à celui qui va lire le code (sans parler des commentaires). Ainsi, si un programme est capable de penser que telle façon de faire quelque chose est une erreur, c’est que certainement un humain qui va passer par là sans savoir comment fonctionne le code va aussi penser à une erreur, ou du moins ne va pas comprendre au premier abord ce qui se passe. </p><p>C’est l’origine de plusieurs pratiques couramment encouragées en programmation. Citons ici <a class="extern" href="http://en.wikipedia.org/wiki/Keep_it_Simple,_Stupid#En_informatique">KISS</a> et la minimisation des effets de bords. La première permet à tout le monde de mieux comprendre un code, de mieux comprendre ce qu’il fait exactement pour pouvoir l’utiliser et le modifier la conscience tranquille. </p><p>La seconde permet de pouvoir utiliser une fonction sans avoir peur qu’elle casse autre chose ailleurs. Ça permet aussi d’utiliser de la composition de fonction sans risque. Il y a beaucoup de choses à dire sur cette pratique, mais retenez que l’intérêt principal est de ne pas cacher les canaux par lesquels transite l’information pour rendre le code le plus compréhensible et facile à modifier possible. Moins le programmeur qui lit le code a de chances d’être surpris, moins il y a de chances d’avoir des problèmes par la suite. </p><p>Ainsi, certains <a class="extern" href="http://fr.wikipedia.org/wiki/Patron_de_conception">'design patterns'</a> ont beau résoudre des problèmes, ils ajoutent souvent une couche d’abstraction qu’il faut pouvoir traverser pour comprendre exactement ce qui se passe. Cette couche empêche l’échange d’informations entre deux parties du code qui intéragissent pourtant directement, ce qui provoquera forcément des bugs, qui auraient pu être détectés autrement. De la même manière, trop de généricité empêche une vision claire du code qu’on a sous les yeux. Cela a causé certains problèmes qui sont devenus tellement fréquents qu’il a fallu inventer d’<a class="extern" href="http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization">autres</a> <i>design patterns</i> pour les éviter. L’important est d’en être conscient et d’ajouter d’autres mesures de protection quand le bon sens où le compilateur ne peut plus aider à filtrer les erreurs. </p><p>De la même manière, utiliser son langage de manière idiomatique et rester consistant dans son formattage, sa façon de penser et les techniques utilisées est important. Cela permet au lecteur de retirer beaucoup d’informations rapidement, et donc de pouvoir se concentrer sur les points importants. </p><p>Si le programme a cru détecter une erreur qui n’était en fait qu’un faux positif, il reste néanmoins important d’essayer de corriger le code : tous les humains qui le liront risqueront de faire la même erreur et de ne pas comprendre ce qui se passe. Dans ce sens là, les exemples présents dans le papier sont instructifs : se sont des erreurs qui peuvent sembler anodines mais qui restent importantes. </p>