it-swarm-pt.tech

O pacote __attribute __ ((packed))/#pragma do gcc é inseguro?

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

o gcc fornece uma extensão de linguagem, __attribute__((packed)), que informa ao compilador para não inserir o preenchimento, permitindo que os membros da estrutura fiquem desalinhados. Por exemplo, se o sistema normalmente exige que todos os objetos int tenham alinhamento de 4 bytes, __attribute__((packed)) pode fazer com que int struct membros sejam alocados em deslocamentos ímpares.

Citando a documentação do gcc:

O atributo `packed 'especifica que uma variável ou um campo de 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 `alinhados '.

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

Mas há algum caso em que isso não seja 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?

136
Keith Thompson

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 revelarão o problema. (No x86, os acessos desalinhados são manipulados no hardware; se você desreferenciar um ponteiro int* que aponte para um endereço ímpar, ele 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 objeto int desalinhado causa um erro de barramento, travando o programa.

Também houve sistemas em que um acesso desalinhado silenciosamente ignora os bits de baixa ordem do endereço, fazendo com que ele acesse o pedaço errado de memória.

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, 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, 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

Em ambos os casos, o programa é compilado sem opções extras, apenas gcc packed.c -o packed.

(Um programa que utiliza uma única estrutura em vez de uma matriz não apresenta o problema de forma confiável, pois o compilador pode alocar a estrutura em um endereço ímpar para que o membro x esteja adequadamente alinhado. Com uma matriz de dois objetos struct foo, pelo menos um ou o outro terá um membro x desalinhado.)

(Nesse caso, p0 aponta para um endereço desalinhado, porque ele aponta para um membro int compactado após um membro char. p1 está corretamente alinhado, pois aponta para o mesmo membro no segundo elemento da matriz, portanto, há dois objetos char que o precedem - e no SPARC Solaris, a matriz arr parece estar alocada em um endereço que é par, mas não um múltiplo de 4.)

Ao se referir ao membro x de um struct foo pelo nome, o compilador sabe que x está potencialmente desalinhado e irá gerar código adicional para acessá-lo corretamente.

Uma vez que o endereço arr[0].x ou arr[1].x tenha sido armazenado em um objeto ponteiro, nem o compilador nem o programa em execução sabem que ele aponta para um objeto int desalinhado. Apenas assume que está alinhado corretamente, resultando (em alguns sistemas) em um erro de barramento ou outra falha similar.

Consertar isso no gcc seria, acredito, 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 compactada, ou (b) gerando códigos mais volumosos e mais lentos que podem manipular objetos alinhados ou desalinhados.

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

UPDATE: A partir de 2018-12-20, esse bug é marcado como FIXED. O patch aparecerá no gcc 9 com a adição de uma nova opção -Waddress-of-packed-member, ativada por padrão.

Quando o endereço do membro compactado de struct ou union é obtido, ele pode Resultar em um valor de ponteiro não alinhado. Este patch adiciona -Waddress-de-packed-membro para verificar o alinhamento na atribuição de ponteiro e avisar o endereço não alinhado, bem como ponteiro não alinhado

Acabei de criar essa versão do gcc a partir do código-fonte. Para o programa acima, ele produz esses 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;
      |               ^~~~~~~~~
125
Keith Thompson

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

O que é não seguro é pegar o ponteiro de dados não alinhados e acessá-lo sem levar isso em conta.

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

47
ams

Como eu disse acima, não pegue um ponteiro para um membro de uma estrutura que esteja cheia. Isso é simplesmente brincar com fogo. Quando você diz __attribute__((__packed__)) ou #pragma pack(1), o que você está realmente dizendo é: "Hey gcc, eu realmente sei o que estou fazendo." Quando acontece que você não, você não pode culpar o compilador corretamente.

Talvez possamos culpar o compilador por sua complacência. Embora o gcc tenha uma opção -Wcast-align, ele não está habilitado por padrão nem com -Wall ou -Wextra. Isto é aparentemente devido aos desenvolvedores do gcc considerando que este tipo de código é uma " abominação " indigna de lidar com - desdém compreensível, mas não ajuda quando um programador inexperiente se atrapalha com isso.

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 compactada (conforme definido acima). Da mesma forma, b é um ponteiro para uma estrutura compactada. O tipo da expressão a.i é (basicamente) um int l-value com alinhamento de 1 byte. c e d são ambos ints normais. Ao ler a.i, o compilador gera código para acesso desalinhado. Quando você lê b->i, o tipo b ainda sabe que está lotado, então também não é problema. e é um ponteiro para um int alinhado com um byte, portanto o compilador sabe como fazer a referência correta também. Mas quando você faz a atribuição f = &a.i, você está armazenando o valor de um ponteiro int desalinhado em uma variável de ponteiro int alinhada - é aí que você deu errado. E eu concordo, o gcc deve ter este aviso ativado por default (nem mesmo em -Wall ou -Wextra).

46
Daniel Santos

Usar este atributo é definitivamente inseguro.

Uma coisa particular que quebra é a habilidade de um union que contém dois ou mais structs para escrever um membro e ler outro se os structs tiverem uma seqüência inicial comum de membros. Seção 6.5.2.3 do padrão C11 estados:

6 Uma garantia especial é feita para simplificar o uso de uniões: se uma união contiver várias estruturas que compartilham uma seqüência inicial comum (veja abaixo), e se o objeto união atualmente contém uma dessas estruturas, É permitido inspecionar a parte inicial comum de qualquer um deles em qualquer lugar que uma declaração do tipo completo da união seja visível. Duas estruturas compartilham uma seqüência inicial comum se os 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 O seguinte é 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 isso. O exemplo a seguir foi executado no Ubuntu 16.04 x64 usando o gcc 5.4.0 com 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;
}

