4 erreurs de programmation C courantes - et 5 astuces pour les éviter

Peu de langages de programmation peuvent égaler C pour la vitesse et la puissance de la machine. Cette affirmation était vraie il y a 50 ans, et elle l'est toujours aujourd'hui. Cependant, il y a une raison pour laquelle les programmeurs ont inventé le terme «footgun» pour décrire le type de puissance de C. Si vous ne faites pas attention, C peut vous faire sauter les orteils ou ceux de quelqu'un d'autre.

Voici quatre des erreurs les plus courantes que vous pouvez commettre avec C et cinq mesures que vous pouvez prendre pour les éviter.

Erreur C courante: ne pas libérer de la mallocmémoire (ou la libérer plus d'une fois)

C'est l'une des grosses erreurs en C, dont beaucoup impliquent la gestion de la mémoire. La mémoire allouée (effectuée à l'aide de la malloc fonction) n'est pas automatiquement éliminée en C. C'est le travail du programmeur de se débarrasser de cette mémoire lorsqu'elle n'est plus utilisée. Ne parvenez pas à libérer des demandes de mémoire répétées et vous vous retrouverez avec une fuite de mémoire. Essayez d'utiliser une région de mémoire qui a déjà été libérée, et votre programme plantera - ou, pire encore, ralentira et deviendra vulnérable à une attaque utilisant ce mécanisme.

Notez qu'une fuite de mémoire ne doit décrire que des situations où la mémoire est censée être libérée, mais ne l'est pas. Si un programme continue d'allouer de la mémoire parce que la mémoire est réellement nécessaire et utilisée pour le travail, alors son utilisation de la mémoire peut être  inefficace , mais à proprement parler ce n'est pas une fuite.

Erreur C courante: lire un tableau hors limites

Ici, nous avons encore une autre des erreurs les plus courantes et les plus dangereuses en C. Une lecture au-delà de la fin d'un tableau peut renvoyer des données inutiles. Une écriture au-delà des limites d'un tableau peut corrompre l'état du programme, le planter complètement ou, pire que tout, devenir un vecteur d'attaque pour les logiciels malveillants.

Alors, pourquoi la vérification des limites d'un tableau est-elle laissée au programmeur? Dans la spécification officielle C, lire ou écrire un tableau au-delà de ses limites est un «comportement indéfini», ce qui signifie que la spécification n'a pas son mot à dire sur ce qui est supposé se produire. Le compilateur n'est même pas obligé de s'en plaindre.

C a longtemps préféré donner le pouvoir au programmeur même à ses risques et périls. Une lecture ou une écriture hors limites n'est généralement pas interceptée par le compilateur, sauf si vous activez spécifiquement les options du compilateur pour s'en prémunir. De plus, il peut être possible de dépasser la limite d'un tableau au moment de l'exécution d'une manière que même une vérification du compilateur ne peut pas protéger.

Erreur C courante: ne pas vérifier les résultats de malloc

malloc et calloc (pour la mémoire pré-remise à zéro) sont les fonctions de la bibliothèque C qui obtiennent la mémoire allouée par tas du système. S'ils ne parviennent pas à allouer de la mémoire, ils génèrent une erreur. À l'époque où les ordinateurs avaient relativement peu de mémoire, il y avait de fortes chances qu'un appel mallocne réussisse pas.

Même si les ordinateurs d'aujourd'hui ont des gigaoctets de RAM à jeter, il y a toujours un risque d' mallocéchec, en particulier sous une pression mémoire élevée ou lors de l'allocation de grandes dalles de mémoire à la fois. Ceci est particulièrement vrai pour les programmes C qui «allouent» d'abord un gros bloc de mémoire du système d'exploitation, puis le divisent pour leur propre usage. Si cette première allocation échoue parce qu'elle est trop grande, vous pourrez peut-être intercepter ce refus, réduire l'allocation et régler l'heuristique d'utilisation de la mémoire du programme en conséquence. Mais si l'allocation de mémoire échoue sans être piégée, l'ensemble du programme pourrait se détériorer.

Erreur C courante: utilisation void*pour des pointeurs génériques vers la mémoire

Utiliser  void* pour pointer vers la mémoire est une vieille habitude - et une mauvaise. Pointeurs à la mémoire doit toujours être char*, unsigned char*ou  uintptr_t*. Les suites de compilateurs C modernes devraient fournir uintptr_tdans le cadre de stdint.h

Lorsqu'il est étiqueté de l'une de ces façons, il est clair que le pointeur fait référence à un emplacement mémoire dans l'abstrait plutôt qu'à un type d'objet non défini. Ceci est doublement important si vous effectuez des calculs de pointeur. Avec  uintptr_t*et similaires, l'élément de taille pointé, et la façon dont il sera utilisé, sont sans ambiguïté. Avec void*, pas tellement.

Éviter les erreurs C courantes - 5 conseils

Comment éviter ces erreurs trop courantes lorsque vous travaillez avec de la mémoire, des tableaux et des pointeurs en C? Gardez ces cinq conseils à l'esprit. 

Structurez les programmes en C afin que la propriété de la mémoire reste claire

Si vous venez de démarrer une application C, cela vaut la peine de réfléchir à la façon dont la mémoire est allouée et libérée comme l'un des principes organisationnels du programme. Si vous ne savez pas où une allocation de mémoire donnée est libérée ou dans quelles circonstances, vous demandez des problèmes. Faites l'effort supplémentaire de rendre la propriété de la mémoire aussi claire que possible. Vous vous ferez une faveur (ainsi qu'aux futurs développeurs).

Telle est la philosophie derrière des langues comme Rust. Rust rend impossible l'écriture d'un programme qui se compile correctement à moins que vous n'exprimiez clairement comment la mémoire est détenue et transférée. C n'a pas de telles restrictions, mais il est sage d'adopter cette philosophie comme un guide dans la mesure du possible.

Utilisez les options du compilateur C qui vous protègent contre les problèmes de mémoire

La plupart des problèmes décrits dans la première moitié de cet article peuvent être signalés à l'aide d'options strictes du compilateur. Les éditions récentes de gcc, par exemple, fournissent des outils comme AddressSanitizer («ASAN») comme option de compilation pour vérifier les erreurs courantes de gestion de la mémoire.

Attention, ces outils ne saisissent absolument pas tout. Ce sont des garde-corps; ils n'attrapent pas le volant si vous partez hors route. En outre, certains de ces outils, comme ASAN, imposent des coûts de compilation et d'exécution, et doivent donc être évités dans les versions de version.

Utilisez Cppcheck ou Valgrind pour analyser le code C pour les fuites de mémoire

Là où les compilateurs eux-mêmes échouent, d'autres outils interviennent pour combler le vide, en particulier lorsqu'il s'agit d'analyser le comportement du programme lors de l'exécution.

Cppcheck exécute une analyse statique sur le code source C pour rechercher les erreurs courantes dans la gestion de la mémoire et les comportements non définis (entre autres).

Valgrind fournit un cache d'outils pour détecter les erreurs de mémoire et de thread lors de l'exécution de programmes C. C'est beaucoup plus puissant que l'utilisation de l'analyse au moment de la compilation, car vous pouvez obtenir des informations sur le comportement du programme lorsqu'il est réellement en ligne. L'inconvénient est que le programme fonctionne à une fraction de sa vitesse normale. Mais c'est généralement bien pour les tests.

Ces outils ne sont pas des balles d'argent et ils n'attraperont pas tout. Mais ils fonctionnent dans le cadre d'une stratégie défensive générale contre la mauvaise gestion de la mémoire chez C.

Automatisez la gestion de la mémoire C avec un garbage collector

Puisque les erreurs de mémoire sont une source évidente de problèmes C, voici une solution simple: ne pas gérer la mémoire en C manuellement. Utilisez un garbage collector. 

Oui, c'est possible en C. Vous pouvez utiliser quelque chose comme le ramasse-miettes Boehm-Demers-Weiser pour ajouter une gestion automatique de la mémoire aux programmes C. Pour certains programmes, l'utilisation du collecteur Boehm peut même accélérer les choses. Il peut même être utilisé comme mécanisme de détection des fuites.

Le principal inconvénient du garbage collector Boehm est qu'il ne peut pas analyser ou libérer de la mémoire qui utilise la valeur par défaut malloc. Il utilise sa propre fonction d'allocation et ne fonctionne que sur la mémoire que vous allouez spécifiquement avec lui.

N'utilisez pas C lorsqu'une autre langue fera l'affaire

Certaines personnes écrivent en C parce qu'elles l'apprécient vraiment et la trouvent fructueuse. Dans l'ensemble, cependant, il est préférable d'utiliser C uniquement lorsque vous le devez, et avec parcimonie, pour les quelques situations où c'est vraiment le choix idéal.

Si vous avez un projet où les performances d'exécution seront principalement limitées par les E / S ou l'accès au disque, l'écrire en C ne le rendra probablement pas plus rapide de la manière qui compte, et ne fera probablement que le rendre plus sujet aux erreurs et difficile à maintenir. Le même programme pourrait bien être écrit en Go ou en Python.

Une autre approche consiste à n'utiliser C que pour les parties vraiment gourmandes en performances de l'application, et un langage plus fiable bien que plus lent pour les autres parties. Encore une fois, Python peut être utilisé pour envelopper des bibliothèques C ou du code C personnalisé, ce qui en fait un bon choix pour les composants plus standard tels que la gestion des options de ligne de commande.