web-dev-qa-db-fra.com

Est-il bien défini de tenir un pointeur mal aligné, tant que vous ne le déréférencez jamais?

J'ai du code C qui analyse les données binaires compressées/non remplies provenant du réseau.

Ce code fonctionnait/fonctionne bien sous Intel/x86, mais lorsque je le compilais sous ARM il plantait souvent.

Le coupable, comme vous l'avez peut-être deviné, était des pointeurs non alignés - en particulier, le code d'analyse ferait des choses douteuses comme ceci:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]);  // misaligned access -- can crash under ARM!

... cela ne va évidemment pas voler dans ARM-land, donc je l'ai modifié pour ressembler davantage à ceci:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord));  // slower but ARM-safe

Ma question (du point de vue du juriste) est la suivante: mon approche "ARM-fixed" est-elle bien définie selon les règles du langage C?

Mon inquiétude est que peut-être même le simple fait d'avoir un pointeur mal aligné-int32_t pourrait suffire pour invoquer un comportement indéfini, même si je ne le déréférence jamais directement. (Si ma préoccupation est valide, je pense que je pourrais résoudre le problème en changeant le type de pNextWord de (const int32_t *) à (const char *), mais je préfère ne pas le faire à moins qu'il ne soit réellement nécessaire de le faire, car cela signifierait faire une arithmétique de pointage à la main)

35
Jeremy Friesner

Non, le nouveau code a toujours un comportement indéfini. C11 6.3.2.3p7 :

  1. Un pointeur vers un type d'objet peut être converti en pointeur vers un type d'objet différent. Si le pointeur résultant n'est pas correctement aligné 68) pour le type référencé, le comportement n'est pas défini. [...]

Cela ne dit rien sur le déréférencement du pointeur - même la conversion a un comportement indéfini.


En effet, le code modifié que vous supposez est [~ # ~] arm [~ # ~] - le coffre-fort peut ne pas être égal Intel - sûr. Les compilateurs sont connus pour générer du code pour Intel qui peut planter sur un accès non aligné . Bien que ce ne soit pas dans le cas lié, il se peut qu'un compilateur intelligent prenne la conversion comme une preuve que l'adresse est bien alignée et utilise un outil spécialisé code pour memcpy.


Mis à part l'alignement, votre premier extrait souffre également d'une violation stricte d'alias. C11 6.5p7 :

  1. Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants: 88)
    • un type compatible avec le type effectif de l'objet,
    • une version qualifiée d'un type compatible avec le type effectif de l'objet,
    • un type qui est le type signé ou non signé correspondant au type effectif de l'objet,
    • un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
    • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union sous-agrégée ou contenue), ou
    • un type de caractère.

Puisque le tableau buf[2048] est statiquement typé, chaque élément étant char, et donc les types effectifs des éléments sont char; vous pouvez accéder au contenu du tableau uniquement en tant que caractères, pas en tant que int32_ts.

C'est-à-dire, même

int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);

a un comportement indéfini.

21
Antti Haapala

Pour analyser en toute sécurité un entier multi-octets sur des compilateurs/plates-formes, vous pouvez extraire chaque octet et les assembler en entier selon l'endian. Par exemple, pour lire un entier de 4 octets à partir du tampon big-endian:

uint8_t* buf = any address;

uint32_t val = 0;
uint32_t  b0 = buf[0];
uint32_t  b1 = buf[1];
uint32_t  b2 = buf[2];
uint32_t  b3 = buf[3];

val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
8
lee qiaoping

Certains compilateurs peuvent supposer qu'aucun pointeur ne contiendra jamais une valeur qui n'est pas correctement alignée pour son type et effectuer des optimisations qui en dépendent. À titre d'exemple simple, considérez:

void copy_uint32(uint32_t *dest, uint32_t *src)
{
  memcpy(dest, src, sizeof (uint32_t));
}

Si dest et src contiennent à la fois des adresses alignées 32 bits, la fonction ci-dessus pourrait être optimisée pour un chargement et un magasin même dans les plates-formes qui ne prennent pas en charge les accès non alignés. Si la fonction avait été déclarée pour accepter des arguments de type void*, cependant, une telle optimisation ne serait pas autorisée sur les plates-formes où les accès 32 bits non alignés se comporteraient différemment d'une séquence d'accès octets, de décalages et d'opérations bit par bit.

4
supercat

Comme mentionné dans la réponse d'Antti Haapala, la simple conversion d'un pointeur vers un autre type lorsque le pointeur résultant n'est pas correctement aligné appelle un comportement non défini conformément à la section 6.3.2.3p7 de la norme C.

Votre code modifié utilise uniquement pNextWord pour passer à memcpy, où il est converti en void *, donc vous n'avez même pas besoin d'une variable de type uint32_t *. Passez simplement l'adresse du premier octet du tampon que vous souhaitez lire à memcpy. Ensuite, vous n'avez pas du tout à vous soucier de l'alignement.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));
2
dbush