Saída:

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

Mesmo que struct s1 e struct s2 tenham uma "seqüência inicial comum", o empacotamento aplicado ao primeiro significa que os membros correspondentes não vivem no mesmo deslocamento de byte. O resultado é que o valor gravado para o membro x.b não é o mesmo que o valor lido do membro y.b, mesmo que o padrão diga que eles devem ser o mesmo.

2
dbush

(O seguinte é um exemplo muito artificial preparado para ilustrar.) Um uso principal de estruturas compactadas é onde você tem um fluxo de dados (digamos 256 bytes) para o qual deseja fornecer significado. Se eu pegar um exemplo menor, suponha que eu tenha um programa rodando no meu Arduino que envie via serial um pacote de 16 bytes que tem 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 de ponteiros.

Agora, com o alinhamento acontecendo, pegar um void * pointer na memória para os dados recebidos e lançá-los para um myStruct * não funcionará a menos que o compilador trate a estrutura como empacotada (isto é, armazena dados na ordem especificada) e usa exatamente 16 bytes para este exemplo). Existem penalidades de desempenho para leituras desalinhadas, portanto, usar estruturas compactadas para dados com os quais seu programa está trabalhando ativamente não é necessariamente uma boa ideia. Mas quando o 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 acesso e coisas que fazem aritmética de ponteiro nos bastidores. Em suma, as estruturas compactadas são para lidar eficientemente com dados compactados, e os dados compactados podem ser o que seu programa recebe para trabalhar. Na maioria das vezes, o código deve ler os valores fora da estrutura, trabalhar com eles e escrevê-los quando terminar. Tudo o mais deve ser feito fora da estrutura compactada. Parte do problema é o material de baixo nível que C tenta esconder do programador, e o salto de aro que é necessário se tais coisas realmente importam para o programador. (Você quase precisa de uma construção de 'layout de dados' diferente na linguagem para poder dizer 'essa coisa tem 48 bytes, foo refere-se aos dados com 13 bytes e deve ser interpretada assim'; e uma construção de dados estruturada separada, onde você diz "eu quero uma estrutura contendo dois inteiros, chamada alice e bob, e um float chamado carol, e eu não me importo como você o implementa" - em C esses dois casos de uso são encaixados na construção da estrutura.

0
John Allsup