Что вызывает множественный однобайтовый доступ к упакованному битовому полю в GCC в отличие от доступа к одному двойномуLinux

Ответить
Anonymous
 Что вызывает множественный однобайтовый доступ к упакованному битовому полю в GCC в отличие от доступа к одному двойному

Сообщение Anonymous »

При исследовании некоторых ложных тестов в системе Linux на базе x86-64Bit Yocto выяснилось, что в регистр карты PCIe происходила многобайтовая запись вместо ожидаемой записи одного двойного слова.
Минимальный пример обсуждаемых блоков кода также доступен в примере godbolt.
Один из регистров, TransferNoBytes отображается в структуру как битовое поле следующим образом.

Код: Выделить всё

#pragma pack(push, 1)
// More structs
struct TransferNoBytes {
union {
struct {
uint32_t tnb : 24;
uint32_t res : 8;
};
uint32_t raw;
};
};
// More structs
#pragma pack(pop)
Исследуя строки данных, я увидел, что при одиночной записи в регистр структура, упакованная и выровненная по 1 байту, в дополнение к использованию GCC, приводила к многобайтовой записи на карту, с чем она не обрабатывалась должным образом, вызывая всевозможные основные внутренние проблемы.
Сборка:

Код: Выделить всё

write_tnb(TransferNoBytes volatile*, unsigned int):
movzx   eax, sil
movzx   edx, BYTE PTR [rdi]
mov     BYTE PTR [rdi], al
mov     eax, esi
shr     rsi, 16
movzx   edx, BYTE PTR [rdi+1]
movzx   eax, ah
movzx   esi, sil
mov     BYTE PTR [rdi+1], al
movzx   eax, BYTE PTR [rdi+2]
mov     BYTE PTR [rdi+2], sil
ret
Clang также был развернут с такой же упаковкой и выравниванием, и это не вызвало таких проблем, поскольку выполняется только одна запись двойного слова, как можно видеть в приведенном выше примере Compiler Explorer.
Сборка:

Код: Выделить всё

write_tnb(TransferNoBytes volatile*, unsigned int):
and     esi, 16777215
mov     eax, -16777216
and     eax, dword ptr [rdi]
or      eax, esi
mov     dword ptr [rdi], eax
ret
Экспериментируя влияние атрибутов и прагм компилятора на выходные данные, следующие наблюдения были сделаны и помещены в таблицу. означает доступ к одному DWORD, как в первом блоке кода сборки, а No означает многобайтовый доступ, как во втором блоке кода сборки.



Компилятор

Код: Выделить всё

#pragma pack(1)

Код: Выделить всё

#pragma pack()

Код: Выделить всё

[[gnu::packed]]

Код: Выделить всё

[[gnu::packed, gnu::aligned(1)]]

Код: Выделить всё

#pragma pack(4)

Код: Выделить всё

[[gnu::packed, gnu::aligned(4)]]



GCC
Нет
Да
Нет
Нет
Да
Да


Clang
Да
Да
Да
Да
Да
Да



При копании в документации GCC единственным намеком на эту разницу, который я нашел, был этот gccint запись:

Если loc находится в памяти, его режим должен быть режимом однобайтового целого числа. Если loc находится в регистре, используемый режим определяется операндом шаблона insv или extv (см. Стандартные имена шаблонов для генерации) и обычно представляет собой целочисленный режим с полным словом, который используется по умолчанию, если ничего не указано.

Поэтому я бы сделал вывод, что GCC должен был обнаружить битовое поле в памяти, что имело бы смысл, учитывая затруднительное положение структуры, отображаемой в память.
Однако это по-прежнему не объясняет, почему это происходит только при явной упаковке структуры с помощью [[gnu::packed]] или #pragma Pack(1), но не с помощью #pragma Pack() или явного пакета и выравнивания по 4 байтам [[gnu::packed, gnu::aligned(4)]].
Кроме того, я проверил вывод pahole в приложении, скомпилированном в режиме отладки с -O0 -g, который не показал различий в макете, по крайней мере, из отладочной информации.
В примере используется #pragma package(1).

Код: Выделить всё

pahole --anon_include --nested_anon_include --show_only_data_members test-gcc.o
GCC:

Код: Выделить всё

struct TransferNoBytes {
union {
struct {
uint32_t   tnb:24;               /*     0: 0  4 */
uint32_t   res:8;                /*     0:24  4 */
};                                       /*     0     4 */
uint32_t           raw;                  /*     0     4 */
};                                               /*     0     4 */

/* size: 4, cachelines: 1, members: 1 */
/* last cacheline: 4 bytes */
};
Clang:

Код: Выделить всё

struct TransferNoBytes {
union {
struct {
uint32_t   tnb:24;               /*     0: 0  4 */
uint32_t   res:8;                /*     0:24  4 */
} __attribute__((__packed__)) __attribute__((__aligned__(1))); /*     0     4 */
uint32_t           raw;                  /*     0     4 */
} __attribute__((__aligned__(1)));               /*     0     4 */

/* size: 4, cachelines: 1, members: 1 */
/* forced alignments: 1 */
/* last cacheline: 4 bytes */
} __attribute__((__packed__));
Интересно, что GCC не добавляет никаких дополнительных атрибутов, в то время как Clang это делает, подразумевая, что Clang фактически выполняет дополнительное выравнивание.
Если атрибуты помещаются в GCC вручную, он даже выдает предупреждение: выравнивание 1 для «TransferNoBytes» меньше 4 [-Wpacked-not-aligned], что дополнительно означает, что GCC не является доволен однобайтовым выравниванием, хотя по-прежнему производит ту же сборку.
Так что проверка отладочной информации также не помогла выявить виновника.
Наконец, в долгосрочной перспективе выравнивание следует переключить на 4, поскольку регистры 32-битные, и не существует особого случая, когда упаковка и выравнивание должны быть равны 1, т. е. не меньше и не больше, чем 32-битные регистры или элементы данных.
Мне все же хотелось бы выяснить, если возможно, что может быть причиной этого, помимо различий в реализации компилятора.

Подробнее здесь: https://stackoverflow.com/questions/797 ... -opposed-t
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Linux»