Добавление указателя и целочисленное переполнение с помощью Clang 5.0 и UBsan?

Я пытаюсь понять проблему, которую мы недавно устранили при использовании Clang 5.0 и Undefined Behavior Sanitizer (UBsan). У нас есть код, который обрабатывает буфер в прямом или обратном направлении. Сокращенный регистр похож на код, показанный ниже .

0-len может выглядеть немного необычно, но он необходим для ранних компиляторов Microsoft .Net. Clang 5.0 и UBsan выявили целочисленное переполнение:

adv-simd.h:1138:26: runtime error: addition of unsigned offset to 0x000003f78cf0 overflowed to 0x000003f78ce0
adv-simd.h:1140:26: runtime error: addition of unsigned offset to 0x000003f78ce0 overflowed to 0x000003f78cd0
adv-simd.h:1142:26: runtime error: addition of unsigned offset to 0x000003f78cd0 overflowed to 0x000003f78cc0
...

Строки 1138, 1140, 1142 (и другие) являются приращением, которое может отставать назад из-за 0-len.

ptr += inc;

Согласно сравнению указателей в C. Они подписаны или не подписаны? (в котором также обсуждается C++), указатели не являются ни подписанными, ни беззнаковыми. . Наши смещения были беззнаковыми, и мы полагались на перенос целочисленных значений без знака для достижения обратного шага.

Код был в порядке с GCC UBsan и Clang 4 и более ранними версиями UBsan. В конце концов мы разрешили его для Clang 5.0 с помощью разработчиков LLVM< /а>. Вместо size_t нам нужно было использовать ptrdiff_t.

Мой вопрос: где было целочисленное переполнение/неопределенное поведение в конструкции? Как ptr + <unsigned> привело к переполнению целого числа со знаком и к неопределенному поведению?


Вот MSVC, который отражает реальный код.

#include <cstddef>
#include <cstdint>
using namespace std;

uint8_t buffer[64];

int main(int argc, char* argv[])
{
    uint8_t * ptr = buffer;
    size_t len = sizeof(buffer);
    size_t inc = 16;

    // This sets up processing the buffer in reverse.
    //   A flag controls it in the real code.
    if (argc%2 == 1)
    {
        ptr += len - inc;
        inc = 0-inc;
    }

    while (len > 16)
    {
        // process blocks
        ptr += inc;
        len -= 16;
    }

    return 0;
}

person jww    schedule 18.12.2017    source источник
comment
Сами указатели не являются ни подписанными, ни неподписанными. Но добавление указателя является знаковым, поскольку указатель может увеличиваться или уменьшаться, когда указатель указывает куда-то внутри массива. Больше ничего нельзя сказать на основе информации в этом вопросе, поскольку ее недостаточно для определения того, существует ли неопределенное поведение, если только предоставляется минимальный воспроизводимый пример.   -  person Sam Varshavchik    schedule 18.12.2017
comment
Если у вас есть указатель на 3-й элемент массива, вы можете добавить к нему -1 и получить указатель на 2-й элемент в массиве. Добавление указателя подписано.   -  person Sam Varshavchik    schedule 18.12.2017
comment
Помнится, несколько месяцев назад (а может, и в прошлом году) живой баг в чем-то важном для той же проблемы, что и в этом MCVE   -  person M.M    schedule 18.12.2017


Ответы (2)


Определение добавления целого числа к указателю (N4659 expr.add/4):

expr.add/4

Здесь я использовал изображение, чтобы сохранить форматирование (об этом будет сказано ниже).

Обратите внимание, что это новая формулировка, которая заменяет менее четкое описание из предыдущих стандартов.

В вашем коде (когда argc нечетное) мы получаем код, эквивалентный:

uint8_t buffer[64];
uint8_t *ptr = buffer + 48;
ptr = ptr + (SIZE_MAX - 15);

Для переменных в стандартной цитате, примененной к вашему коду, i соответствует 48, j соответствует (SIZE_MAX - 15), а n соответствует 64.

Теперь вопрос состоит в том, верно ли, что 0 ≤ i + j ≤ n. Если мы интерпретируем "i + j" как означающее результат выражения i + j, тогда это равно 32, что меньше n. Но если иметь в виду математический результат, то он намного больше, чем n.

Стандарт использует здесь шрифт для математических уравнений и не использует шрифт для исходного кода. также не является допустимым оператором. Поэтому я думаю, что они намереваются использовать это уравнение для описания математического значения, то есть это неопределенное поведение.

person M.M    schedule 18.12.2017
comment
Для обоснования: представьте, что вы работаете в системе с 16-битными size_t, но 32-битными указателями. Компилятор не сможет заменить ptr += 65540 на ptr -= 16, поскольку первый может быть допустимым добавлением в объект максимального размера. - person M.M; 18.12.2017
comment
В системах, где size_t и ptrdiff_t имеют разные размеры, это верно. Сегментированный режим 8086 был интересен, потому что объекты могли быть больше 32 КБ, но вычитание указателя обычно давало 16-битное значение со знаком. Если бы два указателя идентифицировали части объекта на расстоянии 60000 байт друг от друга, разница была бы -5536, но добавление -5536 к одному указателю даст другой. - person supercat; 19.12.2017
comment
@М.М. - Извините, что поднимаю старый вопрос. Я не уверен, что это эквивалентно оригинальной программе: ptr = ptr + (SIZE_MAX - 15);. inc = 0-inc; был собственным оператором и использовал перенос целых чисел. Обтекание четко определено, и выражение завершено до того, как оно было использовано в ptr = ptr + .... ptr + <wrapped expression> все еще находится в исходном массиве. - person jww; 12.07.2018

Стандарт C определяет тип ptrdiff_t как тип, полученный оператором разницы указателей. Система может иметь 32-битную size_t и 64-битную ptrdiff_t; такие определения были бы естественным подходом для системы, которая использует 64-битные линейные или квазилинейные указатели, но требует, чтобы размер отдельных объектов был меньше 4 ГБ каждый.

Если известно, что размер объектов меньше 2 ГБ каждый, сохранение значений типа ptrdiff_t вместо size_t может сделать программу излишне неэффективной. Однако в таком сценарии код не должен использовать size_t для хранения различий указателей, которые могут быть отрицательными, а вместо этого использовать int32_t [который будет достаточно большим, если размер объектов меньше 2 ГБ каждый]. Даже если ptrdiff_t является 64-битным, значение типа int32_t будет правильно расширено по знаку, прежде чем оно будет добавлено или вычтено из любых указателей.

person supercat    schedule 18.12.2017