以下结果对我来说没有任何意义。看起来在执行加法或减法之前,负偏移量被转换为无符号。
double[] x = new double[1000];
int i = 1; // for the overflow it makes no difference if it is long, int or short
int j = -1;
unsafe
{
fixed (double* px = x)
{
double* opx = px+500; // = 0x33E64B8
//unchecked
//{
double* opx1 = opx+i; // = 0x33E64C0
double* opx2 = opx-i; // = 0x33E64B0
double* opx3 = opx+j; // = 0x33E64B0 if unchecked; throws overflow exception if checked
double* opx4 = opx-j; // = 0x33E64C0 if unchecked; throws overflow exception if checked
//}
}
}
尽管使用负偏移量似乎很奇怪,但它有一些用例。在我的例子中,它反映了二维数组中的边界条件。
当然,溢出不会造成太大伤害,因为我可以使用未经检查的值,也可以通过将值的符号反转并应用于操作数的模数来将值的符号移动到操作中。
但这种行为似乎没有记录在案。根据MSDN,我不希望负偏移量有问题:
可以将 int、uint、long 或 ulong 类型的值 n 添加到除 void* 之外的任何类型的指针 p。结果 p+n 是将 n * sizeof(p) 添加到 p 地址后产生的指针。类似地,p-n 是从 p 的地址中减去 n * sizeof(p) 得出的指针。
这个问题在 roslyn\RyuJIT 问题跟踪器中以各种形式多次提出。我第一次发现你可以在这里看到:在检查的上下文中将整数添加到指针时,会生成 add.ovf.un 指令
实际上,如果您查看生成的 IL,您将看到add.ovf.un
("使用溢出检查添加无符号整数")指令是在检查上下文中发出的,而不是在未检查的上下文中发出的。在我们的例子中,这个函数的第一个操作数是无符号的本机int(有点UIntPtr
),表示double*
指针。第二个操作数在该问题发布时(2015 年)和现在都是不同的。
在该问题发布时,第二个操作数是Int32
的,正如您所期望的那样。但是,使用UIntPtr
和Int32
执行add.ovf.un
在 x86 和 x64 中的行为会有所不同。在 x86 中,它抛出溢出异常(对于负数),因为第二个操作数是负数。但是,在 x64 中,JIT 会将该Int32
零扩展到 64 位(因为本机指针现在是 64 位)。它将零扩展它,因为它假定它是无符号的。但是负Int32
的零扩展将导致大的正64 位整数。
结果,如果您在 x64 中向指针添加负Int32
,在出现上述问题时,它不会抛出溢出异常,而是会向指针添加错误的值,这当然会更糟。
问题已以"无法修复"关闭:
感谢您在这里提供详细报告!
鉴于该错误的范围很窄,并且该行为是 与本机编译器一致,我们"不会修复"错误 这一次。
然而,人们对x64所描述的行为不太满意,通过x64,人们可以默默地生成指向未知位置的指针,而不会意识到这一点。经过长时间的辩论,这个问题在2017年作为这个问题的一部分得到了解决。
修复是在检查上下文中将Int32
强制转换为IntPtr
,当您将其添加到指针时。这样做是为了防止在 x64 中自动扩展上述Int32
。
因此,如果您现在查看针对您的案例生成的 IL,您将看到在传递给add.ovf.un
之前,Int32
现在已转换为IntPtr
conv.i
IL 指令。这会导致在检查上下文中向指针添加负整数,以始终在 x86 和 x64 上引发溢出异常。
无论如何,在检查上下文中发出指针添加add.ovf.un
的原始问题没有解决,而且很可能不会解决,因为它被关闭为"不会修复",因此您必须意识到这一点并决定如何在特定场景中克服这个问题。