Ao perguntar sobre o comportamento indefinido comum em C , as pessoas às vezes se referem à regra estrita de alias.
Sobre o que eles estão falando?
Ao perguntar sobre o comportamento indefinido comum em C , as pessoas às vezes se referem à regra estrita de alias.
Sobre o que eles estão falando?
Respostas:
Uma situação típica em que você encontra problemas estritos de aliasing é sobrepor uma estrutura (como uma mensagem de dispositivo / rede) em um buffer do tamanho da palavra do seu sistema (como um ponteiro para uint32_t
s ou uint16_t
s). Quando você sobrepõe uma estrutura a esse buffer ou um buffer a essa estrutura por meio da conversão de ponteiro, é possível violar facilmente regras estritas de alias.
Portanto, nesse tipo de configuração, se eu quiser enviar uma mensagem para algo, eu teria que ter dois ponteiros incompatíveis apontando para o mesmo pedaço de memória. Eu poderia então ingenuamente codificar algo como isto (em um sistema com sizeof(int) == 2
):
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
A regra estrita de alias torna ilegal essa configuração: a exclusão de um ponteiro que alias um objeto que não seja de um tipo compatível ou um dos outros tipos permitidos pelo parágrafo 7 1 do C 2011 6.5 é um comportamento indefinido. Infelizmente, você ainda pode codificar dessa maneira, talvez receber alguns avisos, compilar bem, apenas para ter um comportamento inesperado e estranho ao executar o código.
(O GCC parece um pouco inconsistente em sua capacidade de emitir avisos de alias, às vezes nos dando um aviso amigável e às vezes não.)
Para ver por que esse comportamento é indefinido, temos que pensar sobre o que a regra de aliasing estrita compra o compilador. Basicamente, com essa regra, ele não precisa pensar em inserir instruções para atualizar o conteúdo de buff
cada execução do loop. Em vez disso, ao otimizar, com algumas suposições irritantemente não aplicadas sobre alias, ele pode omitir essas instruções, carregar buff[0]
e buff[1
] nos registros da CPU uma vez antes da execução do loop e acelerar o corpo do loop. Antes da introdução do aliasing estrito, o compilador tinha que viver em um estado de paranóia que o conteúdo de buff
poderia mudar a qualquer momento e de qualquer lugar por qualquer pessoa. Portanto, para obter uma vantagem extra de desempenho e assumindo que a maioria das pessoas não digita dicas, foi introduzida a regra estrita de alias.
Lembre-se, se você acha que o exemplo é artificial, isso pode até acontecer se você estiver passando um buffer para outra função que está enviando para você, se tiver.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
E reescreveu nosso loop anterior para tirar proveito dessa função conveniente
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
O compilador pode ou não ser capaz ou inteligente o suficiente para tentar incorporar o SendMessage e pode ou não decidir carregar ou não o buff novamente. Se SendMessage
faz parte de outra API que é compilada separadamente, provavelmente possui instruções para carregar o conteúdo do buff. Por outro lado, talvez você esteja em C ++ e esta é uma implementação apenas de cabeçalho modelado que o compilador acha que pode incorporar. Ou talvez seja apenas algo que você escreveu em seu arquivo .c para sua própria conveniência. De qualquer forma, um comportamento indefinido ainda pode ocorrer. Mesmo quando sabemos um pouco do que está acontecendo sob o capô, ainda é uma violação da regra, portanto, nenhum comportamento bem definido é garantido. Portanto, apenas agrupar uma função que usa nosso buffer delimitado por palavras não ajuda necessariamente.
Então, como faço para contornar isso?
Use uma união. A maioria dos compiladores suporta isso sem reclamar sobre aliasing estrito. Isso é permitido no C99 e explicitamente permitido no C11.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
Você pode desativar o aliasing estrito no seu compilador ( f [no-] aliasing estrito no gcc))
Você pode usar o char*
alias em vez da palavra do seu sistema. As regras permitem uma exceção para char*
(incluindo signed char
e unsigned char
). Sempre se assume que char*
aliases outros tipos. No entanto, isso não funcionará da outra maneira: não há suposição de que sua estrutura aliase um buffer de caracteres.
Cuidado para iniciantes
Este é apenas um campo minado em potencial ao sobrepor dois tipos um ao outro. Você também deve aprender sobre endianness , alinhamento de palavras e como lidar com problemas de alinhamento através de estruturas de embalagem corretamente.
1 Os tipos que o C 2011 6.5 7 permite que um lvalue acesse são:
unsigned char*
ser usado agora char*
? I tendem a usar unsigned char
, em vez de char
como o tipo subjacente para byte
porque meus bytes não são assinados e eu não quero que a estranheza do comportamento assinado (nomeadamente wrt para overflow)
unsigned char *
é bom.
uint32_t* buff = malloc(sizeof(Msg));
união e as subsequentes unsigned int asBuffer[sizeof(Msg)];
terão tamanhos diferentes e nenhuma está correta. A malloc
ligação é baseada no alinhamento de 4 bytes sob o capô (não faça isso) e a união será 4 vezes maior do que precisa ser ... Entendo que é por clareza, mas isso não me incomoda. menos ...
A melhor explicação que encontrei é de Mike Acton, Entendendo o aliasing estrito . Ele se concentrou um pouco no desenvolvimento do PS3, mas isso é basicamente apenas o GCC.
Do artigo:
"Aliasing estrito é uma suposição, feita pelo compilador C (ou C ++), de que referenciar ponteiros para objetos de tipos diferentes nunca se referirá ao mesmo local de memória (por exemplo, alias um ao outro)."
Então, basicamente, se você int*
apontar para alguma memória que contenha um int
e, em seguida, apontar a float*
para essa memória e usá-la como uma float
violação da regra. Se seu código não respeitar isso, o otimizador do compilador provavelmente quebrará seu código.
A exceção à regra é a char*
, que pode apontar para qualquer tipo.
Esta é a regra estrita de alias, encontrada na seção 3.10 do padrão C ++ 03 (outras respostas fornecem uma boa explicação, mas nenhuma forneceu a própria regra):
Se um programa tentar acessar o valor armazenado de um objeto por meio de um valor l diferente de um dos seguintes tipos, o comportamento será indefinido:
- o tipo dinâmico do objeto,
- uma versão qualificada para cv do tipo dinâmico do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente ao tipo dinâmico do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada para cv do tipo dinâmico do objeto,
- um tipo agregado ou de união que inclua um dos tipos acima mencionados entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida),
- um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
- a
char
ouunsigned char
tipo.
Redação C ++ 11 e C ++ 14 (alterações enfatizadas):
Se um programa tentar acessar o valor armazenado de um objeto por meio de um valor gl diferente de um dos seguintes tipos, o comportamento será indefinido:
- o tipo dinâmico do objeto,
- uma versão qualificada para cv do tipo dinâmico do objeto,
- um tipo semelhante (conforme definido em 4.4) ao tipo dinâmico do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente ao tipo dinâmico do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada para cv do tipo dinâmico do objeto,
- um tipo de agregação ou união que inclui um dos tipos mencionados acima entre seus elementos ou membros de dados não estáticos (incluindo, recursivamente, um elemento ou membro de dados não estático de uma união subagregada ou contida),
- um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
- a
char
ouunsigned char
tipo.
Duas mudanças foram pequenas: glvalue em vez de lvalue e esclarecimento do caso agregado / união.
A terceira alteração oferece uma garantia mais forte (relaxa a forte regra de alias): O novo conceito de tipos semelhantes que agora são seguros para alias.
Também a redação C (C99; ISO / IEC 9899: 1999 6.5 / 7; exatamente a mesma redação é usada na ISO / IEC 9899: 2011 §6.5 ¶7):
Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos 73) ou 88) :
- um tipo compatível com o tipo efetivo do objeto,
- uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada do tipo efetivo do objeto,
- um tipo agregado ou de união que inclua um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida), ou
- um tipo de caractere.
73) ou 88) O objetivo desta lista é especificar as circunstâncias nas quais um objeto pode ou não ser aliasado.
wow(&u->s1,&u->s2)
seria necessário ser legal mesmo quando um ponteiro for usado para modificar u
e isso negaria a maioria das otimizações que o regra de aliasing foi projetada para facilitar.
Isso foi extraído do meu "O que é a regra estrita de alias e por que nos importamos?" escrever.
Em C e C ++, o aliasing tem a ver com quais tipos de expressão temos permissão para acessar valores armazenados. Em C e C ++, o padrão especifica quais tipos de expressão são permitidos para alias quais tipos. O compilador e o otimizador podem assumir que seguimos estritamente as regras de alias, daí o termo regra estrita de alias . Se tentarmos acessar um valor usando um tipo não permitido, ele será classificado como comportamento indefinido ( UB ). Depois de ter um comportamento indefinido, todas as apostas estão desativadas, os resultados do nosso programa não são mais confiáveis.
Infelizmente, com violações estritas de alias, geralmente obtemos os resultados esperados, deixando a possibilidade de que uma versão futura de um compilador com uma nova otimização decida o código que julgávamos válido. Isso é indesejável e é um objetivo que vale a pena entender as regras estritas de alias e como evitar violá-las.
Para entender mais sobre por que nos importamos, discutiremos questões que surgem ao violar regras estritas de aliasing, punição de tipo, já que técnicas comuns usadas na punição de tipo geralmente violam regras estritas de alias e como digitar trocadilho corretamente.
Vejamos alguns exemplos, depois podemos falar exatamente sobre o que dizem os padrões, examinar alguns exemplos adicionais e ver como evitar aliases estritos e capturar violações que perdemos. Aqui está um exemplo que não deve surpreender ( exemplo ao vivo ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Temos um int * apontando para a memória ocupada por um int e esse é um alias válido. O otimizador deve assumir que as atribuições por meio de ip podem atualizar o valor ocupado por x .
O próximo exemplo mostra um alias que leva a um comportamento indefinido ( exemplo ao vivo ):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
Na função foo, pegamos um int * e um float * , neste exemplo, chamamos foo e configuramos os dois parâmetros para apontar para o mesmo local de memória que neste exemplo contém um int . Observe que o reinterpret_cast está dizendo ao compilador para tratar a expressão como se tivesse o tipo especificado por seu parâmetro de modelo. Nesse caso, estamos dizendo para tratar a expressão & x como se tivesse o tipo float * . Podemos esperar ingenuamente que o resultado do segundo corte seja 0, mas com a otimização ativada usando -O2, tanto gcc quanto clang produzem o seguinte resultado:
0
1
O que pode não ser esperado, mas é perfeitamente válido, pois invocamos um comportamento indefinido. Um flutuador não pode validamente alias um objeto int . Portanto, o otimizador pode assumir a constante 1 armazenada ao remover a referência i, que será o valor de retorno, pois um armazenamento através de f não pode afetar validamente um objeto int . A inserção do código no Compiler Explorer mostra que é exatamente isso que está acontecendo ( exemplo ao vivo ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
O otimizador que usa a Análise de alias baseada em tipo (TBAA) assume que 1 será retornado e move diretamente o valor constante no registro eax, que carrega o valor de retorno. O TBAA usa as regras de idiomas sobre quais tipos têm permissão de alias para otimizar cargas e armazenamentos. Nesse caso, o TBAA sabe que um float não pode alias e int e otimiza a carga de i .
O que exatamente o padrão diz que somos permitidos e não permitidos? O idioma padrão não é simples, portanto, para cada item, tentarei fornecer exemplos de código que demonstram o significado.
O padrão C11 diz o seguinte na seção 6.5 Expressões, parágrafo 7 :
Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos: 88) - um tipo compatível com o tipo efetivo do objeto,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
- um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
gcc / clang tem uma extensão e também que permite atribuir unsigned int * para int * mesmo que eles não são tipos compatíveis.
- um tipo que é o tipo assinado ou não assinado, correspondente a uma versão qualificada do tipo efetivo do objeto,
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
- um tipo agregado ou de união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida), ou
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
- um tipo de personagem.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
O rascunho do padrão C ++ 17 na seção [basic.lval], parágrafo 11, diz:
Se um programa tentar acessar o valor armazenado de um objeto por meio de um valor gl gl diferente de um dos seguintes tipos, o comportamento será indefinido: 63 (11.1) - o tipo dinâmico do objeto,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) - uma versão qualificada para cv do tipo dinâmico do objeto,
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) - um tipo semelhante (conforme definido em 7.5) ao tipo dinâmico do objeto,
(11.4) - um tipo que é o tipo assinado ou não assinado correspondente ao tipo dinâmico do objeto,
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) - um tipo que é o tipo assinado ou não assinado, correspondente a uma versão qualificada para cv do tipo dinâmico do objeto,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - um tipo agregado ou de união que inclui um dos tipos mencionados acima entre seus elementos ou membros de dados não estáticos (incluindo, recursivamente, um elemento ou membro de dados não estático de uma união subagregada ou contida),
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) - um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) - um tipo de caractere, caractere não assinado ou std :: byte.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
Vale ressaltar que o caractere assinado não está incluído na lista acima. Essa é uma diferença notável de C, que indica um tipo de caractere .
Chegamos a esse ponto e podemos estar se perguntando: por que queremos usar o apelido? A resposta normalmente é digitar trocadilho , geralmente os métodos usados violam regras estritas de alias.
Às vezes, queremos contornar o sistema de tipos e interpretar um objeto como um tipo diferente. Isso é chamado de punção de tipo , para reinterpretar um segmento de memória como outro tipo. A punção de tipo é útil para tarefas que desejam acessar a representação subjacente de um objeto para visualizar, transportar ou manipular. Áreas típicas que encontramos como punição de tipo sendo usadas são compiladores, serialização, código de rede, etc.
Tradicionalmente, isso é conseguido pegando o endereço do objeto, convertendo-o em um ponteiro do tipo que queremos reinterpretá-lo como e depois acessando o valor, ou seja, usando o alias. Por exemplo:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
Como vimos anteriormente, este não é um alias válido, por isso estamos invocando um comportamento indefinido. Mas tradicionalmente os compiladores não tiravam vantagem das regras estritas de alias e esse tipo de código geralmente funcionava; infelizmente, os desenvolvedores se acostumaram a fazer as coisas dessa maneira. Um método alternativo comum para punição de tipo é por meio de uniões, que são válidas em C, mas com comportamento indefinido em C ++ ( veja o exemplo ao vivo ):
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
Isso não é válido em C ++ e alguns consideram que o objetivo das uniões é unicamente para implementar tipos de variantes e consideram que o uso de uniões para punção de tipo é um abuso.
O método padrão para punção de tipo em C e C ++ é memcpy . Isso pode parecer um pouco pesado, mas o otimizador deve reconhecer o uso do memcpy para punções de tipo, otimizá-lo e gerar um registro para registrar a movimentação. Por exemplo, se sabemos que int64_t tem o mesmo tamanho que o dobro :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
podemos usar o memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
Em um nível de otimização suficiente, qualquer compilador moderno decente gera código idêntico ao método reinterpret_cast ou método de união mencionado anteriormente para punção de tipo . Examinando o código gerado, vemos que ele usa apenas registre mov (exemplo ao vivo do Compiler Explorer ).
No C ++ 20, podemos obter bit_cast ( implementação disponível no link da proposta ), que fornece uma maneira simples e segura de digitar trocadilhos, além de ser utilizável em um contexto constexpr.
A seguir, é apresentado um exemplo de como usar bit_cast para digitar pun um int não assinado a flutuar ( veja ao vivo ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
No caso em que os tipos Para e De não têm o mesmo tamanho, é necessário usar uma estrutura intermediária15. Usaremos uma estrutura que contém uma matriz de caracteres sizeof (int não assinada) ( assume int não assinada de 4 bytes ) como o tipo From e int sem assinatura como o tipo To . :
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
É lamentável precisarmos desse tipo intermediário, mas essa é a restrição atual do bit_cast .
Não temos muitas ferramentas boas para capturar aliasing estrito em C ++, as ferramentas que possuímos capturam alguns casos de violações estritas de aliasing e alguns casos de cargas e armazenamentos desalinhados.
O gcc usando o sinalizador -fstrict-aliasing e -Wstrict-aliasing pode capturar alguns casos, embora não sem falsos positivos / negativos. Por exemplo, os seguintes casos gerarão um aviso no gcc ( veja ao vivo ):
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
embora não capte este caso adicional ( veja ao vivo ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Embora o clang permita esses sinalizadores, aparentemente não implementa os avisos.
Outra ferramenta que temos à nossa disposição é o ASan, que pode capturar cargas e lojas desalinhadas. Embora essas não sejam violações estritamente diretas de alias, elas são um resultado comum de violações estritas de alias. Por exemplo, os seguintes casos gerarão erros de tempo de execução quando criados com clang usando -fsanitize = address
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
A última ferramenta que vou recomendar é específica para C ++ e não é estritamente uma ferramenta, mas uma prática de codificação, não permite transmissões no estilo C. Tanto o gcc quanto o clang produzirão um diagnóstico para os elencos no estilo C usando o elenco -Wold-style . Isso forçará qualquer trocadilho de tipo indefinido a usar reinterpret_cast; em geral, reinterpret_cast deve ser um sinalizador para uma revisão mais detalhada do código. Também é mais fácil pesquisar em sua base de códigos por reinterpret_cast para realizar uma auditoria.
Para C, já temos todas as ferramentas abordadas e também temos o tis-intérprete, um analisador estático que analisa exaustivamente um programa para um grande subconjunto da linguagem C. Dadas as versões C do exemplo anterior, em que o uso de -fstrict-aliasing perde um caso ( veja ao vivo )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter é capaz de capturar todos os três, o exemplo a seguir chama tis-kernal como tis-intérprete (a saída é editada por questões de brevidade):
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
Finalmente, há o TySan, que está atualmente em desenvolvimento. Este desinfetante adiciona informações de verificação de tipo em um segmento de memória de sombra e verifica os acessos para ver se eles violam as regras de alias. A ferramenta deve ser capaz de detectar todas as violações de aliasing, mas pode ter uma grande sobrecarga de tempo de execução.
reinterpret_cast
podem fazer ou o que cout
pode significar. (Está tudo bem mencionar C ++, mas a questão original sobre C e IIUC estes exemplos poderia apenas como validamente ser escrito em C.)
O aliasing estrito não se refere apenas a ponteiros, mas também a referências; escrevi um artigo sobre o wiki do desenvolvedor do impulso e foi tão bem recebido que o transformei em uma página no meu site de consultoria. Explica completamente o que é, por que confunde tanto as pessoas e o que fazer sobre isso. White paper estrito sobre alias . Em particular, explica por que as uniões são um comportamento arriscado para C ++ e por que usar memcpy é a única correção portátil em C e C ++. Espero que isso seja útil.
Como adendo ao que Doug T. já escreveu, aqui está um caso de teste simples que provavelmente o aciona com o gcc:
check.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
Compile com gcc -O2 -o check check.c
. Normalmente (com a maioria das versões do gcc que eu tentei), isso gera um "problema estrito de alias", porque o compilador assume que "h" não pode ser o mesmo endereço que "k" na função "check". Por isso, o compilador otimiza a if (*h == 5)
distância e sempre chama o printf.
Para quem está interessado aqui é o código do assembler x64, produzido pelo gcc 4.6.3, rodando no ubuntu 12.04.2 para x64:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
Portanto, a condição if desapareceu completamente do código do assembler.
long long*
e int64_t
*). Pode-se esperar que um compilador sadio reconheça isso long long*
e int64_t*
possa acessar o mesmo armazenamento se eles forem armazenados de forma idêntica, mas esse tratamento não está mais na moda.
A punção de tipo por meio de projeções de ponteiro (ao contrário de usar uma união) é um exemplo importante de quebrar o aliasing estrito.
fpsync()
diretiva entre escrever como fp e ler como int ou vice-versa [em implementações com pipelines e caches inteiros e FPU separados , essa diretiva pode ser cara, mas não tão cara quanto o compilador executar essa sincronização em todos os acessos à união]. Ou uma implementação pode especificar que o valor resultante nunca será utilizável, exceto em circunstâncias usando Sequências Iniciais Comuns.
De acordo com a lógica C89, os autores da Norma não queriam exigir que os compiladores recebessem código como:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
deve ser necessário recarregar o valor de x
entre a atribuição e a declaração de retorno, a fim de permitir a possibilidade que p
possa apontar para x
, e a atribuição para, *p
consequentemente, alterar o valor de x
. A noção de que um compilador deve ter o direito de presumir que não haverá apelidos em situações como a acima não é controversa.
Infelizmente, os autores do C89 escreveram sua regra de uma maneira que, se lida literalmente, faria até mesmo a seguinte função chamar Undefined Behavior:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
porque usa um lvalue do tipo int
para acessar um objeto do tipo struct S
e int
não está entre os tipos que podem ser usados acessando a struct S
. Como seria absurdo tratar todo uso de membros de estruturas e uniões sem caráter de caractere como comportamento indefinido, quase todo mundo reconhece que há pelo menos algumas circunstâncias em que um valor l de um tipo pode ser usado para acessar um objeto de outro tipo . Infelizmente, o Comitê de Padrões C não conseguiu definir quais são essas circunstâncias.
Grande parte do problema é resultado do Relatório de Defeitos # 028, que perguntou sobre o comportamento de um programa como:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
O Relatório de Defeitos # 28 declara que o programa chama Comportamento indefinido porque a ação de escrever um membro da união do tipo "double" e ler um do tipo "int" chama o comportamento definido pela implementação. Esse raciocínio é absurdo, mas forma a base para as regras do Tipo Efetivo que desnecessariamente complicam a linguagem sem fazer nada para resolver o problema original.
A melhor maneira de resolver o problema original provavelmente seria tratar a nota de rodapé sobre o objetivo da regra como se fosse normativa e tornar a regra inaplicável, exceto nos casos que realmente envolvem acessos conflitantes usando aliases. Dado algo como:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Não há conflito interno inc_int
porque todos os acessos ao armazenamento acessado *p
são feitos com um valor de tipo int
e não há conflito test
porque p
é derivado visivelmente de um struct S
e, na próxima vez em que s
for usado, todos os acessos ao armazenamento que serão feitos atravésp
já terá acontecido.
Se o código fosse ligeiramente alterado ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Aqui, existe um conflito de aliasing entre p
e o acesso à s.x
linha marcada, porque nesse ponto da execução existe outra referência que será usada para acessar o mesmo armazenamento .
Se o Relatório de Defeitos 028 dissesse que o exemplo original invocava o UB por causa da sobreposição entre a criação e o uso dos dois ponteiros, isso tornaria as coisas muito mais claras sem a necessidade de adicionar "Tipos eficazes" ou outra complexidade.
Depois de ler muitas das respostas, sinto a necessidade de adicionar algo:
O aliasing estrito (que descreverei um pouco) é importante porque :
O acesso à memória pode ser caro (desempenho), e é por isso que os dados são manipulados nos registros da CPU antes de serem gravados de volta na memória física.
Se dados em dois registros diferentes de CPU forem gravados no mesmo espaço de memória, não podemos prever quais dados "sobreviverão" quando codificarmos em C.
Na montagem, onde codificamos o carregamento e descarregamento dos registros da CPU manualmente, saberemos quais dados permanecem intactos. Mas C (felizmente) abstrai esse detalhe.
Como dois ponteiros podem apontar para o mesmo local na memória, isso pode resultar em código complexo que lida com possíveis colisões .
Esse código extra é lento e prejudica o desempenho, pois realiza operações extras de leitura / gravação de memória, que são mais lentas e (possivelmente) desnecessárias.
A regra de aliasing estrita nos permite evitar código de máquina redundante nos casos em que deve ser seguro assumir que dois ponteiros não apontam para o mesmo bloco de memória (consulte também orestrict
palavra chave).
O aliasing estrito afirma que é seguro assumir que ponteiros para tipos diferentes apontam para locais diferentes na memória.
Se um compilador perceber que dois ponteiros apontam para tipos diferentes (por exemplo, an int *
e a float *
), ele assumirá que o endereço de memória é diferente e não protegerá contra colisões de endereços de memória, resultando em um código de máquina mais rápido.
Por exemplo :
Vamos assumir a seguinte função:
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
Para lidar com o caso em que a == b
(ambos os ponteiros apontam para a mesma memória), precisamos solicitar e testar a maneira como carregamos dados da memória nos registros da CPU, para que o código possa acabar assim:
carregar a
e b
da memória.
adicionar a
a b
.
salve b
e recarregue a
.
(salve do registro da CPU na memória e carregue da memória no registro da CPU).
adicionar b
a a
.
salve a
(do registro da CPU) na memória.
A etapa 3 é muito lenta porque precisa acessar a memória física. No entanto, é necessário proteger contra instâncias em que a
e b
apontar para o mesmo endereço de memória.
O aliasing estrito nos permitiria evitar isso, informando ao compilador que esses endereços de memória são distintamente diferentes (o que, nesse caso, permitirá uma otimização ainda maior que não pode ser realizada se os ponteiros compartilharem um endereço de memória).
Isso pode ser informado ao compilador de duas maneiras, usando tipos diferentes para apontar. ou seja:
void merge_two_numbers(int *a, long *b) {...}
Usando a restrict
palavra - chave ou seja:
void merge_two_ints(int * restrict a, int * restrict b) {...}
Agora, satisfazendo a regra Strict Aliasing, a etapa 3 pode ser evitada e o código será executado significativamente mais rápido.
De fato, adicionando a restrict
palavra - chave, toda a função pode ser otimizada para:
carregar a
e b
da memória.
adicionar a
a b
.
salvar o resultado para a
e para b
.
Essa otimização não poderia ter sido feita antes, devido à possível colisão (onde a
e b
seria triplicada em vez de dobrada).
b
(não recarregando) e recarregando a
. Espero que esteja mais claro agora.
restrict
, mas eu acho que o último seria, na maioria das circunstâncias, mais eficaz, e o relaxamento de algumas restrições register
permitiria preencher alguns dos casos em restrict
que não ajudaria. Não tenho certeza de que tenha sido "importante" tratar o Padrão como uma descrição completa de todos os casos em que os programadores devem esperar que os compiladores reconheçam evidências de alias, em vez de apenas descrever locais onde os compiladores devem presumir alias, mesmo quando não exista nenhuma evidência específica .
restrict
palavra - chave minimiza não apenas a velocidade das operações, mas também o número delas, o que pode ser significativo ... quero dizer, afinal, a operação mais rápida não é nenhuma operação :)
O aliasing estrito não está permitindo diferentes tipos de ponteiros para os mesmos dados.
Este artigo deve ajudar você a entender o problema em detalhes.
int
uma estrutura que contém um int
).
Tecnicamente, em C ++, a regra estrita de aliasing provavelmente nunca é aplicável.
Observe a definição de indireção ( * operador ):
O operador unary * executa a indireção: a expressão à qual é aplicada deve ser um ponteiro para um tipo de objeto ou um ponteiro para um tipo de função e o resultado é um valor l que se refere ao objeto ou função para a qual a expressão aponta .
Também da definição de glvalue
Um glvalue é uma expressão cuja avaliação determina a identidade de um objeto, (... snip)
Portanto, em qualquer rastreamento de programa bem definido, um glvalue se refere a um objeto. Portanto, a chamada regra estrita de aliasing nunca se aplica. Pode não ser o que os designers queriam.
int foo;
, o que é acessado pela expressão lvalue *(char*)&foo
? Isso é um objeto do tipo char
? Esse objeto passa a existir ao mesmo tempo que foo
? Escrever para foo
alterar o valor armazenado desse objeto do tipo acima mencionado char
? Em caso afirmativo, existe alguma regra que permita que o valor armazenado de um objeto do tipo char
seja acessado usando um lvalue do tipo int
?
int i;
cria quatro objetos de cada tipo de caractere in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` e i
. Finalmente, não há nada no Standard que permita que mesmo um volatile
ponteiro qualificado acesse registros de hardware que não atendem à definição de "objeto".
c
ec++faq
.