64位指针减法,有符号整数下溢和可能的编译器错误?

Jon*_*mer 6 64-bit visual-c++ compiler-bug

我最近撕掉了我的头发调试这段代码(稍为修改以简化演示):

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);
Run Code Online (Sandbox Code Playgroud)

当然,std::upper_bound在64位环境中将两个指针的差异(减去搜索到的数组的开头的结果)分配给int而不是ptrdiff_t是错误的,但是产生的特殊不良行为是非常意外的.当[indexBegin,indexEnd]的数组大小超过2GB时,我希望这会失败,所以差异溢出了一个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]
Run Code Online (Sandbox Code Playgroud)

此代码将删除的指针视为带符号的32位值,将它们符号扩展为64位寄存器,然后再减去它们并将结果乘以另一个符号扩展的32位值,然后使用64-将另一个数组编入索引该计算的结果.尽我所能,我无法弄清楚这是什么理论可能是正确的.如果指针被减去64位值,或者在imul之后有另一条指令,那么将edx符号扩展到rdx(或者最后的mov引用了rax + edx,但我不认为它可用于x86-64),一切都会好的(名义上很危险,但我碰巧知道[indexBegin,indexEnd]的长度永远不会接近2GB).

这个问题在某种程度上是学术性的,因为我的实际错误很容易通过使用64位类型来保持指针差异来修复,但这是一个编译器错误,还是有一些模糊的语言规范部分允许编译器假设减法的操作数将分别适合结果类型?

编辑:我能想到的唯一情况会使编译器做得好,如果允许假设整数下溢永远不会发生(这样如果我减去两个数字并将结果赋给a signed int,编译器就会可以自由地使用更大的有符号整数类型,在这种情况下证明是错误的).这是语言规范允许的吗?

MSa*_*ers 1

有点晚了,但看到最后一次编辑后问题没有得到回答。

是的,溢出是未定义的行为。是的,UB 可能会产生意想不到的效果。特别是,UB 可能会影响已经执行的代码。

实际结果确实是允许编译器在没有溢出的假设下工作。典型的例子是if (x+1<x),编译器可以并且确实用错误的溢出测试来替换if (false)

是的,当您的 32 位变量实际上存储在 64 位寄存器中时,您可能会得到相当混乱的“溢出”行为,因此存在可用于溢出的空间。该寄存器可以保存值1<<32,这表明您无法明智地推理具有未定义行为的 C++ 程序的结果:您实际上拥有一个int带有值MAX_INT+1(!)