C strcpy () – le mal?

Certaines personnes semblent penser que la fonction strcpy() de C est mauvaise ou mauvaise. Bien que j’admette qu’il est généralement préférable d’utiliser strncpy() afin d’éviter les débordements de mémoire tampon, voici ce qui suit (une implémentation de la fonction strdup() pour ceux qui n’ont pas la chance de l’avoir) utilise strcpy() toute sécurité: elle ne doit jamais déborder:

 char *strdup(const char *s1) { char *s2 = malloc(strlen(s1)+1); if(s2 == NULL) { return NULL; } strcpy(s2, s1); return s2; } 

*s2 est garanti que *s2 dispose de suffisamment d’espace pour stocker *s1 , et l’utilisation de strcpy() nous évite d’avoir à stocker le résultat strlen() autre fonction à utiliser ultérieurement comme paramètre de longueur inutile (dans ce cas) de strncpy() . Pourtant, certaines personnes écrivent cette fonction avec strncpy() , voire memcpy() , qui nécessitent un paramètre de longueur. J’aimerais savoir ce que les gens en pensent. Si vous pensez que strcpy() est sans danger dans certaines situations, dites-le. Si vous avez une bonne raison de ne pas utiliser strcpy() dans cette situation, donnez-le-moi. J’aimerais savoir pourquoi il serait peut-être préférable d’utiliser strncpy() ou memcpy() dans des situations comme celle-ci. Si vous pensez que strcpy() est correct, mais pas ici, veuillez expliquer.

Fondamentalement, je veux juste savoir pourquoi certaines personnes utilisent memcpy() alors que d’autres utilisent strcpy() et que d’autres encore utilisent plain strncpy() . Existe-t-il une logique pour en préférer un aux trois (sans tenir compte des contrôles de la mémoire tampon des deux premiers)?

memcpy peut être plus rapide que strcpy et strncpy car il n’est pas nécessaire de comparer chaque octet copié avec ‘\ 0’ et de connaître la longueur de l’object copié. Il peut être mis en œuvre de la même manière avec le périphérique de Duff ou utiliser des instructions d’assembleur qui copient plusieurs octets à la fois, comme movsw et movsd.

Je suis les règles ici . Laissez-moi en citer

strncpy été initialement introduit dans la bibliothèque C pour traiter les champs de noms de longueur fixe dans des structures telles que les entrées de répertoire. De tels champs ne sont pas utilisés de la même manière que les chaînes: la valeur NULL de fin n’est pas nécessaire pour un champ de longueur maximale et la définition d’octets de fin pour les noms plus courts sur NULL garantit des comparaisons efficaces entre les champs. La stratégie n’est pas d’origine une «stratégie liée», et le Comité a préféré reconnaître la pratique existante plutôt que de modifier la fonction afin de mieux l’adapter à une telle utilisation.

Pour cette raison, vous n’obtiendrez pas un '\0' dans une chaîne si vous appuyez sur le n ne pas trouver un '\0' dans la chaîne source jusqu’à présent. Il est facile d’en abuser (bien sûr, si vous connaissez ce piège, vous pouvez l’éviter). Comme le dit la citation, elle n’était pas conçue comme une stratégie limitée. Et je préférerais ne pas l’utiliser sinon nécessaire. Dans votre cas, son utilisation n’est manifestement pas nécessaire et vous l’avez prouvé. Pourquoi alors l’utiliser?

De manière générale, le code de programmation vise également à réduire la redondance. Si vous savez que vous avez une chaîne contenant des caractères ‘n’, pourquoi demander à la fonction de copie de copier un maximum de n caractères? Vous effectuez une vérification redondante. Cela concerne peu les performances, mais beaucoup plus de code cohérent. Les lecteurs vont se demander ce strcpy pourrait faire une strcpy qui pourrait traverser les n caractères et qui rend nécessaire de limiter la copie, il suffit de lire dans les manuels que cela ne peut pas se produire dans ce cas. Et là, la confusion commence à se produire parmi les lecteurs du code.

Pour utiliser rationnellement mem- , str- ou strn- , j’ai choisi parmi eux, comme dans le document lié ci-dessus:

mem- quand je veux copier des octets bruts, comme des octets d’une structure.

