Typ punning - Type punning

In der Informatik , Typ punning ist ein gebräuchlicher Begriff für jede Programmiertechnik , dass gräbt oder circumvents das Typsystem einer Programmiersprache , um eine Wirkung zu erzielen , die innerhalb der Grenzen der formalen Sprache zu erreichen schwierig oder unmöglich sein würden.

In C und C ++ , Konstrukten wie Zeigertypumwandlung und - C ++ fügt Referenzumwandlungstyp und zu dieser Liste - vorgesehen sind, um viele Arten von Typ punning zu ermöglichen, auch wenn einige Arten sind nicht tatsächlich von der Standard - Sprache unterstützt. unionreinterpret_cast

In der Programmiersprache Pascal kann die Verwendung von Datensätzen mit Varianten verwendet werden, um einen bestimmten Datentyp auf mehr als eine Weise oder auf eine Weise zu behandeln, die normalerweise nicht zulässig ist.

Beispiel für Steckdosen

Ein klassisches Beispiel für Typ Punning ist die Berkeley-Sockets- Oberfläche. Die Funktion zum Binden eines geöffneten, aber nicht initialisierten Sockets an eine IP-Adresse wird wie folgt deklariert:

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

Die bind Funktion wird normalerweise wie folgt aufgerufen:

struct sockaddr_in sa = {0};
int sockfd = ...;
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
bind(sockfd, (struct sockaddr *)&sa, sizeof sa);

Die Berkeley-Sockets-Bibliothek basiert im Wesentlichen auf der Tatsache, dass in C ein Zeiger auf struct sockaddr_in frei in einen Zeiger auf konvertierbar ist struct sockaddr ; und zusätzlich, dass die zwei Strukturtypen das gleiche Speicherlayout teilen. Daher bezieht sich ein Verweis auf das Strukturfeld my_addr->sin_family (wo my_addr vom Typ ist struct sockaddr* ) tatsächlich auf das Feld sa.sin_family (wo sa vom Typ ist struct sockaddr_in ). Mit anderen Worten, die Sockets-Bibliothek verwendet Type Punning, um eine rudimentäre Form von Polymorphismus oder Vererbung zu implementieren .

In der Programmierwelt wird häufig die Verwendung von "gepolsterten" Datenstrukturen gesehen, um die Speicherung verschiedener Arten von Werten auf praktisch demselben Speicherplatz zu ermöglichen. Dies wird häufig beobachtet, wenn zwei Strukturen in gegenseitiger Ausschließlichkeit zur Optimierung verwendet werden.

Gleitkomma-Beispiel

Nicht alle Beispiele für Typ-Punning beinhalten Strukturen, wie dies im vorherigen Beispiel der Fall war. Angenommen, wir möchten feststellen, ob eine Gleitkommazahl negativ ist. Wir könnten schreiben:

bool is_negative(float x) {
    return x < 0.0;
}

Angenommen, Gleitkomma-Vergleiche sind teuer, und wenn angenommen float wird , dass sie gemäß dem IEEE-Gleitkomma-Standard dargestellt werden und Ganzzahlen 32 Bit breit sind, könnten wir Typ-Punning durchführen, um das Vorzeichenbit der Gleitkommazahl zu extrahieren Verwenden Sie nur ganzzahlige Operationen:

bool is_negative(float x) {
    unsigned int *ui = (unsigned int *)&x;
    return *ui & 0x80000000;
}

Beachten Sie, dass das Verhalten nicht genau dasselbe ist: In dem speziellen Fall, x dass es eine negative Null ist , ergibt die erste Implementierung, false während die zweite ergibt true . Die erste Implementierung gibt auch false für jeden NaN- Wert zurück, letztere kann jedoch true für NaN-Werte mit gesetztem Vorzeichenbit zurückkehren.

Diese Art von Punning ist gefährlicher als die meisten anderen. Während sich das erste Beispiel nur auf Garantien der Programmiersprache C hinsichtlich des Strukturlayouts und der Zeigerkonvertierbarkeit stützte, stützt sich das zweite Beispiel auf Annahmen über die Hardware eines bestimmten Systems. Einige Situationen, wie z. B. zeitkritischer Code, den der Compiler sonst nicht optimiert , erfordern möglicherweise gefährlichen Code. In diesen Fällen trägt die Dokumentation all dieser Annahmen in Kommentaren und die Einführung statischer Zusicherungen zur Überprüfung der Portabilitätserwartungen dazu bei, den Code wartbar zu halten .

