我最近在调试这段代码时费尽心思(为了简单起见,稍作修改):
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) 的数组大小超过 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]
此代码将被减去的指针视为带符号的 32 位值,在减去它们并将结果乘以另一个带符号扩展的 32 位值之前,将它们符号扩展为 64 位寄存器,然后用 64- 索引另一个数组该计算的位结果。尽我所能,我无法弄清楚在什么理论下这可能是正确的。如果将指针作为 64 位值减去,或者在 imul 之后是否有另一条指令,将 edx 符号扩展为 rdx(或者最终的 mov 引用了 rax+edx,但我认为这在x86-64),一切都会好起来的(名义上很危险,但我碰巧知道 [indexBegin, indexEnd) 的长度甚至永远不会接近 2GB)。
这个问题有点学术性,因为我的实际错误很容易通过仅使用 64 位类型来保存指针差异来修复,但这是编译器错误,还是语言规范中有一些模糊的部分允许编译器假设减法的操作数将分别适合结果类型吗?
编辑:我能想到的唯一情况会使编译器做得很好,如果允许假设整数下溢永远不会发生(这样如果我减去两个数字并将结果分配给 a signed int
,编译器将是可以自由地实际使用更大的有符号整数类型,在这种情况下被证明是错误的)。语言规范允许这样做吗?