Да, __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 . Как я уже сказал, я не думаю, что это практично, но в документации должно быть упомянуто (в настоящее время это не так).
ОБНОВЛЕНИЕ : По состоянию на 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;
| ^~~~~~~~~