I. Introduction

La chose la plus importante que j'ai faite en tant que programmeur ces dernières années est de poursuivre énergiquement l'analyse statique de code. Encore plus précieux que les centaines de bugs sérieux qu'elle m'a évités, c'est le changement de mentalité à propos de ma façon de voir la fiabilité logicielle et la qualité du code.

Il est important de dire d'emblée que la qualité n'est pas tout, et reconnaître ce fait n'est pas une sorte de faute morale. La « valeur » est ce que vous essayez de produire et la « qualité » n'est qu'un aspect de celle-ci, entremêlé avec le coût, les fonctionnalités et d'autres facteurs. Il y a eu de nombreux titres très appréciés et ayant obtenu un immense succès qui étaient remplis de bogues et plantaient fréquemment ; adopter le même style de développement pour un logiciel destiné à une navette spatiale et des jeux vidéo serait idiot. Pourtant, la qualité reste importante.

J'ai toujours pris soin d'écrire du bon code, l'une de mes importantes motivations internes est celle de l'artisan et j'ai toujours envie de m'améliorer. J'ai lu des tas de livres aux titres de chapitres austères comme « Politiques, normes et plans qualité », et mon travail avec Armadillo Aerospace m'a mis en contact avec le monde très différent du développement de logiciel critique.

Il y a dix ans, lors du développement de Quake 3, j'ai acheté une licence pour PC-lint et essayé de l'utiliser – l'idée de pointer automatiquement les erreurs dans mon code avait l'air excellente. Cependant, l'utiliser en ligne de commande et passer au crible les monceaux de commentaires qu'il produit n'a pas réussi à me convaincre, et je l'ai abandonné assez rapidement.

Le nombre de programmeurs et la taille du code ont tous les deux augmenté d'un ordre de grandeur depuis et le langage d'implémentation est passé du C au C++, ce qui constitue un terreau d'autant plus fertile pour les erreurs logicielles. Il y a quelques années, après avoir lu un certain nombre d'articles de recherche sur l'analyse de code statique moderne, j'ai décidé de voir comment les choses avaient changé dans la décennie depuis que j'avais essayé PC-lint.

À ce stade, nous compilions au niveau de warning 4 en désactivant seulement quelques warnings très spécifiques, et le mode warning-as-error forçait les programmeurs à s'y conformer. Malgré la présence de quelques tronçons de code poussiéreux ayant accumulé des années de cochonneries, la plupart du code était assez moderne. Nous pensions avoir une assez bonne base de code.

II. Coverity

Au départ, j'ai pris contact avec Coverity et signé pour une période d'essai. C'est un logiciel sérieux, avec des coûts de licence basés sur le nombre total de lignes de code et nous nous sommes retrouvés avec une estimation à cinq chiffres. Quand ils ont présenté leur analyse, ils ont dit que notre base de code était l'une des plus propres de cette taille qu'ils avaient vu (peut-être qu'ils disent cela à tous les clients pour qu'ils se sentent de bonne humeur), mais ils ont présenté un ensemble d'une centaine de problèmes qui avaient été identifiés. C'était très différent de l'ancien PC-lint. Le ratio signal/bruit était très élevé - la plupart des problèmes soulevés étaient du code manifestement erroné qui pouvait avoir des conséquences graves.

Cela nous a ouvert les yeux, mais le coût était assez élevé pour qu'il nous donne à réfléchir. Il nous restait plus qu'à espérer que le nombre de nouvelles erreurs que nous allions introduire ne soit pas trop important avant de livrer le jeu.

III. Microsoft /analyze

Je me serais probablement convaincu de finalement acheter Coverity, mais alors que j'étais encore en train d'en débattre, Microsoft préempta le débat en intégrant leur fonctionnalité /analyze dans le SDK 360. /analyze était auparavant disponible pour la version haut de gamme, ridiculement chère de Visual Studio, mais il était maintenant disponible pour tous les développeurs 360 sans supplément. Mon interprétation est que Microsoft estime que la qualité des jeux sur la 360 a plus d'impact que la qualité des applications Windows.