str- lors de la copie d’une chaîne terminée par un caractère NULL – uniquement lorsque 100% du nombre de dépassements de str- est str-

strn- lors de la copie d’une chaîne terminée par un caractère NULL d’une longueur maximale, en remplissant les octets restants par zéro. Probablement pas ce que je veux dans la plupart des cas. Il est facile d’oublier le fait avec le remplissage à zéro suivi, mais cela est voulu comme l’explique la citation ci-dessus. Donc, je voudrais juste coder ma propre petite boucle qui copie les caractères, en ajoutant un '\0' :

 char * sstrcpy(char *dst, char const *src, size_t n) { char *ret = dst; while(n-- > 0) { if((*dst++ = *src++) == '\0') return ret; } *dst++ = '\0'; return ret; } 

Juste quelques lignes qui font exactement ce que je veux. Si je voulais la “vitesse brute”, je peux toujours rechercher une implémentation portable et optimisée qui effectue exactement ce travail ssortingcpy lié . Comme toujours, commencez par profiler, puis déconnez-vous.

Plus tard, C a eu des fonctions pour travailler avec des caractères larges, appelées wcs- et wcsn- (pour C99 ). Je les utiliserais de même.

La raison pour laquelle les gens utilisent strncpy et non pas strcpy est que les chaînes ne sont pas toujours terminées par un zéro et qu’il est très facile de saturer le tampon (l’espace que vous avez alloué pour la chaîne avec strcpy) et d’écraser un bit de mémoire non associé.

Avec strcpy, cela peut arriver, avec strncpy, cela n’arrivera jamais . C’est pourquoi strcpy est considéré comme dangereux. Le mal pourrait être un peu fort.

Franchement, si vous manipulez beaucoup de chaînes en C, vous ne devriez pas vous demander si vous devez utiliser strcpy strncpy ou memcpy . Vous devriez trouver ou écrire une bibliothèque de chaînes fournissant une abstraction de niveau supérieur. Par exemple, celui qui garde la trace de la longueur de chaque chaîne, alloue de la mémoire pour vous et fournit toutes les opérations sur les chaînes dont vous avez besoin.

Cela garantira presque certainement que vous ferez très peu des types d’erreurs généralement associées à la gestion de la chaîne C, telles que les dépassements de mémoire tampon, l’oubli de terminer une chaîne avec un octet NUL, etc.

La bibliothèque pourrait avoir des fonctions telles que celles-ci:

 typedef struct MySsortingng MySsortingng; MySsortingng *myssortingng_new(const char *c_str); MySsortingng *myssortingng_new_from_buffer(const void *p, size_t len); void myssortingng_free(MySsortingng *s); size_t myssortingng_len(MySsortingng *s); int myssortingng_char_at(MySsortingng *s, size_t offset); MySsortingng *myssortingng_cat(MySsortingng *s1, ...); /* NULL terminated list */ MySsortingng *myssortingng_copy_subssortingng(MySsortingng *s, size_t start, size_t max_chars); MySsortingng *myssortingng_find(MySsortingng *s, MySsortingng *pattern); size_t myssortingng_find_char(MySsortingng *s, int c); void myssortingng_copy_out(void *output, MySsortingng *s, size_t max_chars); int myssortingng_write_to_fd(int fd, MySsortingng *s); int myssortingng_write_to_file(FILE *f, MySsortingng *s); 

J’en ai écrit un pour le projet Kannel , voir le fichier gwlib / octstr.h. Cela nous a rendu la vie beaucoup plus simple. D’autre part, une telle bibliothèque est assez simple à écrire, vous pouvez donc en écrire une pour vous-même, même s’il ne s’agit que d’un exercice.

Personne n’a encore mentionné strlcpy , développé par Todd C. Miller et Theo de Raadt . Comme ils disent dans leur papier:

L’idée fausse la plus courante est que strncpy() NUL-termine la chaîne de destination. Cela n’est toutefois vrai que si la longueur de la chaîne source est inférieure au paramètre size. Cela peut être problématique lors de la copie d’une entrée utilisateur pouvant être de longueur arbitraire dans un tampon de taille fixe. Le moyen le plus sûr d’utiliser strncpy() dans cette situation est de le transmettre un moins que la taille de la chaîne de destination, puis de terminer la chaîne à la main. De cette façon, vous avez la garantie de toujours avoir une chaîne de destination terminée par NUL.

