Qual é a regra de aliasing estrita?

Sep 19 2008

Ao perguntar sobre o comportamento indefinido comum em C , as pessoas às vezes se referem à regra de aliasing estrita.
Do que eles estão falando?

Respostas

571 DougT. Sep 19 2008 at 09:36

Uma situação típica onde você encontra problemas de aliasing estritos é ao sobrepor uma estrutura (como um dispositivo / mensagem de rede) em um buffer do tamanho da palavra do seu sistema (como um ponteiro para uint32_ts ou uint16_ts). Quando você sobrepõe uma estrutura em tal buffer, ou um buffer em tal estrutura por meio de conversão de ponteiro, você pode facilmente violar regras estritas de aliasing.

Portanto, neste tipo de configuração, se eu quiser enviar uma mensagem para algo, tenho que ter dois ponteiros incompatíveis apontando para o mesmo pedaço de memória. Eu poderia então codificar ingenuamente algo assim:

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 de aliasing estrito torna esta configuração ilegal: desreferenciar um ponteiro que cria um alias de 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 é um comportamento indefinido. Infelizmente, você ainda pode codificar dessa forma, talvez receba alguns avisos, faça com que seja compilado corretamente, apenas para ter um comportamento estranho e inesperado ao executar o código.

(GCC parece um tanto inconsistente em sua capacidade de dar avisos de alias, às vezes dando-nos um aviso amigável e às vezes não.)

Para ver por que esse comportamento é indefinido, temos que pensar sobre o que a regra estrita de aliasing compra o compilador. Basicamente, com esta regra, não é necessário pensar em inserir instruções para atualizar o conteúdo de buffcada execução do loop. Em vez disso, ao otimizar, com algumas suposições irritantemente não forçadas sobre aliasing, ele pode omitir essas instruções, carregar buff[0]e buff[1] nos registros da CPU uma vez antes de o loop ser executado e acelerar o corpo do loop. Antes do aliasing estrito ser introduzido, o compilador tinha que viver em um estado de paranóia de que o conteúdo buffpoderia ser alterado a qualquer momento de qualquer lugar por qualquer pessoa. Portanto, para obter uma vantagem extra de desempenho, e assumindo que a maioria das pessoas não digita ponteiros de trocadilho, foi introduzida a regra de aliasing estrita.

Lembre-se de que, se você acha que o exemplo é inventado, isso pode até acontecer se você estiver passando um buffer para outra função que está enviando para você, se em vez disso você o tiver feito.

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

E reescreveu nosso loop anterior para aproveitar esta 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 embutir SendMessage e pode ou não decidir carregar ou não carregar o buff novamente. Se fizer SendMessageparte de outra API compilada separadamente, provavelmente contém instruções para carregar o conteúdo do buff. Então, novamente, talvez você esteja em C ++ e esta seja uma implementação somente de cabeçalho com modelo 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 algo do que está acontecendo nos bastidores, ainda é uma violação da regra, portanto, nenhum comportamento bem definido é garantido. Portanto, apenas envolver em uma função que leva nosso buffer delimitado por palavras não ajuda necessariamente.

Então, como faço para contornar isso?

  • Use um sindicato. A maioria dos compiladores suporta isso sem reclamar de apelidos estritos. Isso é permitido em C99 e explicitamente permitido em C11.

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

  • Você pode usar char*para aliasing em vez da palavra do seu sistema. As regras permitem uma exceção para char*(incluindo signed chare unsigned char). Sempre se presume que é um char*apelido para outros tipos. No entanto, isso não funcionará de outra maneira: não há suposição de que sua estrutura seja um buffer de chars.

Principiante, cuidado

Este é apenas um campo minado 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 por meio de estruturas de empacotamento corretamente.

Nota de rodapé

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

  • 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 de agregado ou união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de um subagregado ou união contida), ou
  • um tipo de personagem.
244 Niall Sep 19 2008 at 08:38

A melhor explicação que encontrei é de Mike Acton, Understanding Strict Aliasing . É focado um pouco no desenvolvimento do PS3, mas é basicamente apenas GCC.

Do artigo:

"Aliasing estrito é uma suposição, feita pelo compilador C (ou C ++), de que ponteiros de desreferenciamento para objetos de tipos diferentes nunca se referirão ao mesmo local de memória (ou seja, alias uns aos outros).

Então, basicamente, se você tiver um int*apontando para alguma memória contendo um inte, em seguida, apontar um float*para essa memória e usá-lo como um, floatvocê infringirá a 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.

137 BenVoigt Aug 10 2011 at 11:43

Esta é a regra de aliasing estrita, encontrada na seção 3.10 do padrão C ++ 03 (outras respostas fornecem uma boa explicação, mas nenhuma forneceu a regra em si):