Praktische Beispiele für Gleitkomma-Punning sind die durch Quake III populäre schnelle inverse Quadratwurzel , der schnelle FP-Vergleich als Ganzzahlen und das Finden benachbarter Werte durch Inkrementieren als Ganzzahl (Implementieren ). nextafter

Nach Sprache

C und C ++

Zusätzlich zur Annahme über die Bitdarstellung von Gleitkommazahlen verstößt das obige Gleitkomma-Typ-Punning-Beispiel auch gegen die Einschränkungen der C-Sprache für den Zugriff auf Objekte: Der deklarierte Typ von x is wird float jedoch durch einen Typausdruck gelesen unsigned int . Auf vielen gängigen Plattformen kann diese Verwendung von Zeiger-Punning zu Problemen führen, wenn verschiedene Zeiger maschinenspezifisch ausgerichtet werden . Darüber hinaus können Zeiger unterschiedlicher Größe Alias-Zugriffe auf denselben Speicher ausführen, was zu Problemen führt, die vom Compiler nicht überprüft werden.

Verwendung von Zeigern

Ein naiver Versuch der Typ-Punning kann durch Verwendung von Zeigern erreicht werden:

float pi = 3.14159;
uint32_t piAsRawData = *(uint32_t*)&pi;

Nach dem C-Standard sollte (oder muss) dieser Code nicht kompiliert werden. Wenn dies jedoch der Fall ist, piAsRawData enthält er normalerweise die Rohbits von pi.

Gebrauch von union

Es ist ein häufiger Fehler, zu versuchen, Typ-Punning mithilfe von a zu beheben union . (Im folgenden Beispiel wird zusätzlich die IEEE-754-Bitdarstellung für Gleitkommatypen angenommen.)

bool is_negative(float x) {
    union {
        unsigned int ui;
        float d;
    } my_union = { .d = x };
    return my_union.ui & 0x80000000;
}

Der Zugriff my_union.ui nach der Initialisierung des anderen Mitglieds my_union.d ist in C immer noch eine Form der Typ-Punning. Das Ergebnis ist ein nicht angegebenes Verhalten (und ein undefiniertes Verhalten in C ++).

Die Sprache von § 6.5 / 7 kann falsch verstanden werden, um zu implizieren, dass das Lesen alternativer Gewerkschaftsmitglieder zulässig ist. Der Text lautet jedoch "Auf ein Objekt darf nur über ... auf seinen gespeicherten Wert zugegriffen werden ". Es ist ein einschränkender Ausdruck, keine Aussage, dass auf alle möglichen Gewerkschaftsmitglieder zugegriffen werden kann, unabhängig davon, welche zuletzt gespeichert wurden. Die Verwendung von union vermeidet also keines der Probleme, wenn Sie einfach einen Zeiger direkt drücken.

Es kann sogar als weniger sicher angesehen werden als Typ-Punning mit Zeigern, da ein Compiler weniger wahrscheinlich eine Warnung oder einen Fehler meldet, wenn er Typ-Punning nicht unterstützt.

Compiler wie GCC unterstützen Aliasable Value Accesses wie die obigen Beispiele als Spracherweiterung. Bei Compilern ohne eine solche Erweiterung wird die strenge Alias-Regel nur durch ein explizites Memcpy oder durch die Verwendung eines Zeichenzeigers als "Middle Man" verletzt (da diese frei aliasiert werden können).

Ein weiteres Beispiel für Typ Punning finden Sie unter Schritt eines Arrays .

Pascal

Ein Variantendatensatz ermöglicht die Behandlung eines Datentyps als mehrere Arten von Daten, je nachdem, auf welche Variante verwiesen wird. Im folgenden Beispiel wird angenommen, dass die Ganzzahl 16 Bit beträgt, während Longint und Real 32 Bit und das Zeichen 8 Bit annehmen:

