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 int
objetos tenham alinhamento de 4 bytes, __attribute__((packed))
poderá fazer com que os int
membros 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?
fonte
Respostas:
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 umint*
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
int
objeto 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:
No x86 Ubuntu com gcc 4.5.2, ele produz a seguinte saída:
No SPARC Solaris 9 com gcc 4.5.1, ele produz o seguinte:
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
x
membro esteja alinhado corretamente. Com uma matriz de doisstruct foo
objetos, pelo menos um ou outro terá umx
membro desalinhado .)(Nesse caso,
p0
aponta para um endereço desalinhado, porque aponta para umint
membro compactado após umchar
membro.p1
Está alinhado corretamente, pois aponta para o mesmo membro no segundo elemento da matriz, portanto, existem doischar
objetos antes dele - e no SPARC Solaris, o arrayarr
parece estar alocado em um endereço uniforme, mas não múltiplo de 4.)Ao se referir ao membro
x
de umstruct foo
nome, o compilador sabe quex
está potencialmente desalinhado e gerará código adicional para acessá-lo corretamente.Depois que o endereço
arr[0].x
ouarr[1].x
foi armazenado em um objeto ponteiro, nem o compilador nem o programa em execução sabem que apontam para umint
objeto 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-member
opção, ativada por padrão.Acabei de criar essa versão do gcc a partir do código-fonte. Para o programa acima, ele produz estes diagnósticos:
fonte
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-align
opção, ele não está ativado por padrão nem com-Wall
ou-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:
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ãoa.i
é (basicamente) um valor int l com alinhamento de 1 byte.c
ed
são ambosint
s normais . Ao lera.i
, o compilador gera código para acesso não alinhado. Quando você lêb->i
,b
o 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çãof = &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-Wall
ou-Wextra
).fonte
__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.__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.uint32_t
membro produzirá auint32_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].É 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).
fonte
O uso desse atributo é definitivamente inseguro.
Uma coisa em particular que quebra é a capacidade de uma
union
que 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: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:Resultado:
Embora
struct s1
estruct s2
ter 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 membrox.b
não é o mesmo que o valor lido do membroy.b
, mesmo que o padrão diga que eles devem ser os mesmos.fonte
(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:
Então eu posso declarar algo como
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,
fonte