UBSan: загрузка смещенного адреса

** Редактировать: вопрос не в определении доступа к невыровненным данным, а в том, почему memcpy отключает убсанитизаторы, тогда как приведение типов этого не делает, несмотря на генерацию того же кода сборки **

У меня есть пример кода для разбора протокола, который отправляет байтовый массив, разделенный на группы по шесть байтов.

void f(u8 *ba) {
    // I know this array's length is a multiple of 6
    u8 *p = ba;
    u32 a = *(u32 *)p;
    printf("a = %d\n", a);
    p += 4;
    u16 b = *(u16 *)p;
    printf("b = %d\n", b);

    p += 2;
    a = *(u32 *)p;
    printf("a = %d\n", a);
    p += 4;
    b = *(u16 *)p;
    printf("b = %d\n", b);
}

После увеличения моего указателя на 6 и выполнения еще одного 32-разрядного чтения UBSan сообщает об ошибке о выровненной загрузке. Я подавляю эту ошибку, используя memcpy вместо того, чтобы печатать, но я не понимаю, почему. Чтобы было ясно, здесь та же самая процедура без ошибок UBSan,

void f(u8 *ba) {
    // I know this array's length is a multiple of 6 (
    u8 *p = ba;
    u32 a;
    memcpy(&a, p, 4);
    printf("a = %d\n", a);
    p += 4;
    memcpy(&b, p, 2);
    printf("b = %d\n", b);

    p += 2;
    memcpy(&a, p, 4);
    printf("a = %d\n", a);
    p += 4;
    memcpy(&b, p, 2);
    printf("b = %d\n", b);
}

Обе процедуры компилируются в идентичный код сборки (используя movl для 32-разрядного чтения и movzwl для 16-битного чтения), так почему же одно неопределенное поведение, а другое нет? Есть ли memcpy есть какие-то особые свойства, которые что-то гарантируют?

Я не хочу использовать memcpy здесь, потому что я не могу полагаться на то, что компиляторы достаточно хорошо его оптимизируют.

2 ответа

Решение

UB sanitizer используется, чтобы сигнализировать, что код не является строго соответствующим и фактически зависит от неопределенного поведения, которое не гарантируется.

На самом деле стандарт C говорит, что поведение не определено, как только вы приведете указатель на тип, для которого адрес не выровнен соответствующим образом. C11 (черновик, n1570) 6.3.2.3p7:

Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если полученный указатель неправильно выровнен 68) для ссылочного типа, поведение не определено.

Т.е.

u8 *p = ba;
u32 *a = (u32 *)p; // undefined behaviour if misaligned. No dereference required

Наличие этого приведения позволяет компилятору предполагать, что ba был выровнен по 4-байтовой границе (на платформе, где u32 таким образом, он должен быть выровнен, что многие компиляторы будут делать на x86), после чего он может генерировать код, который предполагает выравнивание.

Даже на платформе x86 есть инструкции, которые не дают впечатляющих результатов. Даже невинно выглядящий код может быть скомпилирован в машинный код, который просто вызовет прерывание во время выполнения. Предполагается, что UBSan поймает это в коде, который в противном случае выглядел бы вменяемым и вел себя "как положено" при запуске, но затем завершался ошибкой, если компилировался с другим набором опций или другим уровнем оптимизации.

Компилятор может генерировать точно такой же код для memcpy - и часто так и будет, но это просто потому, что компилятор будет знать, что невыровненный доступ будет работать и достаточно хорошо работать на целевой платформе.

И, наконец:

Я не хочу использовать memcpy здесь, потому что я не могу полагаться на компиляторы, которые делают достаточно хорошую работу по его оптимизации.

Здесь вы говорите: "Я хочу, чтобы мой код работал надежно только тогда, когда он скомпилирован мусором или компиляторами двадцатилетней давности, которые генерируют медленный код. Определенно нет, если он компилируется с теми, которые могли бы оптимизировать его для быстрой работы".

Оригинальный тип вашего объекта лучше всего будет u32массив u32... В противном случае, вы решаете это разумно, используя memcpy, Это вряд ли будет существенным узким местом в современных системах; Я бы не беспокоился об этом.

На некоторых платформах целое число не может существовать по каждому возможному адресу. Рассмотрим максимальный адрес для вашей системы, мы могли бы просто постулировать на 0xFFFFFFFFFFFFFFFF, Четырехбайтовое целое не могло бы существовать здесь, верно?

Иногда на аппаратном уровне выполняется оптимизация для выравнивания шины (серии проводов, ведущих от ЦП к различным периферийным устройствам, памяти и тому, что нет), и один из них состоит в том, чтобы предположить, что адреса для различных типов встречаются только в кратных их размеры, например. Неправильный доступ на такой платформе может вызвать ловушку (segfault).

Следовательно, UBSan правильно предупреждает вас об этой непереносимой и трудной для отладки проблеме.

Эта проблема не только приводит к тому, что некоторые системы не работают полностью, но вы также обнаружите, что ваша система, которая позволяет получить доступ вне выравнивания, требует второй выборки через шину для извлечения второй части целого числа, так или иначе.


В этом коде есть несколько других проблем.

printf("a = %d\n", a);

Если вы хотите распечатать int, вы должны использовать %d, Тем не менее, ваш аргумент u32Не противоречите своим аргументам вот так; это также неопределенное поведение. Я не знаю наверняка, как u32 определен для вас, но я думаю, что ближайшая стандартно-совместимая функция, вероятно, uint32_t (от <stdint.h>). Вы должны использовать "%"PRIu32 в качестве строки формата в любом месте, где вы хотите напечатать uint32_t, PRIu32 (от <inttypes.h>) символ обеспечивает определенную реализацией последовательность символов, которая будет распознаваться реализациями printf функция.

Обратите внимание, что эта проблема повторяется в другом месте, где вы используете u16 введите вместо:

printf("b = %d\n", b);

"%"PRIu16 там, наверное, хватит.

Другие вопросы по тегам