lundi 25 juin 2012

La vérité sur `strncpy' !

Edit : 60 vues depuis l'ouverture du blog, on est dans la régularité. Vous pouvez laisser un commentaire, ça ne mange pas de pain et c'est toujours sympa à voir. Même si vous n'avez rien d'intéressant à dire.

On voit très souvent, sur des forums communautaires, des membres qui conseillent aux autres d'utiliser strncpy à la place de strcpy pour des raisons de sécurité. L'article que je vais vous présenter aujourd'hui tend à prétendre le contraire. À vous de vous forger votre avis à partir des arguments des deux camps.

Un problème de débordement de tampon


Ce que reprochent ces membres à strcpy, ce sont les problèmes de dépassements de tampons (overflow) qui sont engendrés par l'utilisation naïve de cette fonction de copie.

char const *const s1 = "hello";
char              s2[3];
strcpy(s2, s1);


Dans le code ci-dessus, un dépassement mémoire est causé par la taille insuffisante de la chaîne s2.

La solution de facilité, strncpy

 

Pour prévenir ce débordement, beaucoup vont suggérer l'utilisation de strncpy, de cette manière :

char const *const s1 = "hello";
char              s2[3];
strncpy(s2, s1, sizeof s2);


Grâce au paramètre de taille, demandé par strncpy, la fonction va pouvoir vérifier que la chaîne ne déborde pas.

Un paramètre presque dérisoire...


Seulement, ce paramètre de taille est dérisoire. Tout simplement parce que vous devenez garder la trace de beaucoup de paramètres pour éviter le plantage de ces fonctions de manipulations des chaînes de caractère natives : le positionnement du caractère de fin de chaîne en est un exemple. Comme nous le verrons tout à l'heure, strncpy ne gère même pas correctement cet élément...

Par exemple, si votre chaîne de caractère est allouée statiquement, vous pouvez faire quelque chose comme le code ci-dessous pour utiliser strcpy :

if (strlen(s1) + 1 > sizeof s2)    handle_errors();
else                               strcpy(s2, s1);


Simplement, ça peut vous paraître idiot de réinventer la roue, puisque strncpy semble faire la même chose

Une fonction préjudiciable...


La gestion des erreurs

 

Le problème est que strncpy ne gère pas les erreurs. Il est impossible de savoir si la fonction a échoué car la destination était trop petite.

Le positionnement du caractère de fin de chaîne


De plus (et c'est inquiétant), aucun caractère de fin de chaîne n'est ajouté si on n'arrive pas à terme de la chaîne. Si on utilise d'autres fonctions de manipulations de chaînes, on se rend compte qu'elles prennent fortement en compte le positionnement du caractère de fin de chaîne. On a donc une forte vague d'erreurs qui s'enchaînent les unes les autres, et qui peuvent être difficiles à détecter.

On peut alors reprendre notre condition pour savoir si la taille n'est pas suffisante.

if (strlen(s1) + 1 > sizeof s2)    handle_errors();
else                               strncpy(s2, s1, sizeof s2);


Mais strncpy revient alors à utiliser strcpy. Pour remédier à ce problème de zéro, on peut le placer manuellement après l'appel de strncpy.

strncpy(s2, s1, s2_size);
s2[s2_size - 1] = 0;


Mais, là encore, on a un problème si s2_size vaut zéro... Une condition de plus à ajouter, ce qui est préjudiciable dans des cas critiques...

Les performances


strncpy est souvent contraignant pour les performances (pour que la fonction puisse tester la taille correctement, et parce qu'elle remplit les cases inutilisées par des zéros...).

La sécurité

 

Prétendu symbole de sécurité, la fonction est pourtant sujette à l'exploitation du non-positionnement du caractère de fin de chaîne. Cela pourrait faire l'objet d'un autre article. En attendant, je vous renvoie à cette page.

Conclusion

 

Vous l'avez compris, strncpy possède également des défauts. Je ne défends pas particulièrement son utilisation, mais, du moins, il vous serait intéressant d'éviter la propagation de l'idée reçu selon laquelle strncpy est une fonction sûre et efficace.

Mais alors qu'utiliser ? Reste encore snprintf (et oui, c'est du C99 !), qui a un système, certes simple, mais présent, de gestion des erreurs. Ce n'est pas très beau à utiliser, et c'est plus lent sur des petites chaînes. Mais ça me semble être une solution qui offre un bon compromis.

dimanche 24 juin 2012

La cathédrale et le bazar

Bonjour à tous !

Déjà une trentaine de pages vues depuis hier, c'est un bon début, merci aux lecteurs !

Ce billet ne constitue pas vraiment l'apprentissage d'une notion en particulier comme cela avait été fait hier, mais plutôt une petite analyse du fonctionnement de la bibliothèque standard de C.

Comme point de départ aujourd'hui, ce sera le titre d'un essai d'Éric Raymond, le célèbre hacker américain. Fervent défenseur du terme open source, il s'oppose au mode de développement de GNU et de Linux, le qualifiant de bazar.

La bibliothèque standard de C est souvent assimilée à ce mode de développement, et l'exemple léger que nous allons voir aujourd'hui peut potentiellement constituer une preuve du manque de rigueur du bazar.

 Vous avez sûrement déjà rencontré des fonctions qui manipulent des « éléments » d'une taille spécifiée plutôt que des bytes directement : qsort, bsearch, fread, fwrite ou encore calloc par exemple. Chacune de ses fonctions possède deux arguments pour spécifier la taille d'un élément et le nombre d'éléments.

Jusque là, pas de problème. Mais quand on regarde de plus près, on se rend compte qu'il y a des problèmes d'uniformité. Voyez par exemple ces deux prototypes :

void *calloc(size_t nmemb, size_t size);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);



On voit très distinctement que l'ordre des arguments est modifié. Dans un cas, on a « nmemb » suivi de « size » et dans l'autre, on a « size » suivi de « nmemb ».

Bon, et concrètement, qu'est-ce que ça change ? De toute manière, le nombre total de bytes manipulés sera égal au produit des arguments, et, quel que soit l'ordre dans lesquels on les spécifie, ce sera la même chose, non ?

En fait, là où ça coince, c'est dans la valeur retournée par la fonction : c'est le nombre d'éléments (nmemb) manipulés, et pas le nombre de bytes. Si on lit 5 éléments de 3 bytes ou 3 éléments de 5 bytes, le résultat sera sans doute identique, mais la valeur de retour de la fonction sera de 5 éléments dans un cas, et de 3 éléments dans l'autre.

En supposant que la valeur retournée permet de représenter combien d'éléments votre programme doit maintenant traiter, vous allez garder la taille et le nombre d'éléments directement, de peur que vous essayiez de lire 5 éléments et d'en traiter 3, ou bien de lire 3 éléménts et d'en traiter 5 !

Un problème avec le report d'éléments lus et écrits est quand vous avez une erreur d'entrées/sorties ou une fin de fichier au milieu d'un élément. Il est alors de difficile de déterminer exactement ce que vous avez lu. Par exemple, si vous essayiez de lire 5 éléments de 3 bytes et fread vous retourne 2, vous ne pouvez pas savoir si vous avez lu exactement 2 éléments, ou bien 2 et 1 byte puis EOF, etc. Pour contourner cela, on peut utiliser la taille de l'élément.

size_t bytesRead = fread(p, 1UL, 5 * sizeof *p, fp);

// De la même manière, on peut récupérer le nombre d'éléments à traiter
size_t nmemb = bytesRead / sizeof *p;

// On peut déterminer si on a été stoppé au milieu d'un élément.
if (bytesRead % sizeof *p) {
    /* on a été stoppé au milieu d'un élément */
}


J'ai vu pour la première fois cette idée exposée sur CCL, ça m'a paru anecdotique mais intéressant pour avoir un peu de culture geekeste !

samedi 23 juin 2012

Les fonctions imbriquées

Hello world !

Me voici donc pour ce premier billet de blog sérieux. Aujourd'hui, j'avais envie de vous parler des fonctions imbriquées (nested functions en anglais).

Une définition intuitive


Le mécanisme en soi est assez simple, il s'agit simplement de la définition d'une fonction à l'intérieur d'une autre fonction. Par exemple :

int
f(int k)
{
    int
    g(int n)
    {
        return 3 * n;
    }
    return 4 * g(k);
}


Ici, g est imbriquée dans la fonction f. Le principe d'une fonctions imbriquée est qu'elle est accessible par la fonction englobante et par les fonctions imbriquées dans cette fonction englobante.

Ce mécanisme peut se révéler utile, par exemple pour des problématiques de découpage de code en tâches et en sous-tâches, ou encore pour éviter de polluer l'espace de noms.

Un problème d'implémentation


Hélas, les fonctions imbriquées ne sont pas supportées par le C standard, et ceci pour une raison bien simple : que ce soit au moment de la compilation ou au runtime, ça complique beaucoup les opérations.

Concrètement, il y a deux cas de figures :
  • soit on interdit les références vers des variables locales à la fonction englobante dans les fonctions imbriquées, auquel cas les fonctions imbriquées n'auraient pas grand intérêt ;
  • soit on les autorise, et là c'est un peu plus compliqué. 

En effet, il faut faire attention d'accéder à la donnée correcte en faisant attention aux masquages. Et quid des variables allouées sur la pile dans fonction englobante ? Il faut garder ces variables dans un espace mémoire spécifique, où elles ne seront pas détruites... Et je ne parle même pas des appels récursifs et des pointeurs sur fonctions !

int
(*f)(void)
{
    int n = 42;
    int
    g(void)
    {
        return n + 1;
    }
    return &g;
}


Ce n'est pas impossible (des langages comme Algol ou D supportent correctement les fonctions imbriquées), mais ça ne rentre pas vraiment dans l'optique d'un langage natif comme le C.

Une extension gcc


Cependant, c'est une extension du langage, supportée par gcc, et activée lorsque le flag -fnested-functions est lui-même présent lors de la compilation. Le mécanisme fonctionne grâce à un trampoline.

Conclusion


Vous l'avez donc compris, le domaine des fonctions imbriquées est assez obscur, que ce soit par leur fonctionnement ou par leur lisibilité.

À mes yeux, la solution la plus propre reste d'utiliser des bonnes vieilles fonctions statiques communiquant avec quelques variables globales, ou bien en utilisant un pointeur de structure contenant le contexte en question...

Réservons les fonctions imbriquées pour des cas extrêmes !

Bienvenue sur le blog « À propos du C » !

Bonjour à tous les lecteurs (pour le moment, ils sont peu nombreux, et c'est normal).

Certains me connaissent peut-être, puisqu'ils ont sûrement découvert ce blog à travers un des sites d'informatique que je fréquente (pseudo : Lucas-84).

Cette plateforme de blog me permettra de rédiger tous les petits trucs qui me passent par la tête, notamment à propos du langage C.

Bonne lecture.