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

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

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

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

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

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

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

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

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

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

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

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

    Я отправил отчет об ошибке 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 = ai; int d = b->i; int *e __attribute__((aligned(1))) = &a.i; int *f = &a.i; 

    Здесь тип a является упакованной структурой (как определено выше). Аналогично, b является указателем на упакованную структуру. Тип выражения ai (в основном) – int l-значение с выравниванием по 1 байт. c и d являются нормальными int s. При чтении ai компилятор генерирует код для неравномерного доступа. Когда вы читаете b->i , тип b все еще знает, что он упакован, поэтому проблем нет. e является указателем на однобайтовый выровненный int, поэтому компилятор знает, как правильно разыгрывать это. Но когда вы выполняете присвоение f = &a.i , вы сохраняете значение неглавного указателя int в выровненной переменной указателя int – вот где вы поступили не так. И я согласен, gcc должен включить это предупреждение по умолчанию (даже не в -Wall или -Wextra ).

    (Ниже приведено очень искусственный пример, подготовленный для иллюстрации.) Одним из основных применений упакованных структур является то, где у вас есть stream данных (скажем, 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 ++ и пишете class с методами доступа и т. Д., Который выполняет арифметику указателей за кулисами. Короче говоря, упакованные структуры предназначены для эффективной работы с упакованными данными, и упакованные данные могут быть тем, с чем вам поручена ваша программа. По большей части код должен считывать значения из структуры, работать с ними и записывать их по завершении. Все остальное должно быть сделано за пределами упакованной структуры. Часть проблемы – это материал низкого уровня, который С пытается скрывать от программиста, и прыжки с обручем, которые необходимы, если такие вещи действительно имеют значение для программиста. (Вам почти нужна другая конструкция «компоновки данных» на этом языке, так что вы можете сказать, что «эта вещь имеет длину 48 байт, foo относится к данным 13 байтов в и должна интерпретироваться таким образом», а также отдельная структурированная структура данных, где вы говорите: «Мне нужна структура, содержащая два ints, называемые alice и bob, и float, называемый carol, и мне все равно, как вы его реализуете» – в C оба эти случая использования shoehorned в struct struct.)

    Давайте будем гением компьютера.