O __attribute __ ((empacotado)) / #pragma do gcc não é seguro?

164

Em C, o compilador colocará os membros de uma estrutura na ordem em que são declarados, com possíveis bytes de preenchimento inseridos entre os membros ou após o último membro, para garantir que cada membro esteja alinhado corretamente.

O gcc fornece uma extensão de idioma __attribute__((packed)), que informa ao compilador para não inserir preenchimento, permitindo que os membros da estrutura sejam desalinhados. Por exemplo, se o sistema normalmente exigir que todos os intobjetos tenham alinhamento de 4 bytes, __attribute__((packed))poderá fazer com que os intmembros da estrutura sejam alocados em deslocamentos ímpares.

Citando a documentação do gcc:

O atributo `compactado 'especifica que um campo de variável ou estrutura deve ter o menor alinhamento possível - um byte para uma variável e um bit para um campo, a menos que você especifique um valor maior com o atributo` align'.

Obviamente, o uso dessa extensão pode resultar em requisitos de dados menores, mas com código mais lento, pois o compilador deve (em algumas plataformas) gerar código para acessar um membro desalinhado, um byte por vez.

Mas há casos em que isso não é seguro? O compilador sempre gera código correto (embora mais lento) para acessar membros desalinhados de estruturas compactadas? É possível fazê-lo em todos os casos?

Keith Thompson
fonte
1
O relatório de bug do gcc agora está marcado como CORRIGIDO com a adição de um aviso na atribuição do ponteiro (e uma opção para desativar o aviso). Detalhes na minha resposta .
Keith Thompson

Respostas:

148

Sim, __attribute__((packed))é potencialmente inseguro em alguns sistemas. O sintoma provavelmente não aparecerá em um x86, o que apenas torna o problema mais insidioso; testes em sistemas x86 não revelam o problema. (No x86, os acessos desalinhados são tratados no hardware; se você desferir um int*ponteiro que aponte para um endereço ímpar, será um pouco mais lento do que se estivesse alinhado corretamente, mas você obterá o resultado correto.)

Em alguns outros sistemas, como o SPARC, a tentativa de acessar um intobjeto desalinhado causa um erro de barramento, travando o programa.

Também existem sistemas nos quais um acesso desalinhado ignora silenciosamente os bits de ordem inferior do endereço, fazendo com que ele acesse o bloco de memória errado.

Considere o seguinte programa:

