Digite trocadilho - Type punning

Na ciência da computação , o trocadilho é um termo comum para qualquer técnica de programação que subverte ou contorne o sistema de tipos de uma linguagem de programação a fim de obter um efeito que seria difícil ou impossível de se obter dentro dos limites da linguagem formal.

Em C e C ++ , construções como conversão de tipo de ponteiro e - C ++ adiciona conversão de tipo de referência e a esta lista - são fornecidas para permitir muitos tipos de trocadilhos, embora alguns tipos não sejam realmente suportados pela linguagem padrão. unionreinterpret_cast

Na linguagem de programação Pascal , o uso de registros com variantes pode ser usado para tratar um determinado tipo de dados de mais de uma maneira, ou de uma maneira normalmente não permitida.

Exemplo de sockets

Um exemplo clássico de trocadilho é encontrado na interface de soquetes de Berkeley . A função para vincular um soquete aberto, mas não inicializado, a um endereço IP é declarada da seguinte maneira:

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

A bind função geralmente é chamada da seguinte maneira:

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

A biblioteca de sockets de Berkeley baseia-se fundamentalmente no fato de que em C , um ponteiro para struct sockaddr_in é livremente convertível em um ponteiro para struct sockaddr ; e, além disso, que os dois tipos de estrutura compartilham o mesmo layout de memória. Portanto, uma referência ao campo de estrutura my_addr->sin_family (onde my_addr é do tipo struct sockaddr* ) irá realmente se referir ao campo sa.sin_family (onde sa é do tipo struct sockaddr_in ). Em outras palavras, a biblioteca de sockets usa trocadilhos para implementar uma forma rudimentar de polimorfismo ou herança .

Freqüentemente visto no mundo da programação é o uso de estruturas de dados "preenchidas" para permitir o armazenamento de diferentes tipos de valores no que é efetivamente o mesmo espaço de armazenamento. Isso geralmente é visto quando duas estruturas são usadas em exclusividade mútua para otimização.

Exemplo de ponto flutuante

Nem todos os exemplos de trocadilhos envolvem estruturas, como o exemplo anterior. Suponha que desejamos determinar se um número de ponto flutuante é negativo. Poderíamos escrever:

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

No entanto, supondo que as comparações de ponto flutuante sejam caras, e também supondo que float seja representado de acordo com o padrão de ponto flutuante IEEE e os inteiros tenham 32 bits de largura, poderíamos usar o tipo trocadilho para extrair o bit de sinal do número de ponto flutuante usando apenas operações inteiras:

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

Observe que o comportamento não será exatamente o mesmo: no caso especial de x ser zero negativo , a primeira implementação cede false enquanto a segunda cede true . Além disso, a primeira implementação retornará false para qualquer valor NaN , mas o último pode retornar true para valores NaN com o bit de sinal definido.

Esse tipo de trocadilho é mais perigoso do que a maioria. Enquanto o primeiro exemplo se baseou apenas nas garantias feitas pela linguagem de programação C sobre o layout da estrutura e a conversibilidade do ponteiro, o último exemplo se baseia em suposições sobre o hardware de um sistema específico. Algumas situações, como código de tempo crítico que o compilador não consegue otimizar , podem exigir código perigoso. Nesses casos, documentar todas essas suposições em comentários e introduzir asserções estáticas para verificar as expectativas de portabilidade ajuda a manter o código sustentável .

Exemplos práticos de trocadilhos de ponto flutuante incluem raiz quadrada inversa rápida popularizada por Quake III , comparação rápida de FP como inteiros e localização de valores vizinhos incrementando como um inteiro (implementação nextafter ).

Por idioma

C e C ++

Além da suposição sobre a representação de bits de números de ponto flutuante, o exemplo de trocadilho de tipo de ponto flutuante acima também viola as restrições da linguagem C sobre como os objetos são acessados: o tipo declarado de x é, float mas é lido por meio de uma expressão do tipo unsigned int . Em muitas plataformas comuns, esse uso de trocadilhos pode criar problemas se diferentes ponteiros estiverem alinhados de maneiras específicas da máquina . Além disso, ponteiros de tamanhos diferentes podem aliases de acessos à mesma memória , causando problemas que não são verificados pelo compilador.

Uso de ponteiros

Uma tentativa ingênua de trocadilho pode ser alcançada usando ponteiros:

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

De acordo com o padrão C, esse código não deve (ou melhor, não precisa) compilar; no entanto, se o fizer, piAsRawData normalmente contém os bits brutos de pi.

Uso de union

É um erro comum tentar corrigir o trocadilho com o uso de a union . (O exemplo a seguir também pressupõe a representação de bits IEEE-754 para tipos de ponto flutuante).

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

Acessar my_union.ui após inicializar o outro membro, my_union.d ainda é uma forma de trocadilho em C e o resultado é um comportamento não especificado (e um comportamento indefinido em C ++).

A linguagem do § 6.5 / 7 pode ser mal interpretada para implicar que a leitura de membros alternativos do sindicato é permitida. No entanto, o texto diz "Um objeto deve ter seu valor armazenado acessado apenas por ...". É uma expressão limitante, não uma declaração de que todos os membros de união possíveis podem ser acessados, independentemente de qual foi armazenado pela última vez. Portanto, o uso de union evita nenhum dos problemas de simplesmente fazer um trocadilho diretamente.