Se um programa tentar acessar o valor armazenado de um objeto por meio de um lvalue diferente de um dos seguintes tipos, o comportamento será indefinido:

  • o tipo dinâmico do objeto,
  • uma versão cv-qualificada 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 cv-qualificada do tipo dinâmico do objeto,
  • um tipo de agregado ou união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de um subagregado ou união contida),
  • um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
  • a charou unsigned chardigite.

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 glvalue diferente de um dos seguintes tipos, o comportamento será indefinido:

  • o tipo dinâmico do objeto,
  • uma versão cv-qualificada 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 cv-qualificada do tipo dinâmico do objeto,
  • 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áticos de um subagregado ou união contida),
  • um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
  • a charou unsigned chardigite.

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 regra de aliasing forte): O novo conceito de tipos semelhantes que agora são seguros para atalhos.


Também a redação C (C99; ISO / IEC 9899: 1999 6.5 / 7; a mesma redação exata é 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 com ou sem sinal correspondente a uma versão qualificada do tipo efetivo do objeto,
  • um tipo de agregado ou união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de um subagregado ou união contida), ou
  • um tipo de personagem.

73) ou 88) O objetivo desta lista é especificar as circunstâncias em que um objeto pode ou não ter um alias.

93 ShafikYaghmour Jul 08 2018 at 09:07

Observação

Este é um trecho do meu "O que é a regra de aliasing estrito e por que nos importamos?" escrever.

O que é aliasing estrito?

Em C e C ++, o aliasing tem a ver com os tipos de expressão por meio dos quais podemos acessar os valores armazenados. Em C e C ++, o padrão especifica quais tipos de expressão têm permissão para criar um alias de quais tipos. O compilador e o otimizador podem assumir que seguimos estritamente as regras de aliasing, daí o termo regra de aliasing estrito . Se tentarmos acessar um valor usando um tipo não permitido, ele será classificado como comportamento indefinido ( UB ). Assim que tivermos um comportamento indefinido, todas as apostas serão canceladas, os resultados do nosso programa não são mais confiáveis.

Infelizmente, com violações de aliasing estritas, geralmente obteremos os resultados esperados, deixando a possibilidade de uma versão futura de um compilador com uma nova otimização quebrar o código que pensávamos ser válido. Isso é indesejável e vale a pena entender as regras estritas de aliasing e como evitar violá-las.

Para entender mais sobre por que nos importamos, discutiremos os problemas que surgem ao violar regras estritas de aliasing, tipo trocadilho, uma vez que as técnicas comuns usadas no trocadilho frequentemente violam regras estritas de aliasing e como digitar o trocadilho corretamente.

Exemplos preliminares

Vejamos alguns exemplos, então podemos falar exatamente sobre o que o (s) padrão (ões) dizem, examinar alguns exemplos adicionais e então ver como evitar aliasing estrito e detectar violações que 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 por meio de ip podem atualizar o valor ocupado por x .

O próximo exemplo mostra o 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, pegamos um int * e um float * , neste exemplo chamamos foo e definimos ambos os parâmetros para apontar para o mesmo local de memória que neste exemplo contém um int . Observe, o reinterpret_cast está dizendo ao compilador para tratar a expressão como se ela tivesse o tipo especificado por seu parâmetro de modelo. Neste caso, estamos dizendo a ele para tratar a expressão & x como se ela tivesse o tipo float * . Podemos ingenuamente esperar que o resultado do segundo cout seja 0, mas com a otimização habilitada 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 o comportamento indefinido. Um float não pode dar um alias válido a um objeto int . Portanto, o otimizador pode assumir que a constante 1 armazenada ao desreferenciar i será o valor de retorno, uma vez que um armazenamento por meio de f não pode afetar validamente um objeto int . Conectar o código no Compiler Explorer mostra que isso é 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 Type-Based Alias ​​Analysis (TBAA) assume que 1 será retornado e move diretamente o valor constante para o registrador eax que carrega o valor de retorno. A TBAA usa as regras de linguagem sobre quais tipos têm permissão para criar alias para otimizar cargas e armazenamentos. Nesse caso, a TBAA sabe que um flutuador não pode fazer alias e int e otimiza a carga de i .

Agora, para o Livro de Regras

O que exatamente o padrão diz que podemos ou não podemos fazer? A linguagem padrão não é direta, portanto, para cada item, tentarei fornecer exemplos de código que demonstrem o significado.

