2010-03-12 23 views
51

Je veux connaître les inconvénients de scanf().Inconvénients de scanf

Dans de nombreux sites, j'ai lu que l'utilisation de scanf pouvait provoquer des dépassements de tampon. Quelle est la raison pour ça? Y at-il d'autres inconvénients avec scanf?

+0

Voir aussi [Un guide du débutant loin de 'scanf()'] (http://sekrit.de/webdocs/c/beginners-guide-away- from-scanf.html). –

Répondre

48

Les problèmes avec scanf sont (au minimum):

  • utilisant %s pour obtenir une chaîne de l'utilisateur, ce qui conduit à la possibilité que la chaîne peut être plus long que votre tampon, ce qui provoque le débordement.
  • la possibilité d'un échec d'analyse laissant votre pointeur de fichier dans un emplacement indéterminé.

Je préfère de beaucoup utiliser fgets pour lire des lignes entières afin de limiter la quantité de données lues. Si vous avez un tampon de 1 Ko, et que vous y lisiez une ligne avec fgets, vous pouvez dire si la ligne était trop longue du fait qu'il n'y a pas de caractère de fin de ligne (dernière ligne d'un fichier sans modification de ligne).

Ensuite, vous pouvez vous plaindre à l'utilisateur, ou allouer plus d'espace pour le reste de la ligne (en continu si nécessaire jusqu'à ce que vous avez suffisamment d'espace). Dans les deux cas, il n'y a pas de risque de dépassement de tampon.

Une fois que vous avez lu la ligne, vous savez que vous êtes placé à la ligne suivante, donc il n'y a pas de problème. Vous pouvez ensuite sscanf votre chaîne au contenu de votre coeur sans avoir à enregistrer et restaurer le pointeur de fichier pour la relecture.

Voici un extrait de code que j'utilise fréquemment pour éviter tout débordement de tampon lorsque je demande des informations à l'utilisateur.

Il peut être facilement ajusté pour utiliser un fichier autre que l'entrée standard si nécessaire et vous pouvez également lui attribuer son propre tampon (et l'augmenter jusqu'à ce qu'il soit assez grand) avant de le renvoyer à l'appelant (bien que l'appelant serait alors responsable de la libérer, bien sûr).

#include <stdio.h> 
#include <string.h> 

#define OK  0 
#define NO_INPUT 1 
#define TOO_LONG 2 
static int getLine (char *prmpt, char *buff, size_t sz) { 
    int ch, extra; 

    // Get line with buffer overrun protection. 
    if (prmpt != NULL) { 
     printf ("%s", prmpt); 
     fflush (stdout); 
    } 
    if (fgets (buff, sz, stdin) == NULL) 
     return NO_INPUT; 

    // If it was too long, there'll be no newline. In that case, we flush 
    // to end of line so that excess doesn't affect the next call. 
    if (buff[strlen(buff)-1] != '\n') { 
     extra = 0; 
     while (((ch = getchar()) != '\n') && (ch != EOF)) 
      extra = 1; 
     return (extra == 1) ? TOO_LONG : OK; 
    } 

    // Otherwise remove newline and give string back to caller. 
    buff[strlen(buff)-1] = '\0'; 
    return OK; 
} 

 

// Test program for getLine(). 

int main (void) { 
    int rc; 
    char buff[10]; 

    rc = getLine ("Enter string> ", buff, sizeof(buff)); 
    if (rc == NO_INPUT) { 
     // Extra NL since my system doesn't output that on EOF. 
     printf ("\nNo input\n"); 
     return 1; 
    } 

    if (rc == TOO_LONG) { 
     printf ("Input too long [%s]\n", buff); 
     return 1; 
    } 

    printf ("OK [%s]\n", buff); 

    return 0; 
} 

Un essai:

$ ./tstprg 
Enter string>[CTRL-D] 
No input 

$ ./tstprg 
Enter string> a 
OK [a] 

$ ./tstprg 
Enter string> hello 
OK [hello] 

$ ./tstprg 
Enter string> hello there 
Input too long [hello the] 

$ ./tstprg 
Enter string> i am pax 
OK [i am pax] 
+0

'if (fgets (buff, sz, stdin) == NULL) renvoient NO_INPUT;' Pourquoi avez-vous utilisé 'NO_INPUT' comme valeur de retour? 'fgets' renvoie' NULL' uniquement en cas d'erreur. –

