Typ punning - Type punning

Ve vědě o počítačích , typ pěchování je společný termín pro jakýkoli programovací techniky, která rozvrací nebo obchází typ systému o programovacím jazyku za účelem dosažení efektu, který by bylo obtížné nebo nemožné dosáhnout v mezích formálního jazyka.

V C a C ++ jsou konstrukty jako převod typu ukazatele a - C ++ přidává převod referenčního typu a do tohoto seznamu - k dispozici, aby umožnily mnoho druhů dělení typu, ačkoli některé druhy standardní jazyk ve skutečnosti nepodporuje. unionreinterpret_cast

V programovacím jazyce Pascal lze použít záznamy s variantami k ošetření konkrétního datového typu více než jedním způsobem nebo způsobem, který není běžně povolen.

Příklad zásuvek

Jeden klasický příklad děrování typu se nachází v rozhraní zásuvek Berkeley . Funkce, která váže otevřený, ale neinicializovaný soket na adresu IP, je deklarována takto:

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

bind Funkce se obvykle nazývá takto:

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

Knihovna soketů Berkeley se zásadně opírá o skutečnost, že v C je ukazatel na struct sockaddr_in volně převoditelný na ukazatel na struct sockaddr ; a navíc, že ​​dva typy struktur sdílejí stejné rozložení paměti. Proto odkaz na pole struktury my_addr->sin_family (kde my_addr je typu struct sockaddr* ) bude ve skutečnosti odkazovat na pole sa.sin_family (kde sa je typu struct sockaddr_in ). Jinými slovy, knihovna soketů používá typ punning k implementaci primitivní formy polymorfismu nebo dědičnosti .

Ve světě programování je často vidět použití „polstrovaných“ datových struktur, aby bylo možné ukládat různé druhy hodnot do skutečně stejného úložného prostoru. To je často vidět, když se pro optimalizaci používají dvě struktury ve vzájemné exkluzivitě.

Příklad s plovoucí desetinnou čárkou

Ne všechny příklady dělení typu zahrnují struktury, jako tomu bylo v předchozím příkladu. Předpokládejme, že chceme zjistit, zda je číslo s plovoucí desetinnou čárkou záporné. Mohli bychom napsat:

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

Avšak za předpokladu, že srovnání s plovoucí desetinnou čárkou jsou drahá, a také za předpokladu, že float je reprezentována podle standardu IEEE s plovoucí desetinnou čárkou a celá čísla jsou široká 32 bitů, mohli bychom se zapojit do typu punning, abychom extrahovali bit znaménka čísla s plovoucí desetinnou čárkou pouze pomocí celočíselných operací:

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

Všimněte si, že chování nebude úplně stejný: ve zvláštním případě x , že záporná nula , první implementaci výnosy false , zatímco druhý výnosů true . První implementace se také vrátí false pro jakoukoli hodnotu NaN , ale druhá se může vrátit true pro hodnoty NaN s nastaveným bitem znaménka.

Tento druh trestu je nebezpečnější než většina ostatních. Zatímco první příklad se spoléhal pouze na záruky učiněné programovacím jazykem C o rozložení struktury a převoditelnosti ukazatele, druhý příklad se opírá o předpoklady o hardwaru konkrétního systému. Některé situace, například časově kritický kód, který kompilátor jinak nedokáže optimalizovat , mohou vyžadovat nebezpečný kód. V těchto případech zdokumentování všech takových předpokladů v komentářích a zavedení statických tvrzení k ověření očekávání přenositelnosti pomůže udržovat kód udržovatelný .

Mezi praktické příklady punningu s plovoucí desetinnou čárkou patří rychlá inverzní odmocnina popularizovaná Quake III , rychlé srovnání FP jako celá čísla a hledání sousedních hodnot inkrementací jako celé číslo (implementace nextafter ).

Podle jazyka

C a C ++

Kromě předpokladu o bitové reprezentaci čísel s plovoucí desetinnou čárkou výše uvedený příklad s plovoucí desetinnou čárkou také porušuje omezení jazyka C týkající se přístupu k objektům: deklarovaný typ x je, float ale čte se prostřednictvím výrazu typu unsigned int . Na mnoha běžných platformách může toto použití děrování ukazatele způsobit problémy, pokud jsou různé ukazatele zarovnány způsobem specifickým pro stroj . Kromě toho mohou ukazatele různých velikostí alias přístupy do stejné paměti , což způsobuje problémy, které nejsou zaškrtnuty kompilátorem.

Použití ukazatelů

Naivního pokusu o dělení typu lze dosáhnout pomocí ukazatelů:

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

Podle standardu C by tento kód neměl (nebo spíše nemusí) kompilovat, ale pokud ano, pak piAsRawData obvykle obsahuje surové bity pí.

Použití union

Běžnou chybou je pokus o odstranění typového punku pomocí a union . (Následující příklad navíc předpokládá bitovou reprezentaci IEEE-754 pro typy s plovoucí desetinnou čárkou).

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

Přístup my_union.ui po inicializaci druhého člena my_union.d ,, je stále formou zadávání typů v C a výsledkem je nespecifikované chování (a nedefinované chování v C ++).

Jazyk § 6.5 / 7 může být chybně vykládán, což znamená, že čtení alternativních členů odborů je přípustné. Text však zní: „K objektu bude mít přístup k jeho uložené hodnotě pouze ...“. Jedná se o omezující výraz, nikoli o prohlášení, že lze přistupovat ke všem možným členům odboru bez ohledu na to, který byl naposledy uložen. Takže použití se union vyhne žádnému z problémů jednoduchým puncováním ukazatele přímo.