Pode até ser considerado menos seguro do que o trocadilho de tipo usando ponteiros, uma vez que um compilador terá menos probabilidade de relatar um aviso ou erro se não suportar o trocadilho de tipo.

Compiladores como o GCC suportam acessos de valor aliasable como os exemplos acima como uma extensão de linguagem. Em compiladores sem essa extensão, a regra de apelido estrito é quebrada apenas por um memcpy explícito ou usando um ponteiro char como um "intermediário" (já que esses podem ter apelidos livremente).

Para outro exemplo de trocadilho de tipo, consulte Stride de uma matriz .

Pascal

Um registro de variante permite tratar um tipo de dados como vários tipos de dados, dependendo de qual variante está sendo referenciada. No exemplo a seguir, o inteiro é presumido como 16 bits, enquanto o inteiro longo e real são presumidos como 32, enquanto o caractere é presumido como sendo 8 bits:

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

Em Pascal, copiar um real para um inteiro converte-o no valor truncado. Este método traduziria o valor binário do número de ponto flutuante em qualquer coisa como um inteiro longo (32 bits), que não será o mesmo e pode ser incompatível com o valor inteiro longo em alguns sistemas.

Esses exemplos podem ser usados ​​para criar conversões estranhas, embora, em alguns casos, possa haver usos legítimos para esses tipos de construções, como para determinar a localização de dados específicos. No exemplo a seguir, um ponteiro e um inteiro longo são presumidos como sendo de 32 bits:

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

Onde "novo" é a rotina padrão em Pascal para alocar memória para um ponteiro e "hex" é presumivelmente uma rotina para imprimir a string hexadecimal que descreve o valor de um inteiro. Isso permitiria a exibição do endereço de um ponteiro, algo que normalmente não é permitido. (Os ponteiros não podem ser lidos ou escritos, apenas atribuídos.) Atribuir um valor a uma variante inteira de um ponteiro permitiria examinar ou gravar em qualquer local na memória do sistema:

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

Essa construção pode causar uma verificação de programa ou violação de proteção se o endereço 0 estiver protegido contra leitura na máquina em que o programa está sendo executado ou no sistema operacional sob o qual está sendo executado.

A técnica de elenco de reinterpretação de C / C ++ também funciona em Pascal. Isso pode ser útil, quando, por exemplo. lendo dwords de um fluxo de bytes e queremos tratá-los como flutuantes. Aqui está um exemplo prático, onde reinterpretamos um dword para um float:

type
    pReal = ^Real;

var
    DW : DWord;
    F  : Real;

F := pReal(@DW)^;

C #

Em C # (e outras linguagens .NET), o trocadilho é um pouco mais difícil de conseguir por causa do sistema de tipos, mas pode ser feito mesmo assim, usando ponteiros ou associações de estrutura.

Ponteiros

C # só permite ponteiros para os chamados tipos nativos, ou seja, qualquer tipo primitivo (exceto string ), enum, array ou struct que seja composto apenas de outros tipos nativos. Observe que os ponteiros são permitidos apenas em blocos de código marcados como 'inseguros'.

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

Sindicatos estruturais

As uniões estruturais são permitidas sem qualquer noção de código 'inseguro', mas requerem a definição de um novo tipo.

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

Código CIL bruto

CIL bruto pode ser usado em vez de C #, porque não tem a maioria das limitações de tipo. Isso permite, por exemplo, combinar dois valores enum de um tipo genérico:

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

Isso pode ser contornado pelo seguinte código 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
}

O cpblk opcode CIL permite alguns outros truques, como converter uma estrutura em uma matriz de bytes:

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

Referências

  1. ^ Herf, Michael (dezembro de 2001). "truques radix" . estereopsia: gráficos .
  2. ^ "Truques de flutuação estúpida" . Random ASCII - blog de tecnologia de Bruce Dawson . 24 de janeiro de 2012.
  3. ^ a b ISO / IEC 9899: 1999 s6.5 / 7
  4. ^ "§ 6.5.2.3/3, nota de rodapé 97", ISO / IEC 9899: 2018 (PDF) , 2018, p. 59, arquivado do original (PDF) em 30/12/2018, se o membro usado para ler o conteúdo de um objeto de união não for o mesmo que o último membro usado para armazenar um valor no objeto, a parte apropriada do a representação do objeto do valor é reinterpretada como uma representação do objeto no novo tipo, conforme descrito em 6.2.6 ( um processo às vezes chamado de “trocadilho de tipo” ). Esta pode ser uma representação de armadilha.
  5. ^ "§ J.1 / 1, ponto 11", ISO / IEC 9899: 2018 (PDF) , 2018, p. 403, arquivado do original (PDF) em 30-12-2018, Os seguintes não são especificados:… Os valores de bytes que correspondem a membros da união diferentes do último armazenado em (6.2.6.1).
  6. ^ ISO / IEC 14882: 2011 Seção 9.5
  7. ^ GCC: Não-Bugs

links externos