C неопределенное поведение. Строгое правило сглаживания или неправильное выравнивание?

Я не могу объяснить поведение выполнения этой программы:

#include  #include  #include  typedef char u8; typedef unsigned short u16; size_t f(u8 *keyc, size_t len) { u16 *key2 = (u16 *) (keyc + 1); size_t hash = len; len = len / 2; for (size_t i = 0; i < len; ++i) hash += key2[i]; return hash; } int main() { srand(time(NULL)); size_t len; scanf("%lu", &len); u8 x[len]; for (size_t i = 0; i < len; i++) x[i] = rand(); printf("out %lu\n", f(x, len)); } 

Итак, когда он скомпилирован с -O3 с gcc и запускается с аргументом 25, он вызывает segfault. Без оптимизации он отлично работает. Я разобрал его: он векторизован, и компилятор предполагает, что массив key2 выровнен по 16 байтам, поэтому он использует movdqa . Очевидно, это UB, хотя я не могу это объяснить. Я знаю о правиле строжайшего aliasing, и это не тот случай (я надеюсь), потому что, насколько я знаю, строгое правило псевдонимов не работает с char s. Почему gcc предполагает, что этот указатель выровнен? Clang отлично работает, даже с оптимизацией.

РЕДАКТИРОВАТЬ

Я изменил unsigned char на char и удалил const , он все равно segfaults.

EDIT2

Я знаю, что этот код не очень хорош, но он должен работать нормально, насколько я знаю о правиле строжайшего aliasing. Где именно нарушение?

    Код действительно нарушает правило строгого сглаживания. Тем не менее, существует не только нарушение псевдонимов, и авария не происходит из-за нарушения псевдонимов . Это происходит потому, что unsigned short указатель неправильно выровнен ; даже само преобразование указателя не определено, если результат не согласован соответствующим образом.

    C11 (проект n1570) Приложение J.2 :

    1 Поведение не определено в следующих случаях:

    ….

    • Преобразование между двумя типами указателей приводит к неправильному выравниванию результата (6.3.2.3).

    С 6.3.2.3p7, говорящим

    […] Если результирующий указатель неправильно выровнен [68] для ссылочного типа, поведение не определено. […]

    unsigned short имеет требование выравнивания 2 для вашей реализации (x86-32 и x86-64), с которым вы можете протестировать

     _Static_assert(_Alignof(unsigned short) == 2, "alignof(unsigned short) == 2"); 

    Однако вы вынуждаете u16 *key2 указывать на неглавный адрес:

     u16 *key2 = (u16 *) (keyc + 1); // we've already got undefined behaviour *here*! 

    Есть бесчисленные программисты, которые настаивают на том, что неравномерный доступ гарантированно работает на практике на x86-32 и x86-64 во всем мире, и на практике не было бы проблем – ну, все они неправы.

    В основном, что происходит, компилятор замечает, что

     for (size_t i = 0; i < len; ++i) hash += key2[i]; 

    могут быть выполнены более эффективно с использованием команд SIMD, если они соответствующим образом выровнены. Значения загружаются в регистры SSE с использованием MOVDQA , что требует согласования аргумента с 16 байтами :

    Когда операнд источника или получателя является операндом памяти, операнд должен быть выровнен по 16-байтовой границе или будет генерироваться исключение общей защиты (#GP).

    В случаях, когда указатель не был правильно выровнен при запуске, компилятор будет генерировать код, который будет суммировать первые 1-7 неподписанных шорт один за другим, пока указатель не будет выровнен с 16 байтами.

    Конечно, если вы начинаете с указателя, указывающего на нечетный адрес, даже не добавляя 7 раз 2, он будет привязан к адресу, который выровнен до 16 байтов. Конечно, компилятор даже не будет генерировать код, который будет определять этот случай, поскольку «поведение не определено, если преобразование между двумя типами указателей приводит к некорректному результату результата» - и полностью игнорирует ситуацию с непредсказуемыми результатами , что означает, что операнд в MOVDQA не будет правильно выровнен, что приведет к сбою программы.


    Легко доказать, что это может произойти даже без нарушения каких-либо правил строгого сглаживания. Рассмотрим следующую программу, состоящую из двух единиц перевода (если оба f и его вызывающий помещены в одну единицу перевода, мой GCC достаточно умен, чтобы заметить, что мы используем здесь упакованную структуру и не генерируем код с MOVDQA ) :

    единица перевода 1 :

     #include  #include  size_t f(uint16_t *keyc, size_t len) { size_t hash = len; len = len / 2; for (size_t i = 0; i < len; ++i) hash += keyc[i]; return hash; } 

    единица перевода 2

     #include  #include  #include  #include  #include  size_t f(uint16_t *keyc, size_t len); struct mystruct { uint8_t padding; uint16_t contents[100]; } __attribute__ ((packed)); int main(void) { struct mystruct s; size_t len; srand(time(NULL)); scanf("%zu", &len); char *initializer = (char *)s.contents; for (size_t i = 0; i < len; i++) initializer[i] = rand(); printf("out %zu\n", f(s.contents, len)); } 

    Теперь скомпилируйте и соедините их вместе:

     % gcc -O3 unit1.c unit2.c % ./a.out 25 zsh: segmentation fault (core dumped) ./a.out 

    Обратите внимание, что там нет нарушения псевдонимов. Единственная проблема заключается в uint16_t *keyc .

    С -fsanitize=undefined возникает следующая ошибка:

     unit1.c:10:21: runtime error: load of misaligned address 0x7ffefc2d54f1 for type 'uint16_t', which requires 2 byte alignment 0x7ffefc2d54f1: note: pointer points here 00 00 00 01 4e 02 c4 e9 dd b9 00 83 d9 1f 35 0e 46 0f 59 85 9b a4 d7 26 95 94 06 15 bb ca b3 c7 ^ 

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

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

    Но преобразование произвольного указателя на символ в указатель на объект и разыменование полученного указателя нарушает правило строгого псевдонима и вызывает неопределенное поведение.

    Таким образом, в вашем коде следующая строка: UB:

     const u16 *key2 = (const u16 *) (keyc + 1); // keyc + 1 did not originally pointed to a u16: UB 
    Давайте будем гением компьютера.