Il existe des contre-arguments pour l’utilisation de strlcpy ; la page Wikipedia fait remarquer que

Drepper affirme que strlcpy et strlcat facilitent l’ignorance des erreurs de troncature et peuvent donc introduire plus de bogues qu’ils n’en suppriment. *

Cependant, j’estime que cela oblige simplement les personnes qui savent ce qu’elles font à append une terminaison NULL manuelle, en plus d’un ajustement manuel de l’argument de strncpy . L’utilisation de strlcpy permet d’éviter plus facilement les dépassements de mémoire tampon, car vous avez omis de mettre fin à la mémoire tampon.

Notez également que l’absence de strlcpy dans la glibc ou dans les bibliothèques de Microsoft ne doit pas être un obstacle à l’utilisation; vous pouvez trouver le code source de strlcpy et de vos amis dans n’importe quelle dissortingbution BSD, et la licence vous permettra probablement de réaliser votre projet commercial / non commercial. Voir le commentaire en haut de strlcpy.c .

Personnellement, je suis d’avis que si le code peut être prouvé valide et si rapidement, il est parfaitement acceptable. Autrement dit, si le code est simple et donc bien correct, alors tout va bien.

Cependant, votre hypothèse semble être que pendant l’exécution de votre fonction, aucun autre thread ne modifiera la chaîne pointée par s1 . Que se passe-t-il si cette fonction est interrompue après l’allocation réussie de la mémoire (et donc l’appel de strlen ), la chaîne se développe et si vous avez une condition de dépassement de la mémoire tampon puisque strcpy dans l’octet NULL.

Ce qui suit pourrait être mieux:

 char * strdup(const char *s1) { int s1_len = strlen(s1); char *s2 = malloc(s1_len+1); if(s2 == NULL) { return NULL; } strncpy(s2, s1, s1_len); return s2; } 

Maintenant, la chaîne peut pousser sans faute de votre part et vous êtes en sécurité. Le résultat ne sera pas un dup, mais ce ne sera pas non plus un débordement fou.

La probabilité que le code que vous avez fourni soit réellement un bogue est assez faible (assez proche de l’inexistant, voire inexistant, si vous travaillez dans un environnement qui ne prend pas en charge le threading). C’est juste quelque chose à penser.

ETA : Voici une implémentation légèrement meilleure:

 char * strdup(const char *s1, int *retnum) { int s1_len = strlen(s1); char *s2 = malloc(s1_len+1); if(s2 == NULL) { return NULL; } strncpy(s2, s1, s1_len); retnum = s1_len; return s2; } 

Là, le nombre de caractères est renvoyé. Vous pouvez également:

 char * strdup(const char *s1) { int s1_len = strlen(s1); char *s2 = malloc(s1_len+1); if(s2 == NULL) { return NULL; } strncpy(s2, s1, s1_len); s2[s1_len+1] = '\0'; return s2; } 

Ce qui le terminera par un octet NUL . De toute façon, c’est mieux que celui que j’ai rapidement assemblé.

Je suis d’accord. Cependant, je recommanderais contre strncpy() , car il strncpy() toujours votre sortie à la longueur indiquée. C’est une décision historique qui, à mon avis, était vraiment regrettable, car elle aggrave sérieusement la performance.

Considérons le code comme ceci:

 char buf[128]; strncpy(buf, "foo", sizeof buf); 

Cela n’écrira pas les quatre caractères attendus dans buf , mais écrira à la place “foo” suivi de 125 caractères zéro. Si, par exemple, vous collectez beaucoup de chaînes courtes, cela signifie que vos performances réelles sont bien pires que prévu.

Si disponible, je préfère utiliser snprintf() , en écrivant ce qui précède comme snprintf() :

 snprintf(buf, sizeof buf, "foo"); 

Si au lieu de cela copier une chaîne non constante, c’est comme ceci:

 snprintf(buf, sizeof buf, "%s", input); 

Ceci est important, car si l’ input contient% caractères, snprintf() les interprétera, ouvrant ainsi toute une panoplie de boîtes de vers.

