gcc, strict-aliasing e casting por meio de uma união

Você tem alguma história de terror para contar? O Manual do GCC adicionou recentemente um aviso sobre o aliasing -fstrict e a seleção de um ponteiro por meio de uma união:

[…] Tomando o endereço, lançando o ponteiro resultante e desreferenciando o resultado tem um comportamento indefinido [ênfase adicionada], mesmo que o casting use um tipo de união, por exemplo:

union a_union { int i; double d; }; int f() { double d = 3.0; return ((union a_union *)&d)->i; } 

Alguém tem um exemplo para ilustrar esse comportamento indefinido?

Note que esta pergunta não é sobre o que o padrão C99 diz, ou não diz. Trata-se do funcionamento real do gcc e de outros compiladores existentes atualmente.

Eu estou apenas supondo, mas um problema em potencial pode estar na configuração de d para 3.0. Como d é uma variável temporária que nunca é lida diretamente e que nunca é lida através de um ponteiro ‘um tanto compatível’, o compilador pode não se incomodar em configurá-la. E então f () retornará algum lixo da pilha.

Minha tentativa simples e ingênua falha. Por exemplo:

 #include  union a_union { int i; double d; }; int f1(void) { union a_union t; td = 3333333.0; return ti; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3) } int f2(void) { double d = 3333333.0; return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior' } int main(void) { printf("%d\n", f1()); printf("%d\n", f2()); return 0; } 

funciona bem, dando em CYGWIN:

 -2147483648 -2147483648 

Olhando para o assembler, vemos que o gcc otimiza completamente t away: f1() simplesmente armazena a resposta pré-calculada:

 movl $-2147483648, %eax 

enquanto f2() empurra 3333333.0 para a pilha de ponto flutuante e , em seguida, extrai o valor de retorno:

 flds LC0 # LC0: 1246458708 (= 3333333.0) (--> 80 bits) fstpl -8(%ebp) # save in d (64 bits) movl -8(%ebp), %eax # return value (32 bits) 

E as funções também estão embutidas (o que parece ser a causa de alguns bugs sutis de aliases restritas), mas isso não é relevante aqui. (E esse montador não é tão relevante, mas adiciona detalhes corroborativos.)

Observe também que, obviamente, os endereços tomados estão errados (ou certos , se você estiver tentando ilustrar um comportamento indefinido). Por exemplo, assim como sabemos que isso está errado:

 extern void foo(int *, double *); union a_union t; td = 3.0; foo(&t.i, &t.d); // undefined behavior 

nós também sabemos que isso é errado:

 extern void foo(int *, double *); double d = 3.0; foo(&((union a_union *)&d)->i, &d); // undefined behavior 

Para uma discussão de fundo sobre isso, veja por exemplo:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http://davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
(= página de pesquisa no Google, em seguida, visualizar a página em cache)

Qual é a regra de aliasing estrita?
C99 regras de aliasing estritas em C ++ (GCC)

No primeiro link, minuta das atas de uma reunião da ISO há sete meses, um participante observa na seção 4.16:

Existe alguém que acha que as regras são claras o suficiente? Ninguém é realmente capaz de interpretá-los.

Outras notas: Meu teste foi com gcc 4.3.4, com -O2; as opções -O2 e -O3 implicam -localização -fstrict. O exemplo do Manual do GCC assume sizeof (double) > = sizeof (int); Não importa se eles são desiguais.

Além disso, conforme observado por Mike Acton no link cellperformace, -Wstrict-aliasing=2 , mas não =3 , produz um warning: dereferencing type-punned pointer might break strict-aliasing rules para o exemplo aqui.

O fato de o GCC estar alertando sobre os sindicatos não significa necessariamente que os sindicatos não funcionem atualmente. Mas aqui está um exemplo um pouco menos simples que o seu:

 #include  struct B { int i1; int i2; }; union A { struct B b; double d; }; int main() { double d = 3.0; #ifdef USE_UNION ((union A*)&d)->b.i2 += 0x80000000; #else ((int*)&d)[1] += 0x80000000; #endif printf("%g\n", d); } 

Saída:

 $ gcc --version gcc (GCC) 4.3.4 20090804 (release) 1 Copyright (C) 2008 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ gcc -oalias alias.c -O1 -std=c99 && ./alias -3 $ gcc -oalias alias.c -O3 -std=c99 && ./alias 3 $ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias -3 $ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias -3 

Portanto, no GCC 4.3.4, a união “salva o dia” (supondo que eu queira a saída “-3”). Ele desativa a otimização que depende do aliasing estrito e que resulta na saída “3” no segundo caso (apenas). Com -Wall, USE_UNION também desativa o aviso de digitação.

Eu não tenho o gcc 4.4 para testar, mas por favor, dê este código. Seu código, na verdade, testa se a memory de d é inicializada antes de retornar a uma união: o meu testa se foi modificado.

