Pergunta

Quando perguntando sobre indefinido comportamento em C , almas mais iluminado do que a que se refere a regra aliasing estrita.
O que eles estão falando?

Foi útil?

Solução

Uma situação típica você encontrar problemas aliasing estritas é quando cobre um struct (como um dispositivo / msg de rede) em um buffer do tamanho da palavra do seu sistema (como um ponteiro para uint32_ts ou uint16_ts). Quando você sobrepor uma struct em um tal tampão, ou um tampão para tal struct através de ponteiro lançando você pode facilmente violar as regras aliasing estritas.

Assim, neste tipo de configuração, se eu quiser enviar uma mensagem para algo que eu teria que ter dois ponteiros incompatíveis apontando para o mesmo pedaço de memória. Eu poderia código de alguma coisa, então, ingenuamente, como este:

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 aliasing estrita torna esta configuração ilegal: dereferencing um ponteiro que aliases um objeto que não é de um tipo compatível ou um dos outros tipos permitidos por C 2011 6,5 parágrafo 7 1 é indefinido comportamento. Infelizmente, você ainda pode código desta forma, talvez obter alguns avisos, tê-lo compilar bem, só para ter um comportamento inesperado estranho quando você executar o código.

(GCC parece um pouco inconsistente em sua capacidade de dar aliasing avisos, por 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 aliasing estrita compra o compilador. Basicamente, com esta regra, ele não tem que pensar sobre como inserir instruções para atualizar o conteúdo de buff cada execução do loop. Em vez disso, quando otimizar, com alguns pressupostos irritantemente não impostos sobre aliasing, pode omitir essas instruções, buff[0] carga e buff[1] em registros de CPU uma vez antes do loop é executado, e acelerar o corpo do loop. Antes aliasing estrita foi introduzido, o compilador teve que viver em um estado de paranóia que o conteúdo de buff poderia mudar a qualquer momento de qualquer lugar por qualquer pessoa. Assim, para obter uma vantagem de desempenho extra, e assumindo que a maioria das pessoas não escreva-pun ponteiros, a regra aliasing estrita foi introduzido.

Tenha em mente, se você acha que o exemplo é planejado, isso pode mesmo acontecer se você estiver passando um buffer para outra função fazer o envio para você, se em vez você tem.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

E reescreveu nosso loop mais cedo para aproveitar essa 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 de ou bastante inteligente para tentar inline SendMessage e pode ou não pode decidir para carregar ou não carregar lustre novamente. Se SendMessage é parte de outro API que é compilado separadamente, ele provavelmente tem instruções para conteúdos de carga do lustre. Então novamente, talvez você está em C ++ e este é um cabeçalho templated única implementação que o compilador acha que pode inline. Ou talvez seja apenas algo que você escreveu em seu arquivo .c para sua própria conveniência. Enfim comportamento indefinido ainda pode acontecer. Mesmo quando sabemos pouco do que está acontecendo sob o capô, ainda é uma violação da regra para o comportamento não bem definido é garantida. Então, só por envolvimento em uma função que leva o nosso tampão palavra delimitado não necessariamente ajuda.

Então, como posso resolver isso?

  • Use uma união. A maioria dos compiladores suportar isso sem reclamar aliasing estrita. Isso é permitido em C99 e explicitamente permitido em C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • Você pode desativar aliasing estrita em seu compilador ( f [no-] estrita-aliasing no gcc))

  • Você pode usar char* para aliasing em vez da palavra de seu sistema. As regras permitem uma exceção para char* (incluindo signed char e unsigned char). sempre assumido que char* aliases outros tipos. No entanto isso não vai funcionar a outra maneira: não há'S nenhuma suposição de que o seu struct aliases um buffer de caracteres.

Beginner cuidado

Este é apenas um campo minado em potencial quando sobrepondo dois tipos um ao outro. Você também deve aprender sobre endianness , palavra de alinhamento , e como para lidar com problemas de alinhamento através embalagem estruturas corretamente.

Nota de rodapé

1 Os tipos que C 2011 6.5 7 permite que um lvalue de acesso são:

  • um tipo compatível com o tipo efetiva do objeto,
  • uma versão qualificada de um tipo compatível com o tipo efetiva do objeto,
  • um tipo que é o assinaram ou tipo sem sinal correspondente ao tipo efetiva do objeto,
  • um tipo que é o sinal ou sem sinal de tipo correspondente a uma versão qualificado do tipo efetiva do objeto,
  • um tipo de agregado ou união que inclui um dos tipos acima mencionados, entre os seus membros (incluindo, de forma recursiva, um membro de um subaggregate ou união continha), ou
  • um tipo de personagem.

