Я пытаюсь понять проблему, которую мы недавно решили при использовании 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;
}
Определение добавления целого числа к указателю: (N4659 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? Если мы интерпретируем «я + J», чтобы означать результат выражения i + j
, тогда это равно 32
который меньше чем n
, Но если это означает математический результат, то он намного больше, чем n
,
Стандарт использует здесь шрифт для математических уравнений и не использует шрифт для исходного кода. ≤
также не является действительным оператором. Поэтому я думаю, что они намерены использовать это уравнение для описания математического значения, то есть это неопределенное поведение.
Стандарт 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
будет должным образом расширен знак, прежде чем он будет добавлен или вычтен из любых указателей.