Btw, a maneira segura de ler metade de um duplo como um int é:

 double d = 3; int i; memcpy(&i, &d, sizeof i); return i; 

Com a otimização no GCC, isso resulta em:

  int thing() { 401130: 55 push %ebp 401131: 89 e5 mov %esp,%ebp 401133: 83 ec 10 sub $0x10,%esp double d = 3; 401136: d9 05 a8 20 40 00 flds 0x4020a8 40113c: dd 5d f0 fstpl -0x10(%ebp) int i; memcpy(&i, &d, sizeof i); 40113f: 8b 45 f0 mov -0x10(%ebp),%eax return i; } 401142: c9 leave 401143: c3 ret 

Portanto, não há nenhuma chamada real para o memcpy. Se você não está fazendo isso, você merece o que ganha se o sindicato parar de trabalhar no GCC 😉

Sua afirmação de que o código a seguir está “errado”:

 extern void foo(int *, double *); union a_union t; td = 3.0; foo(&t.i, &t.d); // undefined behavior 

… está errado. Apenas pegar o endereço dos dois membros do sindicato e passá-los para uma function externa não resulta em um comportamento indefinido; você só obtém isso de desreferenciar um desses pointers de uma maneira inválida. Por exemplo, se a function foo retornar imediatamente sem desreferenciar os pointers que você a transmitiu, o comportamento não será indefinido. Com uma leitura estrita do padrão C99, há até alguns casos em que os pointers podem ser desreferenciados sem invocar um comportamento indefinido; por exemplo, ele poderia ler o valor referenciado pelo segundo ponteiro e, em seguida, armazenar um valor pelo primeiro ponteiro, desde que ambos apontem para um object alocado dinamicamente (ou seja, um sem um “tipo declarado”).

O aliasing ocorre quando o compilador tem dois pointers diferentes para a mesma parte da memory. Por typecasting um ponteiro, você está gerando um novo ponteiro temporário. Se o otimizador reordenar as instruções de assembly, por exemplo, acessar os dois pointers pode dar dois resultados totalmente diferentes – ele pode reordenar uma leitura antes de gravar no mesmo endereço. É por isso que é um comportamento indefinido.

É improvável que você veja o problema em um código de teste muito simples, mas ele aparecerá quando houver muita coisa acontecendo.

Acho que o aviso é deixar claro que os sindicatos não são um caso especial, mesmo que você espere que eles sejam.

Veja este artigo da Wikipédia para mais informações sobre o aliasing: http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

Bem, é um pouco de necro-postagem, mas aqui está uma história de terror. Eu estou portando um programa que foi escrito com a suposição de que a ordem de bytes nativa é grande endian. Agora eu preciso que ele trabalhe no little endian também. Infelizmente, não posso simplesmente usar a ordem de bytes nativos em todos os lugares, pois os dados podem ser acessados ​​de várias maneiras. Por exemplo, um inteiro de 64 bits poderia ser tratado como dois inteiros de 32 bits ou como quatro inteiros de 16 bits, ou mesmo como 16 inteiros de 4 bits. Para piorar as coisas, não há como descobrir exatamente o que é armazenado na memory, porque o software é um interpretador de algum tipo de código de bytes, e os dados são formados por esse código de bytes. Por exemplo, o código de byte pode conter instruções para escrever uma matriz de inteiros de 16 bits e, em seguida, acessar um par deles como uma flutuação de 32 bits. E não há como prever ou alterar o código de bytes.

Portanto, eu tive que criar um conjunto de classs de wrapper para trabalhar com valores armazenados na ordem big endian, independentemente do endianness nativo. Funcionou perfeitamente no Visual Studio e no GCC no Linux sem otimizações. Mas com o gcc-O2, o inferno se soltou. Depois de muita debugging, descobri que o motivo estava aqui:

 double D; float F; Ul *pF=(Ul*)&F; // Ul is unsigned long *pF=pop0->lu.r(); // r() returns Ul D=(double)F; 

Esse código foi usado para converter uma representação de 32 bits de um flutuante armazenado em um inteiro de 32 bits para duplicar. Parece que o compilador decidiu fazer a atribuição a * pF após a atribuição a D – o resultado foi que na primeira vez em que o código foi executado, o valor de D era lixo e os valores consequentes estavam “atrasados” em 1 iteração.