+0

@Fabio, pas tout à fait. Il renvoie également null si le flux est fermé avant toute entrée. C'est le cas d'être pris ici. Ne faites pas l'erreur que NO_INPUT signifie une entrée vide (en appuyant sur ENTER avant toute autre chose) - ce dernier vous donne une chaîne vide sans code d'erreur NO_INPUT. – paxdiablo

+0

La dernière norme POSIX autorise 'char * buf; scanf ("% ms", &buf); 'qui vous allouera suffisamment d'espace avec' malloc' (il doit donc être libéré plus tard), ce qui empêcherait les dépassements de tampon – dreamlax

5

Oui, vous avez raison. Il y a une faille de sécurité majeure dans scanf famille (scanf, sscanf, fscanf ..etc) esp lors de la lecture d'une chaîne, car ils ne prennent pas la longueur de la mémoire tampon (dans lequel ils sont la lecture) en compte.

Exemple:

char buf[3]; 
sscanf("abcdef","%s",buf); 

clairement le tampon buf peut contenir MAX 3 car. Mais le sscanf va essayer de mettre "abcdef" provoquant en elle dépassement de mémoire tampon.

+2

Vous pouvez fournir "% 10s" comme spécificateur de format et ne lira pas plus de 10 caractères dans le tampon – dreamlax

+0

Oui..peut fonctionner ... mais utilise-t-on toujours scanf avec une largeur? – codaddict

+5

Bien sûr - il est possible d'utiliser l'API en toute sécurité.Il est également possible d'utiliser de la dynamite pour enlever la saleté –

12

De l'comp.lang.c FAQ: Why does everyone say not to use scanf? What should I use instead?

scanf a un certain nombre de problèmes, voir les questions 12.1712.18a et 12.19. En outre, son format %s a le même problème que gets() (voir la question 12.23) - il est difficile de garantir que le tampon de réception ne débordera pas. Plus généralement, scanf est conçu pour une entrée formatée relativement structurée (son nom est en fait dérivé de "formaté en balayage"). Si vous faites attention, il vous dira s'il a réussi ou échoué, mais il peut vous dire seulement approximativement où il a échoué, et pas du tout comment ou pourquoi. Vous avez très peu l'occasion de faire une récupération d'erreur. Cependant, l'entrée interactive de l'utilisateur est l'entrée la moins structurée qui soit. Une interface utilisateur bien conçue permettra à l'utilisateur de saisir à peu près n'importe quoi, pas seulement des lettres ou des signes de ponctuation, mais aussi plus ou moins de caractères que prévu, ou aucun caractère (soit, juste la touche RETURN), ou EOF prématurée, ou quoi que ce soit. Il est presque impossible de traiter avec élégance tous ces problèmes potentiels lors de l'utilisation scanf; il est beaucoup plus facile de lire des lignes entières (avec fgets ou similaire), puis de les interpréter, soit en utilisant sscanf ou d'autres techniques. (Des fonctions comme strtol, strtok et atoi sont souvent utiles, voir également les questions 12.16 et 13.6.) Si vous utilisez une variante scanf, assurez-vous de vérifier la valeur de retour pour vous assurer que le nombre attendu d'éléments a été trouvé. En outre, si vous utilisez %s, veillez à protéger contre le dépassement de tampon.

Notez, en passant, que les critiques de scanf ne sont pas nécessairement des actes d'accusation de fscanf et sscanf. scanf lit à partir de stdin, qui est généralement un clavier interactif et est donc le moins contraint, ce qui entraîne le plus de problèmes. Quand un fichier de données a un format connu, d'un autre côté, il peut être approprié de le lire avec fscanf. Il est parfaitement approprié d'analyser les chaînes avec sscanf (tant que la valeur de retour est vérifiée), car il est si facile de reprendre le contrôle, de redémarrer le scan, de rejeter l'entrée si elle ne correspond pas ...

Liens supplémentaires:

Références: K & R2 Sec. 7.4 p. 159

4

Il est très difficile d'obtenir scanf pour faire ce que vous voulez. Bien sûr, vous pouvez, mais des choses comme scanf("%s", buf); sont aussi dangereuses que gets(buf);, comme tout le monde l'a dit.

À titre d'exemple, ce que paxdiablo fait dans sa fonction de lecture peut se faire avec quelque chose comme:

