Comportement non défini du maths du pointeur sur un tableau C ++

Pourquoi le résultat de ce programme est 4 ?

 #include  int main() { short A[] = {1, 2, 3, 4, 5, 6}; std::cout << *(short*)((char*)A + 7) << std::endl; return 0; } 

D’après ma compréhension, sur le système little endian x86, où char a un octet et deux octets courts, la sortie doit être 0x0500 , car les données du tableau A sont comme suit en hexadécimal:

 01 00 02 00 03 00 04 00 05 00 06 00 

Nous avançons depuis le début de 7 octets, puis lisons 2 octets. Qu’est-ce que je manque?

Vous violez les règles ssortingctes de crénelage ici Vous ne pouvez pas simplement lire à mi-chemin dans un object et prétendre que c’est un object tout seul. Vous ne pouvez pas inventer des objects hypothétiques en utilisant des décalages d’octets comme celui-ci. GCC a parfaitement le droit de faire des choses folles, comme de remonter le temps et d’assassiner Elvis Presley, lorsque vous lui transmettez votre programme.

Ce que vous êtes autorisé à faire est d’inspecter et de manipuler les octets qui constituent un object arbitraire, à l’aide d’un caractère char* . En utilisant ce privilège:

 #include  #include  int main() { short A[] = {1, 2, 3, 4, 5, 6}; short B; std::copy( (char*)A + 7, (char*)A + 7 + sizeof(short), (char*)&B ); std::cout << std::showbase << std::hex << B << std::endl; } // Output: 0x500 

( démo en direct )

Mais vous ne pouvez pas simplement "créer" un object inexistant dans la collection d'origine.

De plus, même si vous avez un compilateur à qui il est possible d'ignorer ce problème (par exemple, avec le commutateur -fno-ssortingct-aliasing GCC), l'object créé n'est pas correctement aligné pour aucune architecture traditionnelle actuelle. Un short ne peut pas légalement vivre à cet emplacement impair en mémoire , vous ne pouvez donc pas prétendre en avoir un. Il n’ya aucun moyen de savoir à quel point le comportement du code original n’est pas défini; En fait, si vous transmettez à GCC le -fsanitize=undefined , il vous en dira autant.

Je simplifie un peu.

Le programme a un comportement indéfini en raison de la conversion d’un pointeur mal aligné sur (short*) . Cela enfreint les règles de 6.3.2.3 p6 dans C11, ce qui n’a rien à voir avec un aliasing ssortingct comme le prétendent d’autres réponses:

Un pointeur sur un type d’object peut être converti en un pointeur sur un type d’object différent. Si le pointeur résultant n’est pas correctement aligné pour le type référencé, le comportement est indéfini.

Dans [expr.static.cast] p13, C ++ indique que la conversion du caractère non aligné char* en short* donne une valeur de pointeur non spécifiée, qui peut être un pointeur non valide, qui ne peut pas être déréférencé.

Le moyen correct d’inspecter les octets consiste à char* pas en rediffusant short* et en prétendant qu’il y a un short à une adresse où un short ne peut pas vivre.

C’est sans doute un bug dans GCC.

Tout d’abord, il convient de noter que votre code appelle un comportement indéfini, en raison d’une violation des règles de repliement ssortingct.

Cela dit, voici pourquoi je considère que c’est un bug:

  1. La même expression, lorsqu’elle est affectée pour la première fois à un short ou un short * intermédiaire, provoque le comportement attendu. C’est uniquement lorsque vous passez l’expression directement en tant qu’argument de fonction que le comportement inattendu se manifeste.

  2. Il se produit même lorsqu’il est compilé avec -O0 -fno-ssortingct-aliasing .

J’ai réécrit votre code en C pour éliminer toute possibilité de folie C ++. Votre question est était c tagué après tout! J’ai ajouté la fonction pshort pour m’assurer que la nature variadic printf n’était pas impliquée.

 #include  static void pshort(short val) { printf("0x%hx ", val); } int main(void) { short A[] = {1, 2, 3, 4, 5, 6}; #define EXP ((short*)((char*)A + 7)) short *p = EXP; short q = *EXP; pshort(*p); pshort(q); pshort(*EXP); printf("\n"); return 0; } 

Après avoir compilé avec gcc (GCC) 7.3.1 20180130 (Red Hat 7.3.1-2) :

 gcc -O0 -fno-ssortingct-aliasing -g -Wall -Werror endian.c 

Sortie:

 0x500 0x500 0x4 

Il semble que GCC génère un code différent lorsque l’expression est utilisée directement en tant qu’argument, même si j’utilise clairement la même expression ( EXP ).

Dumping avec objdump -Mintel -S --no-show-raw-insn endian :

 int main(void) { 40054d: push rbp 40054e: mov rbp,rsp 400551: sub rsp,0x20 short A[] = {1, 2, 3, 4, 5, 6}; 400555: mov WORD PTR [rbp-0x16],0x1 40055b: mov WORD PTR [rbp-0x14],0x2 400561: mov WORD PTR [rbp-0x12],0x3 400567: mov WORD PTR [rbp-0x10],0x4 40056d: mov WORD PTR [rbp-0xe],0x5 400573: mov WORD PTR [rbp-0xc],0x6 #define EXP ((short*)((char*)A + 7)) short *p = EXP; 400579: lea rax,[rbp-0x16] ; [rbp-0x16] is A 40057d: add rax,0x7 400581: mov QWORD PTR [rbp-0x8],rax ; [rbp-0x08] is p short q = *EXP; 400585: movzx eax,WORD PTR [rbp-0xf] ; [rbp-0xf] is A plus 7 bytes 400589: mov WORD PTR [rbp-0xa],ax ; [rbp-0xa] is q pshort(*p); 40058d: mov rax,QWORD PTR [rbp-0x8] ; [rbp-0x08] is p 400591: movzx eax,WORD PTR [rax] ; *p 400594: cwde 400595: mov edi,eax 400597: call 400527  pshort(q); 40059c: movsx eax,WORD PTR [rbp-0xa] ; [rbp-0xa] is q 4005a0: mov edi,eax 4005a2: call 400527  pshort(*EXP); 4005a7: movzx eax,WORD PTR [rbp-0x10] ; [rbp-0x10] is A plus 6 bytes ******** 4005ab: cwde 4005ac: mov edi,eax 4005ae: call 400527  printf("\n"); 4005b3: mov edi,0xa 4005b8: call 400430  return 0; 4005bd: mov eax,0x0 } 4005c2: leave 4005c3: ret 

  • J’obtiens le même résultat avec GCC 4.9.4 et GCC 5.5.0 depuis le hub Docker