Milagrosamente, não houve outros problemas naquele momento. Então, decidi seguir em frente e testar meu novo código na plataforma original, o HP-UX em um processador RISC com ordem nativa big endian. Agora ele quebrou de novo, desta vez na minha nova turma:

 typedef unsigned long long Ur; // 64-bit uint typedef unsigned char Uc; class BEDoubleRef { double *p; public: inline BEDoubleRef(double *p): p(p) {} inline operator double() { Uc *pu = reinterpret_cast(p); Ur n = (pu[7] & 0xFFULL) | ((pu[6] & 0xFFULL) << 8) | ((pu[5] & 0xFFULL) << 16) | ((pu[4] & 0xFFULL) << 24) | ((pu[3] & 0xFFULL) << 32) | ((pu[2] & 0xFFULL) << 40) | ((pu[1] & 0xFFULL) << 48) | ((pu[0] & 0xFFULL) << 56); return *reinterpret_cast(&n); } inline BEDoubleRef &operator=(const double &d) { Uc *pc = reinterpret_cast(p); const Ur *pu = reinterpret_cast(&d); pc[0] = (*pu >> 56) & 0xFFu; pc[1] = (*pu >> 48) & 0xFFu; pc[2] = (*pu >> 40) & 0xFFu; pc[3] = (*pu >> 32) & 0xFFu; pc[4] = (*pu >> 24) & 0xFFu; pc[5] = (*pu >> 16) & 0xFFu; pc[6] = (*pu >> 8) & 0xFFu; pc[7] = *pu & 0xFFu; return *this; } inline BEDoubleRef &operator=(const BEDoubleRef &d) { *p = *dp; return *this; } }; 

Por alguma razão muito estranha, o primeiro operador de atribuição atribuiu apenas corretamente os bytes 1 a 7. O byte 0 sempre teve algum absurdo nele, o que quebrou tudo, pois há um bit de sinal e uma parte do pedido.

Eu tentei usar os sindicatos como solução alternativa:

 union { double d; Uc c[8]; } un; Uc *pc = un.c; const Ur *pu = reinterpret_cast(&d); pc[0] = (*pu >> 56) & 0xFFu; pc[1] = (*pu >> 48) & 0xFFu; pc[2] = (*pu >> 40) & 0xFFu; pc[3] = (*pu >> 32) & 0xFFu; pc[4] = (*pu >> 24) & 0xFFu; pc[5] = (*pu >> 16) & 0xFFu; pc[6] = (*pu >> 8) & 0xFFu; pc[7] = *pu & 0xFFu; *p = un.d; 

mas também não funcionou. Na verdade, foi um pouco melhor – só falhou em números negativos.

Neste ponto, estou pensando em adicionar um teste simples para o endianness nativo e, em seguida, fazer tudo através de pointers char* com if (LITTLE_ENDIAN) volta. Para piorar as coisas, o programa faz uso pesado de todos os sindicatos, o que parece funcionar bem por enquanto, mas depois de toda essa bagunça eu não ficaria surpreso se de repente ele quebra sem motivo aparente.

Você viu isso ? Qual é a regra de aliasing estrita?

O link contém um link secundário para este artigo com exemplos do gcc. http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Tentar uma união como essa estaria mais perto do problema.

 union a_union { int i; double *d; }; 

Dessa forma você tem 2 tipos, um int e um duplo * apontando para a mesma memory. Neste caso, usando o duplo (*(double*)&i) poderia causar o problema.

Aqui está o meu: acho que isso é um bug em todo o GCC v5.xe posterior

 #include  #include  #include  template  class simd { public: typedef Vector_type vector_type; typedef Scalar_type scalar_type; typedef union conv_t_union { Vector_type v; Scalar_type s[sizeof(Vector_type) / sizeof(Scalar_type)]; conv_t_union(){}; } conv_t; static inline constexpr int Nsimd(void) { return sizeof(Vector_type) / sizeof(Scalar_type); } Vector_type v; template  friend inline simd SimdApply(const functor &func, const simd &v) { simd ret; simd::conv_t conv; conv.v = vv; for (int i = 0; i < simd::Nsimd(); i++) { conv.s[i] = func(conv.s[i]); } ret.v = conv.v; return ret; } }; template  struct RealFunctor { scalar operator()(const scalar &a) const { return std::real(a); } }; template  inline simd real(const simd &r) { return SimdApply(RealFunctor(), r); } typedef simd, __m128d> vcomplexd; int main(int argc, char **argv) { vcomplexd a,b; av=_mm_set_pd(2.0,1.0); b = real(a); vcomplexd::conv_t conv; conv.v = bv; for(int i=0;i 

Deveria dar

 c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 c010200:~ peterboyle$ ./a.out (1,0) 

Mas sob -O3: eu acho que isso é errado e um erro de compilation

 c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3 c010200:~ peterboyle$ ./a.out (0,0) 

Sob g ++ 4.9

 c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3 c010200:~ peterboyle$ ./a.out (1,0) 

Sob llvm xcode

 c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3 c010200:~ peterboyle$ ./a.out (1,0) 

Eu realmente não entendo seu problema. O compilador fez exatamente o que deveria fazer no seu exemplo. A conversão de union é o que você fez em f1 . Em f2 é um typecast ponteiro normal, que você lançou a uma união é irrelevante, ainda é um casting de ponteiro