Outras dicas

A melhor explicação que eu encontrei é por Mike Acton, Entendimento Aliasing Strict. É focado um pouco sobre o desenvolvimento PS3, mas isso é basicamente apenas GCC.

Do artigo:

"aliasing estrita é uma suposição, feita pelo compilador C (ou C ++), que dereferencing ponteiros para objetos de diferentes tipos nunca se referir ao local mesma memória (ou seja, apelido uns aos outros)."

Então, basicamente, se você tiver um apontador int* para alguma memória contendo um int e depois que você apontar um float* a essa memória e usá-lo como um float você quebrar a regra. Se o seu código não respeita isso, então otimizador do compilador provavelmente irá quebrar seu código.

A exceção à regra é um char*, o que é permitido para apontar para qualquer tipo.

Esta é a regra aliasing estrito, encontrado na seção 3.10 do C ++ 03 padrão (outras respostas fornecem uma boa explicação, mas nenhum desde que a regra em si):

Se um programa tenta acessar o valor armazenado de um objeto através de um lvalue diferente de um dos seguintes tipos do comportamento é indefinido:

  • o tipo dinâmico do objeto,
  • uma versão cv qualificado do tipo dinâmico do objeto,
  • um tipo que é o assinaram ou tipo sem sinal correspondente ao tipo dinâmico do objeto,
  • um tipo que é o sinal ou sem sinal de tipo correspondente a uma versão cv-qualificado do tipo dinâmico do objecto,
  • um tipo de agregado ou união que inclui um dos tipos acima mencionados, entre os seus membros (incluindo, de forma recursiva, um membro de um subaggregate ou união continha),
  • um tipo que é um tipo de classe de base (possivelmente cv-qualificado) do tipo dinâmico do objecto,
  • a char ou unsigned char tipo.

C ++ 11 e C ++ 14 redação (mudanças enfatizado):

Se um programa tenta acessar o valor armazenado de um objeto através de um glvalue diferente de um dos seguintes tipos do comportamento é indefinido:

  • o tipo dinâmico do objeto,
  • uma versão cv qualificado do tipo dinâmico do objeto,
  • um tipo semelhante (tal como definido em 4.4) para o tipo dinâmico do objecto,
  • um tipo que é o assinaram ou tipo sem sinal correspondente ao tipo dinâmico do objeto,
  • um tipo que é o sinal ou sem sinal de tipo correspondente a uma versão cv-qualificado do tipo dinâmico do objecto,
  • um tipo de agregado ou união que inclui um dos tipos acima mencionados entre os seus elementos ou membros de dados não-estáticos (incluindo, de forma recursiva, um elemento ou membro de dados não-estático de um subaggregate ou união contido),
  • um tipo que é um tipo de classe de base (possivelmente cv-qualificado) do tipo dinâmico do objecto,
  • a char ou unsigned char tipo.

Duas mudanças foram pequenas:. glvalue em vez de lvalue , e esclarecimento do caso / união agregado

A terceira mudança faz uma garantia mais forte (relaxa o forte regra aliasing):. O novo conceito de tipos semelhantes , que são agora seguro apelido


Também o C formulação (C99; ISO / IEC 9899: 1999 6,5 / 7; exacta a mesma formulação é utilizada na norma ISO / IEC 9899: 2011 §6.5 ¶7):

Um objeto deve ter um valor armazenado acessado apenas por um lvalue expressão que tem um dos seguintes tipos 73) ou 88) :

  • um tipo compatível com o tipo efetiva do objeto,
  • uma versão qualificada de um tipo compatível com o tipo eficaz de o objeto,
  • um tipo que é o sinal ou sem sinal tipo correspondente ao Tipo efetiva do objeto,
  • um tipo que é o sinal ou sem sinal de tipo correspondente a um versão quali fi cado do tipo efetiva do objeto,
  • um tipo de agregado ou união que inclui um dos acima mencionados tipos entre seus membros (incluindo, de forma recursiva, um membro de uma subaggregate ou união contida), ou
  • um tipo de personagem.

73) ou 88) A intenção desta lista é especificar essas circunstâncias em que um objeto pode ou não ser alias.

Nota

Este é um excerto do meu "Qual é a regra Aliasing Strict e por que nos preocupar?" write-up.

O que é aliasing estrita?

