64-битное вычитание указателя, недопустимое целое число со знаком и возможная ошибка компилятора - PullRequest
6 голосов
/ 09 марта 2011

Я недавно вырвал свои волосы, отлаживая этот кусок кода (слегка модифицированный для простоты изложения):

char *packedData;
unsigned char* indexBegin, *indexEnd;
int block, row;

// +------ bad! 
// v
  int cRow = std::upper_bound( indexBegin, indexEnd, row&255 ) - indexBegin - 1;

char value = *(packedData + (block + cRow) * bytesPerRow);

Конечно, присвоение разности двух указателей (результат std::upper_bound минус начало искомого массива) для int, а не ptrdiff_t, является неправильным в 64-битной среде, но это особенно плохое поведение это было очень неожиданно. Я ожидал, что это не получится, если размер массива в [indexBegin, indexEnd) будет больше 2 ГБ, так что разница превысит int; но на самом деле произошел сбой, когда indexBegin и indexEnd имели значения на противоположных сторонах 2 ^ 31 (т.е. indexBegin = 0x7fffffe0, indexEnd = 0x80000010). Дальнейшие исследования выявили следующий код сборки x86-64 (сгенерированный MSVC ++ 2005 с оптимизацией):

; (inlined code of std::upper_bound, which leaves indexBegin in rbx,
; the result of upper_bound in r9, block at *(r12+0x28), and data at
; *(r12+0x40), immediately precedes this point)
movsxd    rcx, r9d                   ; movsxd?!
movsxd    rax, ebx                   ; movsxd?!
sub       rcx, rax
lea       rdx, [rcx+rdi-1]
movsxd    rax, dword ptr [r12+28h]
imul      rdx, rax
mov       rax, qword ptr [r12+40h]
mov       rcx, byte ptr[rdx+rax]

Этот код обрабатывает из вычитаемых указателей как 32-разрядные значения со знаком, расширяя их в знак в 64-разрядные регистры перед их вычитанием и умножая результат на другое 32-разрядное значение со знаком, расширенным знаком, и затем индексирует другой массив с 64-битным результатом этого вычисления. Как ни старайся, я не могу понять, под какой теорией это когда-либо будет правильным. Если бы указатели были вычтены как 64-битные значения, или была другая инструкция, сразу после imul, это edx с расширенным знаком в rdx (или последний финальный mov ссылался на rax + edx, но я не думаю, что это доступно в x86-64), все будет хорошо (номинально опасно, но я знаю, что [indexBegin, indexEnd) никогда не достигнет 2 ГБ в длину).

Вопрос несколько академический, поскольку моя настоящая ошибка легко исправляется с помощью простого использования 64-битного типа для хранения различий указателей, но это ошибка компилятора или какая-то неясная часть спецификации языка, которая позволяет компилятор предположить, что операнды вычитания будут индивидуально вписываться в тип результата?

РЕДАКТИРОВАТЬ : единственная ситуация, о которой я могу подумать, это сделать то, что компилятор сделал хорошо, если допустить, что целочисленные переполнения никогда не произойдут (так что если я вычту два числа и назначу в результате signed int компилятор может свободно использовать больший целочисленный тип со знаком, что в данном случае оказывается неверным). Это разрешено спецификацией языка?

Ответы [ 2 ]

1 голос
/ 21 сентября 2017

Немного поздно, но, поскольку вопрос не был получен после последнего РЕДАКТИРОВАТЬ .

Да, переполнение является неопределенным поведением.И да, UB может иметь неинтуитивный эффект.В частности, может показаться, что UB влияет на уже выполненный код.

Практическим следствием действительно является то, что компилятору разрешено работать при условии отсутствия переполнения.Классическим примером является if (x+1<x), ошибочный тест на переполнение, которое компиляторы могут заменить и действительно заменяют на if (false).

И да, вы можете получить довольно запутанное поведение "переполнения", когда ваша 32-битная переменная фактическихранится в 64-битном регистре, поэтому есть место для переполнения.Этот регистр может содержать значение 1<<32, которое показывает, как вы не можете разумно рассуждать о результатах программы на C ++ с неопределенным поведением: у вас фактически есть int со значением MAX_INT+1 (!)

1 голос
/ 10 марта 2011

Преобразование C ++ из указателей в не-логические типы происходит следующим образом:

  1. Преобразование в целое число без знака равного размера с указателем
  2. Преобразование из целого числа без знака в тип назначения(целое в вашем случае)

Теперь компилятор видит целочисленное вычитание.Он может выполнять это любым способом, который сочтет нужным, при условии, что он сохраняет знак.Итак, Visual-C ++ решил выполнить это с использованием 64-битных регистров.

Вы можете проверить этот рабочий порядок, приведя правую часть к unsigned int и перед присвоением вашему lvalue.Это приведет к плохому поведению, которое вы ожидали.

...