Небезопасен ли пакет gcc для __attribute __ ((упакованный)) / #pragma?

164

В C компилятор будет размещать элементы структуры в порядке, в котором они объявлены, с возможными байтами заполнения, вставляемыми между элементами или после последнего, чтобы гарантировать правильное выравнивание каждого элемента.

gcc предоставляет расширение языка __attribute__((packed)), которое говорит компилятору не вставлять отступы, позволяя неправильно выравнивать члены структуры. Например, если система обычно требует, чтобы все intобъекты имели 4-байтовое выравнивание, это __attribute__((packed))может привести intк тому, что члены структуры будут размещаться с нечетным смещением.

Цитирование документации gcc:

Атрибут «упакованный» указывает, что поле переменной или структуры должно иметь наименьшее возможное выравнивание - один байт для переменной и один бит для поля, если только вы не укажете большее значение с атрибутом «выровненный».

Очевидно, что использование этого расширения может привести к меньшим требованиям к данным, но к более медленному коду, поскольку компилятор должен (на некоторых платформах) генерировать код для доступа к смещенному элементу байт за раз.

Но есть ли случаи, когда это небезопасно? Всегда ли компилятор генерирует правильный (хотя и более медленный) код для доступа к выровненным элементам упакованных структур? Возможно ли это сделать во всех случаях?

Кит Томпсон
источник
1
Отчет об ошибке gcc теперь помечен как FIXED с добавлением предупреждения о назначении указателя (и возможностью отключить предупреждение). Подробности в моем ответе .
Кит Томпсон

Ответы:

148

Да, __attribute__((packed))потенциально небезопасно в некоторых системах. Симптом, вероятно, не появится на x86, что только делает проблему более коварной; тестирование на системах x86 не выявит проблемы. (На x86 неправильно выровненный доступ обрабатывается аппаратно; если вы разыменуете int*указатель, указывающий на нечетный адрес, он будет немного медленнее, чем если бы он был правильно выровнен, но вы получите правильный результат.)

В некоторых других системах, таких как SPARC, попытка получить доступ к выровненному intобъекту вызывает ошибку шины, приводящую к сбою программы.

Также были системы, в которых неправильно выровненный доступ тихо игнорирует младшие биты адреса, заставляя его обращаться к неправильному фрагменту памяти.

Рассмотрим следующую программу:

#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;
}

На x86 Ubuntu с gcc 4.5.2 выдает следующий вывод:

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

На SPARC Solaris 9 с gcc 4.5.1 выдает следующее:

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

В обоих случаях программа компилируется без дополнительных опций, просто 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опции, включенной по умолчанию.

Если адрес упакованного члена структуры или объединения взят, это может привести к значению указателя без выравнивания. Этот патч добавляет -Waddress-of-pack-member для проверки выравнивания при назначении указателя и предупреждения о не выровненном адресе, а также о невыровненном указателе

Я только что построил эту версию GCC из исходного кода. Для вышеупомянутой программы это производит эти диагностики:

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;
      |               ^~~~~~~~~
Кит Томпсон
источник
1
потенциально смещен, и будет генерировать ... что?
Almo
5
неправильно выровненные элементы структуры в ARM делают странные вещи: некоторые обращения приводят к сбоям, другие приводят к тому, что извлекаемые данные перестраиваются нелогично или включают смежные неожиданные данные.
Wallyk
8
Кажется, что сама упаковка безопасна, но использование упакованных элементов может быть небезопасным. Старые процессоры на базе ARM также не поддерживали доступ к памяти без выравнивания, более новые версии поддерживают, но я знаю, что Symbian OS по-прежнему запрещает доступ без выравнивания при работе на этих более новых версиях (поддержка отключена).
Джеймс
14
Другой способ исправить это в gcc - использовать систему типов: требовать, чтобы указатели на элементы упакованных структур могли быть назначены только указателям, которые сами помечены как упакованные (то есть потенциально не выровненные). Но на самом деле: упакованные структуры, просто скажи нет.
Кафе
9
@Flavius: Моя главная цель состояла в том, чтобы получить информацию там. См. Также meta.stackexchange.com/questions/17463/…
Кит Томпсон
62