scanf("%10[^\n]%*[^\n]", buf)); 
getchar(); 

Le plus va lire une ligne, stocker les 10 premiers caractères non-retour à la ligne dans buf et puis jetez tout jusqu'à (et y compris) une nouvelle ligne.Ainsi, la fonction de paxdiablo pourrait être écrit en utilisant scanf la manière suivante:

#include <stdio.h> 

enum read_status { 
    OK, 
    NO_INPUT, 
    TOO_LONG 
}; 

static int get_line(const char *prompt, char *buf, size_t sz) 
{ 
    char fmt[40]; 
    int i; 
    int nscanned; 

    printf("%s", prompt); 
    fflush(stdout); 

    sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1); 
    /* read at most sz-1 characters on, discarding the rest */ 
    i = scanf(fmt, buf, &nscanned); 
    if (i > 0) { 
     getchar(); 
     if (nscanned >= sz) { 
      return TOO_LONG; 
     } else { 
      return OK; 
     } 
    } else { 
     return NO_INPUT; 
    } 
} 

int main(void) 
{ 
    char buf[10+1]; 
    int rc; 

    while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) { 
     if (rc == TOO_LONG) { 
      printf("Input too long: "); 
     } 
     printf("->%s<-\n", buf); 
    } 
    return 0; 
} 

L'un des autres problèmes avec scanf est son comportement en cas de débordement. Par exemple, lors de la lecture d'un int:

int i; 
scanf("%d", &i); 

ci-dessus ne peut être utilisé en toute sécurité en cas d'un trop-plein. Même pour le premier cas, lire une chaîne est beaucoup plus simple à faire avec fgets plutôt qu'avec scanf.

+0

Wow Alok! Quand avez-vous surpassé 10k? Félicitations et continuez! :) – dreamlax

+0

@dreamlax: J'ai passé 10k ce mois-ci je pense. On dirait que tu es assez proche aussi. Merci pour le message. :-) –

46

La plupart des réponses jusqu'à présent semblent se concentrer sur le problème de débordement de la chaîne de caractères. En réalité, les spécificateurs de format pouvant être utilisés avec les fonctions scanf prennent en charge le paramètre de largeur de champ explicite, qui limite la taille maximale de l'entrée et empêche le débordement de la mémoire tampon. Cela rend les accusations populaires de dangers de débordement de tampon de chaîne présents dans scanf pratiquement sans fondement. Prétendre que scanf est en quelque sorte analogue à gets dans le respect est complètement incorrect. Il existe une différence qualitative majeure entre scanf et gets: scanf fournit à l'utilisateur des fonctionnalités de prévention de débordement de chaîne de caractères, tandis que gets ne le fait pas. On peut arguer que ces caractéristiques scanf sont difficiles à utiliser, car la largeur de champ doit être intégrée dans la chaîne de format (il n'y a aucun moyen de passer à travers un argument variadique, comme cela peut être fait dans printf). C'est réellement vrai. scanf est en effet plutôt mal conçu à cet égard. Mais néanmoins toute réclamation qui scanf est d'une manière ou d'une autre irrémédiablement cassée en ce qui concerne la sûreté de dépassement de chaîne-tampon est complètement fausse et habituellement faite par les programmeurs paresseux.

Le vrai problème avec scanf a une nature complètement différente, même si elle est aussi débordement. Lorsque la fonction scanf est utilisée pour convertir des représentations décimales de nombres en valeurs de types arithmétiques, elle n'offre aucune protection contre le dépassement arithmétique. Si un débordement se produit, scanf génère un comportement indéfini. Pour cette raison, la seule façon appropriée d'effectuer la conversion dans la bibliothèque standard C est les fonctions de la famille strto.... Donc, pour résumer ce qui précède, le problème avec scanf est qu'il est difficile (bien que possible) d'utiliser correctement et en toute sécurité avec des tampons de chaîne. Et il est impossible d'utiliser en toute sécurité pour l'entrée arithmétique. Ce dernier est le vrai problème. Le premier est juste un inconvénient.

P.S. Ce qui précède est destiné à être sur la famille entière de scanf fonctions (y compris également fscanf et sscanf). Avec scanf spécifiquement, le problème évident est que l'idée même d'utiliser une fonction strictement formatée pour lire potentiellement interactif entrée est plutôt discutable.

+2

