Является gcc __attribute __ ((упакован))/#pragma pack небезопасно?

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

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

Указание документации gcc:

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

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

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

Ответ 1

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

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

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

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

#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 -o f-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;
      |               ^~~~~~~~~

Ответ 2

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

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

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

Ответ 3

Как сказано выше, не принимайте указатель на член структурированной упаковки. Это просто игра с огнем. Когда вы говорите __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-value с выравниванием по 1 байт. c и d являются нормальными int s. При чтении a.i компилятор генерирует код для неравномерного доступа. Когда вы читаете b->i, тип b все еще знает, что он упакован, поэтому проблем нет. e является указателем на однобайтовый выровненный int, поэтому компилятор знает, как правильно разыгрывать это. Но когда вы выполняете присвоение f = &a.i, вы сохраняете значение неизмененного указателя int в выровненной переменной указателя int - то, где вы поступили неправильно. И я согласен, gcc должен включить это предупреждение по умолчанию (даже в -Wall или -Wextra).

Ответ 4

(Ниже приводится очень искусственный пример, приготовленный для иллюстрации.) Одним из основных применений упакованных структур является то, где у вас есть поток данных (скажем, 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 байт). Существуют штрафы за неуплаченные прочтения, поэтому использование упакованных структур для данных, с которыми ваша программа активно работает, не обязательно является хорошей идеей. Но когда ваша программа снабжена списком байтов, упакованные структуры упрощают запись программ, которые обращаются к содержимому.

В противном случае вы закончите использование С++ и напишите класс с помощью методов доступа и материалов, которые выполняют арифметику указателей за кулисами. Короче говоря, упакованные структуры предназначены для эффективной обработки упакованных данных, и упакованные данные могут быть тем, с чем вам поручена ваша программа. По большей части код должен считывать значения из структуры, работать с ними и записывать их по завершении. Все остальное должно быть сделано за пределами упакованной структуры. Часть проблемы - это материал низкого уровня, который С пытается скрывать от программиста, и прыжки с обручем, которые необходимы, если такие вещи действительно имеют значение для программиста. (Вам почти нужна другая конструкция "компоновки данных" на этом языке, так что вы можете сказать, что "эта вещь имеет длину 48 байт, foo относится к данным 13 байтов в и должна интерпретироваться таким образом", а также отдельная структурированная структура данных, где вы говорите: "Мне нужна структура, содержащая два ints, называемые alice и bob, и float, называемый carol, и мне все равно, как вы его реализуете" - в C оба эти случая использования shoehorned в struct struct.)

Ответ 5

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

Одна особенность, которую он нарушает, - это способность 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)) введен, это ломает это. Например:

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