Techniquement, l'outil de Microsoft effectue seulement une analyse locale, il devrait donc être inférieur à l'analyse globale de Coverity, mais il a suffi de l'activer pour qu'il déverse une montagne d'erreurs, beaucoup plus que ce que Coverity avait signalé. Certes, il y avait beaucoup de faux positifs, mais il y avait aussi beaucoup de choses vraiment, vraiment effrayantes.

J'ai progressé lentement dans le code, en corrigeant en premier lieu mon code personnel, puis le reste du code système, puis le code du jeu. J'y travaillais pendant des bouts de temps libre, de sorte que le processus s'est étendu sur deux ou trois mois. L'un des avantages secondaires de cette longue période est d'avoir montré de manière concluante qu'il signalait des choses très importantes - durant ce temps il y avait eu une chasse au bug épique, mobilisant de nombreux programmeurs pendant de nombreux jours et qui a fini par être attribuée à quelque chose que /analyze avait signalé, mais que je n'avais pas encore corrigé. Il y a eu plusieurs autres cas, moins dramatiques, où le débogage a conduit directement à quelque chose déjà signalé par /analyze. C'était des problèmes réels.

Finalement, j'ai eu tout le code utilisé pour générer l'exécutable 360 compilant sans warnings avec /analyze, je l'ai alors établi comme comportement par défaut pour les builds 360. Chaque programmeur travaillant sur la 360 a ensuite vu son code analysé à chaque fois qu'il lançait un build, de sorte qu'il remarquait par lui-même les erreurs qu'il venait de commettre, plutôt que ce soit moi qui les corrige silencieusement plus tard. Cela ralentissait la compilation quelque peu, mais /analyze est de loin l'outil d'analyse le plus rapide avec lequel j'ai travaillé, et il en vaut tellement la peine.

Nous avons eu une période où, sur l'un des projets, l'option d'analyse statique fut accidentellement désactivée pendant quelques mois, et quand je l'ai remarquée et réactivée, il y avait eu des tas de nouvelles erreurs introduites dans l'intervalle. De même, les programmeurs travaillant seulement sur PC ou PS3 archivaient du code défectueux et ils ne le réalisaient pas jusqu'à ce qu'ils obtiennent un rapport « build 360 échoué » par mail. C'était des démonstrations que le processus normal de développement produit constamment ces classes d'erreurs et /analyze nous avait effectivement protégé de nombre d'entre elles.

Bruce Dawson a blogué sur le travail avec /analyze un certain nombre de fois : code reliability.

IV. PVS-Studio

Parce que nous utilisions /analyze uniquement sur le code pour 360, nous avions encore beaucoup de codes non couverts par l'analyse – les codes spécifique pour PC et PS3, ainsi que tous les utilitaires tournant uniquement sur PC.

L'outil suivant sur lequel j'ai porté mon attention était PVS-Studio. Il s'intègre bien avec Visual Studio, et possède un mode de démonstration pratique (essayez-le !). Par rapport à /analyze, PVS-Studio est douloureusement lent, mais il a détecté un certain nombre d'autres erreurs importantes, même dans du code qui était déjà complètement propre avec /analyze. En plus de détecter des erreurs logiques, PVS-Studio détecte aussi un certain nombre de modèles communs d'erreurs de programmation, même si ça reste du code tout à fait raisonnable. Cela garantit de produire des faux positifs, mais que je sois damné si nous n'avions pas des instances de ces patterns d'erreurs communs nécessitant des corrections.

Il y a un certain nombre de bons articles sur le site de PVS-Studio, la plupart avec des exemples de codes tirés de projets open source montrant exactement le genre de choses que l'on trouve. J'ai envisagé d'ajouter des exemples représentatifs d'avertissements d'analyse de code dans cet article, mais il y a déjà des exemples mieux documentés présentés là-bas. Allez les voir, et ne vous moquez pas en pensant : « jamais je n'écrirais ça ! ».

V. PC-lint

