web-dev-qa-db-fra.com

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

Je ne connais pas très bien le standard C, alors je vous prie de supporter cela avec moi.

Je voudrais savoir si la norme garantit que memcpy(0,0,0) est sûr.

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

Mais pouvons-nous considérer que les régions de la mémoire se chevauchent ici?

68
Matthieu M.

J'ai une version préliminaire de la norme C (ISO/IEC 9899: 1999), et il y a des choses amusantes à dire à propos de cet appel. Pour commencer, il est mentionné (§7.21.1/2) en ce qui concerne memcpy que

Où un argument déclaré comme size_tn spécifie la longueur du tableau pour un fonction, n peut avoir la valeur zéro lors d’un appel de cette fonction. Sauf indication explicite sinon, dans la description d'une fonction particulière dans ce sous-paragraphe, arguments de pointeur sur un tel appel doit toujours avoir des valeurs valides, comme décrit au 7.1.4 . Sur un tel appel, un fonction qui localise un caractère ne trouve aucune occurrence, une fonction qui compare deux les séquences de caractères retournent zéro, et une fonction qui copie les caractères copie zéro personnages.

La référence indiquée ici pointe vers ceci:

Si un argument pour une fonction a une valeur non valide (telle qu'une valeur En dehors du domaine de la fonction, ou un pointeur en dehors de l'espace d'adressage du programme, ou un pointeur null , ou un pointeur sur la mémoire non modifiable lorsque le paramètre correspondant n'est pas qualifié de type 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 la spécification C, appelant

memcpy(0, 0, 0)

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

Cela dit, je serais absolument étonné si une implémentation réelle de memcpy se cassait si vous le faisiez, car la plupart des implémentations intuitives auxquelles je peux penser ne feraient rien du tout si vous disiez copier zéro octet.

66
templatetypedef

Juste pour le plaisir, les notes de publication de gcc-4.9 indiquent que son optimiseur utilise ces règles et permet, par exemple, de supprimer les conditions conditionnelles.

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

qui donne ensuite 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 sur le comportement de gcc-4.9; le comportement peut être conforme aux normes, mais pouvoir appeler memmove (0,0,0) est parfois une extension utile de ces normes.

21
user1998586

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

Voir commit 168e635 (16 juil. 2017), et commit 1773664 , commit f331ab9 , commit 5783980 (15 juil. 2017) de René Scharfe (rscharfe) .
(Fusion par Junio ​​C Hamano - gitster - dans commit 32f9025 , 11 août 2017)

Il utilise une macro helper MOVE_ARRAY qui calcule la taille En fonction du nombre d'éléments spécifié pour nous et prend en charge NULL Pointeurs lorsque ce nombre est égal à zéro.
Raw memmove(3) Les appels avec NULL peuvent Faire en sorte que le compilateur optimise (trop rapidement) les vérifications ultérieures NULL.

MOVE_ARRAY ajoute un assistant sûr et pratique pour déplacer des plages d'entrées de tableau potentiellement superposées.
Il en déduit la taille de l'élément, se multiplie automatiquement et en toute sécurité pour obtenir la taille en octets, effectue une vérification de sécurité de type élémentaire en comparant les tailles d'élément et contrairement à memmove(3), il prend en charge les pointeurs NULL si et seulement 0 doivent être déplacés.

#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, en tant qu’expression (@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))
0
VonC