Небезопасен ли пакет gcc __attribute __ ((упакованный)) / #pragma pack? - PullRequest
143 голосов
/ 20 декабря 2011

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

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

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

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

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

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

Ответы [ 5 ]

129 голосов
/ 20 декабря 2011

Да, __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;
}

В Ubuntu x86 с 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;
      |               ^~~~~~~~~
51 голосов
/ 08 апреля 2013

Как уже говорилось в ams, не берите указатель на член структуры, который упакован.Это просто игра с огнем.Когда вы говорите __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).

48 голосов
/ 20 декабря 2011

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

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

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

2 голосов
/ 02 апреля 2019

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

Одна особенность, которую он нарушает, - это способность 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 имеют "общую начальную последовательность", упаковка, примененная к первому, означает, что соответствующие элементы не живут с одинаковым байтовым смещением. В результате значение, записанное в элемент x.b, не совпадает со значением, считанным из элемента y.b, хотя в стандарте указано, что они должны совпадать.

0 голосов
/ 16 августа 2015

(Ниже приведен очень искусственный пример, подготовленный для иллюстрации.) Одно из основных применений упакованных структур - это когда у вас есть поток данных (скажем, 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 байт и должна интерпретироваться таким образом»; и отдельная конструкция структурированных данных,где вы говорите: «Мне нужна структура, содержащая два целых числа, называемые alice и bob, и число с плавающей точкой, называемое carol, и мне все равно, как вы это реализуете» - в C оба эти сценария использования включены в структуру struct.)

...