C#中的64位指针算术,检查算术溢出更改行为

C#中的64位指针算术,检查算术溢出更改行为,c#,math,64-bit,C#,Math,64 Bit,我有一些不安全的C#代码,它在64位机器上运行,在类型为byte*的大内存块上执行指针运算。它在大多数情况下都能正常工作,但当事情变得越来越大时,我经常会在指针不正确的地方出现某种损坏 奇怪的是,如果我打开“检查算术溢出/下溢”,一切正常。我没有任何溢出异常。但是由于对性能的巨大影响,我需要在不使用此选项的情况下运行代码 是什么导致了这种行为差异?请仔细检查您的不安全代码。在分配的内存块之外读取或写入内存会导致“损坏”。我在回答我自己的问题,因为我已经解决了这个问题,但我仍然有兴趣阅读有关为什么

我有一些不安全的C#代码,它在64位机器上运行,在类型为
byte*
的大内存块上执行指针运算。它在大多数情况下都能正常工作,但当事情变得越来越大时,我经常会在指针不正确的地方出现某种损坏

奇怪的是,如果我打开“检查算术溢出/下溢”,一切正常。我没有任何溢出异常。但是由于对性能的巨大影响,我需要在不使用此选项的情况下运行代码


是什么导致了这种行为差异?

请仔细检查您的不安全代码。在分配的内存块之外读取或写入内存会导致“损坏”。

我在回答我自己的问题,因为我已经解决了这个问题,但我仍然有兴趣阅读有关为什么选中
会改变行为的注释

此代码演示了问题以及解决方案(添加前始终将偏移量转换为
long
):

这将返回(在64位计算机上运行时):


(顺便说一句,在我的项目中,我首先以托管代码的形式编写了所有代码,没有这些不安全的指针算术污点,但发现它占用了太多内存。这只是一个爱好项目;如果它崩溃了,唯一受到伤害的是我。)

这里选中和不选中的区别实际上是IL中的一个bug,或者只是一些糟糕的源代码(我不是语言专家,所以我不会评论C#编译器是否为含糊不清的源代码生成了正确的IL)。我使用4.0.30319.1版本的C#编译器编译了这段测试代码(尽管2.0版本的验证似乎做了同样的事情)。我使用的命令行选项是:/o+/unsafe/debug:pdbonly

对于未检查的块,我们有以下IL代码:

//000008:     unchecked
//000009:     {
//000010:         Console.WriteLine("{0:x}", (long)(testPtr + offset));
  IL_000a:  ldstr      "{0:x}"
  IL_000f:  ldloc.0
  IL_0010:  ldloc.1
  IL_0011:  add
  IL_0012:  conv.u8
  IL_0013:  box        [mscorlib]System.Int64
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
在IL偏移量11处,加法得到2个操作数,一个为byte*类型,另一个为uint32类型。根据CLI规范,它们实际上分别规范化为本机int和int32。根据CLI规范(准确地说是分区III),结果将是native int。因此,必须将secodn操作数升级为native int类型。根据规范,这是通过符号扩展实现的。因此uint.MaxValue(在有符号表示法中为0xFFFFFF或-1)被符号扩展为0xFFFFFFFFFF。然后将两个操作数相加(0x0000000008000000L+(-1L)=0x0000000007FFFFFFFFL)。conv操作码仅用于验证目的,以便将本机int转换为int64,在生成的代码中,int64是nop

现在对于选中的块,我们有以下IL:

//000012:     checked
//000013:     {
//000014:         Console.WriteLine("{0:x}", (long)(testPtr + offset));
  IL_001d:  ldstr      "{0:x}"
  IL_0022:  ldloc.0
  IL_0023:  ldloc.1
  IL_0024:  add.ovf.un
  IL_0025:  conv.ovf.i8.un
  IL_0026:  box        [mscorlib]System.Int64
  IL_002b:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
除了add和conv操作码之外,它实际上是相同的。对于add操作码,我们添加了2个后缀。第一个是“.ovf”后缀,它有一个明显的含义:检查溢出,但也需要“启用第二个后缀:“.un”。(即没有“add.un”,只有“add.ovf.un”)。“.un”有两种效果。最明显的一点是,加法和溢出检查就像操作数是无符号整数一样进行。在我们的CS类中,希望我们都记得,由于2的补码二进制编码,有符号加法和无符号加法是相同的,所以“.un”实际上只影响溢出检查,对吗

请记住,在IL堆栈上,我们没有2个64位的数字,我们有一个int32和一个本机int(在标准化之后)。“.un”表示从int32到本机的转换被视为“conv.u”,而不是像上面那样的默认“conv.i”。因此uint.MaxValue被零扩展为0x00000000FFFFFFFFL。然后,add正确地生成0x0000000107FFFFFFL。conv操作码确保无符号操作数可以表示为有符号int64(它可以)

您的修复程序只适用于64位。在IL级别,更正确的修复方法是显式地将uint32操作数转换为本机int或无符号本机int,然后检查和取消检查对于32位和64位都是相同的。

这是一个C编译器错误()。C#编译器生成的MSIL将
uint
操作数解释为有符号。根据C#规范,这是错误的,以下是相关章节(18.5.6):

18.5.6指针算法

在不安全的环境中,
+
-
运算符(§7.8.4和§7.8.5)可应用于除
void*
之外的所有指针类型的值。因此,对于每个指针类型
T*
,隐式定义了以下运算符:

T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
T* operator +(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);
T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);
long operator –(T* x, T* y);
给定指针类型
T*
的表达式
p
和类型
int
的表达式
N
uint
long
,或
ulong
,表达式
p+N
N+p
计算类型
T*
的指针值,该值由添加
N*sizeof(T)得到
P
给出的地址。同样,表达式
P-N
计算
T*
类型的指针值,该值是从
P
给出的地址减去
N*sizeof(T)
得到的

给定指针类型
T*
的两个表达式
p
Q
,表达式
p–Q
计算
p
Q
给出的地址之间的差值,然后将该差值除以
sizeof(T)
。结果的类型总是
long
。实际上,
P-Q
计算为
((长)(P)-(长)(Q))/sizeof(T)

如果指针算术运算溢出指针类型的域,结果将以实现定义的方式截断,但不会产生异常


您可以向指针添加
uint
,但不会发生隐式转换。而且手术并不成功
//000012:     checked
//000013:     {
//000014:         Console.WriteLine("{0:x}", (long)(testPtr + offset));
  IL_001d:  ldstr      "{0:x}"
  IL_0022:  ldloc.0
  IL_0023:  ldloc.1
  IL_0024:  add.ovf.un
  IL_0025:  conv.ovf.i8.un
  IL_0026:  box        [mscorlib]System.Int64
  IL_002b:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
T* operator +(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);
T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);
long operator –(T* x, T* y);