Je pense que strncpy est mal aussi.

Pour vous protéger réellement des erreurs de programmation de ce type, vous devez empêcher toute écriture de code qui (a) semble correct et (b) dépasse le tampon.

Cela signifie que vous avez besoin d’une véritable abstraction de chaîne, qui stocke la mémoire tampon et la capacité de manière opaque, les lie, pour toujours, et vérifie les limites. Sinon, vous finissez par passer des cordes et leurs capacités dans tout le magasin. Une fois que vous arrivez à de véritables opérations sur les chaînes, comme modifier le milieu d’une chaîne, il est presque aussi facile de passer la longueur incorrecte à strncpy (et surtout strncat), que d’appeler strcpy avec une destination trop petite.

Bien sûr, vous pouvez toujours demander s’il faut utiliser strncpy ou strcpy pour implémenter cette abstraction: strncpy est plus sûr là-bas à condition de bien comprendre ce qu’elle fait. Mais dans le code d’application de traitement des chaînes, s’appuyer sur strncpy pour éviter les débordements de mémoire tampon revient à porter un demi-préservatif.

Ainsi, votre strdup-replacement pourrait ressembler à quelque chose comme ceci (ordre de définition modifié pour vous garder en suspens):

 ssortingng *ssortingng_dup(const ssortingng *s1) { ssortingng *s2 = ssortingng_alloc(ssortingng_len(s1)); if (s2 != NULL) { ssortingng_set(s2,s1); } return s2; } static inline size_t ssortingng_len(const ssortingng *s) { return strlen(s->data); } static inline void ssortingng_set(ssortingng *dest, const ssortingng *src) { // potential (but unlikely) performance issue: strncpy 0-fills dest, // even if the src is very short. We may wish to optimise // by switching to memcpy later. But strncpy is better here than // strcpy, because it means we can use ssortingng_set even when // the length of src is unknown. strncpy(dest->data, src->data, dest->capacity); } ssortingng *ssortingng_alloc(size_t maxlen) { if (maxlen > SIZE_MAX - sizeof(ssortingng) - 1) return NULL; ssortingng *self = malloc(sizeof(ssortingng) + maxlen + 1); if (self != NULL) { // empty ssortingng self->data[0] = '\0'; // strncpy doesn't NUL-terminate if it prevents overflow, // so exclude the NUL-terminator from the capacity, set it now, // and it can never be overwritten. self->capacity = maxlen; self->data[maxlen] = '\0'; } return self; } typedef struct ssortingng { size_t capacity; char data[0]; } ssortingng; 

Le problème de ces abstractions de chaînes est que personne ne peut en convenir (par exemple, si les idiosyncrasies de strncpy mentionnées dans les commentaires ci-dessus sont bonnes ou mauvaises, si vous avez besoin de chaînes immuables et / ou à copier en écriture partageant des tampons lorsque vous créez une sous-chaîne. , etc). Donc, bien qu’en théorie, vous devriez en prendre un sur le marché, vous pouvez en obtenir un par projet.

J’aurais tendance à utiliser memcpy si j’avais déjà calculé la longueur. Bien que strcpy soit généralement optimisé pour fonctionner avec des mots-clés, il est strcpy fournir à la bibliothèque autant d’informations que possible pour qu’elle puisse utiliser la copie optimale. mécanisme.

Mais pour l’exemple que vous donnez, peu importe – si ça va échouer, ce sera dans l’ordre du jour initial, donc strncpy ne vous achète rien en termes de sécurité (et vraisemblablement, strncpy est plus lent que les deux check bounds and for nul), et toute différence entre memcpy et strcpy ne vaut pas la peine de changer de code de manière spéculative.

La perversité survient lorsque les gens l’utilisent comme ceci (bien que le bas soit super simplifié)

 void BadFunction(char *input) { char buffer[1024]; //surely this will **always** be enough strcpy(buffer, input); ... } 

Ce qui est une situation surprenante qui arrive souvent.

Mais oui, strcpy est aussi bon que strncpy dans toutes les situations dans lesquelles vous allouez de la mémoire pour le tampon de destination et avez déjà utilisé strlen pour rechercher la longueur.

strlen trouve jusqu’à la dernière place null.

Mais en réalité, les tampons ne sont pas nullement terminés.

c’est pourquoi les gens utilisent différentes fonctions.

Bien, strcpy () n’est pas aussi diabolique que strdup () – au moins, strcpy () fait partie de la norme C.

Dans la situation que vous décrivez, strcpy est un bon choix. Cette strdup n’aura des ennuis que si la s1 n’a pas été terminée avec un ‘\ 0’.

J’appendais un commentaire indiquant pourquoi il n’y a pas de problème avec strcpy, pour empêcher les autres (et vous-même, dans un an) de s’interroger trop longtemps sur son exactitude.

Strncpy semble souvent en sécurité, mais peut vous causer des ennuis. Si la “chaîne” source est plus courte que count, elle insère “\ 0” sur la cible jusqu’à atteindre le décompte. Cela peut être mauvais pour la performance. Si la chaîne source est plus longue que count, strncpy n’ajoute pas un ‘\ 0’ à la cible. Cela risque de vous causer des ennuis plus tard si vous vous attendez à une “chaîne” terminée par “\ 0”. Donc, strncpy devrait également être utilisé avec prudence!

Je n’utiliserais memcpy que si je ne travaillais pas avec les chaînes terminées avec “\ 0”, mais cela semble être une question de goût.

 char *strdup(const char *s1) { char *s2 = malloc(strlen(s1)+1); if(s2 == NULL) { return NULL; } strcpy(s2, s1); return s2; } 

Problèmes:

  1. Si s1 n’est pas terminé, strlen provoque l’access à la mémoire non allouée, le programme plante.
  2. s1 est non terminé, strlen sans provoquer l’access à la mémoire non allouée à partir d’une autre partie de votre application. Il est renvoyé à l’utilisateur (problème de sécurité) ou analysé par une autre partie de votre programme (heisenbug apparaît).
  3. s1 n’est pas terminé, strlen donne un malloc que le système ne peut pas satisfaire, renvoie NULL. strcpy est passé à NULL, le programme plante.
  4. s1 est non terminé, strlen se traduit par un malloc très volumineux, le système alloue beaucoup trop de mémoire pour effectuer la tâche à accomplir, devient instable.
  5. Dans le meilleur des cas, le code est inefficace, strlen requirejs l’access à tous les éléments de la chaîne.

Il y a probablement d’autres problèmes … Regardez, la terminaison nulle n’est pas toujours une mauvaise idée. Il y a des situations où, pour l’efficacité du calcul ou pour réduire les besoins en stockage, cela a du sens.

Pour écrire un code à usage général, par exemple la logique métier a-t-elle un sens? Non.

 char* dupstr(char* str) { int full_len; // includes null terminator char* ret; char* s = str; #ifdef _DEBUG if (! str) toss("arg 1 null", __WHENCE__); #endif full_len = strlen(s) + 1; if (! (ret = (char*) malloc(full_len))) toss("out of memory", __WHENCE__); memcpy(ret, s, full_len); // already know len, so strcpy() would be slower return ret; } 

Cette réponse utilise size_t et memcpy() pour strdup() simple et rapide.

Il est size_t d’utiliser le type size_t car c’est le type renvoyé par strlen() et utilisé par malloc() et memcpy() . int n’est pas le type approprié pour ces opérations.

memcpy() est rarement plus lent que strcpy() ou strncpy() et souvent beaucoup plus rapidement.

 // Assumption: `s1` points to a C ssortingng. char *strdup(const char *s1) { size_t size = strlen(s1) + 1; char *s2 = malloc(size); if(s2 != NULL) { memcpy(s2, s1, size); } return s2; } 

§7.1.1 1 “Une chaîne de caractères est une séquence contiguë de caractères terminée par et incluant le premier caractère nul. …”

Votre code est terriblement inefficace car il parcourt deux fois la chaîne pour le copier.

Une fois dans strlen ().

Puis encore dans strcpy ().

Et vous ne vérifiez pas s1 pour NULL.

Stocker la longueur dans une variable supplémentaire ne vous coûte à peu près rien, le fait de parcourir chaque chaîne deux fois pour la copier est un péché capital.