Как сказано выше, не используйте указатель на член структуры, который упакован. Это просто игра с огнем. Когда вы говорите __attribute__((__packed__))или #pragma pack(1), на самом деле вы говорите: «Привет, GCC, я действительно знаю, что делаю». Когда оказывается, что вы этого не делаете, вы не можете справедливо обвинять компилятор.

Возможно, мы можем обвинить компилятор в его самоуспокоенности. Хотя у gcc есть -Wcast-alignопция, она не включена по умолчанию, ни с помощью -Wallили -Wextra. По-видимому, это связано с тем, что разработчики gcc считают этот тип кода «мертвой мозговой» мерзостью, недостойной обращения - понятное презрение, но это не помогает, когда в него врезается неопытный программист.

Учтите следующее:

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;

Здесь тип 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).

Даниэль Сантос
источник
6
+1 за объяснение того, как использовать указатели с не выровненными структурами!
Soumya
@ Сумий Спасибо за очки! :) Имейте в виду, однако, что __attribute__((aligned(1)))это расширение GCC и не является переносимым. Насколько мне известно, единственный действительно переносимый способ сделать невыровненный доступ в C (с любой комбинацией компилятор / аппаратное обеспечение) - это побайтная копия памяти (memcpy или аналогичная). Некоторое оборудование даже не имеет инструкций для невыровненного доступа. Мой опыт связан с arm и x86, которые могут делать и то, и другое, хотя невыровненный доступ медленнее. Поэтому, если вам когда-нибудь понадобится сделать это с высокой производительностью, вам нужно будет понюхать аппаратное обеспечение и использовать специфичные для арки приемы.
Даниэль Сантос
4
@Soumya К сожалению, __attribute__((aligned(x)))теперь, кажется, игнорируется, когда используется для указателей. :( У меня пока нет полной информации об этом, но, __builtin_assume_aligned(ptr, align)похоже, с помощью gcc можно сгенерировать правильный код. Когда я получу более краткий ответ (и, надеюсь, отчет об ошибке), я обновлю свой ответ.
Daniel Santos
@DanielSantos: Качественный компилятор, который я использую (Keil), распознает «упакованные» квалификаторы для указателей; если структура объявлена ​​«упакованной», взятие адреса uint32_tчлена приведет к uint32_t packed*; попытка чтения с такого указателя, например, на Cortex-M0, вызовет IIRC вызов подпрограммы, которая займет ~ 7x столько же, сколько и обычное чтение, если указатель не выровнен, или ~ 3x, если он выровнен, но в любом случае будет вести себя предсказуемо [встроенный код будет занимать 5 раз дольше, независимо от того, выровнен он или нет].
суперкат
49

Это совершенно безопасно, если вы всегда получаете доступ к значениям через структуру через .(точку) или ->запись.

Что не безопасно принимать указатель выровненным данных , а затем к нему доступ , не принимая это во внимание.

Кроме того, даже если известно, что каждый элемент в структуре не выровнен, он определенным образом не выровнен, поэтому структура в целом должна быть выровнена так, как ожидает компилятор, иначе возникнут проблемы (на некоторых платформах или в будущем, если будет изобретен новый способ оптимизации непривязанного доступа).

военно-картографическая служба
источник
Хм, интересно, что произойдет, если вы поместите одну упакованную структуру в другую упакованную структуру, где выравнивание будет другим? Интересный вопрос, но это не должно изменить ответ.
Ams
GCC также не всегда выравнивает саму структуру. Например: struct foo {int x; символ с; } __attribute __ ((упаковано)); struct bar {char c; struct foo f; }; Я обнаружил, что bar :: f :: x не обязательно будет выровнен, по крайней мере, для некоторых разновидностей MIPS.
Антон
3
@antonm: Да, структура внутри упакованной структуры вполне может быть не выровненной, но, опять же, компилятор знает, каково выравнивание каждого поля, и это совершенно безопасно, если вы не пытаетесь использовать указатели на структуру. Вы должны представить структуру внутри структуры как один плоский набор полей с дополнительным именем только для удобства чтения.
Амс
6

Использование этого атрибута определенно небезопасно.

Одна особенность, которую он нарушает, - это способность объекта, unionсодержащего две или более структур, написать один элемент и прочитать другой, если структуры имеют общую начальную последовательность элементов. Раздел 6.5.2.3 стандарта C11 гласит:

6 Одна специальная гарантия сделана для того, чтобы упростить использование объединений: если объединение содержит несколько структур, которые имеют общую начальную последовательность (см. Ниже), и если объект объединения в настоящее время содержит одну из этих структур, разрешается проверять общая начальная часть любого из них везде, где видна декларация завершенного типа объединения. Две структуры совместно используют общую начальную последовательность, если соответствующие элементы имеют совместимые типы (и, для битовых полей, одинаковой ширины) для последовательности из одного или нескольких начальных элементов.

...

9 Пример 3 Ниже приведен фрагмент действителен:

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)
/*
...
*/