Em C e C ++ aliasing tem a ver com os tipos de expressão que têm permissão para acessar valores armazenados completamente. Em ambos C e C ++ especifica o padrão que tipos de expressão são permitidos para alias de quais os tipos. O compilador e otimizador estão autorizados a assumir que seguir as regras aliasing rigorosamente, daí o termo regra estrita aliasing . Se tentar acessar um valor usando um tipo não permitidos, é classificado como comportamento indefinido ( UB ). Uma vez que temos um comportamento indefinido todas as apostas estão fora, os resultados do nosso programa não são confiáveis.

Infelizmente com violações aliasing estrito, nós, muitas vezes, obter os resultados que esperamos, deixando a possibilidade da versão de um futuro de um compilador com uma nova otimização vai quebrar o código que pensávamos era válido. Isso é indesejável e é um objetivo de valor para entender as regras aliasing estritas e como evitar violá-las.

Para entender mais sobre por que nos preocupamos, vamos discutir as questões que surgem quando violando as regras aliasing estritas, tipo trocadilhos desde técnicas comuns usadas no tipo trocadilhos muitas vezes violam as regras aliasing estritas e como digitar pun corretamente.

exemplos preliminares

Vamos olhar alguns exemplos, então podemos falar sobre exatamente o que o padrão (s) dizer, examinar alguns exemplos adicionais e depois ver como evitar rigorosas aliasing e captura violações perdemos. Aqui está um exemplo que não deve ser surpreendente ( 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 este é um aliasing válido. O otimizador deve assumir que as atribuições através de ip pode atualizar o valor ocupado por x .

O exemplo seguinte apresenta aliasing 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 tomamos um int * e float * , neste exemplo que chamamos de foo e definir ambos os parâmetros para apontar para o mesmo local de memória que neste exemplo contém um int . Note, o reinterpret_cast está dizendo o compilador para tratar a expressão como se tivesse o tipo specificed pelo seu parâmetro de modelo. Neste caso, estamos dizendo a ele para tratar a expressão & x como se tivesse tipo float * . Nós podemos ingenuamente esperar o resultado da segunda cout ser 0 , mas com a otimização ativada usando -O2 tanto gcc e produtos clang o seguinte resultado:

0
1

O que não pode ser esperado, mas é perfeitamente válido, uma vez que invocaram um comportamento indefinido. A flutuador não pode validamente apelido de um int objeto. Portanto, o otimizador pode assumir a constante 1 armazenado quando dereferencing i será o valor de retorno desde a loja através de f não pôde validamente afetar um int objeto. Conectando o código no compilador Explorer mostra este é exatamente o 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 usando Tipo-Based Análise Alias ??(TBAA) assume 1 irá ser devolvido e directamente movimentoé o valor constante no registrador eax que carrega o valor de retorno. TBAA utiliza as regras de línguas sobre que tipos estão autorizados a alias para cargas otimizar e lojas. Neste caso TBAA sabe que um flutuador não pode alias e int e otimiza afastado a carga de i .

Agora, a Regra-Book

O que faz exatamente o padrão dizer que são permitidos e não é permitido fazer? A língua padrão não é simples, por isso para cada item Vou tentar dar exemplos de código que demonstra o significado.

O que a palavra padrão C11?

O C11 padrão diz o seguinte na seção 6,5 Expressões n.º 7 :

Um objeto deve ter um valor armazenado acessado apenas por uma expressão lvalue que tem um dos seguintes tipos: 88) - um tipo compatível com o tipo efetiva 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 efetiva 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 assinaram ou tipo sem sinal correspondente ao tipo efetiva 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 * e int * mesmo que eles não são tipos compatíveis.

- um tipo que é o sinal ou sem sinal de tipo correspondente a uma versão qualificado do tipo efetiva 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 de agregado ou união que inclui um dos tipos acima mencionados, entre os seus membros (incluindo, de forma recursiva, um membro de um subaggregate ou união continha), 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 caractere

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 que o C ++ 17 Projecto de Norma dizer

A C ++ 17 projecto de norma na seção [basic.lval] parágrafo 11 diz:

Se um programa tenta acessar o valor armazenado de um objeto através de uma glvalue diferente de um dos seguintes tipos do comportamento é 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 cv-qualificado do tipo dinâmico do objecto,

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 (tal como definido em 7.5) para o tipo dinâmico do objecto,

(11,4) - um tipo que é a assinatura ou tipo sem sinal correspondente para o tipo dinâmico do objecto,

