Чи небезпечний __attribute __attribute __ ((упакований)) gcc / #pragma?


164

У C компілятор викладе члени структури в тому порядку, в якому вони оголошені, з можливими байтами вставки, вставленими між членами або після останнього члена, щоб забезпечити правильне вирівнювання кожного члена.

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

Цитуючи документацію gcc:

Атрибут `упакований 'вказує, що змінна або структура структури повинна мати найменше можливе вирівнювання - один байт для змінної та один біт для поля, якщо ви не вкажете більше значення за атрибутом` вирівнювання'.

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

Але чи є випадки, коли це небезпечно? Чи компілятор завжди генерує правильний (хоч і повільніший) код для доступу до нерівних членів упакованих структур? Чи можливо це зробити так у всіх випадках?


1
Звіт про помилку gcc тепер позначений як ЗАМИСЛЕНО з додаванням попередження про призначення покажчика (та опцією відключення попередження). Деталі у моїй відповіді .
Кіт Томпсон

Відповіді:


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 було б, я вважаю, недоцільно. Загальне рішення вимагає для кожної спроби скинути вказівник на будь-який тип з нетривіальними вимогами до вирівнювання або (a) доведення під час компіляції, що покажчик не вказує на несогласованного члена упакованої структури, або (b) генеруючи більш об'ємний і повільний код, який може обробляти або вирівняні або нерівні об'єкти.

Я надіслав звіт про помилку 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
потенційно не вирівнюється і генерує ... що?
Альмо

5
несогласовані елементи структури в ARM чинять дивні речі: одні звернення викликають помилки, інші призводять до перестановки отриманих даних контр-інтуїтивно або включають суміжні несподівані дані.
wallyk

8
Здається, що упаковка сама по собі є безпечною, але спосіб використання упакованих елементів може бути небезпечним. Старіші процесори на основі ARM також не підтримували нерівномірний доступ до пам'яті, новіші версії, але я знаю, що Symbian OS все ще вимикає нестандартний доступ під час роботи на цих новіших версіях (підтримка відключена).
Джеймс

14
Іншим способом виправити це в gcc було б використовувати систему типів: вимагати, щоб покажчики на члени упакованих структур могли бути призначені лише покажчикам, які самі позначені як упаковані (тобто потенційно нерівні). Але справді: упаковані структури, просто скажіть ні.
caf

9
@Flavius: Основна моя мета полягала в тому, щоб отримати інформацію там. Дивіться також meta.stackexchange.com/questions/17463/…
Кіт Томпсон,

62

Як було сказано вище, не приймайте вказівник на упаковану структуру. Це просто гра з вогнем. Коли ти кажеш, __attribute__((__packed__))або #pragma pack(1)те, що ти насправді кажеш, «Ей, гкк, я дійсно знаю, що роблю». Коли виявиться, що ви цього не зробите, ви не можете правильно звинувачувати компілятор.

Можливо, ми можемо звинуватити компілятора в його поступливості. Незважаючи на те, що 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's type все ще знає, що він упакований, так що жодних проблем їх теж немає. eє вказівником на однобайтний вирівнюваний int, тому компілятор знає, як правильно відмітити це. Але коли ви робите завдання f = &a.i, ви зберігаєте значення нерівного вказівника int у вирівняній змінній вказівника int - саме там ви пішли не так. І я погоджуюся, gcc має це попередження увімкненоза замовчуванням (навіть не в -Wallабо -Wextra).


6
+1 для пояснення, як використовувати вказівники з нерівними структурами!
Soumya

@Soumya Дякую за очки! :) Але майте на увазі, що __attribute__((aligned(1)))це розширення gcc і не є портативним. Наскільки мені відомо, єдиний справді портативний спосіб зробити несанкціонований доступ у C (з будь-якою компіляцією / апаратною комбінацією) - це байтова копія пам'яті (memcpy або подібне). Деяке обладнання навіть не має інструкцій щодо несанкціонованого доступу. Моя експертиза з рукою та x86, що може робити і те, і інше, але нестандартний доступ повільніше. Тож якщо вам коли-небудь доведеться це робити з високою продуктивністю, вам доведеться нюхати обладнання та користуватися специфічними підказками.
Даніель Сантос

