В C компилятор будет размещать элементы структуры в порядке, в котором они объявлены, с возможными байтами заполнения, вставляемыми между элементами или после последнего, чтобы гарантировать правильное выравнивание каждого элемента.
gcc предоставляет расширение языка __attribute__((packed))
, которое говорит компилятору не вставлять отступы, позволяя неправильно выравнивать члены структуры. Например, если система обычно требует, чтобы все int
объекты имели 4-байтовое выравнивание, это __attribute__((packed))
может привести int
к тому, что члены структуры будут размещаться с нечетным смещением.
Цитирование документации gcc:
Атрибут «упакованный» указывает, что поле переменной или структуры должно иметь наименьшее возможное выравнивание - один байт для переменной и один бит для поля, если только вы не укажете большее значение с атрибутом «выровненный».
Очевидно, что использование этого расширения может привести к меньшим требованиям к данным, но к более медленному коду, поскольку компилятор должен (на некоторых платформах) генерировать код для доступа к смещенному элементу байт за раз.
Но есть ли случаи, когда это небезопасно? Всегда ли компилятор генерирует правильный (хотя и более медленный) код для доступа к выровненным элементам упакованных структур? Возможно ли это сделать во всех случаях?
источник
Ответы:
Да,
__attribute__((packed))
потенциально небезопасно в некоторых системах. Симптом, вероятно, не появится на x86, что только делает проблему более коварной; тестирование на системах x86 не выявит проблемы. (На x86 неправильно выровненный доступ обрабатывается аппаратно; если вы разыменуетеint*
указатель, указывающий на нечетный адрес, он будет немного медленнее, чем если бы он был правильно выровнен, но вы получите правильный результат.)В некоторых других системах, таких как SPARC, попытка получить доступ к выровненному
int
объекту вызывает ошибку шины, приводящую к сбою программы.Также были системы, в которых неправильно выровненный доступ тихо игнорирует младшие биты адреса, заставляя его обращаться к неправильному фрагменту памяти.
Рассмотрим следующую программу:
На x86 Ubuntu с gcc 4.5.2 выдает следующий вывод:
На SPARC Solaris 9 с gcc 4.5.1 выдает следующее:
В обоих случаях программа компилируется без дополнительных опций, просто
gcc packed.c -o packed
.(Программа, которая использует единственную структуру, а не массив, надежно не демонстрирует проблему, так как компилятор может разместить структуру по нечетному адресу, чтобы
x
член был правильно выровнен. С массивом из двухstruct foo
объектов, по крайней мере, одного или другого будет иметь смещенныйx
член.)(В этом случае
p0
указывает на неверно выровненный адрес, потому что он указывает на упакованныйint
элемент, следующий заchar
элементом.p1
Бывает правильно выровненным, поскольку он указывает на тот же элемент во втором элементе массива, поэтомуchar
перед ним стоят два объекта - а в SPARC Solaris массивarr
, по-видимому, размещен по адресу, который является четным, но не кратным 4.)При обращении к члену
x
изstruct foo
по имени, компилятор знает , чтоx
потенциально криво, и будет генерировать дополнительный код для доступа к нему правильно.Как только адрес
arr[0].x
илиarr[1].x
был сохранен в объекте указателя, ни компилятор, ни работающая программа не знают, что он указывает на не выровненныйint
объект. Он просто предполагает, что он правильно выровнен, что приводит (в некоторых системах) к ошибке шины или аналогичному другому отказу.Я полагаю, что исправить это в gcc было бы нецелесообразно. Общее решение потребовало бы для каждой попытки разыменования указателя на любой тип с нетривиальными требованиями выравнивания либо (а) доказать во время компиляции, что указатель не указывает на неправильно выровненный элемент упакованной структуры, либо (б) генерирование более объемного и медленного кода, который может обрабатывать либо выровненные, либо выровненные объекты.
Я отправил отчет об ошибке gcc . Как я уже сказал, я не думаю, что это практично, но в документации должно быть упомянуто (в настоящее время это не так).
ОБНОВЛЕНИЕ : По состоянию на 2018-12-20 эта ошибка помечена как ИСПРАВЛЕННАЯ. Патч появится в gcc 9 с добавлением новой
-Waddress-of-packed-member
опции, включенной по умолчанию.Я только что построил эту версию GCC из исходного кода. Для вышеупомянутой программы это производит эти диагностики:
источник
Как сказано выше, не используйте указатель на член структуры, который упакован. Это просто игра с огнем. Когда вы говорите
__attribute__((__packed__))
или#pragma pack(1)
, на самом деле вы говорите: «Привет, GCC, я действительно знаю, что делаю». Когда оказывается, что вы этого не делаете, вы не можете справедливо обвинять компилятор.Возможно, мы можем обвинить компилятор в его самоуспокоенности. Хотя у gcc есть
-Wcast-align
опция, она не включена по умолчанию, ни с помощью-Wall
или-Wextra
. По-видимому, это связано с тем, что разработчики gcc считают этот тип кода «мертвой мозговой» мерзостью, недостойной обращения - понятное презрение, но это не помогает, когда в него врезается неопытный программист.Учтите следующее:
Здесь тип
a
представляет собой упакованную структуру (как определено выше). Точно так жеb
указатель на упакованную структуру. Тип выраженияa.i
(в основном) представляет собой int l-значение с выравниванием в 1 байт.c
иd
оба нормальныеint
с. При чтенииa.i
компилятор генерирует код для выравниваемого доступа. Когда вы читаетеb->i
,b
тип все еще знает, что он упакован, так что никаких проблем с ними нет.e
является указателем на выровненный по байту int, поэтому компилятор знает, как правильно разыменовать это. Но когда вы делаете присваиваниеf = &a.i
, вы сохраняете значение невыровненного указателя int в выровненной переменной указателя int - вот где вы ошиблись. И я согласен, gcc должен включить это предупреждениепо умолчанию (даже не в-Wall
или-Wextra
).источник
__attribute__((aligned(1)))
это расширение GCC и не является переносимым. Насколько мне известно, единственный действительно переносимый способ сделать невыровненный доступ в C (с любой комбинацией компилятор / аппаратное обеспечение) - это побайтная копия памяти (memcpy или аналогичная). Некоторое оборудование даже не имеет инструкций для невыровненного доступа. Мой опыт связан с arm и x86, которые могут делать и то, и другое, хотя невыровненный доступ медленнее. Поэтому, если вам когда-нибудь понадобится сделать это с высокой производительностью, вам нужно будет понюхать аппаратное обеспечение и использовать специфичные для арки приемы.__attribute__((aligned(x)))
теперь, кажется, игнорируется, когда используется для указателей. :( У меня пока нет полной информации об этом, но,__builtin_assume_aligned(ptr, align)
похоже, с помощью gcc можно сгенерировать правильный код. Когда я получу более краткий ответ (и, надеюсь, отчет об ошибке), я обновлю свой ответ.uint32_t
члена приведет кuint32_t packed*
; попытка чтения с такого указателя, например, на Cortex-M0, вызовет IIRC вызов подпрограммы, которая займет ~ 7x столько же, сколько и обычное чтение, если указатель не выровнен, или ~ 3x, если он выровнен, но в любом случае будет вести себя предсказуемо [встроенный код будет занимать 5 раз дольше, независимо от того, выровнен он или нет].Это совершенно безопасно, если вы всегда получаете доступ к значениям через структуру через
.
(точку) или->
запись.Что не безопасно принимать указатель выровненным данных , а затем к нему доступ , не принимая это во внимание.
Кроме того, даже если известно, что каждый элемент в структуре не выровнен, он определенным образом не выровнен, поэтому структура в целом должна быть выровнена так, как ожидает компилятор, иначе возникнут проблемы (на некоторых платформах или в будущем, если будет изобретен новый способ оптимизации непривязанного доступа).
источник
Использование этого атрибута определенно небезопасно.
Одна особенность, которую он нарушает, - это способность объекта,
union
содержащего две или более структур, написать один элемент и прочитать другой, если структуры имеют общую начальную последовательность элементов. Раздел 6.5.2.3 стандарта C11 гласит:Когда
__attribute__((packed))
это введено, это нарушает это. Следующий пример был запущен на Ubuntu 16.04 x64 с использованием gcc 5.4.0 с отключенной оптимизацией:Вывод:
Несмотря на то,
struct s1
иstruct s2
имеют «общую исходную последовательность», упаковка применяется к бывшим означает , что соответствующие члены не живут в одном байте. В результате значение, записанное в memberx.b
, не совпадает со значением, считанным из membery.b
, хотя стандарт говорит, что они должны быть одинаковыми.источник
(Ниже приведен очень искусственный пример, подготовленный для иллюстрации.) Одно из основных применений упакованных структур - это когда у вас есть поток данных (скажем, 256 байтов), которому вы хотите придать смысл. Если я возьму небольшой пример, предположим, что на моем Arduino запущена программа, которая через последовательный порт отправляет пакет из 16 байтов, который имеет следующее значение:
Тогда я могу объявить что-то вроде
и затем я могу ссылаться на байты targetAddr через aStruct.targetAddr, а не возиться с арифметикой указателя.
Теперь, когда происходит выравнивание, перенос указателя void * в память на полученные данные и приведение его к myStruct * не будут работать, если компилятор не обработает структуру как упакованную (то есть он хранит данные в указанном порядке и использует ровно 16 байт для этого примера). Для невыровненных операций чтения существуют потери производительности, поэтому использование упакованных структур для данных, с которыми активно работает ваша программа, не всегда является хорошей идеей. Но когда ваша программа снабжена списком байтов, упакованные структуры облегчают написание программ, которые получают доступ к содержимому.
В противном случае вы в конечном итоге используете C ++ и пишете класс с методами доступа и тому подобным, который выполняет арифметику указателей за кулисами. Короче говоря, упакованные структуры предназначены для эффективной работы с упакованными данными, а упакованные данные могут быть тем, с чем ваша программа должна работать. По большей части ваш код должен считывать значения из структуры, работать с ними и записывать их обратно, когда закончите. Все остальное должно быть сделано за пределами упакованной структуры. Частично проблема заключается в низкоуровневых вещах, которые C пытается скрыть от программиста, и обручах, которые необходимы, если такие вещи действительно имеют значение для программиста. (Вам почти нужна другая конструкция «макета данных» в языке, чтобы вы могли сказать, что «эта вещь имеет длину 48 байтов, foo относится к данным размером 13 байтов и должна интерпретироваться таким образом»; и отдельная конструкция структурированных данных,
источник