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

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

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

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

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

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

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

4 ответа

Решение

Да, __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. Как я уже сказал, я не думаю, что это практично, но в документации должно быть упомянуто (в настоящее время это не так).

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

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

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

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

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

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

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

Другие вопросы по тегам