type
    VariantRecord = record
        case RecType : LongInt of
            1: (I : array[1..2] of Integer);  (* not show here: there can be several variables in a variant record's case statement *)
            2: (L : LongInt               );
            3: (R : Real                  );
            4: (C : array[1..4] of Char   );
        end;

var
    V  : VariantRecord;
    K  : Integer;
    LA : LongInt;
    RA : Real;
    Ch : Character;


V.I[1] := 1;
Ch     := V.C[1];  (* this would extract the first byte of V.I *)
V.R    := 8.3;   
LA     := V.L;     (* this would store a Real into an Integer *)

Wenn Sie in Pascal ein Real in eine Ganzzahl kopieren, wird es in den abgeschnittenen Wert konvertiert. Diese Methode würde den Binärwert der Gleitkommazahl in eine lange Ganzzahl (32 Bit) umwandeln, die nicht identisch ist und auf einigen Systemen möglicherweise nicht mit dem Wert für lange Ganzzahlen kompatibel ist.

Diese Beispiele könnten verwendet werden, um seltsame Konvertierungen zu erstellen, obwohl es in einigen Fällen legitime Verwendungen für diese Arten von Konstrukten geben kann, beispielsweise zum Bestimmen der Positionen bestimmter Daten. Im folgenden Beispiel wird angenommen, dass ein Zeiger und eine Longint 32 Bit sind:

type
    PA = ^Arec;

    Arec = record
        case RT : LongInt of
            1: (P : PA     );
            2: (L : LongInt);
        end;

var
    PP : PA;
    K  : LongInt;


New(PP);
PP^.P := PP;
WriteLn('Variable PP is located at address ', Hex(PP^.L));

Wobei "neu" die Standardroutine in Pascal zum Zuweisen von Speicher für einen Zeiger ist und "hex" vermutlich eine Routine zum Drucken der hexadezimalen Zeichenfolge, die den Wert einer Ganzzahl beschreibt. Dies würde die Anzeige der Adresse eines Zeigers ermöglichen, was normalerweise nicht zulässig ist. (Zeiger können nicht gelesen oder geschrieben, sondern nur zugewiesen werden.) Das Zuweisen eines Werts zu einer ganzzahligen Variante eines Zeigers würde das Untersuchen oder Schreiben an eine beliebige Stelle im Systemspeicher ermöglichen:

PP^.L := 0;
PP    := PP^.P;  (* PP now points to address 0     *)
K     := PP^.L;  (* K contains the value of word 0 *)
WriteLn('Word 0 of this machine contains ', K);

Dieses Konstrukt kann eine Programmprüfung oder eine Schutzverletzung verursachen, wenn die Adresse 0 auf dem Computer, auf dem das Programm ausgeführt wird, oder auf dem Betriebssystem, unter dem es ausgeführt wird, vor dem Lesen geschützt ist.

Die Neuinterpretation der Cast-Technik aus C / C ++ funktioniert auch in Pascal. Dies kann nützlich sein, wenn z. Lesen von Wörtern aus einem Byte-Stream, und wir möchten sie als float behandeln. Hier ist ein Arbeitsbeispiel, in dem wir ein Dword neu interpretieren und in einen Float umwandeln:

type
    pReal = ^Real;

var
    DW : DWord;
    F  : Real;

F := pReal(@DW)^;

C #

In C # (und anderen .NET-Sprachen) ist das Typ-Punning aufgrund des Typsystems etwas schwieriger zu erreichen, kann jedoch mithilfe von Zeigern oder Strukturverbindungen durchgeführt werden.

Zeiger

C # erlaubt nur Zeiger auf sogenannte native Typen, dh alle primitiven Typen (außer string ), Aufzählungen, Arrays oder Strukturen, die nur aus anderen nativen Typen bestehen. Beachten Sie, dass Zeiger nur in Codeblöcken zulässig sind, die als "unsicher" gekennzeichnet sind.

float pi = 3.14159;
uint piAsRawData = *(uint*)&pi;

Strukturgewerkschaften

Strukturverbände sind ohne den Begriff "unsicherer" Code zulässig, erfordern jedoch die Definition eines neuen Typs.

[StructLayout(LayoutKind.Explicit)]
struct FloatAndUIntUnion
{
    [FieldOffset(0)]
    public float DataAsFloat;

    [FieldOffset(0)]
    public uint DataAsUInt;
}

// ...

FloatAndUIntUnion union;
union.DataAsFloat = 3.14159;
uint piAsRawData = union.DataAsUInt;

Roher CIL-Code

Raw CIL kann anstelle von C # verwendet werden, da die meisten Typbeschränkungen nicht vorhanden sind. Auf diese Weise können beispielsweise zwei Aufzählungswerte eines generischen Typs kombiniert werden:

TEnum a = ...;
TEnum b = ...;
TEnum combined = a | b; // illegal

Dies kann durch den folgenden CIL-Code umgangen werden:

.method public static hidebysig
    !!TEnum CombineEnums<valuetype .ctor ([mscorlib]System.ValueType) TEnum>(
        !!TEnum a,
        !!TEnum b
    ) cil managed
{
    .maxstack 2

    ldarg.0 
    ldarg.1
    or  // this will not cause an overflow, because a and b have the same type, and therefore the same size.
    ret
}

Der cpblk CIL-Opcode ermöglicht einige andere Tricks, z. B. das Konvertieren einer Struktur in ein Byte-Array:

.method public static hidebysig
    uint8[] ToByteArray<valuetype .ctor ([mscorlib]System.ValueType) T>(
        !!T& v // 'ref T' in C#
    ) cil managed
{
    .locals init (
        [0] uint8[]
    )

    .maxstack 3

    // create a new byte array with length sizeof(T) and store it in local 0
    sizeof !!T
    newarr uint8
    dup           // keep a copy on the stack for later (1)
    stloc.0

    ldc.i4.0
    ldelema uint8

    // memcpy(local 0, &v, sizeof(T));
    // <the array is still on the stack, see (1)>
    ldarg.0 // this is the *address* of 'v', because its type is '!!T&'
    sizeof !!T
    cpblk

    ldloc.0
    ret
}

Verweise

  1. ^ Herf, Michael (Dezember 2001). "Radix Tricks" . Stereopsis: Grafiken .
  2. ^ "Dumme Float Tricks" . Zufälliger ASCII - Tech Blog von Bruce Dawson . 24. Januar 2012.
  3. ^ a b ISO / IEC 9899: 1999 s6.5 / 7
  4. ^ "§ 6.5.2.3/3, Fußnote 97", ISO / IEC 9899: 2018 (PDF) , 2018, p. 59, archiviert vom Original (PDF) am 30.12.2018. Wenn das Mitglied, das zum Lesen des Inhalts eines Vereinigungsobjekts verwendet wurde, nicht mit dem Mitglied identisch ist, das zuletzt zum Speichern eines Werts im Objekt verwendet wurde, ist der entsprechende Teil des Die Objektdarstellung des Werts wird wie in 6.2.6 beschrieben als Objektdarstellung im neuen Typ neu interpretiert ( ein Prozess, der manchmal als „Typ-Punning“ bezeichnet wird ). Dies könnte eine Trap-Darstellung sein.
  5. ^ "§ J.1 / 1, Punkt 11", ISO / IEC 9899: 2018 (PDF) , 2018, p. 403, archiviert vom Original (PDF) am 30.12.2018, Folgendes ist nicht spezifiziert:… Die Werte von Bytes, die anderen Gewerkschaftsmitgliedern als dem zuletzt in (6.2.6.1) gespeicherten entsprechen.
  6. ^ ISO / IEC 14882: 2011 Abschnitt 9.5
  7. ^ GCC: Nicht-Bugs

Externe Links

  • Abschnitt des GCC- Handbuchs über -fstrict-Aliasing , in dem einige Arten von Punning besiegt werden
  • Fehlerbericht 257 zum C99- Standard, der im Übrigen "Typ Punning" in Bezug auf definiert union und die Probleme im Zusammenhang mit dem implementierungsdefinierten Verhalten des letzten Beispiels oben diskutiert
  • Fehlerbericht 283 über die Verwendung von Gewerkschaften für Typ Punning