Enfin, je suis retourné à PC-lint, couplé avec Visual Lint pour intégration dans l'IDE. Dans la grande tradition Unix, il peut être configuré pour faire à peu près n'importe quoi, mais ce n'est pas très amical, et on est en général loin du « clé en main ». J'ai acheté un pack de cinq licences, mais cela a été si problématique qu'il me semble que tous les autres développeurs ayant essayé ont abandonné. La flexibilité présente des avantages – j'ai pu le configurer pour analyser l'ensemble de notre code spécifique à la plate-forme PS3 , mais ce fut un travail pénible.

Une fois de plus, même dans du code qui avait été nettoyé à la fois par /analyze et PVS-Studio, de nouvelles erreurs significatives ont été trouvées. J'ai fait un réel effort pour nettoyer notre base de code avec Lint, mais je n'ai pas réussi. Je l'ai fait pour le code système, mais me suis essoufflé face à tous les rapports dans le code du jeu. J'ai trié en m'occupant en priorité des classes de rapports qui m'inquiétaient le plus, et en ignorant l'essentiel des rapports qui étaient plus d'ordre stylistique ou concernant des soucis potentiels.

Essayer de moderniser une base de code importante pour être propre aux niveaux maximums offerts par PC-lint est probablement futile. J'ai fait un peu de programmation « feu vert » où j'ai servilement traité un par un chaque commentaire Lint, mais c'est un ajustement que la plupart des programmeurs expérimentés C/C++ ne vont pas vouloir faire. J'ai encore besoin de passer un peu de temps à essayer de déterminer le bon ensemble de warnings à activer pour permettre de tirer le meilleur profit de PC-lint.

VI. Discussion

J'ai appris beaucoup de choses grâce à ce processus. Je crains que certaines de ces choses ne soient pas facilement transmissibles. Et que sans parcourir personnellement des centaines de rapports, en très peu de temps et en ressentant encore et encore ce nœud dans l?estomac, « tout est OK » ou « ce n'est pas si mal » seront les réponses par défaut.

La première étape est d'admettre sans réserve que le code que vous écrivez est truffé d'erreurs. C'est une pilule amère à avaler pour beaucoup de gens, mais sans elle, la plupart des propositions de changement seront vues avec irritation voire franche hostilité. Vous devez chercher à avoir des critiques de votre code.

L'automatisation est nécessaire. Il est fréquent d'avoir une sorte de satisfaction suffisante en entendant des récits de faillites retentissantes des systèmes automatiques, mais pour chaque échec de l'automatisation, les échecs de l'homme sont légions. Les exhortations à « écrire du meilleur code », les projets de revue de code, de programmation en binôme, et ainsi de suite, ne sont tout simplement pas suffisants, en particulier dans un environnement avec des dizaines de programmeurs travaillant sur des délais très courts. La valeur qu'il y a à capturer même le plus petit sous-ensemble d'erreurs accessibles à l'analyse statique à tous les coups est immense.

J'ai remarqué qu'à chaque fois que PVS-Studio était mis à jour, il trouvait quelque chose dans notre base de code avec les nouvelles règles. Cela semble vouloir dire que si vous avez une base de code suffisamment grande, toute classe d'erreur qui est syntaxiquement légale existe probablement là-dedans. Dans un grand projet, la qualité du code est tout aussi statistique que les propriétés physiques des matériaux – des défauts existent partout, on ne peut qu'espérer minimiser l'impact qu'ils ont sur les utilisateurs.

Les outils d'analyse travaillent avec une main attachée dans le dos, en étant obligés de déduire des informations à partir de langages qui ne fournissent pas nécessairement ce qu'ils veulent, et en faisant généralement des hypothèses très conservatrices. Vous devez collaborer autant que possible – favorisez l'indexation plutôt que l'arithmétique de pointeurs, essayez de garder votre graphe d'appel dans un seul fichier source, utiliser les annotations explicites, etc. Tout ce qui n'est pas clair pour un outil d'analyse statique n'est probablement pas clair non plus pour vos collègues programmeurs. Le dédain classique du hacker pour la rigueur et les contraintes est une vision à court terme – les besoins des gros projets, ayant une longue durée de vie et de multiples développeurs sont simplement différents du travail rapide que vous faites pour vous-même.