#include <stdio.h>
#include <stddef.h>
int main(void)
{
    struct foo {
        char c;
        int x;
    } __attribute__((packed));
    struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
    int *p0 = &arr[0].x;
    int *p1 = &arr[1].x;
    printf("sizeof(struct foo)      = %d\n", (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
    printf("arr[0].x = %d\n", arr[0].x);
    printf("arr[1].x = %d\n", arr[1].x);
    printf("p0 = %p\n", (void*)p0);
    printf("p1 = %p\n", (void*)p1);
    printf("*p0 = %d\n", *p0);
    printf("*p1 = %d\n", *p1);
    return 0;
}

No x86 Ubuntu com gcc 4.5.2, ele produz a seguinte saída:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20

No SPARC Solaris 9 com gcc 4.5.1, ele produz o seguinte:

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error

Nos dois casos, o programa é compilado sem opções extras, apenas gcc packed.c -o packed.

(Um programa que usa uma única estrutura em vez de uma matriz não exibe o problema de maneira confiável, pois o compilador pode alocar a estrutura em um endereço ímpar para que o xmembro esteja alinhado corretamente. Com uma matriz de dois struct fooobjetos, pelo menos um ou outro terá um xmembro desalinhado .)

(Nesse caso, p0aponta para um endereço desalinhado, porque aponta para um intmembro compactado após um charmembro. p1Está alinhado corretamente, pois aponta para o mesmo membro no segundo elemento da matriz, portanto, existem dois charobjetos antes dele - e no SPARC Solaris, o array arrparece estar alocado em um endereço uniforme, mas não múltiplo de 4.)

Ao se referir ao membro xde um struct foonome, o compilador sabe que xestá potencialmente desalinhado e gerará código adicional para acessá-lo corretamente.

Depois que o endereço arr[0].xou arr[1].xfoi armazenado em um objeto ponteiro, nem o compilador nem o programa em execução sabem que apontam para um intobjeto desalinhado . Apenas assume que está alinhado corretamente, resultando (em alguns sistemas) em um erro de barramento ou outra falha semelhante.

Consertar isso no gcc seria, na minha opinião, impraticável. Uma solução geral exigiria, para cada tentativa de desreferenciar um ponteiro para qualquer tipo com requisitos de alinhamento não triviais (a) provar em tempo de compilação que o ponteiro não aponta para um membro desalinhado de uma estrutura empacotada, ou (b) gerar código mais lento e volumoso que pode manipular objetos alinhados ou desalinhados.

Enviei um relatório de bug do gcc . Como eu disse, não acredito que seja prático corrigi-lo, mas a documentação deve mencioná-lo (atualmente não o faz).

ATUALIZAÇÃO : A partir de 20/12/2018, esse bug está marcado como CORRIGIDO. O patch aparecerá no gcc 9 com a adição de uma nova -Waddress-of-packed-memberopção, ativada por padrão.

Quando o endereço do membro compactado da estrutura ou união é obtido, isso pode resultar em um valor de ponteiro não alinhado. Este patch adiciona -Waddress-of -pack-member para verificar o alinhamento na atribuição do ponteiro e avisar o endereço não alinhado, bem como o ponteiro não alinhado

Acabei de criar essa versão do gcc a partir do código-fonte. Para o programa acima, ele produz estes diagnósticos:

c.c: In function main’:
c.c:10:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   10 |     int *p0 = &arr[0].x;
      |               ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of struct foo may result in an unaligned pointer value [-Waddress-of-packed-member]
   11 |     int *p1 = &arr[1].x;
      |               ^~~~~~~~~
Keith Thompson
fonte
1
está potencialmente desalinhado e gerará ... o quê?
Almo
5
elementos struct desalinhados no ARM produzem coisas estranhas: alguns acessos causam falhas, outros fazem com que os dados recuperados sejam reorganizados de forma contra-intuitiva ou incorporem dados inesperados adjacentes.
amigos estão dizendo sobre wallyk
8
Parece que a embalagem em si é segura, mas como os membros embalados são usados ​​pode não ser seguro. As CPUs mais antigas baseadas em ARM também não suportam acessos de memória desalinhados, mas as versões mais recentes sim, mas eu sei que o Symbian OS ainda não permite acessos desalinhados ao executar nessas versões mais recentes (o suporte está desativado).
James
14
Outra maneira de corrigi-lo no gcc seria usar o sistema de tipos: exigir que ponteiros para membros de estruturas compactadas possam ser atribuídos apenas a ponteiros marcados como compactados (ou seja, potencialmente desalinhados). Mas na verdade: estruturas compactadas, basta dizer não.
caf
9
@Flavius: Meu principal objetivo era divulgar as informações. Veja também meta.stackexchange.com/questions/17463/…
Keith Thompson
62

Como as ams disseram acima, não leve um ponteiro para um membro de uma estrutura que está empacotada. Isso é simplesmente brincar com fogo. Quando você diz __attribute__((__packed__))ou #pragma pack(1), o que você realmente está dizendo é "Ei, gcc, eu realmente sei o que estou fazendo". Quando acontece que você não o faz, não pode culpar corretamente o compilador.

Talvez possamos culpar o compilador por sua complacência. Embora o gcc tenha uma -Wcast-alignopção, ele não está ativado por padrão nem com -Wallou -Wextra. Aparentemente, isso ocorre porque os desenvolvedores do gcc consideram esse tipo de código uma " abominação " com morte cerebral indigna de ser tratada - desdém compreensível, mas não ajuda quando um programador inexperiente se depara com ele.

Considere o seguinte:

struct  __attribute__((__packed__)) my_struct {
    char c;
    int i;
};

struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;

Aqui, o tipo de aé uma estrutura empacotada (conforme definido acima). Da mesma forma, bé um ponteiro para uma estrutura empacotada. O tipo da expressão a.ié (basicamente) um valor int l com alinhamento de 1 byte. ce dsão ambos ints normais . Ao ler a.i, o compilador gera código para acesso não alinhado. Quando você lê b->i, bo tipo ainda sabe que está lotado, então não há problema algum. eé um ponteiro para um int alinhado a um byte, para que o compilador saiba como desreferenciar isso corretamente também. Mas quando você faz a atribuição f = &a.i, está armazenando o valor de um ponteiro int desalinhado em uma variável alinhada int pointer - foi aí que você errou. E eu concordo, o gcc deve ter esse aviso ativado porpadrão (nem mesmo em -Wallou -Wextra).

Daniel Santos
fonte
6
+1 para explicar como usar ponteiros com estruturas desalinhadas!
Soumya 28/06
@Soumya Obrigado pelos pontos! :) No entanto, lembre-se de que __attribute__((aligned(1)))é uma extensão do gcc e não é portátil. Que eu saiba, a única maneira realmente portátil de acessar acesso desalinhado em C (com qualquer combinação de compilador / hardware) é com uma cópia de memória em bytes (memcpy ou similar). Alguns hardwares nem sequer têm instruções para acesso desalinhado. Minha experiência é com arm e x86, que podem fazer as duas coisas, embora o acesso não alinhado seja mais lento. Portanto, se você precisar fazer isso com alto desempenho, precisará farejar o hardware e usar truques específicos do arco.
Daniel Santos
4
@Soumya Infelizmente, __attribute__((aligned(x)))agora parece ser ignorado quando usado para ponteiros. :( Ainda não tenho todos os detalhes disso, mas usar o __builtin_assume_aligned(ptr, align)gcc parece gerar o código correto. Quando eu tiver uma resposta mais concisa (e espero que seja um relatório de bug), atualizarei minha resposta.
Daniel Santos
@DanielSantos: Um compilador de qualidade que eu uso (Keil) reconhece qualificadores "compactados" para ponteiros; se uma estrutura é declarada "empacotada", o endereço de um uint32_tmembro produzirá a uint32_t packed*; tentar ler de um ponteiro como, por exemplo, em um Cortex-M0, o IIRC chamará uma sub-rotina que levará ~ 7x enquanto uma leitura normal, se o ponteiro estiver desalinhado ou ~ 3x, se estiver alinhado, mas se comportará de maneira previsível em ambos os casos [o código em linha levaria cinco vezes mais, alinhado ou desalinhado].
Supercat
49

É perfeitamente seguro, desde que você sempre acesse os valores através da estrutura através da .(ponto) ou da ->notação.

O que não é seguro é levar o ponteiro dos dados desalinhados e acessá-los sem levar isso em conta.

Além disso, mesmo que cada item na estrutura esteja desalinhado, ele é desalinhado de uma maneira específica . Portanto, a estrutura como um todo deve ser alinhada conforme o compilador espera ou haverá problemas (em algumas plataformas ou no futuro, se uma nova maneira for inventada para otimizar acessos desalinhados).

ams
fonte
Hmm, eu me pergunto o que acontece se você colocar uma estrutura empacotada dentro de outra estrutura empacotada, onde o alinhamento seria diferente? Pergunta interessante, mas não deve mudar a resposta.
20911
O GCC também nem sempre alinhará a estrutura. Por exemplo: struct foo {int x; char c; } __atributo __ ((empacotado)); barra de estrutura {char c; struct foo f; }; Descobri que bar :: f :: x não estará necessariamente alinhado, pelo menos em certos tipos de MIPS.
Anton
3
@antonm: Sim, uma estrutura dentro de uma estrutura compactada pode estar desalinhada, mas, novamente, o compilador sabe qual é o alinhamento de cada campo e é perfeitamente seguro, desde que você não tente usar ponteiros na estrutura. Você deve imaginar uma estrutura dentro de uma estrutura como uma série simples de campos, com o nome extra apenas para facilitar a leitura.
AMS
6

O uso desse atributo é definitivamente inseguro.

Uma coisa em particular que quebra é a capacidade de uma unionque contém duas ou mais estruturas para escrever um membro e ler outro se as estruturas tiverem uma sequência inicial comum de membros. A seção 6.5.2.3 da norma C11 declara:

6 Uma garantia especial é feita para simplificar o uso de uniões: se uma união contiver várias estruturas que compartilham uma sequência inicial comum (veja abaixo) e se o objeto de união atualmente contiver uma dessas estruturas, é permitido inspecionar o parte inicial comum de qualquer um deles em qualquer lugar em que seja visível uma declaração do tipo completo da união. Duas estruturas compartilham uma sequência inicial comum se membros correspondentes tiverem tipos compatíveis (e, para campos de bits, as mesmas larguras) para uma sequência de um ou mais membros iniciais.

...

9 EXEMPLO 3 A seguir, um fragmento válido:

union {
    struct {
        int    alltypes;
    }n;
    struct {
        int    type;
        int    intnode;
    } ni;
    struct {
        int    type;
        double doublenode;
    } nf;
}u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/*
...
*/
if (u.n.alltypes == 1)
if (sin(u.nf.doublenode) == 0.0)
/*
...
*/

Quando __attribute__((packed))é introduzido, isso quebra. O exemplo a seguir foi executado no Ubuntu 16.04 x64 usando o gcc 5.4.0 com as otimizações desativadas:

#include <stdio.h>
#include <stdlib.h>

struct s1
{
    short a;
    int b;
} __attribute__((packed));

struct s2
{
    short a;
    int b;
};

union su {
    struct s1 x;
    struct s2 y;
};

int main()
{
    union su s;
    s.x.a = 0x1234;
    s.x.b = 0x56789abc;

    printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
    printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
    return 0;
}

Resultado:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

Embora struct s1e struct s2ter uma "sequência inicial comum", a embalagem aplicado aos antigos meios que os membros correspondentes não vivem no deslocamento do mesmo byte. O resultado é que o valor gravado no membro x.bnão é o mesmo que o valor lido do membro y.b, mesmo que o padrão diga que eles devem ser os mesmos.

dbush
fonte
Alguém pode argumentar que, se você agrupar uma das estruturas e não a outra, não esperará que elas tenham layouts consistentes. Mas sim, esse é outro requisito padrão que pode ser violado.
21319 Keith Thompson
1

(A seguir, um exemplo muito artificial elaborado para ilustrar.) Um dos principais usos de estruturas compactadas é onde você tem um fluxo de dados (digamos 256 bytes) ao qual deseja fornecer significado. Se eu pegar um exemplo menor, suponha que eu tenha um programa em execução no meu Arduino que envie via serial um pacote de 16 bytes com o seguinte significado:

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

Então eu posso declarar algo como

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

e então posso me referir aos bytes targetAddr via aStruct.targetAddr, em vez de mexer na aritmética do ponteiro.

Agora, com as coisas de alinhamento acontecendo, levar um ponteiro * vazio na memória para os dados recebidos e convertê-lo em um myStruct * não funcionará , a menos que o compilador trate a estrutura como empacotada (ou seja, armazena dados na ordem especificada e usa exatamente 16 bytes para este exemplo). Existem penalidades de desempenho para leituras não alinhadas, portanto, usar estruturas compactadas para dados com os quais seu programa está trabalhando ativamente não é necessariamente uma boa ideia. Porém, quando seu programa é fornecido com uma lista de bytes, as estruturas compactadas facilitam a gravação de programas que acessam o conteúdo.

Caso contrário, você acaba usando C ++ e escrevendo uma classe com métodos de acessador e outras coisas que apontam aritmética nos bastidores. Em suma, as estruturas compactadas destinam-se a lidar eficientemente com os dados compactados, e os dados compactados podem ser o que seu programa deve trabalhar. Na maioria das vezes, o código deve ler valores da estrutura, trabalhar com eles e escrevê-los de volta quando terminar. Todo o resto deve ser feito fora da estrutura compactada. Parte do problema é o material de baixo nível que C tenta ocultar do programador e o salto que é necessário se essas coisas realmente importam para o programador. (Você quase precisa de uma construção diferente de 'layout de dados' na linguagem para poder dizer 'essa coisa tem 48 bytes de comprimento, foo refere-se aos dados de 13 bytes e deve ser interpretada assim' '; e uma construção de dados estruturados separada,

John Allsup
fonte
A menos que esteja faltando alguma coisa, isso não responde à pergunta. Você argumenta que o empacotamento da estrutura é conveniente (qual é), mas não aborda a questão de saber se é seguro. Além disso, você afirma que as penalidades de desempenho para leituras não alinhadas; isso é verdade para x86, mas não para todos os sistemas, como demonstrei na minha resposta.
Keith Thompson