O que o padrão C11 diz?

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 com ou sem sinal 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 permite atribuir int * sem sinal a int * mesmo que eles não sejam 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 de agregado ou união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de um subagregado ou união 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 que o Draft Standard C ++ 17 diz

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 glvalue diferente de um dos seguintes tipos, o 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-qualificada 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 com ou sem sinal 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 com ou sem sinal correspondente a uma versão cv-qualificada 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 acima mencionados entre seus elementos ou membros de dados não estáticos (incluindo, recursivamente, um elemento ou membro de dados não estáticos de um subagregado ou união 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 por 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 char, unsigned char 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 a pena notar que char assinado não está incluído na lista acima, esta é uma diferença notável de C que diz um tipo de caractere .

What is Type Punning

We have gotten to this point and we may be wondering, why would we want to alias for? The answer typically is to type pun, often the methods used violate strict aliasing rules.

Sometimes we want to circumvent the type system and interpret an object as a different type. This is called type punning, to reinterpret a segment of memory as another type. Type punning is useful for tasks that want access to the underlying representation of an object to view, transport or manipulate. Typical areas we find type punning being used are compilers, serialization, networking code, etc…

Traditionally this has been accomplished by taking the address of the object, casting it to a pointer of the type we want to reinterpret it as and then accessing the value, or in other words by aliasing. For example:

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 ) ;

As we have seen earlier this is not a valid aliasing, so we are invoking undefined behavior. But traditionally compilers did not take advantage of strict aliasing rules and this type of code usually just worked, developers have unfortunately gotten used to doing things this way. A common alternate method for type punning is through unions, which is valid in C but undefined behavior in C++ (see live example):

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

This is not valid in C++ and some consider the purpose of unions to be solely for implementing variant types and feel using unions for type punning is an abuse.

How do we Type Pun correctly?

The standard method for type punning in both C and C++ is memcpy. This may seem a little heavy handed but the optimizer should recognize the use of memcpy for type punning and optimize it away and generate a register to register move. For example if we know int64_t is the same size as double:

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

we can use memcpy:

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

At a sufficient optimization level any decent modern compiler generates identical code to the previously mentioned reinterpret_cast method or union method for type punning. Examining the generated code we see it uses just register mov (live Compiler Explorer Example).

C++20 and bit_cast

In C++20 we may gain bit_cast (implementation available in link from proposal) which gives a simple and safe way to type-pun as well as being usable in a constexpr context.

The following is an example of how to use bit_cast to type pun a unsigned int to float, (see it live):

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

In the case where To and From types don't have the same size, it requires us to use an intermediate struct15. We will use a struct containing a sizeof( unsigned int ) character array (assumes 4 byte unsigned int) to be the From type and unsigned int as the To type.:

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 ;
}

It is unfortunate that we need this intermediate type but that is the current constraint of bit_cast.

Catching Strict Aliasing Violations

We don't have a lot of good tools for catching strict aliasing in C++, the tools we have will catch some cases of strict aliasing violations and some cases of misaligned loads and stores.

gcc using the flag -fstrict-aliasing and -Wstrict-aliasing can catch some cases although not without false positives/negatives. For example the following cases will generate a warning in gcc (see it live):

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)));

although it will not catch this additional case (see it live):

int *p;

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

Although clang allows these flags it apparently does not actually implement the warnings.

Another tool we have available to us is ASan which can catch misaligned loads and stores. Although these are not directly strict aliasing violations they are a common result of strict aliasing violations. For example the following cases will generate runtime errors when built with clang using -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]

The last tool I will recommend is C++ specific and not strictly a tool but a coding practice, don't allow C-style casts. Both gcc and clang will produce a diagnostic for C-style casts using -Wold-style-cast. This will force any undefined type puns to use reinterpret_cast, in general reinterpret_cast should be a flag for closer code review. It is also easier to search your code base for reinterpret_cast to perform an audit.

For C we have all the tools already covered and we also have tis-interpreter, a static analyzer that exhaustively analyzes a program for a large subset of the C language. Given a C verions of the earlier example where using -fstrict-aliasing misses one case (see it live)

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 is able to catch all three, the following example invokes tis-kernal as tis-interpreter (output is edited for brevity):

./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.

Finally there is TySan which is currently in development. This sanitizer adds type checking information in a shadow memory segment and checks accesses to see if they violate aliasing rules. The tool potentially should be able to catch all aliasing violations but may have a large run-time overhead.

44 phorgan1 Jun 20 2011 at 06:46

Strict aliasing doesn't refer only to pointers, it affects references as well, I wrote a paper about it for the boost developer wiki and it was so well received that I turned it into a page on my consulting web site. It explains completely what it is, why it confuses people so much and what to do about it. Strict Aliasing White Paper. In particular it explains why unions are risky behavior for C++, and why using memcpy is the only fix portable across both C and C++. Hope this is helpful.

34 IngoBlackman May 14 2013 at 09:37

As addendum to what Doug T. already wrote, here is a simple test case which probably triggers it with 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 with gcc -O2 -o check check.c . Usually (with most gcc versions I tried) this outputs "strict aliasing problem", because the compiler assumes that "h" cannot be the same address as "k" in the "check" function. Because of that the compiler optimizes the if (*h == 5) away and always calls the printf.

