Por que bit endianness é um problema em bitfields?

Qualquer código portátil que use bitfields parece distinguir entre plataformas little e big-endian. Veja a declaração de struct iphdr no kernel linux para um exemplo de tal código. Não consigo entender por que a bit endianness é um problema.

Tanto quanto eu entendo, bitfields são puramente compilações construídas, usadas para facilitar manipulações de nível de bits.

Por exemplo, considere o seguinte campo de bits:

 struct ParsedInt { unsigned int f1:1; unsigned int f2:3; unsigned int f3:4; }; uint8_t i; struct ParsedInt *d = &i; 

Aqui, escrever d->f2 é simplesmente uma forma compacta e legível de dizer (i>>1) & (1<<4 - 1) .

No entanto, as operações de bits são bem definidas e funcionam independentemente da arquitetura. Então, por que os bitfields não são portáteis?

    Pelo padrão C, o compilador está livre para armazenar o campo de bits praticamente de qualquer maneira aleatória que desejar. Você nunca pode fazer suposições sobre onde os bits são alocados. Aqui estão apenas algumas coisas relacionadas ao campo de bits que não são especificadas pelo padrão C:

    Comportamento não especificado

    • O alinhamento da unidade de armazenamento endereçável alocada para manter um campo de bits (6.7.2.1).

    Comportamento definido pela implementação

    • Se um campo de bits pode ultrapassar um limite de unidade de armazenamento (6.7.2.1).
    • A ordem de alocação de campos de bits dentro de uma unidade (6.7.2.1).

    Big / little endian é claro também definido pela implementação. Isso significa que sua estrutura poderia ser alocada das seguintes maneiras (assumindo 16 bits ints):

     PADDING : 8 f1 : 1 f2 : 3 f3 : 4 or PADDING : 8 f3 : 4 f2 : 3 f1 : 1 or f1 : 1 f2 : 3 f3 : 4 PADDING : 8 or f3 : 4 f2 : 3 f1 : 1 PADDING : 8 

    Qual deles se aplica? Adivinhe ou leia a documentação de back-end detalhada do seu compilador. Adicione a complexidade de inteiros de 32 bits, em big endian ou little endian, para isso. Em seguida, adicione o fato de que o compilador tem permissão para adicionar qualquer número de bytes de preenchimento em qualquer lugar dentro de seu campo de bits, porque ele é tratado como uma estrutura (ele não pode adicionar preenchimento no início da estrutura, mas em qualquer outro lugar).

    E então eu nem sequer mencionei o que acontece se você usar “int” como tipo de campo de bits = comportamento definido pela implementação, ou se você usar qualquer outro tipo que não (não assinado) int = comportamento definido pela implementação.

    Então, para responder à pergunta, não existe código de bit-field portátil, porque o padrão C é extremamente vago com o modo como os campos de bits devem ser implementados. A única coisa com que os campos de bits podem ser confiáveis ​​é ser blocos de valores booleanos, em que o programador não está preocupado com a localização dos bits na memory.

    A única solução portátil é usar os operadores bit-wise em vez dos campos bit. O código de máquina gerado será exatamente o mesmo, mas determinístico. Operadores bit-wise são 100% portáveis ​​em qualquer compilador C para qualquer sistema.

    Tanto quanto eu entendo, bitfields são puramente construtores compiladores

    E isso é parte do problema. Se o uso de campos de bits fosse restrito ao que o compilador ‘possuía’, então como o compilador empacotava bits ou os ordenava não seria de grande interesse para ninguém.

    No entanto, os campos de bits provavelmente são usados ​​muito mais frequentemente para modelar construções que são externas ao domínio do compilador – registros de hardware, o protocolo ‘wire’ para comunicações ou o layout do formato de arquivo. Essas coisas têm requisitos estritos de como os bits devem ser definidos, e usar campos de bits para modelá-los significa que você precisa depender de uma implementação definida e – pior ainda – do comportamento não especificado de como o compilador irá estruturar o campo de bits. .

    Em suma, os campos de bits não são especificados o suficiente para torná-los úteis para as situações em que eles parecem mais utilizados.

    ISO / IEC 9899: 6.7.2.1 / 10

    Uma implementação pode alocar qualquer unidade de armazenamento endereçável grande o suficiente para manter um campo de bits. Se houver espaço suficiente, um campo de bits que segue imediatamente outro campo de bits em uma estrutura deve ser empacotado em pedaços adjacentes da mesma unidade. Se o espaço insuficiente permanecer, se um campo de bits que não se encheckbox é colocado na unidade seguinte ou sobrepõe-se a unidades adjacentes é de fi nido pela implementação. A ordem de alocação de campos de bits dentro de uma unidade (ordem alta para ordem baixa ou ordem baixa para ordem alta) é definida pela implementação. O alinhamento da unidade de armazenamento endereçável não está especificado.

    É mais seguro usar operações de mudança de bit em vez de fazer suposições sobre ordenação ou alinhamento de campo de bits ao tentar escrever código portátil, independentemente da capacidade de endianhamento ou de bits do sistema.

    Veja também EXP11-C. Não aplique operadores esperando um tipo para dados de um tipo incompatível .

    Acessos de campo de bits são implementados em termos de operações no tipo subjacente. No exemplo, unsigned int . Então, se você tem algo como:

     struct x { unsigned int a : 4; unsigned int b : 8; unsigned int c : 4; }; 

    Quando você acessa o campo b , o compilador acessa um unsigned int inteiro unsigned int e, em seguida, desloca e mascara o intervalo de bits apropriado. (Bem, não precisa , mas podemos fingir que sim.)

    No big endian, o layout será algo assim (o bit mais significativo primeiro):

     AAAABBBB BBBBCCCC 

    Em little endian, layout será assim:

     BBBBAAAA CCCCBBBB 

    Se você quiser acessar o layout big endian de little endian ou vice-versa, você terá que fazer algum trabalho extra. Esse aumento na portabilidade tem uma penalidade de desempenho e, como o struct layout já não é portável, os implementadores de idioma foram com a versão mais rápida.

    Isso faz muitas suposições. Observe também que sizeof(struct x) == 4 na maioria das plataformas.

    Os campos de bits serão armazenados em uma ordem diferente, dependendo do endianness da máquina, isso pode não importar em alguns casos, mas em outros pode ser importante. Digamos, por exemplo, que sua estrutura ParsedInt representou flags em um pacote enviado através de uma rede, uma pequena máquina endian e uma grande máquina endian lêem esses flags em uma ordem diferente do byte transmitido, o que obviamente é um problema.

    Para ecoar os pontos mais importantes: Se você estiver usando isso em uma única plataforma de compilador / HW como uma construção somente de software, a capacidade de endian não será um problema. Se você estiver usando código ou dados em várias plataformas OU precisar corresponder a layouts de bits de hardware, isso será um problema. E um monte de software profissional é multi-plataforma, portanto, tem que se importar.

    Aqui está o exemplo mais simples: eu tenho código que armazena números em formato binário em disco. Se eu não escrever e ler esses dados para o disco eu mesmo explicitamente byte por byte, então ele não será o mesmo valor se for lido de um sistema endian oposto.

    Exemplo concreto:

    int16_t s = 4096; // um número de 16 bits assinado …

    Vamos dizer que meu programa vem com alguns dados no disco que eu quero ler. Digamos que eu queira carregá-lo como 4096 neste caso …

    fread ((void *) & s, 2, fp); // lendo do disco como binário …

    Aqui eu o leio como um valor de 16 bits, não como bytes explícitos. Isso significa que se o meu sistema for compatível com o endianness armazenado no disco, recebo 4096 e, se isso não acontecer, recebo 16 !!!!!

    Portanto, o uso mais comum de endianness é carregar números binários em massa e fazer um bswap se você não corresponder. No passado, armazenávamos dados no disco como big endian, porque a Intel era o cara estranho e fornecia instruções de alta velocidade para trocar os bytes. Hoje em dia, a Intel é tão comum que muitas vezes faz do Little Endian o padrão e troca quando em um sistema big endian.

    Uma abordagem neutra mais lenta, mas endian, é fazer TODAS as E / S por bytes, ou seja:

    uint_8 ubyte; int_8 sbyte; int16_t s; // lê s no modo neutro endian

    // Vamos escolher little endian como nossa ordem de bytes escolhida:

    fread ((void *) & ubyte, 1, fp); // Somente ler 1 byte de cada vez fread ((void *) & sbyte, 1, fp); // Apenas leia 1 byte de cada vez

    // Reconstruir s

    s = ubyte | (sByte < < 8);

    Note que isto é idêntico ao código que você escreveria para fazer um swap endian, mas você não precisa mais checar o endianness. E você pode usar macros para tornar isso menos doloroso.

    Eu usei o exemplo de dados armazenados usados ​​por um programa. A outra aplicação principal mencionada é escrever registradores de hardware, onde esses registradores têm uma ordenação absoluta. Um lugar muito comum que surge é com charts. Obtenha o endianness errado e seus canais de cor vermelha e azul se invertem! Novamente, o problema é de portabilidade – você pode simplesmente se adaptar a uma determinada plataforma de hardware e placa gráfica, mas se quiser que o mesmo código funcione em máquinas diferentes, você deve testar.

    Aqui está um teste clássico:

    typedef union {uint_16 s; uint_8 b [2]; } EndianTest_t;

    Teste de EndianTest_t = 4096;

    if (teste.b [0] == 12) printf (“Big Endian detectado! \ n”);

    Observe que os problemas de bitfield também existem, mas são ortogonais aos problemas de endianness.