Когда __attribute__((packed))это введено, это нарушает это. Следующий пример был запущен на Ubuntu 16.04 x64 с использованием gcc 5.4.0 с отключенной оптимизацией:

#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;
}

Вывод:

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

Несмотря на то, struct s1и struct s2имеют «общую исходную последовательность», упаковка применяется к бывшим означает , что соответствующие члены не живут в одном байте. В результате значение, записанное в member x.b, не совпадает со значением, считанным из member y.b, хотя стандарт говорит, что они должны быть одинаковыми.

dbush
источник
Кто-то может возразить, что если вы упакуете одну из структур, а не другую, вы не будете ожидать, что они будут иметь согласованные макеты. Но да, это еще одно стандартное требование, которое оно может нарушать.
Кит Томпсон
1

(Ниже приведен очень искусственный пример, подготовленный для иллюстрации.) Одно из основных применений упакованных структур - это когда у вас есть поток данных (скажем, 256 байтов), которому вы хотите придать смысл. Если я возьму небольшой пример, предположим, что на моем Arduino запущена программа, которая через последовательный порт отправляет пакет из 16 байтов, который имеет следующее значение:

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

Тогда я могу объявить что-то вроде

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

и затем я могу ссылаться на байты targetAddr через aStruct.targetAddr, а не возиться с арифметикой указателя.

Теперь, когда происходит выравнивание, перенос указателя void * в память на полученные данные и приведение его к myStruct * не будут работать, если компилятор не обработает структуру как упакованную (то есть он хранит данные в указанном порядке и использует ровно 16 байт для этого примера). Для невыровненных операций чтения существуют потери производительности, поэтому использование упакованных структур для данных, с которыми активно работает ваша программа, не всегда является хорошей идеей. Но когда ваша программа снабжена списком байтов, упакованные структуры облегчают написание программ, которые получают доступ к содержимому.

В противном случае вы в конечном итоге используете C ++ и пишете класс с методами доступа и тому подобным, который выполняет арифметику указателей за кулисами. Короче говоря, упакованные структуры предназначены для эффективной работы с упакованными данными, а упакованные данные могут быть тем, с чем ваша программа должна работать. По большей части ваш код должен считывать значения из структуры, работать с ними и записывать их обратно, когда закончите. Все остальное должно быть сделано за пределами упакованной структуры. Частично проблема заключается в низкоуровневых вещах, которые C пытается скрыть от программиста, и обручах, которые необходимы, если такие вещи действительно имеют значение для программиста. (Вам почти нужна другая конструкция «макета данных» в языке, чтобы вы могли сказать, что «эта вещь имеет длину 48 байтов, foo относится к данным размером 13 байтов и должна интерпретироваться таким образом»; и отдельная конструкция структурированных данных,

Джон Аллсуп
источник
Если я что-то упустил, это не ответит на вопрос. Вы утверждаете, что структурная упаковка удобна (а она есть), но вы не решаете вопрос о ее безопасности. Кроме того, вы утверждаете, что ухудшение производительности за невыровненные чтения; это верно для x86, но не для всех систем, как я продемонстрировал в своем ответе.
Кит Томпсон