4
@Soumya На жаль, __attribute__((aligned(x)))тепер, здається, ігнорується, коли використовується для покажчиків. :( У мене ще немає повної інформації про це, але, __builtin_assume_aligned(ptr, align)мабуть, використовується gcc для створення правильного коду. Коли я отримаю більш коротку відповідь (і, сподіваюся, звіт про помилку), я оновлю свою відповідь.
Даніель Сантос

@DanielSantos: Компілятор якості, який я використовую (Keil), розпізнає "упаковані" кваліфікатори для покажчиків; якщо структура оголошена "упакованою", прийняття адреси uint32_tчлена дасть a uint32_t packed*; намагаючись прочитати з такого вказівника, наприклад, Cortex-M0 IIRC викликатиме підпрограму, яка буде тривати ~ 7x до тих пір, як звичайне зчитування, якщо покажчик не вирівняний або ~ 3x, якщо він вирівняний, але буде вести себе передбачувано в будь-якому випадку [рядковий код буде тривати 5 разів, незважаючи на вирівнювання чи вирівнювання].
supercat


49

Це абсолютно безпечно, якщо ви завжди отримуєте доступ до значень через структуру через .(крапка) або ->позначення.

Що не безпечно - це взяти покажчик нестандартних даних, а потім отримати доступ до них, не враховуючи цього.

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


Хм, мені цікаво, що станеться, якщо ви помістите одну упаковану структуру в іншу упаковану структуру, де вирівнювання було б іншим? Цікаве запитання, але воно не повинно змінювати відповідь.
Ams

GCC також не завжди вирівнює саму структуру. Наприклад: struct foo {int x; char c; } __attribute __ ((упаковано)); Stru bar {char c; struct foo f; }; Я виявив, що смуга :: f :: x не обов'язково буде вирівнюватися, принаймні щодо певних ароматів MIPS.
Антон

3
@antonm: Так, структура в упакованій структурі цілком може бути не узгодженою, але, знову ж таки, компілятор знає, що таке вирівнювання кожного поля, і це абсолютно безпечно, якщо ви не намагаєтесь використовувати вказівники в структуру. Ви повинні уявити собі структуру в структурі як одну плоску серію полів, із додатковою назвою лише для читабельності.
Ams

6

Використання цього атрибута, безумовно, небезпечно.

Одна особлива річ, яку він порушує, - це здатність, unionяка містить дві або більше структур, щоб написати один член і прочитати іншого, якщо структури мають загальну початкову послідовність членів. Розділ 6.5.2.3 стандарту С11 зазначає:

6 Одна спеціальна гарантія робиться з метою спрощення використання об'єднань: якщо об'єднання містить декілька структур, які поділяють загальну початкову послідовність (див. Нижче), і якщо об'єкт об'єднання в даний час містить одну з цих структур, дозволено перевіряти загальна початкова частина будь-якого з них де завгодно, щоб було видно декларацію про завершений тип об'єднання. Tw o структури мають спільну початкову послідовність, якщо відповідні члени мають сумісні типи (і для бітових полів однакові ширини) для послідовності одного або декількох початкових членів.

...

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, навіть якщо стандарт говорить, що вони повинні бути однаковими.


Можна стверджувати, що якщо ви упакуєте одну з конструкцій, а не іншу, то ви не збираєтесь очікувати, що вони матимуть послідовні схеми. Але так, це ще одна стандартна вимога, яку вона може порушити.
Кіт Томпсон

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, а не напередодні арифметики вказівника.

Тепер, коли відбувається вирівнювання матеріалів, взяття недійсного * вказівника в пам'яті на отримані дані та передавання його на myStruct * не працюватиме, якщо компілятор не розглядає структуру як упаковану (тобто зберігає дані у визначеному порядку та використовує точно 16 байти для цього прикладу). Існують штрафні санкції за нерівні читання, тому використання упакованих структур для даних, з якими активно працює програма, не обов'язково є доброю ідеєю. Але коли вашій програмі надається список байтів, упаковані структури спрощують написання програм, які отримують доступ до вмісту.

В іншому випадку ви користуєтеся C ++ і пишете клас з методами аксесуарів та іншим способом, який робить арифметику вказівника за кадром. Коротше кажучи, упаковані структури призначені для ефективної роботи з упакованими даними, і упаковані дані можуть бути вашою програмою. Здебільшого ви код повинні читати значення зі структури, працювати з ними та записувати їх назад, коли буде зроблено. Все інше слід робити поза упакованою структурою. Частиною проблеми є низький рівень матеріалів, який С намагається приховати від програміста, і обруч, який потрібен, якщо такі речі дійсно мають значення для програміста. (Вам майже потрібна інша конструкція "макет даних" на мові, щоб ви могли сказати "ця річ довжиною 48 байт; foo посилається на 13 байтів даних, і їх слід інтерпретувати таким чином"; окрему структуровану структуру даних,


Якщо я чогось не пропускаю, це не відповідає на питання. Ви стверджуєте, що упаковка структури зручна (що це таке), але ви не вирішуєте питання про те, чи безпечно це. Крім того, ви стверджуєте, що покарання за невиконання читання; це правда для x86, але не для всіх систем, як я продемонстрував у своїй відповіді.
Кіт Томпсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.