81 votes

Est-il garanti d'être sûr d'effectuer memcpy(0,0,0) ?

Je ne suis pas très au fait de la norme C, alors soyez indulgent avec moi.

Je voudrais savoir s'il est garanti, par la norme, que memcpy(0,0,0) est sûr.

La seule restriction que j'ai pu trouver est que si les régions de mémoire se chevauchent, alors le comportement est indéfini...

Mais peut-on considérer que les régions de mémoire se chevauchent ici ?

7 votes

Mathématiquement, l'intersection de deux ensembles vides est vide.

0 votes

Je voulais vérifier pour vous ce que (x)libC fait pour vous, mais comme c'est l'asm (elibc/glibc ici), c'est un peu trop compliqué pour un petit matin :)

0 votes

Por qué serait vous faites ça ? À propos, le chevauchement des régions de mémoire n'est pas la seule raison pour laquelle UB avec memcpy .

71voto

templatetypedef Points 129554

J'ai une version préliminaire de la norme C (ISO/IEC 9899:1999), et elle a des choses amusantes à dire sur cet appel. Pour commencer, elle mentionne (§7.21.1/2) à propos de memcpy que

Lorsqu'un argument déclaré comme size_t n spécifie la longueur du tableau pour un n peut avoir la valeur zéro lors d'un appel à cette fonction. Sauf mention explicite contraire dans la description d'une fonction particulière dans ce paragraphe, arguments du pointeur lors d'un tel appel doivent toujours avoir des valeurs valides, comme décrit au point 7.1.4. . Lors d'un tel appel, une qui localise un caractère ne trouve pas d'occurrence, une fonction qui compare deux caractères deux séquences de caractères renvoie zéro, et une fonction qui copie des caractères copie zéro caractères.

La référence indiquée ici va dans ce sens :

Si un argument d'une fonction a une valeur invalide (telle qu'une valeur hors du domaine de la fonction, ou un pointeur hors de l'espace d'adressage du programme, ou un pointeur nul ou un pointeur vers une mémoire non modifiable lorsque l'indicateur n'est pas qualifié de const) ou un type (après promotion) non attendu par une fonction avec un nombre variable d'arguments, le comportement est indéfini .

Donc il semble que selon les spécifications du C, appeler

memcpy(0, 0, 0)

entraîne un comportement indéfini, car les pointeurs nuls sont considérés comme des "valeurs invalides".

Cela dit, je serais tout à fait étonné si une mise en œuvre réelle de memcpy brisé si vous faisiez cela, puisque la plupart des implémentations intuitives auxquelles je pense ne feraient rien du tout si vous disiez de copier zéro octet.

1 votes

+1 J'ai manqué ce paragraphe et suis allé directement à memcpy . Je suis stupide. J'aurais dû me douter que les programmeurs ne répéteraient pas ce genre d'informations pour chaque description de fonction.

3 votes

Je peux affirmer que les parties citées du projet de norme sont identiques dans le document final. Il ne devrait pas y avoir de problème avec un tel appel, mais il s'agirait toujours d'un comportement non défini sur lequel vous vous appuyez. La réponse à la question "est-ce garanti" est donc "non".

0 votes

@DevSolar- Merci de le confirmer ! Cela aurait été très embarrassant si tout ce que j'avais dit était complètement faux. :-)

24voto

user1998586 Points 128

Juste pour le plaisir, les notes de version de gcc-4.9 indiquent que son optimiseur utilise ces règles, et peut par exemple supprimer la conditionnelle dans

int copy (int* dest, int* src, size_t nbytes) {
    memmove (dest, src, nbytes);
    if (src != NULL)
        return *src;
    return 0;
}

qui donne alors des résultats inattendus lorsque copy(0,0,0) est appelé (voir https://gcc.gnu.org/gcc-4.9/porting_to.html ).

Je suis quelque peu ambivalent à propos du comportement de gcc-4.9 ; le comportement peut être conforme aux standards, mais être capable d'appeler memmove(0,0,0) est parfois une extension utile de ces standards.

2 votes

Intéressant. Je comprends votre ambivalence mais c'est le cœur des optimisations en C : le compilateur suppose que que les développeurs suivent certaines règles et donc déduit que certaines optimisations sont valables (ce qui est le cas si les règles sont respectées).

0 votes

@MatthieuM. : Oui, mais c'est particulièrement stupide. Un coin grossier a été ajouté à la spécification de memcpy qui n'existe dans aucune implémentation que je connaisse, non pas pour augmenter l'uniformité, mais pour s'en éloigner délibérément.

0 votes

@tmyklebu : Il serait raisonnable sur certaines plateformes de mettre en œuvre. memcpy comme quelque chose comme char *end=src+count;while(src<end){*dest++=*src++;} [ce qui évite de devoir modifier count pendant la boucle]. Le passage d'un src Le pointeur déclencherait l'UB même si count est égal à zéro.

0voto

VonC Points 414372

Vous pouvez également considérer cette utilisation de memmove vu dans Git 2.14.x (Q3 2017)

Véase commettre 168e635 (16 juillet 2017), et commit 1773664 , commettre f331ab9 , commettre 5783980 (15 juillet 2017) par René Scharfe ( rscharfe ) .
(fusionné par Junio C Hamano -- gitster -- en commit 32f9025 , 11 août 2017)

Il utilise un macro auxiliaire MOVE_ARRAY qui calcule la taille sur la base du nombre d'éléments spécifiés pour nous et supporte NULL lorsque ce nombre est égal à zéro.
Brut memmove(3) appels avec NULL peut faire en sorte que le compilateur optimise (avec trop d'empressement) les éléments suivants NULL des contrôles.

MOVE_ARRAY ajoute une aide sûre et pratique pour déplacer des plages d'entrées de tableaux qui peuvent se chevaucher.
Il déduit la taille de l'élément, multiplie automatiquement et en toute sécurité pour obtenir la taille en octets, effectue un contrôle de sécurité de type de base en comparant les tailles des éléments et les différences entre les éléments. memmove(3) il soutient NULL les pointeurs si 0 élément doit être déplacé.

#define MOVE_ARRAY(dst, src, n) move_array((dst), (src), (n), sizeof(*(dst)) + \
    BUILD_ASSERT_OR_ZERO(sizeof(*(dst)) == sizeof(*(src))))
static inline void move_array(void *dst, const void *src, size_t n, size_t size)
{
    if (n)
        memmove(dst, src, st_mult(size, n));
}

Exemples :

- memmove(dst, src, (n) * sizeof(*dst));
+ MOVE_ARRAY(dst, src, n);

Il utilise le macro BUILD_ASSERT_OR_ZERO qui affirme une dépendance au moment de la construction, comme une expression (avec @cond étant la condition de compilation qui doit être vraie).
La compilation échouera si la condition n'est pas vraie, ou ne peut pas être évaluée par le compilateur.

#define BUILD_ASSERT_OR_ZERO(cond) \
(sizeof(char [1 - 2*!(cond)]) - 1)

Exemple :

#define foo_to_char(foo)                \
     ((char *)(foo)                     \
      + BUILD_ASSERT_OR_ZERO(offsetof(struct foo, string) == 0))

-3voto

Kai Petzke Points 191

Non, memcpy(0,0,0) n'est pas sûr. La bibliothèque standard n'échouera probablement pas lors de cet appel. Cependant, dans un environnement de test, un code supplémentaire pourrait être présent dans memcpy() pour détecter les dépassements de tampon et autres problèmes. Et la façon dont cette version spéciale de memcpy() réagit aux pointeurs NULL n'est pas définie.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X