Ответ 1
Код действительно нарушает правило строгого сглаживания. Тем не менее, существует не только нарушение псевдонимов, а сбой не происходит из-за нарушения псевдонимов. Это происходит потому, что указатель unsigned short
неправильно выровнен; даже сама преобразование имеет поведение undefined, если результат не согласован соответствующим образом.
C11 (черновик n1570) Приложение J.2:
1 Поведение undefined в следующих случаях:
....
- Преобразование между двумя типами указателей вызывает результат, который неправильно выровнен (6.3.2.3).
С 6.3.2.3p7, говоря
[...] Если результирующий указатель неправильно выровнен [68] для ссылочного типа, поведение undefined. [...]
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 байтов. Конечно, компилятор даже не сгенерирует код, который будет обнаруживать этот случай, так как "поведение undefined, если преобразование между двумя типами указателей вызывает результат, который неправильно выровнен" и игнорирует ситуация полностью с непредсказуемыми результатами, что означает, что операнд MOVDQA
не будет правильно выровнен, что приведет к сбою программы.
Нетрудно проверить, что это может произойти, даже не нарушая правил строгого сглаживания. Рассмотрим следующую программу, состоящую из 2 единиц перевода (если оба f
и его вызывающий помещены в одну единицу перевода, мой GCC достаточно умен, чтобы заметить, что мы используем здесь упакованную структуру, а сгенерировать код с помощью MOVDQA
):
единица перевода 1:
#include <stdlib.h>
#include <stdint.h>
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 <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <inttypes.h>
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
^