为什么这个byte*和uint的加法没有进位到更高的双字中?

9

现在已在Microsoft Connect上提交,如果您认为需要修复,请投票支持。我也大幅简化了测试用例:

byte* data = (byte*) 0x76543210;
uint offset = 0x80000000;
byte* wrong = data + offset;
byte* correct = data + (uint) 0x80000000;

// "wrong" is now 0xFFFFFFFFF6543210 (!)
// "correct" is 0xF6543210

从IL代码来看,据我所知,C#编译器已经做得没问题了,而错误在于JIT编译器。


原始问题: 这里到底发生了什么?

byte* data = (byte*)Marshal.AllocHGlobal(0x100);

uint uioffset = 0xFFFF0000;
byte* uiptr1 = data + uioffset;
byte* uiptr2 = data + (uint)0xFFFF0000;

ulong uloffset = 0xFFFF0000;
byte* ulptr1 = data + uloffset;
byte* ulptr2 = data + (ulong)0xFFFF0000;

Action<string, ulong> dumpValue =
    (name, value) => Console.WriteLine("{0,8}: {1:x16}", name, value);

dumpValue("data",     (ulong)data);
dumpValue("uiptr1",   (ulong)uiptr1);
dumpValue("uiptr2",   (ulong)uiptr2);
dumpValue("ulptr1",   (ulong)ulptr1);
dumpValue("ulptr2",   (ulong)ulptr2);

这个测试需要以x64平台为目标的64位操作系统。
输出:
  data: 000000001c00a720    (原始指针)
uiptr1: 000000001bffa720    (带有高字无法进位的指针)
uiptr2: 000000011bffa720    (带有正确高字进位的指针)
ulptr1: 000000011bffa720    (带有正确高字进位的指针)
ulptr2: 000000011bffa720    (带有正确高字进位的指针)
               ^
               看这里
那么这是一个bug还是我搞错了什么?

你是为 x86 还是 x64 还是 AnyCPU 进行编译? - user541686
我的猜测是,由于在第一种情况下您正在向uint变量添加指针,编译器(出于某种原因)选择进行32位算术运算,导致生成一个32位值,然后将其提升回指针(截断)。在第二种情况下,32位立即值被提升为64位值,并且编译器执行64位算术运算,保留高位字。 - Jeff Mercado
@Mehrdad 编译为 x64,否则 (ulong) ptr2 不可能溢出到第33位。 - Roman Starkov
看一下规范的18.5.6节末尾:如果指针算术操作超出了指针类型的域,结果将以实现定义的方式被截断,但不会产生任何异常。 我不确定他们所说的指针类型是什么,指针就是指针。也许这里与此有关? - Jeff Mercado
@JeffMercado 可能……要确定,我们需要知道他们所说的“overflow”的含义。我不确定这是否算作溢出-毕竟,ulong + uint并不会导致溢出,除非ulong发生溢出。 - Roman Starkov
显示剩余2条评论
2个回答

4

请在此处提交一个更简单的重现案例。很好奇,它看起来与您链接的内容非常相关,但据我所知,MSIL是“正确的”,因此必须被错误地JITted。 - Roman Starkov
@romkyns,你有没有看过Grant在另一个问题上的阐述?MSIL是错误的,因为它会导致符号扩展,因为它没有使用add.un指令。另外,请告诉我们你是否使用了/checked编译选项。 - Ben Voigt
这段代码在没有使用/checked的情况下编译,且代码中也没有明确的指令来更改这一点。你说得对,实际上这是同一个错误。另外,为了方便未来读者阅读这些评论:add.un是一条虚构的指令;缺失了(不)检查和(不)有符号的组合。 - Roman Starkov

3

(正在施工中的答案)

我检查了生成的 x64 汇编代码,以下是我的观察:

基指针:

data:
00000000024539E0

正确携带指针:

data + (uint)0xFFFF0000:
00000001024439E0

指令的反汇编:

    byte* ptr2 = data + ((uint)0xFFFF0000); // redundant cast to be extra sure
00000084  mov         ecx,0FFFF0000h 
00000089  mov         rax,qword ptr [rsp+20h] 
0000008e  add         rax,rcx 
00000091  mov         qword ptr [rsp+38h],rax 

指针带有错误的进位:

data + offset:
00000000024439E0

指令的反汇编:

    uint offset = 0xFFFF0000;
0000006a  mov         dword ptr [rsp+28h],0FFFF0000h 
    byte* ptr1 = data + offset;
00000072  movsxd      rcx,dword ptr [rsp+28h] ; (1)
00000077  mov         rax,qword ptr [rsp+20h] 
0000007c  add         rax,rcx 
0000007f  mov         qword ptr [rsp+30h],rax 

指令(1)将无符号int32转换为带符号的长整型,并进行符号扩展(是bug还是特性?)。因此,rcx 包含 0xFFFFFFFFFFFF0000,而应该包含 0x00000000FFFF0000 才能使加法正常工作。
根据64位算术:
0xFFFFFFFFFFFF0000 +
0x00000000024539E0 =
0x00000000024439E0

添加溢出了。

我不知道这是一个错误还是预期的行为,我将在尝试得出任何结论之前检查SSCLI。编辑:请参见Ben Voigt的答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接