[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