Je dois juste préciser, ce n'est pas que vous ne pouvez pas lire l'entrée arithmétique en toute sécurité, plus que vous ne pouvez pas le faire correctement * et * robuste pour entrée sale. Pour moi, il y a une énorme différence entre écraser mon programme et/ou ouvrir le système d'exploitation pour attaquer et obtenir simplement quelques fausses valeurs lorsque les utilisateurs essaient un méfait intentionnel. Qu'est-ce que je me soucie s'ils ont tapé 1431337.4044194872987 et ont obtenu 4.0 à la place? De toute façon, ils sont entrés 4.0. (Quelquefois cela peut être important, mais à quelle fréquence?) –

+0

AnT, qu'entendez-vous par «convertir des représentations décimales de nombres en valeurs de types arithmétiques»? Pouvez-vous donner un exemple? Merci – snowfox

+0

@snowfox: Je veux simplement dire convertir une * chaîne * '" 123 "' en une représentation interne * entière du nombre '123'. – AnT

3

Les problèmes que j'ai avec la famille *scanf():

  • potentiel de dépassement de tampon avec% s et% [spécificateurs de conversion. Oui, vous pouvez spécifier une largeur de champ maximale, mais contrairement à printf(), vous ne pouvez pas en faire un argument dans l'appel scanf(); il doit être codé en dur dans le spécificateur de conversion.
  • Potentiel de dépassement arithmétique avec% d,% i, etc.
  • Capacité limitée de détection et de rejet des entrées mal formées. Par exemple, "12w4" n'est pas un nombre entier valide, mais scanf("%d", &value); convertira avec succès et attribuera 12 à value, laissant le "w4" bloqué dans le flux d'entrée pour bloquer une lecture future. Idéalement, toute la chaîne d'entrée doit être rejetée, mais scanf() ne vous donne pas un mécanisme facile pour le faire.

Si vous savez que votre entrée va toujours être bien formé avec des chaînes de longueur fixe et des valeurs numériques qui ne flirtent pas avec trop-plein, puis scanf() est un excellent outil. Si vous utilisez des entrées ou des entrées interactives dont la solidité n'est pas garantie, utilisez autre chose.

+1

Quelles sont les autres alternatives saines pour lire les chaînes de caractères et les valeurs numériques en toute sécurité? –

2

De nombreuses réponses discuter ici les problèmes de dépassement potentiels de l'utilisation scanf("%s", buf), mais la dernière spécification POSIX plus ou moins résout ce problème en fournissant un caractère d'allocation d'affectation m qui peut être utilisé au format spécificateurs pour c, s, et [ formats. Cela permettra à scanf d'allouer autant de mémoire que nécessaire avec malloc (il doit donc être libéré plus tard avec free).

Un exemple de son utilisation:

char *buf; 
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char. 

// use buf 

free(buf); 

Voir here. Les inconvénients de cette approche sont qu'elle est un ajout relativement récent à la spécification POSIX et qu'elle n'est pas du tout spécifiée dans la spécification C, elle reste donc peu utilisable pour l'instant.

3

Il y a un gros problème avec scanf -comme des fonctions - l'absence de toute sécurité de type. Autrement dit, vous pouvez coder ceci:

int i; 
scanf("%10s", &i); 

Enfer, même cela est « bien »:

scanf("%10s", i); 

Il est pire que printf -comme fonctions, parce que scanf attend un pointeur, si les accidents sont plus susceptibles .

Bien sûr, il existe des vérificateurs de spécification de format, mais ceux-ci ne sont pas parfaits et ne font pas partie du langage ou de la bibliothèque standard.

2

L'avantage de scanf est une fois que vous apprendrez comment utiliser l'outil, comme vous devriez toujours faire en C, il a usecases extrêmement utile. Vous pouvez apprendre à utiliser scanf et vos amis en lisant et en comprenant the manual. Si vous ne pouvez pas passer à travers ce manuel sans problèmes de compréhension graves, cela signifierait probablement que vous ne connaissez pas très bien C.


scanf et les amis ont souffert des choix de conception malheureux qui a rendu difficile (et parfois impossible) d'utiliser correctement sans lire la documentation, comme les autres réponses ont montré. Cela se produit tout au long de C, malheureusement, donc si je devais déconseiller d'utiliser scanf alors je déconseillerais probablement d'utiliser C.

L'un des plus grands inconvénients semble être purement la réputation qu'il gagne parmi les non-initiés; Comme avec de nombreuses fonctionnalités utiles de C, nous devrions être bien informés avant de l'utiliser. La clé est de réaliser que, comme pour le reste de C, il semble succinct et idiomatique, mais cela peut être subtilement trompeur. Ceci est omniprésent en C; il est facile pour les débutants d'écrire un code qu'ils pensent logique et qui pourrait même fonctionner pour eux au début, mais cela n'a aucun sens et peut échouer de manière catastrophique.

Par exemple, les non-initiés attendent généralement que le délégué %s causerait une ligne à lire, et tout cela peut sembler intuitive, il est pas nécessairement vrai. Il est plus approprié de décrire le champ lu comme un mot. La lecture du manuel est fortement conseillée pour chaque fonction.

Quelle serait la réponse à cette question sans mentionner son manque de sécurité et le risque de débordement de la mémoire tampon? Comme nous l'avons déjà mentionné, C n'est pas un langage sûr et nous permettra de couper les coins, éventuellement d'appliquer une optimisation au détriment de l'exactitude ou plus probablement parce que nous sommes des programmeurs paresseux. Ainsi, quand nous savons que le système ne recevra jamais une chaîne plus grande qu'un nombre fixe d'octets, nous avons la possibilité de déclarer un tableau de cette taille et de renoncer à la vérification des limites. Je ne vois pas vraiment cela comme une chute; c'est une option. Encore une fois, la lecture du manuel est fortement conseillée et nous révélerait cette option.

Les programmeurs paresseux ne sont pas les seuls piqués par scanf . Il n'est pas rare de voir des gens essayer de lire les valeurs float ou double en utilisant %d, par exemple. Ils ont généralement tort de croire que la mise en œuvre effectuera une sorte de conversion en coulisse, ce qui serait logique car des conversions similaires se produisent dans le reste de la langue, mais ce n'est pas le cas ici. Comme je l'ai dit plus tôt, scanf et amis (et en effet le reste de C) sont trompeuses; ils semblent succincts et idiomatiques mais ils ne le sont pas.

Les programmeurs inexpérimentés ne sont pas obligés de considérer le succès de l'opération. Supposons que l'utilisateur entre quelque chose de complètement non numérique quand nous avons dit scanf pour lire et convertir une séquence de chiffres décimaux en utilisant %d. La seule façon d'intercepter de telles données erronées est de vérifier la valeur de retour, et à quelle fréquence est-ce que nous prenons la peine de vérifier la valeur de retour? Tout comme fgets, lorsque scanf et les amis ne lisent pas ce qu'ils sont censés lire, le flux restera dans un état inhabituel; - Dans le cas de fgets, s'il n'y a pas suffisamment d'espace pour stocker une ligne complète, le reste de la ligne non lue pourrait être traité de manière erronée comme s'il s'agissait d'une nouvelle ligne alors qu'elle ne l'est pas. - Dans le cas de scanf et des amis, une conversion a échoué comme indiqué ci-dessus, les données erronées ne sont pas lues dans le flux et peuvent être traitées de manière erronée comme si elles faisaient partie d'un champ différent.

Il n'est pas plus facile d'utiliser scanf que d'utiliser fgets.Si nous vérifions le succès en recherchant un '\n' lorsque nous utilisons fgets ou en inspectant la valeur de retour lorsque nous utilisons scanf et amis, et nous constatons que nous avons lu une ligne incomplète en utilisant fgets ou échoué à lire un champ en utilisant scanf , alors nous sommes confrontés à la même réalité: nous sommes susceptibles de ignorer l'entrée (généralement jusqu'à et y compris la nouvelle ligne suivante)! Yuuuuuuck!

Malheureusement, scanf rend simultanément difficile (non intuitif) et facile (le moins grand nombre de frappes) d'ignorer les entrées de cette manière. Face à cette réalité d'abandon de l'entrée utilisateur, certains ont essayé scanf("%*[^\n]%*c"); , sans se rendre compte que le délégué %*[^\n] échouera lorsqu'il ne rencontrera rien d'autre qu'un saut de ligne, et donc le retour à la ligne sera toujours laissé sur le flux.

Une légère adaptation, en séparant les deux délégués de format et nous voyons un certain succès ici: scanf("%*[^\n]"); getchar();. Essayez de le faire avec si peu de frappes en utilisant un autre outil;)