Mohlo by to být dokonce považováno za méně bezpečné než dělení typu pomocí ukazatelů, protože kompilátor bude méně pravděpodobně hlásit varování nebo chybu, pokud nepodporuje dělení typu.

Překladače jako GCC podporují přístupy s aliasovatelnými hodnotami jako výše uvedené příklady jako rozšíření jazyka. U překladačů bez takového rozšíření je pravidlo přísného aliasu porušeno pouze explicitní memcpy nebo použitím ukazatele char jako „prostředníka“ (protože tyto lze volně pojmenovat).

Další příklad punningu typu najdete v části Stride pole .

Pascal

Záznam varianty umožňuje zacházet s datovým typem jako s více druhy dat podle toho, na kterou variantu se odkazuje. V následujícím příkladu se předpokládá , že celé číslo je 16 bitů, zatímco longint a real jsou považovány za 32, zatímco znak je považován za 8 bitů:

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

V Pascalu kopírování reálného na celé číslo jej převede na zkrácenou hodnotu. Tato metoda by převedla binární hodnotu čísla s plovoucí desetinnou čárkou na cokoli jako dlouhé celé číslo (32 bitů), které nebude stejné a může být nekompatibilní s hodnotou dlouhého celého čísla v některých systémech.

Tyto příklady lze použít k vytvoření podivných převodů, i když v některých případech může existovat legitimní použití pro tyto typy konstrukcí, například pro určení umístění konkrétních dat. V následujícím příkladu se předpokládá, že ukazatel a longint jsou 32 bitů:

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

Kde „new“ je standardní rutina v Pascalu pro alokaci paměti pro ukazatel a „hex“ je pravděpodobně rutina pro tisk hexadecimálního řetězce popisujícího hodnotu celého čísla. To by umožnilo zobrazení adresy ukazatele, což není obvykle povoleno. (Ukazatele nelze číst ani zapisovat, pouze je přiřadit.) Přiřazení hodnoty celočíselné variantě ukazatele by umožnilo zkoumání nebo zápis do libovolného umístění v systémové paměti:

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

Tato konstrukce může způsobit kontrolu programu nebo narušení ochrany, pokud je adresa 0 chráněna proti čtení na stroji, na kterém program běží, nebo pod operačním systémem, pod kterým běží.

Technika reinterpretování cast z C / C ++ funguje také v Pascalu. To může být užitečné, když např. čtení dwords z bajtového proudu a chceme s nimi zacházet jako s floatem. Zde je funkční příklad, kde jsme reinterpret-cast dword na float:

type
    pReal = ^Real;

var
    DW : DWord;
    F  : Real;

F := pReal(@DW)^;

C#

V C # (a dalších jazycích .NET) je typ punning o něco těžší dosáhnout kvůli systému typů, ale lze to udělat přesto pomocí ukazatelů nebo strukturních svazků.

Ukazatele

C # umožňuje pouze ukazatele na takzvané nativní typy, tj. Jakýkoli primitivní typ (kromě string ), enum, pole nebo strukturu, která je složena pouze z jiných nativních typů. Ukazatele jsou povoleny pouze v blocích kódu označených jako „nebezpečné“.

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

Strukturální odbory

Strukturální odbory jsou povoleny bez jakéhokoli pojmu „nebezpečný“ kód, ale vyžadují definici nového typu.

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

Nezpracovaný kód CIL

Místo C # lze použít raw CIL , protože nemá většinu omezení typu. To umožňuje jednomu například kombinovat dvě hodnoty výčtu obecného typu:

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

To lze obejít následujícím kódem CIL:

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

cpblk CIL operační kód umožňuje pro některé další triky, jako je například převod struct bajtové pole:

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

Reference

  1. ^ Herf, Michael (prosinec 2001). "radixové triky" . stereopsis: grafika .
  2. ^ „Stupid Float Tricks“ . Náhodný ASCII - technologický blog Bruce Dawsona . 24. ledna 2012.
  3. ^ a b ISO / IEC 9899: 1999 s6,5 / 7
  4. ^ „§ 6.5.2.3/3, poznámka pod čarou 97“, ISO / IEC 9899: 2018 (PDF) , 2018, s. 59, archivováno z původního (PDF) 30. 12. 2018, Pokud člen použitý ke čtení obsahu sjednocovacího objektu není stejný jako člen naposledy použitý k uložení hodnoty do objektu, příslušná část objektová reprezentace hodnoty je reinterpretována jako objektová reprezentace v novém typu, jak je popsáno v 6.2.6 ( proces někdy nazývaný „typ punning“ ). Může to být reprezentace pasti.
  5. ^ „§ J.1 / 1, odrážka 11“, ISO / IEC 9899: 2018 (PDF) , 2018, s. 403, archivováno z původního (PDF) dne 30.12.2018, Nespecifikovány jsou následující:… Hodnoty bajtů, které odpovídají jiným členům odboru, než tomu, který byl naposledy uložen do (6.2.6.1).
  6. ^ ISO / IEC 14882: 2011 Část 9.5
  7. ^ GCC: Non-Bugs

externí odkazy

  • Sekce z GCC příručce -fstrict-aliasingu , který porazí nějaký typ pěchování
  • Zpráva o vadách 257 podle standardu C99 , mimochodem definující "typ punning" z hlediska union a diskutující problémy týkající se chování definovaného implementací posledního výše uvedeného příkladu
  • Zpráva o vadách 283 o použití odborů pro dělení typu