Les pointeurs NULL sont la plus grande source d'erreur en C/C++, du moins dans notre code. L'utilisation duale d'une valeur unique à la fois comme un flag et comme une adresse provoque un nombre incroyable de problèmes fatals. En C++ les références doivent être préférées par rapport aux pointeurs chaque fois que c'est possible. Bien qu'une référence ne soit « en réalité » qu'un pointeur, elle possède le contrat implicite de ne pas être NULL. Faites le check du NULL au moment de transformer un pointeur en référence et vous pourrez par la suite ignorer le problème. Il y a de nombreux patterns profondément enracinés dans la programmation de jeu vidéo qui sont tout simplement dangereux, mais je ne suis pas sûr de savoir comment migrer en douceur loin de tout ces check de NULL.

Les erreurs de formatage de printf étaient la deuxième plus grande source d'erreur dans notre base de code, accentuées par le fait que le passage d'une idStr au lieu d'un idStr::c_str() entraîne presque toujours un crash, cependant annoter toutes nos fonctions variadiques avec les annotations de /analyze pour que les types soient correctement contrôlés a tué ce problème pour de bon. Il y avait des dizaines d'erreurs de ce style, cachées dans des messages de warnings riches en enseignement qui se transformaient en crash quand certaines conditions inhabituelles activaient le chemin d'exécution, ce qui est aussi un commentaire sur la façon dont la couverture du code de nos tests en général faisait défaut.

Un grand nombre des erreurs graves rapportées sont dues à des modifications dans du code longtemps après qu'il a été écrit. Un motif d'erreur incroyablement courant est d'avoir un bout de code en parfait état qui vérifie un pointeur NULL avant de faire une opération, mais une modification du code plus tard change de sorte que le pointeur est utilisé à nouveau sans vérifier. Examiné séparément du contexte, ceci peut être un argument en faveur de la complexité du code, mais quand vous revenez sur le déroulement de ce qui c'est passé, il est clair que cela provient plus d'un manque de communication sur les préconditions d'utilisation avec le développeur qui modifie le code.

Par définition, vous ne pouvez pas vous concentrer sur tout, donc concentrez-vous sur le code qui va être livré aux clients, plutôt que sur le code qui sera utilisé en interne. Migrer de force le code destiné à être livré sur des projets de développement isolés. Un article récent a remarqué que toutes les différentes métriques de qualité du code étaient corrélées au moins aussi fortement avec le taux d'erreurs que la taille du code, ce qui donne à la taille du code seule essentiellement la même capacité de prédiction du taux d'erreur. Réduisez la taille de votre code important.

Si vous n'êtes pas profondément effrayés par tous les problèmes additionnels soulevés par la programmation parallèle, vous n'avez pas réfléchi suffisamment à ce sujet.

Il est impossible de faire du vrai contrôle de test dans le développement logiciel, mais je crois que le succès que nous avons eu avec l'analyse de code a été suffisamment clair pour que je puisse le dire sans détour : il est irresponsable de ne pas l'utiliser. Il y a des données objectives dans les rapports automatiques des crashs de console montrant que Rage, en dépit d'être à la pointe de la technologie à bien des égards, est remarquablement plus robuste que la plupart des titres contemporains. Le lancement PC de Rage a malheureusement été tragiquement entaché par des problèmes de pilote - je parierais qu'AMD n'utilise pas l'analyse statique de code sur leurs pilotes graphiques.

Ce qu'il faut retenir : si votre version de Visual Studio possède /analyze, activez-le et essayez-le. Si je devais choisir un seul outil, je choisirais l'option Microsoft. Pour tous ceux qui travaillent avec Visual Studio, essayez au moins la démonstration de PVS-Studio. Si vous développez des logiciels commerciaux, l'achat d'outils d'analyse statique est de l'argent bien dépensé.

Un dernier commentaire venant de Twitter : Dave Revell @dave_revell : « Plus je passe du code à l'analyse statique, plus je m'émerveille que les ordinateurs démarrent tout court. »

VII. Remerciements

Cet article est une traduction autorisée de Static Code Analysis par John Carmack.

Merci à Luc Hermitte et à mitkl pour leur relecture technique, à CinePhil et à _Max_ pour leur relecture orthographique.