// 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 sinal ou sem sinal de tipo correspondente a uma versão cv-qualificado do tipo dinâmico do objecto,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11,6) - um tipo de agregado ou união que inclui um dos tipos acima mencionados entre os seus elementos ou membros de dados nonstatic (incluindo, de forma recursiva, um elemento ou membro de dados não estático de um subaggregate ou união continha),

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 de base (possivelmente cv-qualificado) do tipo dinâmico do objecto,

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 char, char não assinado, ou tipo 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
}

Digno de nota assinado carvão não está incluído na lista acima, esta é uma diferença notável de C que diz um tipo de caractere .

O que é Tipo trocadilhos

Temos chegado a este ponto e que pode estar se perguntando, por que iria querer alias para? A resposta normalmente é Tipo pun , muitas vezes os métodos utilizadosviolar as regras aliasing estritas.

Às vezes queremos contornar o sistema de tipos e interpretar um objeto como um tipo diferente. Isso é chamado tipo trocadilhos , para reinterpretar um segmento de memória como outro tipo. Tipo trocadilhos é útil para tarefas que querem ter acesso à representação subjacente de um objeto para a vista, o transporte ou manipular. As áreas típicas que encontramos tipo trocadilhos sendo utilizados são compiladores, serialização, código de rede, etc ...

Tradicionalmente, esta tem sido realizado tomando o endereço do objeto, convertê-lo para um ponteiro do tipo queremos reinterpretá-lo como e, em seguida, acessar o valor, ou em outras palavras por aliasing. 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 no início deste não é um aliasing válida, por isso estamos invocando um comportamento indefinido. Mas, tradicionalmente compiladores não aproveitar de regras aliasing rigorosos e este tipo de código geralmente apenas trabalhou, os desenvolvedores têm, infelizmente, se acostumado a fazer as coisas desta maneira. Um método alternativo comum para o tipo de trocadilhos é através de sindicatos, que é válida em C, mas comportamento indefinido em C ++ ( ver ao vivo exemplo ):

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

Isto não é válida em C ++ e alguns consideram o objetivo dos sindicatos de ser exclusivamente para a implementação de tipos de variantes e sentir usando sindicatos para o tipo de trocadilhos é um abuso.

Como podemos Digite Pun corretamente?

O método padrão para tipo punning em C e C ++ é memcpy . Isto pode parecer um pouco pesado entregou, mas o otimizador deve reconhecer o uso de memcpy tipo trocadilhos e otimizá-lo para longe e gerar um registo de registrar movimento. Por exemplo, se sabemos int64_t é o mesmo tamanho que duplo :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

podemos usar memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Em um nível de otimização suficientes qualquer compilador moderno decente gera código idêntico ao reinterpret_cast método previamente mencionado ou união método para tipo trocadilhos . Examinando o código gerado vemos ele usa apenas registrar mov ( vivo Exemplo Explorador Compiler ).

C ++ 20 e bit_cast

Em C ++ 20 podemos ganhar bit_cast ( implementação disponível no link da proposta ) que dá uma maneira simples e segura para digitar-pun, bem como sendo utilizáveis ??num contexto constexpr.

