[th/gcc4] apps fixing guide #2 - aliasing violations.

Paweł Sikora pluto w agmk.net
Czw, 4 Maj 2006, 20:22:39 CEST


w dzisiejszym odcinku zajmiemy się tym czego tygryski nie lubią najbardziej,
czyli triki, które działały i przestały :>

na początek fragment manuala:

[ cite man gcc ]

-fstrict-aliasing

           Allows the compiler to assume the strictest aliasing rules
           applicable to the language being compiled.  For C (and C++),
           this activates optimizations based on the type of expressions.
           In particular, an object of one type is assumed never to reside
           at the same address as an object of a different type,
           unless the types are almost the same.
           For example, an "unsigned int" can alias an "int",
           but not a "void*" or a "double".
           A character type may alias any other type.

           (...)

           The practice of reading from a different union member than
           the one most recently written to (called "type-punning") is common.
           Even with -fstrict-aliasing, type-punning is allowed, provided
           the memory is accessed through the union type.

           (...)

           Enabled at levels -O2, -O3, -Os.

[ /cite man gcc ]

teraz kilka (nie)działających przykładów:

[1] OK.
tu korzystamy z dozwolonego użycia unii do maniuplacji
na wspólnym obszarze pamięci.

unsigned short little_endian_lo_word( unsigned x )
{
	union
	{
		unsigned short s[2];
		unsigned i;
	} u ;
	u.i = x;
	return u.s[0];
}

[2] ŹLE!!!
tu naruszamy reguły aliasing-u (btw: ktoś zna dobre tłumacznie tego słowa?)
ponieważ typ ushort nie może egzystować równocześnie z uint, o czym kompilator
nas ładnie informuje:

warning: dereferencing type-punned pointer will break strict-aliasing rules

unsigned short little_endian_lo_word_BAD( unsigned x )
{
	return *(unsigned short*)&x;
}

[3] OK.
tutaj nadal jest poprawnie, bo typ znakowy może się nakładać na inne typy.

bool tell_endian()
{
	unsigned x = 1;
	return *(char*)&x;
}

jednak dobrą praktyką jest unikać rzutowania przez wskaźniki
i zapisać powyższy przykład z użyciem unii -> [4].

[4] OK.
bool tell_endian()
{
	union
	{
		int i;
		char c;
	} u;
	u.i = 1;
	return u.c;
}

naumiani? no to teraz coś lepszego...

[5] ŹLE!!!
#include <stdio.h>
int main()
{
	unsigned long long val = 0x123456789ABCDEF0LLU;
	unsigned char h[6];
	*(unsigned short*)h = (val >> 32) & 0xffff;
	*(unsigned int*)(h + 2) = val & 0xffffffff;
	unsigned int res = *((unsigned int*)h);
	printf ("%08x\n", res);
	return 0;
}

$ gcc 5.c -Wall && ./a.out
def05678
$ gcc 5.c -Wall -O2 && ./a.out
def0be40

pięknie, diagnostyka kompilatora nie wykryła naruszenia aliasing-u,
a optymalizacje na nim bazujące założyły, że jest prawidłowy strict-aliasing
i zrobiły swoje produkując nam przy -O2 śmieci.
oczywiście z opcjami -O2 -fno-strict-aliasing wynik będzie taki jak przy -O0.

no to ostatni przykład:

[6] ŹLE!!!
float quickBinaryToFloat( unsigned const& in )
{
	return reinterpret_cast< float const& >( in ) ;
}

float foo( unsigned x )
{
	unsigned y = ( x * 2 ) + 1;
	return quickBinaryToFloat( y );
}

$ g++ 6.cpp -Wall -O3 -c -m32 -fomit-frame-pointer
 objdump -dC 6.o

quickBinaryToFloat przeszło bez diagnostyki.
na pierwszy rzut oka wygląda, że jest ok, ale...

00000000 <quickBinaryToFloat(unsigned int const&)>:
   0:   8b 44 24 04             mov    0x4(%esp),%eax
   4:   d9 00                   flds   (%eax)
   6:   c3                      ret

...po rozwinięciu w innej metodzie dostaliśmy śmietnik,
bo przykład jest ZŁY !!! i narusza reguły aliasing-u.

00000010 <foo(unsigned int)>:
  10:   83 ec 10                sub    $0x10,%esp
  13:   d9 44 24 0c             flds   0xc(%esp)
                               ^^^^^^^^^ zostało tylko rzutowanie z 'x'
                                         a y=.... wcieło. generalni mogło
                                         co chciało, bo to my daliśmy ciała.
  17:   83 c4 10                add    $0x10,%esp
  1a:   c3                      ret


reasumując jestem za dodaniem do %rpm{c,xx}flags następujących flag:

* -fwrapv
* -fno-strict-aliasig
* -Wall

lub przełączeniem domyślnej optymalizacji na -O1 (tak jak bodajże w freebsd).
wszystko tylko po to, żeby ta cała masa oprogramowania napisana x-lat temu
pod gcc-2.95/3.3 wciąż działała w th, no chyba, że ktoś ma ochotę na audyt
tego całego kodu? ;)


Więcej informacji o liście dyskusyjnej pld-devel-pl