Hinweis
Dies ist ein Auszug aus meinem "Was ist die Strict Aliasing Rule und warum ist sie wichtig?" schreiben.
Was ist striktes Aliasing?
In C und C++ hat Aliasing damit zu tun, über welche Ausdruckstypen wir auf gespeicherte Werte zugreifen dürfen. Sowohl in C als auch in C++ legt die Norm fest, welche Ausdruckstypen welche Typen aliasieren dürfen. Der Compiler und der Optimierer dürfen davon ausgehen, dass wir die Aliasing-Regeln strikt befolgen, daher der Begriff strenge Aliasing-Regel . Wenn wir versuchen, auf einen Wert mit einem nicht zulässigen Typ zuzugreifen, wird er als undefiniertes Verhalten ( UB ). Sobald wir ein undefiniertes Verhalten haben, sind die Ergebnisse unseres Programms nicht mehr zuverlässig.
Leider erhalten wir bei strikten Aliasing-Verletzungen oft die Ergebnisse, die wir erwarten, so dass die Möglichkeit besteht, dass eine zukünftige Version eines Compilers mit einer neuen Optimierung den Code, den wir für gültig hielten, kaputt macht. Dies ist nicht wünschenswert, und es ist ein lohnendes Ziel, die strengen Aliasing-Regeln zu verstehen und zu wissen, wie man ihre Verletzung vermeiden kann.
Um zu verstehen, warum uns das wichtig ist, werden wir Probleme erörtern, die auftreten, wenn strenge Aliasing-Regeln verletzt werden, Typ-Punning, da gängige Techniken, die beim Typ-Punning verwendet werden, oft strenge Aliasing-Regeln verletzen, und wie man Typ-Punning korrekt durchführt.
Vorläufige Beispiele
Schauen wir uns einige Beispiele an, dann können wir darüber sprechen, was die Norm(en) genau besagen, einige weitere Beispiele untersuchen und dann sehen, wie man striktes Aliasing vermeidet und Verstöße erkennt, die wir übersehen haben. Hier ist ein Beispiel, das nicht überraschen sollte ( Live-Beispiel ):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
Wir haben eine int* die auf den von einer anderen Person belegten Speicher verweist int und dies ist ein gültiges Aliasing. Der Optimierer muss davon ausgehen, dass Zuweisungen durch ip könnte den Wert aktualisieren, der von x .
Das nächste Beispiel zeigt Aliasing, das zu undefiniertem Verhalten führt ( Live-Beispiel ):
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?
}
In der Funktion foo wir nehmen eine int* und eine Schwimmer* , in diesem Beispiel nennen wir foo und setzen Sie beide Parameter so, dass sie auf denselben Speicherplatz zeigen, der in diesem Beispiel eine int . Hinweis: Die uminterpretieren_gießen weist den Compiler an, den Ausdruck so zu behandeln, als hätte er den durch seinen Template-Parameter festgelegten Typ. In diesem Fall weisen wir ihn an, den Ausdruck &x als hätte sie die Schrift Schwimmer* . Wir können naiverweise erwarten, dass das Ergebnis der zweiten cout zu sein 0 aber mit aktivierter Optimierung durch -O2 liefern sowohl gcc als auch clang das folgende Ergebnis:
0
1
Das ist zwar nicht zu erwarten, aber durchaus zulässig, da wir ein undefiniertes Verhalten aufgerufen haben. A Schwimmer kann kein gültiges Alias für eine int Objekt. Daher kann der Optimierer davon ausgehen, dass das Konstante 1 bei der Dereferenzierung gespeichert i wird der Rückgabewert sein, da eine Speicherung durch f nicht wirksam auf ein int Objekt. Das Einfügen des Codes in den Compiler-Explorer zeigt, dass genau das passiert( Live-Beispiel ):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
Der Optimierer verwendet Typbasierte Alias-Analyse (TBAA) setzt voraus. 1 zurückgegeben und verschiebt den konstanten Wert direkt in das Register eax die den Rückgabewert enthält. TBAA verwendet die Regeln der Sprache darüber, welche Typen als Alias erlaubt sind, um das Laden und Speichern zu optimieren. In diesem Fall weiß TBAA, dass ein Schwimmer kann nicht alias und int und optimiert die Belastung durch i .
Nun zum Regelbuch
Was genau besagt die Norm, was wir tun dürfen und was nicht? Die Sprache der Norm ist nicht einfach, daher werde ich versuchen, für jeden Punkt Codebeispiele zu geben, die die Bedeutung verdeutlichen.
Was besagt die C11-Norm?
があります。 C11 Norm heißt es im Abschnitt 6.5 Ausdrücke Absatz 7 :
Auf den gespeicherten Wert eines Objekts darf nur durch einen lvalue-Ausdruck zugegriffen werden, der einen der folgenden Typen hat: 88) - einen Typ, der mit dem effektiven Typ des Objekts kompatibel ist,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
- eine qualifizierte Version eines Typs, der mit dem tatsächlichen Typ des Objekts kompatibel ist,
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
- einen Typ, der dem vorzeichenbehafteten oder vorzeichenlosen Typ entspricht, der dem effektiven Typ des Objekts entspricht,
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 hat eine Erweiterung y auch die die Zuordnung von unsigned int* a int* auch wenn sie nicht kompatibel sind.
- einen Typ, der der Typ mit oder ohne Vorzeichen ist, der einer qualifizierten Version des effektiven Typs des Objekts entspricht,
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
- ein Aggregat oder eine Vereinigung, das/die einen der oben genannten Typen zu seinen/ihren Mitgliedern zählt (einschließlich, rekursiv, ein Mitglied eines Unteraggregats oder einer enthaltenen Vereinigung), oder
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 );
- einen Zeichentyp.
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.
Was der C++17-Standardentwurf sagt
Der C++17-Standardentwurf in Abschnitt [basic.lval] Absatz 11 sagt:
Wenn ein Programm versucht, auf den gespeicherten Wert eines Objekts über einen glvalue von einem anderen als einem der folgenden Typen zuzugreifen, ist das Verhalten undefiniert: 63 (11.1) - der dynamische Typ des Objekts,
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) - eine cv-qualifizierte Version des dynamischen Typs des Objekts,
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) - ein Typ, der dem dynamischen Typ des Objekts ähnlich ist (wie in 7.5 definiert),
(11.4) - ein Typ mit oder ohne Vorzeichen, der dem dynamischen Typ des Objekts entspricht,
// 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) - ein Typ, der der vorzeichenbehaftete oder vorzeichenlose Typ ist, der einer cv-qualifizierten Version des dynamischen Typs des Objekts entspricht,
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6) - ein Aggregat oder eine Vereinigung, das/die unter seinen/ihren Elementen oder nichtstatischen Datenelementen einen der oben genannten Typen enthält (einschließlich, rekursiv, eines Elements oder nichtstatischen Datenelements eines Unteraggregats oder einer enthaltenen Vereinigung),
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) - ein Typ, der ein (möglicherweise cv-qualifizierter) Basisklassentyp des dynamischen Typs des Objekts ist,
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) - ein char, unsigned char oder std::byte Typ.
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
}
Erwähnenswert signiertes Zeichen nicht in der obigen Liste enthalten ist, ist dies ein bemerkenswerter Unterschied zu C die besagt einen Zeichentyp .
Was ist Type Punning?
Wir sind an diesem Punkt angelangt und fragen uns vielleicht, warum wir einen Alias verwenden wollen? Die Antwort lautet normalerweise Wortspiel Oft verletzen die verwendeten Methoden strenge Aliasing-Regeln.
Manchmal möchte man das Typsystem umgehen und ein Objekt als einen anderen Typ interpretieren. Dies wird als Typenschreibweise , um ein Speichersegment in einen anderen Typ umzudeuten. Typenschreibweise ist nützlich für Aufgaben, die Zugriff auf die zugrundeliegende Darstellung eines Objekts zur Ansicht, zum Transport oder zur Manipulation benötigen. Typische Bereiche, in denen Type Punning eingesetzt wird, sind Compiler, Serialisierung, Netzwerkcode usw
Traditionell wird dies erreicht, indem man die Adresse des Objekts nimmt, sie in einen Zeiger des Typs umwandelt, als den wir sie neu interpretieren wollen, und dann auf den Wert zugreift, oder mit anderen Worten durch Aliasing. Zum Beispiel:
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 ) ;
Wie wir bereits gesehen haben, ist dies kein gültiges Aliasing, so dass wir ein undefiniertes Verhalten aufrufen. Aber traditionell haben die Compiler keine strengen Aliasing-Regeln verwendet, und diese Art von Code funktionierte in der Regel einfach, und die Entwickler haben sich leider daran gewöhnt, die Dinge auf diese Weise zu tun. Eine gängige alternative Methode für Typ-Punning ist die Verwendung von Unions, die in C gültig ist, aber undefiniertes Verhalten in C++ ( siehe Live-Beispiel ):
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
Dies ist in C++ nicht gültig, und einige sind der Ansicht, dass der Zweck von Unions ausschließlich in der Implementierung von Variantentypen besteht, und halten die Verwendung von Unions für Typ-Punning für einen Missbrauch.
Wie schreibt man Pun richtig?
Die Standardmethode für Typenschreibweise ist sowohl in C als auch in C++ memcpy . Dies mag ein wenig schwerfällig erscheinen, aber der Optimierer sollte die Verwendung von memcpy para Typenschreibweise und optimieren Sie es weg und erzeugen Sie einen Umzug von Register zu Register. Wenn wir zum Beispiel wissen int64_t die gleiche Größe hat wie doppelt :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
können wir memcpy :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
Bei einem ausreichenden Optimierungsgrad erzeugt jeder anständige moderne Compiler identischen Code wie der zuvor erwähnte uminterpretieren_gießen Methode oder Gewerkschaft Methode für Typenschreibweise . Wenn wir den generierten Code untersuchen, sehen wir, dass er nur das Register mov verwendet ( live Compiler Explorer Beispiel ).
C++20 und bit_cast
In C++20 können wir gewinnen bit_cast ( Umsetzung im Link des Vorschlags verfügbar ), die einen einfachen und sicheren Weg zu type-pun bietet und auch in einem constexpr-Kontext verwendet werden kann.
Im Folgenden wird ein Beispiel für die Verwendung von bit_cast ein Wortspiel zu tippen unsigned int a Schwimmer , ( es live sehen ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
In dem Fall, dass An y Von Typen nicht die gleiche Größe haben, müssen wir eine Zwischenstruktur15 verwenden. Wir werden eine Struktur verwenden, die eine sizeof( unsigned int ) Zeichenanordnung ( geht von 4 Byte unsigned int aus ) als die Von Typ und unsigned int als die An tippen:
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 ;
}
Es ist bedauerlich, dass wir diesen Zwischentyp brauchen, aber das ist die derzeitige Einschränkung von bit_cast .
Abfangen von Strict Aliasing-Verletzungen
Wir haben nicht viele gute Werkzeuge, um striktes Aliasing in C++ zu erkennen. Die Werkzeuge, die wir haben, erkennen einige Fälle von strikten Aliasing-Verletzungen und einige Fälle von falsch ausgerichteten Lade- und Speichervorgängen.
gcc unter Verwendung des Flags -fstrict-aliasing y -Strich-Aliasing kann einige Fälle erfassen, wenn auch nicht ohne falsch positive/negative Ergebnisse. Zum Beispiel werden die folgenden Fälle eine Warnung in gcc erzeugen ( es live sehen ):
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)));
obwohl es diesen zusätzlichen Fall nicht erfassen wird ( es live sehen ):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Obwohl clang diese Flags zulässt, implementiert es die Warnungen offenbar nicht wirklich.
Ein weiteres Werkzeug, das uns zur Verfügung steht, ist ASan, das falsch ausgerichtete Lasten und Lager aufspüren kann. Obwohl es sich hierbei nicht direkt um strikte Aliasing-Verletzungen handelt, sind sie eine häufige Folge von strikten Aliasing-Verletzungen. Die folgenden Fälle erzeugen zum Beispiel Laufzeitfehler, wenn sie mit Clang unter Verwendung von -fsanitize=Adresse
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]
Das letzte Werkzeug, das ich empfehlen werde, ist C++-spezifisch und kein Werkzeug im engeren Sinne, sondern eine Programmierpraxis: Lassen Sie keine Casts im Stil von C zu. Sowohl gcc als auch clang erzeugen eine Diagnose für Casts im C-Stil mit -Wildnis-Guss . Dadurch werden alle undefinierten Typ-Puns gezwungen, reinterpret_cast zu verwenden. Im Allgemeinen sollte reinterpret_cast ein Kennzeichen für eine genauere Codeüberprüfung sein. Es ist auch einfacher, Ihre Codebasis nach reinterpret_cast zu durchsuchen, um eine Prüfung durchzuführen.
Für C haben wir alle bereits abgedeckten Werkzeuge und außerdem tis-interpreter, einen statischen Analyzer, der ein Programm für eine große Teilmenge der Sprache C vollständig analysiert. Angenommen, eine C-Version des früheren Beispiels, bei der mit -fstrict-aliasing übersieht einen Fall ( es live sehen )
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 ist in der Lage, alle drei zu fangen, das folgende Beispiel ruft tis-kernal als tis-interpreter auf (die Ausgabe ist der Kürze halber editiert):
./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.
Schließlich gibt es TySan das sich derzeit in der Entwicklung befindet. Dieser Sanitizer fügt Informationen zur Typüberprüfung in ein Schattenspeichersegment ein und prüft Zugriffe darauf, ob sie gegen Aliasing-Regeln verstoßen. Das Tool sollte potenziell in der Lage sein, alle Aliasing-Verletzungen zu erkennen, kann aber einen großen Laufzeit-Overhead haben.
8 Stimmen
Vielleicht interessieren Sie sich auch für einen Artikel, den ich kürzlich geschrieben habe Was ist die Strict Aliasing Rule und warum ist sie wichtig? . Es deckt eine Menge Material ab, das hier nicht behandelt wird, oder bietet in einigen Bereichen einen moderneren Ansatz.