A seguir é um exemplo de como usar o bit_cast para digitar pun um unsigned int e flutuador , ( vê-lo ao vivo ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

No caso em que Para e From tipos não têm o mesmo tamanho, que nos obriga a usar um struct15 intermediária. Vamos usar uma estrutura que contém um sizeof (int não assinado) array de caracteres ( assume 4 byte unsigned int ) para ser o From tipo e unsigned int como o Para tipo:.

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 que precisamos deste tipo intermediário, mas que é a restrição atual de bit_cast .

Catching Violações Aliasing Strict

Nós não temos um monte de boas ferramentas para a captura de aliasing estrita em C ++, as ferramentas que temos vai pegar alguns casos de violações aliasing rígidas e alguns casos de cargas desalinhadas e lojas.

gcc usando a bandeira -fstrict-aliasing e -Wstrict-aliasing pode pegar alguns casos, embora não sem positivos falsos / negativos. Por exemplo, os seguintes casos irá gerar um aviso no gcc ( vê-lo 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 vai pegar neste caso adicional ( vê-lo ao vivo ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Embora clang permite que essas bandeiras, aparentemente, não realmente implementar as advertências.

Outra ferramenta que temos disponível para nós é Asan que pode pegar cargas desalinhadas e lojas. Embora estes não são diretamente violações aliasing estritas eles são um resultado comum de violações aliasing estritas. Por exemplo, os seguintes casos irão gerar erros de execução, quando construído com clang usando -fsanitize = endereço

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 eu recomendo é C ++ específico e não estritamente uma ferramenta, mas uma prática de codificação, não permitem moldes de estilo C. Ambos gcc e clang irá produzir um diagnóstico para moldes de estilo C usando -style -Wold-cast . Isto irá forçar nenhum trocadilhos tipo indefinido de usar reinterpret_cast, em reinterpret_cast geral deve ser uma bandeira para mais perto revisão do código. É também mais fácil para pesquisar sua base de código para reinterpret_cast para realizar uma auditoria.

Para C temos todas as ferramentas já cobertas e também temos tis-intérprete, um analisador estático que exaustivamente analisa um programa para um grande subconjunto da linguagem C. Dada a C verions do exemplo anterior, onde usando -fstrict-aliasing erra um caso ( vê-lo 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, dos seguintes exemplo invoca tis-kernal como tis-intérprete (saída é editado por 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á TySan que está atualmente em desenvolvimento. Este desinfetante adiciona informações verificação de tipo em um segmento de memória sombra e verificações de acessos para ver se eles violam aliasing regras. A ferramenta potencialmente deve ser capaz de capturar todas as violações aliasing, mas pode ter uma grande sobrecarga de tempo de execução.

aliasing estrito não se refere apenas aos ponteiros, afeta referências bem, eu escrevi um artigo sobre isso para o wiki impulso desenvolvedor e foi tão bem recebido que eu transformou-o em uma página no meu site de consultoria. Ele explica completamente o que é, por isso confunde as pessoas tanto e o que fazer sobre isso. . Em particular, explica por que os sindicatos são comportamentos de risco para C ++, e por que usar memcpy é a única corrigir portátil através de C e C ++. Espero que este seja útil.

Como adendo ao que Doug T. já escreveu, aqui é um caso de teste simples que provavelmente aciona-lo com 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;
}

Compilar com gcc -O2 -o check check.c. Normalmente (com a maioria das versões gcc Tentei) este saídas "strict problema aliasing", porque o compilador assume que "h" não pode ser o mesmo endereço como "k" na função "verificação". Devido a que o compilador otimiza a if (*h == 5) longe e sempre chama o printf.

Para aqueles que estão interessados ??aqui está o código x64 assembler, produzido por gcc 4.6.3, rodando em Ubuntu 12.04.2 para x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Assim, a se a condição é completamente desaparecido a partir do código assembler.

Digite trocadilhos via ponteiro moldes (em vez de usar uma união) é um grande exemplo de quebrando aliasing estrita.

De acordo com a lógica C89, os autores do Padrão não queria exigir que os compiladores dado código como:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

devem ser obrigados a recarregar o valor de x entre a atribuição e instrução de retorno, de modo a permitir a possibilidade de que p pode apontar para x, ea atribuição de *p pode, consequentemente, alterar o valor de x. A noção de que um compilador deve ter direito a presumir que não haverá aliasing em situações como o acima foi não-controversa.

Infelizmente, os autores do C89 escreveu sua regra de uma forma que, se lido literalmente, faria mesmo a seguinte função de invocação Undefined Comportamento:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

porque ele 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 utilizados acessar um struct S. Porque seria absurdo para tratar todo o uso de membros caracteres de tipo não de estruturas e sindicatos como um comportamento indefinido, quase todos reconhecem que existem pelo menos algumas circunstâncias em que um lvalue de um tipo podem ser usados ??para acessar um objeto de outro tipo . Infelizmente, Comitê de Padrões C não foi capaz de definir o que essas circunstâncias são.

Grande parte do problema é resultado de Defeito Relatório # 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);
}

Relatório Defect # 28 estados que as invoca programa comportamento indefinido porque a ação de escrever um membro de união do tipo "double" e lendo um dos tipo "int" invoca o comportamento definido pela implementação. Tal raciocínio é absurda, mas constitui a base para as regras Tipo eficaz que desnecessariamente complicar a língua enquanto não fazer nada para resolver o problema original.

A melhor maneira de resolver o problema original seria provavelmente para tratar a nota de rodapé sobre o propósito da regra como se fosse normativo, e fez a regra inexequível, exceto em casos que na verdade envolvem acessos conflitantes usando pseudônimos. Dada 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á nenhum conflito dentro inc_int porque todos os acessos ao armazenamento acessados ??através *p é feito com um lvalue do tipo int, e não há conflito no test porque p está visivelmente derivado de uma struct S, e pelo próximo s tempo é usado, todos os acessos a esse armazenamento que irá sempre ser feitas através p já terá acontecido.

Se o código foram alterados ligeiramente ...

 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, há um conflito aliasing entre p eo acesso a s.x na linha marcada porque naquele ponto da execução existe outra referência que será usado para acessar o mesmo armazenamento .

Had Defeito Relatório 028 disse que o exemplo original invocado UB por causa da sobreposição entre a criação e utilização dos dois ponteiros, que teria feito coisas muito mais claras sem ter que adicionar "Tipos eficaz" ou outro tal complexidade.

Depois de ler muitas das respostas, eu sinto a necessidade de acrescentar algo:

aliasing estrita (que eu vou descrever em um bit) é importante porque :

  1. Acesso à memória pode ser caro (desempenho sábio), razão pela qual dados são manipulados em registros de CPU antes de ser escrito de volta para a memória física.

  2. Se os dados em dois registradores de CPU diferentes serão gravados no mesmo espaço de memória, não podemos prever quais os dados serão "sobreviver" quando o código em C.

    Na montagem, onde codificar a carga e descarga de registos da CPU manualmente, vamos saber quais dados permanecem intactos. Mas C (felizmente) abstrai esse detalhe distância.

Uma vez que dois ponteiros podem apontar para o mesmo local na memória, isso poderia resultar em código complexo que lida com possíveis colisões .

Este código extra é lento e dói desempenho , uma vez que executa memória extra operações de leitura / gravação que são tanto mais lento e (possivelmente) desnecessário.

O regra aliasing estrita nos permite evitar código redundante máquina em casos em que deve ser seguro assumir que dois ponteiros não apontam para o mesmo bloco de memória (ver também a palavra-chave restrict).

O aliasing estrita afirma que é seguro assumir que os ponteiros para diferentes tipos apontar para locais diferentes na memória.

Se um compilador avisos que dois ponteiros apontam para diferentes tipos (por exemplo, um int * e uma float *), ele assumirá o endereço de memória é diferente e não proteger contra colisões de endereço de memória, resultando em código de máquina mais rápida.

Por exemplo :

Vamos supor a seguinte função:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

A fim de lidar com o caso em que a == b (ambos os ponteiros apontam para a mesma memória), precisamos ordem e testar os dados de carga maneira que nós da memória para os registradores da CPU, para que o código pode acabar assim:

  1. a carga e b da memória.

  2. add a para b.

  3. Salvar b e Recarregar a.

    (save do registro de CPU para a memória ea carga da memória para o registo de CPU).

  4. add b para a.

  5. save a (do registo CPU) para a memória.

Passo 3 é muito lento porque ele precisa acessar a memória física. No entanto, é necessário para proteger contra casos em que a e ponto b para o mesmo endereço de memória.

aliasing estrita nos permitiria evitar isso, dizendo que o compilador que esses endereços de memória são muito diferentes (que, neste caso, vai permitir ainda mais a otimização que não pode ser realizada se os ponteiros compartilhar um endereço de memória).

  1. Isto pode ser dito para o compilador de duas maneiras, usando tipos diferentes para apontar para. ou seja:.

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Usando a palavra-chave restrict. ou seja:.

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Agora, satisfazendo a regra Aliasing Strict, passo 3 pode ser evitada e o código será executado significativamente mais rápido.

Na verdade, adicionando a palavra-chave restrict, toda a função poderia ser otimizado para:

  1. a carga e b da memória.

  2. add a para b.

  3. save resultado, tanto para a e b.

Esta otimização não poderia ter sido feito antes, por causa da possível colisão (onde a e b seria triplicou em vez do dobro).

aliasing rigorosa não é permitir que diferentes tipos de ponteiro para os mesmos dados.

Este artigo deve ajudar a compreender o problema com todos os detalhes.

Tecnicamente em C ++, a regra aliasing estrita é provavelmente nunca aplicável.

Observe a definição de engano ( * operador ):

O unário executa operador * indireto: a expressão a que é aplicado deve ser um ponteiro para um tipo de objecto, ou um ponteiro para uma tipo de função e O resultado é um lvalue referindo-se ao objeto ou função para que os pontos de expressão .

Também a partir a definição de glvalue

A glvalue é uma expressão cuja avaliação determina a identidade do um objeto, (... snip)

Assim, em qualquer traço programa bem definido, um glvalue refere-se a um objeto. Então, o chamado regra aliasing estrita não se aplica, nunca. Isto pode não ser o que os designers queria.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top