Introduction
Les logiciels critiques pour la sécurité, tels que ceux utilisés dans les vaisseaux spatiaux ou les systèmes automobiles, nécessitent un code extrêmement fiable. Pour y répondre, le Jet Propulsion Laboratory de la NASA a créé en 2006 les règles de codage
"Power of 10". Ces directives concises suppriment les constructions complexes en langage C qui sont difficiles à analyser et garantissent que le code reste simple, vérifiable et fiable.
Aujourd'hui, des outils comme Aikido Code Quality peuvent être configurés avec des vérifications personnalisées pour appliquer les dix règles à chaque nouvelle demande d'extraction. Dans cet article, nous expliquons chaque règle, pourquoi elle est importante, et nous fournissons des exemples de code montrant des approches correctes et incorrectes.
L'importance de ces règles
Les règles de la NASA se concentrent sur la lisibilité, l'analysabilité et la fiabilité, qui sont essentielles pour les applications critiques telles que le contrôle des engins spatiaux et les logiciels de vol. En interdisant les constructions obscures en C et en imposant des vérifications défensives, les lignes directrices facilitent l'examen du code et la preuve de son exactitude. Elles complètent des normes telles que MISRA C en abordant des modèles que les analyseurs statiques manquent souvent. Par exemple, le fait d'éviter la récursivité et la mémoire dynamique rend l'utilisation des ressources prévisible, tandis que l'application de contrôles de la valeur de retour permet de détecter de nombreux bogues au moment de la compilation.
En fait, l'étude de la NASA sur un système embarqué de grande diffusion, tel que le microprogramme de l'accélérateur électronique de Toyota, a révélé des centaines de violations des règles. Cela montre que les projets réels se heurtent souvent aux mêmes problèmes que ceux que ces règles sont censées prévenir. Chaque règle de la liste prévient une catégorie d'erreurs courantes (boucles incontrôlées, déréférences de pointeurs nuls, effets secondaires invisibles, etc.) Les ignorer peut entraîner des défaillances subtiles au moment de l'exécution, des failles de sécurité ou un comportement non déterministe. En revanche, le respect des dix règles rend la vérification statique beaucoup plus facile.
Les outils automatisés sont importants. Les plateformes de qualité du code peuvent être configurées pour détecter les constructions ou les modèles interdits. Ces règles s'appliquent automatiquement à chaque demande d'extraction, ce qui permet de détecter les problèmes avant que le code ne soit fusionné.
Faire le lien entre le contexte et les règles
Avant de se plonger dans les règles individuelles, il est important de comprendre le contexte :
- Langage cible: Les règles "Puissance de 10" de la NASA ont été écrites pour le langage C, un langage qui bénéficie d'un soutien important (compilateurs, analyseurs, débogueurs), mais qui est également réputé pour ses comportements non définis. Elles ne supposent pas de garbage collection ou de gestion avancée de la mémoire. En utilisant uniquement un langage C simple et bien structuré, il est possible de tirer parti de l'analyse statique pour prouver les propriétés du programme.
- Analyse statique: De nombreuses règles existent pour faciliter les contrôles automatisés. Par exemple, l'interdiction de la récursivité (règle 1) et l'exigence de limites de boucle (règle 2) permettent aux outils de prouver le nombre d'itérations ou l'utilisation de la pile qu'une fonction peut avoir. De même, l'interdiction des macros complexes et la limitation des pointeurs (règles 8 et 9) rendent les modèles de code explicites plutôt que cachés dans la magie du préprocesseur ou dans de multiples indirections.
- Flux de développement : Dans les pipelines DevSecOps modernes, ces règles font partie des vérifications CI. Les outils de qualité du code peuvent s'intégrer à GitHub, GitLab ou Bitbucket pour examiner chaque demande d'extraction et détecter à la fois les problèmes simples et les schémas plus complexes. Vous pouvez créer une règle personnalisée pour chaque ligne directrice de la NASA, par exemple "signaler toute utilisation de goto ou d'appels de fonction récursifs" ou "s'assurer que chaque boucle a une limite littérale". Une fois configurées, ces règles sont appliquées automatiquement lors de chaque analyse de code future, ce qui permet de détecter rapidement les violations et de fournir des conseils sur la manière de les corriger.
En bref, les 10 règles de la NASA incarnent une programmation C défensive et analysable. Nous énumérons ci-dessous chaque règle, montrons à quoi ressemble le bon et le mauvais code, et expliquons pourquoi la règle existe et quels risques elle atténue.
Les 10 règles de codage de la NASA
1. Éviter les flux de contrôle complexes.
N'utilisez pas de goto, setjmp ou longjmp, évitez d'écrire des fonctions récursives dans n'importe quelle partie du code.
❌ Exemple de non-conformité
// Non-compliant: recursive function call
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n-1); // recursion (direct)
}Exemple de conformité (utilise une boucle)
// Compliant: uses an explicit loop instead of recursion
int factorial(int n) {
int result = 1;
for (int i = n; i > 1; --i) {
result *= i;
}
return result;
}Pourquoi c'est important: La récursivité et les gotos créent un flux de contrôle non linéaire sur lequel il est difficile de raisonner. Les appels récursifs rendent le graphe d'appel cyclique et la profondeur de la pile illimitée ; le goto crée un code spaghetti. En utilisant des boucles simples et un code linéaire, un analyseur statique peut facilement vérifier l'utilisation de la pile et les chemins du programme. Le non-respect de cette règle peut entraîner des débordements de pile inattendus ou des chemins logiques difficiles à vérifier manuellement.
2. Les boucles doivent avoir des limites supérieures fixes.
Chaque boucle doit avoir une limite vérifiable à la compilation.
❌ Exemple de non-conformité (boucle non bornée) :
// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
doSomething(array[i]);
i++;
}✅ Exemple de conformité (boucle à liaison fixe) :
// Compliant: loop with explicit fixed upper bound and assert
#define MAX_LEN 100
for (int i = 0; i < MAX_LEN; i++) {
if (array[i] == 0) break;
doSomething(array[i]);
}Pourquoi cela est-il important ? Les boucles non bornées peuvent s'exécuter à l'infini ou dépasser les limites de ressources. Avec une borne fixe, les outils peuvent prouver de manière statique le nombre maximal d'itérations. Dans les systèmes critiques, une limite manquante peut provoquer un emballement de la boucle. En imposant une limite explicite (ou une taille de tableau statique), nous nous assurons que les boucles se terminent de manière prévisible. Sans cette règle, une erreur dans la logique de la boucle pourrait ne pas être détectée avant le déploiement (par exemple, une erreur d'un par un qui provoque une boucle infinie).
3. Pas de mémoire dynamique après l'initialisation.
Évitez les opérations malloc/free ou toute utilisation du tas dans le code en cours d'exécution ; utilisez uniquement l'allocation fixe ou l'allocation de la pile.
❌ Exemple non conforme (utilise malloc)
// Non-compliant: dynamic allocation inside the code
void storeData(int size) {
int *buffer = malloc(size * sizeof(int));
if (buffer == NULL) return;
// ... use buffer ...
free(buffer);
}Exemple de conformité (allocation statique)
// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
int buffer[MAX_SIZE];
// ... use buffer without dynamic alloc ...
}Importance de la question : L'allocation dynamique de mémoire pendant l'exécution peut entraîner un comportement imprévisible, une fragmentation de la mémoire ou des échecs d'allocation, en particulier dans les systèmes aux ressources limitées tels que les engins spatiaux ou les contrôleurs intégrés. Si malloc ou free échoue en cours de mission, le logiciel peut se bloquer ou se comporter de manière imprévisible. L'utilisation d'une mémoire de taille fixe ou allouée par pile garantit un comportement déterministe, simplifie la validation et prévient les fuites de mémoire pendant l'exécution.
4. Les fonctions tiennent sur une page (~60 lignes).
Chaque fonction doit être courte (environ ≤ 60 lignes).
❌ Exemple de non-conformité
// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
// ... imagine 100+ lines of code doing many tasks ...
}✅ Exemple de conformité (fonctions modulaires)
// Compliant: break the task into clear sub-functions
void processAllData() {
preprocessData();
analyzeData();
postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData() { /* ... */ }
void postprocessData(){ /* ... */ }Pourquoi cela est-il important ? Les fonctions extrêmement longues sont difficiles à comprendre, à tester et à vérifier en tant qu'unité. En limitant chaque fonction à une tâche conceptuelle (et à une page imprimée), les revues de code et les vérifications statiques deviennent réalisables. Si une fonction s'étend sur un trop grand nombre de lignes, des erreurs logiques ou des conditions limites peuvent passer inaperçues. La décomposition du code en fonctions plus petites améliore la clarté et facilite l'application d'autres règles (comme la densité d'assertions et les contrôles de retour par fonction).
5. Utilisez au moins deux assertions par fonction.
Chaque fonction doit effectuer des contrôles défensifs.
❌ Exemple de non-conformité (pas d'instructions) :
int get_element(int *array, size_t size, size_t index) {
return array[index];
}✅ Exemple de conformité (avec assertions) :
int get_element(int *array, size_t size, size_t index) {
assert(array != NULL); // Assertion 1: pointer validity
assert(index < size); // Assertion 2: bounds check
if (array == NULL) return -1; // Recovery: return error
if (index >= size) return -1; // Recovery: return error
return array[index];
}Pourquoi c'est important : Les assertions constituent la première ligne de défense contre les conditions non valides. La NASA a constaté qu'une densité d'assertions plus élevée augmentait considérablement les chances de détecter les bogues. Avec au moins deux assertions par fonction (vérifiant les conditions préalables, les limites, les invariants), le code documente lui-même ses hypothèses et signale immédiatement les anomalies pendant les tests. Sans assertions, une valeur inattendue peut se propager silencieusement et provoquer un échec loin de la source de l'erreur.
6. Déclarer les données avec une portée minimale.
Gardez les variables aussi locales que possible ; évitez les variables globales.
❌ Exemple de non-conformité (données globales) :
// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
statusFlag = f;
}✅ Exemple de conformité (portée locale) :
// Compliant: local variable inside function
void setStatus(int f) {
int statusFlag = f;
// ... use statusFlag only here ...
}Pourquoi cela est-il important ? La réduction du champ d'application réduit le couplage et les interactions involontaires. Si une variable n'est nécessaire qu'à l'intérieur d'une fonction, le fait de la déclarer globalement risque d'être modifié par d'autres codes de manière inattendue. En gardant les données locales, chaque fonction devient plus autonome et sans effets secondaires, ce qui simplifie l'analyse et les tests. Les violations (comme la réutilisation d'un état global) peuvent conduire à des bogues difficiles à trouver en raison de l'aliasing ou de modifications inattendues.
7. Vérifier toutes les valeurs de retour des fonctions et tous les paramètres.
L'appelant doit examiner chaque valeur de retour non vide ; chaque fonction doit valider ses paramètres d'entrée.
Exemple non conforme (ignore la valeur de retour)
int bad_mission_control(int velocity, int time) {
int distance;
calculate_trajectory(velocity, time, &distance); // Didn't check!
return distance; // Could be garbage if calculation failed
}✅ Exemple de conformité
int good_mission_control(int velocity, int time) {
int distance;
int status = calculate_trajectory(velocity, time, &distance);
if (status != 0) { // Checked the return value
return -1; // Propagate error to caller
}
return distance; // Safe to use
}Pourquoi cela est-il important ? Ignorer les valeurs de retour ou les paramètres invalides est une source majeure de bogues. Par exemple, ne pas vérifier malloc peut conduire à un déréférencement de pointeur nul. De même, ne pas valider les entrées (par exemple, les indices de tableaux ou les chaînes de format) peut entraîner des débordements de mémoire tampon ou des plantages. La NASA exige que chaque retour soit traité (ou explicitement transformé en void pour signaler l'intention) et que chaque argument soit vérifié. Cette approche "fourre-tout" permet de s'assurer qu'aucune erreur n'est ignorée silencieusement.
8. Limiter le préprocesseur aux inclusions et aux macros simples.
Évitez les macros complexes ou les astuces de compilation conditionnelle.
❌ Exemple de non-conformité (marco complexe) :
#définir DECLARE_FUNC(name) void func_##name(void)
DECLARE_FUNC(init) ; // Se développe en : void func_init(void)✅ Exemple conforme (macros simples / inline) :
// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256Pourquoi cela est-il important ? Les macros complexes (en particulier les macros multilignes ou les macros de type fonction) peuvent cacher la logique, perturber le flux de contrôle et contrecarrer l'analyse statique. Limiter le préprocesseur à des tâches triviales (par exemple les constantes et les en-têtes) permet de conserver un code explicite. Par exemple, le remplacement des macros par des fonctions en ligne améliore la vérification des types et la débogabilité. Sans cette règle, de subtils bogues d'expansion de macros ou des erreurs de compilation conditionnelle pourraient passer inaperçus lors des révisions.
9. Limiter l'utilisation des pointeurs.
Limiter l'indirection à un seul niveau - éviter les int** et les pointeurs de fonction.
❌ Exemple de non-conformité (inderection multiple) :
// Non conforme : pointeur double et pointeur de fonction
int **doublePtr ;
int (*funcPtr)(int) = someFunction ;✅ Exemple conforme (pointeur unique) :
// Conforme : pointeur à un niveau, pas de pointeur de fonction
int *singlePtr ;
// Utiliser un appel explicite au lieu d'un pointeur de fonction
int result = someFunction(5) ;Pourquoi cela est-il important ? Les multiples niveaux de pointeurs et de pointeurs de fonction compliquent le flux de données et rendent difficile le suivi de la mémoire ou du code auquel on accède. Les analyseurs statiques doivent résoudre chaque indirection, ce qui peut être indécidable en général. En se limitant aux références à un seul pointeur, le code reste plus simple et plus sûr. Le non-respect de cette règle peut entraîner un aliasing peu clair (un pointeur modifiant des données par l'intermédiaire d'un autre) ou un comportement de rappel inattendu, qui sont tous deux risqués dans des contextes où la sécurité est essentielle.
10. Compiler avec tous les avertissements activés et les corriger.
Activer tous les avertissements du compilateur et les traiter avant la publication.
❌ Exemple de non-conformité (code avec avertissements)
// Non-compliant: code that generates warnings (uninitialized, suspicious assignment)
int x;
if (x = 5) { // bug: should be '==' or initialize x
// ...
}
printf("%d\n", x); // warning: 'x' is used uninitialized✅ Exemple conforme (compilation propre)
// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
// ...
}
printf("%d\n", x);Pourquoi cela est-il important ? Les avertissements du compilateur signalent souvent de véritables bogues (comme des variables non initialisées, des incompatibilités de type ou des affectations involontaires). La règle de la NASA impose qu'aucun avertissement ne soit ignoré. Avant toute publication, le code doit être compilé sans avertissement avec des paramètres de verbosité maximale. Cette pratique permet de détecter rapidement de nombreuses erreurs insignifiantes. Si un avertissement ne peut être résolu, le code doit être restructuré ou documenté de manière à ce que l'avertissement ne se produise jamais.
Chacune de ces règles élimine une catégorie d'erreurs cachées. Lorsqu'elles sont suivies ensemble, elles rendent le code C beaucoup plus prévisible et vérifiable.
Conclusion
Les 10 règles de la NASA (la "puissance des 10") constituent une norme de codage claire et efficace pour les logiciels C critiques. En évitant les constructions complexes et en imposant des vérifications, elles réduisent les risques de bogues cachés et rendent possible l'analyse statique. Dans le cadre d'un développement moderne, ces directives peuvent être automatisées à l'aide d'outils de contrôle de la qualité du code. Des règles personnalisées peuvent être définies pour signaler toute violation des directives de la NASA, et ces règles peuvent s'appliquer à chaque demande d'extraction, fournissant ainsi un retour d'information immédiat aux développeurs.
L'adoption précoce de ces contrôles permet d'obtenir un code plus sûr, de meilleure qualité et plus facile à maintenir. Même en dehors de l'aérospatiale, les principes restent valables : des fonctions petites et claires, des boucles explicites, une programmation défensive et pas de gymnastique des pointeurs à faire dresser les cheveux sur la tête. Le respect et l'automatisation de ces règles à l'aide d'un outil de contrôle de la qualité du code permettent à votre équipe de détecter rapidement les erreurs et de produire des logiciels plus fiables.
.avif)
