Les 10 règles de codage essentielles de la NASA pour développer des logiciels critiques
Les projets logiciels grands et complexes utilisent divers standards et directives de codage. Ces directives établissent les règles de base qui doivent être suivies lors de l'écriture du logiciel. Habituellement, elles déterminent :
a) Comment le code devrait-il être structuré ?
b) Quelles fonctionnalités du langage devraient ou ne devraient pas être utilisées ?
Pour être efficaces, l'ensemble des règles doit être petit et assez spécifique pour être facilement compris et mémorisé.
Les meilleurs programmeurs du monde travaillant à la NASA suivent un ensemble de directives pour développer du code critique pour la sécurité. En fait, de nombreuses agences, y compris le Laboratoire de propulsion à réaction (JPL) de la NASA, se concentrent sur le code écrit en langage de programmation C. C'est parce qu'il y a un support d'outils extensif pour ce langage, comme des extracteurs de modèles logiques, débogueurs, compilateur stable, analyseurs de code source forts, et outils de métriques.
Dans les cas critiques, il devient nécessaire d'appliquer ces règles, surtout là où la vie humaine peut dépendre de sa correction et efficacité. Par exemple, les programmes logiciels utilisés pour contrôler les avions, vaisseaux spatiaux ou centrales nucléaires.
Mais savez-vous quels standards les agences spatiales utilisent pour faire fonctionner leurs machines ? Ci-dessous, nous avons listé les 10 règles de codage de la NASA établies par le scientifique principal du JPL, Gerard J. Holzmann. Elles se concentrent toutes principalement sur les paramètres de sécurité, et vous pourriez les appliquer à d'autres langages de programmation également.
Règle n° 1 – Flux de contrôle simple
Écrivez des programmes avec des constructions de flux de contrôle très simples – N'utilisez pas les constructions setjmp ou longjmp, les instructions goto, et la récursion directe ou indirecte.
Raison : Un flux de contrôle simple résulte en une meilleure clarté du code et de plus fortes capacités de vérification. Sans récursion, il n'y aura pas de graphe d'appel de fonction cyclique. Ainsi, toutes les exécutions qui sont supposées être bornées restent réellement bornées.
Règle n° 2 – Borne supérieure fixe pour les boucles
Toutes les boucles doivent avoir une borne supérieure fixe. Il doit être possible pour un outil de vérification de prouver statiquement qu'une borne supérieure prédéfinie sur le nombre d'itérations d'une boucle ne peut pas être dépassée.
La règle est considérée comme violée si la borne de boucle ne peut pas être prouvée statiquement.
Raison : La présence de bornes de boucle et l'absence de récursion empêchent le code incontrôlé. Cependant, la règle ne s'applique pas aux itérations qui sont censées être non-terminantes (par exemple, l'ordonnanceur de processus). Dans de tels cas, la règle inverse s'applique – Il doit être prouvable statiquement que l'itération ne peut pas se terminer.
Règle n° 3 – Pas d'allocation mémoire dynamique
N'utilisez pas d'allocation mémoire dynamique après l'initialisation.
Raison : Les allocateurs de mémoire comme malloc, et les collecteurs de déchets ont souvent un comportement imprévisible qui peut exceptionnellement impacter la performance. De plus, des erreurs de mémoire peuvent aussi se produire à cause d'une erreur du programmeur, ce qui inclut :
- Tenter d'allouer plus de mémoire que physiquement disponible
- Oublier de libérer la mémoire
- Continuer à utiliser la mémoire après qu'elle a été libérée
- Dépasser les limites de la mémoire allouée
Forcer tous les modules à vivre dans une zone de stockage pré-allouée et fixe peut éliminer ces problèmes et faciliter la vérification de l'utilisation de la mémoire.
Une façon de réclamer dynamiquement la mémoire en l'absence d'allocation mémoire depuis le tas est d'utiliser la mémoire de pile.
Règle n° 4 – Pas de fonctions volumineuses
Aucune fonction ne devrait être plus longue que ce qui pourrait être imprimé sur une seule feuille de papier dans un format de référence standard avec une ligne par déclaration et une ligne par instruction. Cela signifie qu'une fonction ne devrait pas avoir plus de 60 lignes de code.
Raison : Les fonctions excessivement longues sont souvent un signe de mauvaise structure. Chaque fonction devrait être une unité logique compréhensible ainsi que vérifiable. Il est assez difficile de comprendre une unité logique qui s'étend sur plusieurs écrans sur un affichage d'ordinateur.
Règle n° 5 – Faible densité d'assertions
La densité d'assertions du programme devrait faire une moyenne d'un minimum de deux assertions par fonction. Les assertions sont utilisées pour vérifier des conditions anormales qui ne devraient jamais se produire dans les exécutions réelles. Elles devraient être définies comme des tests booléens. Quand une assertion échoue, une action de récupération explicite devrait être prise.
Si un outil de vérification statique prouve qu'une assertion ne peut jamais échouer ou ne jamais tenir, la règle est considérée comme violée.
Raison : Selon les statistiques d'effort de codage de l'industrie, les tests unitaires capturent au moins un défaut par 10 à 100 lignes de code. Les chances d'intercepter les défauts augmentent avec la densité d'assertions.
L'utilisation d'assertions est aussi importante car elles font partie d'une forte stratégie de codage défensif. Elles sont utilisées pour vérifier les pré et post conditions d'une fonction, paramètre, et valeur de retour d'une fonction et invariants de boucle. Les assertions peuvent être sélectivement désactivées après le test du code critique pour la performance.
Règle n° 6 – Déclarer les objets de données au plus petit niveau de portée
Celle-ci soutient le principe de base du masquage des données. Tous les objets de données doivent être déclarés au plus petit niveau de portée possible.
Raison : Si un objet n'est pas en portée, sa valeur ne peut pas être référencée ou corrompue. Cette règle décourage la réutilisation de variables pour des objectifs multiples, incompatibles, ce qui peut compliquer le diagnostic de pannes.
Règle n° 7 – Vérifier les paramètres et la valeur de retour
La valeur(s) de retour des fonctions non-void devrait être vérifiée par chaque fonction appelante, et la validité des paramètres devrait être vérifiée à l'intérieur de chaque fonction.
Dans sa forme la plus stricte, cette règle signifie que même la valeur de retour des instructions printf et close de fichier devrait être vérifiée.
Raison : Si la réponse à une erreur n'est pas à juste titre différente de la réponse au succès, on devrait explicitement vérifier une valeur de retour. C'est généralement le cas avec les appels à close et printf. Il est acceptable de caster explicitement la valeur de retour de la fonction à void – indiquant que le codeur décide explicitement (pas accidentellement) d'ignorer une valeur de retour.
Règle n° 8 – Utilisation limitée du préprocesseur
L'utilisation du préprocesseur devrait être limitée à l'inclusion de fichiers d'en-tête et définitions de macros. Les appels de macros récursifs, le collage de jetons, et les listes d'arguments variables ne sont pas autorisés.
Il devrait y avoir une justification pour plus d'une ou deux directives de compilation conditionnelle même dans les grands efforts de développement d'applications, au-delà du code standard boilerplate, qui évite l'inclusion multiple du même fichier d'en-tête. Chaque telle utilisation doit être signalée par un outil de vérification et justifiée dans le code.
Raison : Le préprocesseur C est un outil puissant et ambigu qui peut détruire la clarté du code et confondre de nombreux vérificateurs basés sur le texte. L'effet de constructions dans le code de préprocesseur non borné pourrait être exceptionnellement difficile à déchiffrer, même avec une définition de langage formelle en main.
La prudence contre la compilation conditionnelle est tout aussi importante – avec seulement 10 directives de compilation conditionnelle, il pourrait y avoir 1024 versions possibles (2^10) du code, ce qui augmenterait l'effort de test requis.
Règle n° 9 – Utilisation limitée des pointeurs
L'utilisation des pointeurs doit être restreinte. Pas plus d'un niveau de déréférencement n'est permis. Les opérations de déréférencement de pointeurs ne doivent pas être cachées à l'intérieur de déclarations typedef ou définitions de macros.
Les pointeurs de fonction ne sont pas non plus autorisés.
Raison : Les pointeurs sont facilement mal utilisés, même par des experts. Ils rendent difficile le suivi ou l'analyse du flux de données dans un programme, surtout par des analyseurs statiques basés sur des outils.
Les pointeurs de fonction restreignent aussi le type de vérifications effectuées par les analyseurs statiques. Ainsi, ils ne devraient être utilisés que s'il y a une forte justification pour leur implémentation. Si des pointeurs de fonction sont utilisés, il devient presque impossible pour un outil de prouver l'absence de récursion, donc des méthodes alternatives devraient être fournies pour compenser cette perte de capacités analytiques.
Règle n° 10 – Compiler tout le code
Tout le code doit être compilé dès le premier jour de développement. Les avertissements du compilateur doivent être activés au réglage le plus pointilleux du compilateur. Le code doit compiler avec ces réglages sans aucun avertissement.
Tout le code devrait être vérifié quotidiennement avec au moins un (de préférence plus d'un) analyseur de code source statique à l'état de l'art et devrait passer le processus d'analyse avec zéro avertissement.
Raison : Il y a beaucoup d'analyseurs de code source efficaces disponibles sur le marché ; quelques-uns sont des outils gratuits. Il n'y a absolument aucune excuse pour qu'un codeur n'utilise pas cette technologie facilement disponible. Si le compilateur ou l'analyseur statique est confus, le code causant la confusion/erreur devrait être réécrit pour qu'il devienne plus trivialement valide.
Que dit la NASA à propos de ces règles ?
Technologie industrielle
- Températures critiques des supraconducteurs élémentaires et à haute température (Kelvin)
- Règles de dérivation essentielles : aide-mémoire rapide
- Règles fondamentales des primitives : constantes, sommes et différences
- BigStitcher : Imagerie 3D de tissus de précision simplifiée
- Construire un avenir plus sûr : faire progresser les solutions intelligentes de sécurité alimentaire
- Pourquoi moderniser votre flotte réduit les coûts et booste la durabilité
- Au-delà du code : compétences clés pour réussir les projets d'automatisation
- Maîtriser la conformité UID : 3 règles essentielles que tout fournisseur DoD doit suivre
- 4 stratégies éprouvées pour concevoir des checklists de maintenance efficaces