For those who are interested here is the x64 assembler code, produced by gcc 4.6.3, running on ubuntu 12.04.2 for x64:

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

So the if condition is completely gone from the assembler code.

18 supercat Apr 27 2017 at 05:42

According to the C89 rationale, the authors of the Standard did not want to require that compilers given code like:

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

should be required to reload the value of x between the assignment and return statement so as to allow for the possibility that p might point to x, and the assignment to *p might consequently alter the value of x. The notion that a compiler should be entitled to presume that there won't be aliasing in situations like the above was non-controversial.

Unfortunately, the authors of the C89 wrote their rule in a way that, if read literally, would make even the following function invoke Undefined Behavior:

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

because it uses an lvalue of type int to access an object of type struct S, and int is not among the types that may be used accessing a struct S. Because it would be absurd to treat all use of non-character-type members of structs and unions as Undefined Behavior, almost everyone recognizes that there are at least some circumstances where an lvalue of one type may be used to access an object of another type. Unfortunately, the C Standards Committee has failed to define what those circumstances are.

Much of the problem is a result of Defect Report #028, which asked about the behavior of a program like:

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);
}

Defect Report #28 states that the program invokes Undefined Behavior because the action of writing a union member of type "double" and reading one of type "int" invokes Implementation-Defined behavior. Such reasoning is nonsensical, but forms the basis for the Effective Type rules which needlessly complicate the language while doing nothing to address the original problem.

The best way to resolve the original problem would probably be to treat the footnote about the purpose of the rule as though it were normative, and made the rule unenforceable except in cases which actually involve conflicting accesses using aliases. Given something like:

 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;
 }

There's no conflict within inc_int because all accesses to the storage accessed through *p are done with an lvalue of type int, and there's no conflict in test because p is visibly derived from a struct S, and by the next time s is used, all accesses to that storage that will ever be made through p will have already happened.

If the code were changed slightly...

 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;
 }

Here, there is an aliasing conflict between p and the access to s.x on the marked line because at that point in execution another reference exists that will be used to access the same storage.

Had Defect Report 028 said the original example invoked UB because of the overlap between the creation and use of the two pointers, that would have made things a lot more clear without having to add "Effective Types" or other such complexity.

17 ChrisJester-Young Sep 19 2008 at 08:38

Type punning via pointer casts (as opposed to using a union) is a major example of breaking strict aliasing.

11 Myst Dec 24 2017 at 19:04

After reading many of the answers, I feel the need to add something:

Strict aliasing (which I'll describe in a bit) is important because:

  1. Memory access can be expensive (performance wise), which is why data is manipulated in CPU registers before being written back to the physical memory.

  2. If data in two different CPU registers will be written to the same memory space, we can't predict which data will "survive" when we code in C.

    In assembly, where we code the loading and unloading of CPU registers manually, we will know which data remains intact. But C (thankfully) abstracts this detail away.

Since two pointers can point to the same location in the memory, this could result in complex code that handles possible collisions.

This extra code is slow and hurts performance since it performs extra memory read / write operations which are both slower and (possibly) unnecessary.

The Strict aliasing rule allows us to avoid redundant machine code in cases in which it should be safe to assume that two pointers don't point to the same memory block (see also the restrict keyword).

The Strict aliasing states it's safe to assume that pointers to different types point to different locations in the memory.

If a compiler notices that two pointers point to different types (for example, an int * and a float *), it will assume the memory address is different and it will not protect against memory address collisions, resulting in faster machine code.

For example:

Lets assume the following function:

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

In order to handle the case in which a == b (both pointers point to the same memory), we need to order and test the way we load data from the memory to the CPU registers, so the code might end up like this:

  1. load a and b from memory.

  2. add a to b.

  3. save b and reload a.

    (save from CPU register to the memory and load from the memory to the CPU register).

  4. add b to a.

  5. save a (from the CPU register) to the memory.

Step 3 is very slow because it needs to access the physical memory. However, it's required to protect against instances where a and b point to the same memory address.

Strict aliasing would allow us to prevent this by telling the compiler that these memory addresses are distinctly different (which, in this case, will allow even further optimization which can't be performed if the pointers share a memory address).

  1. This can be told to the compiler in two ways, by using different types to point to. i.e.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. Using the restrict keyword. i.e.:

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

Now, by satisfying the Strict Aliasing rule, step 3 can be avoided and the code will run significantly faster.

In fact, by adding the restrict keyword, the whole function could be optimized to:

  1. load a and b from memory.

  2. add a to b.

  3. save result both to a and to b.

This optimization couldn't have been done before, because of the possible collision (where a and b would be tripled instead of doubled).

6 JasonDagit Sep 19 2008 at 08:33

Strict aliasing is not allowing different pointer types to the same data.

This article should help you understand the issue in full detail.