如何解决C#中的64位指针算术,检查算术溢出是否改变行为
|| 我有一些不安全的C#代码,它在64位计算机上运行的类型为“ 0”的大型内存块上执行指针算术运算。它在大多数时候都能正常工作,但是当事情变大时,我经常会遇到指针不正确的损坏。 奇怪的是,如果我打开“检查算术上溢/下溢”,则一切正常。我没有任何溢出异常。但是由于性能下降,我需要在不使用此选项的情况下运行代码。 是什么造成这种行为差异?解决方法
这是C#编译器错误(在Connect上提交)。 @Grant表明,由C#编译器生成的MSIL将
uint
操作数解释为带符号。根据C#规范,这是错误的,这是相关部分(18.5.6):
18.5.6指针算法
在不安全的上下文中,可以将+
和-
运算符(第7.8.4节和第7.8.5节)应用于除void*
之外的所有指针类型的值。因此,对于每种指针类型“ 5”,都隐式定义了以下运算符:
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);
给定指针类型为'5'的表达式P
和类型为int
,uint
,long
或ulong
的表达式N
,表达式P + N
和N + P
计算类型T*
的指针值,该值是将N * sizeof(T)
加到P
给出的地址而得到的。同样,表达式“ 19”计算类型“ 5”的指针值,该值是从“ 7”给出的地址中减去“ 17”而得到的。
给定指针类型为“ 5”的两个表达式expression7ѭ和Q
,表达式P – Q
计算P
和Q
给出的地址之间的差,然后将该差除以ѭ29then。结果的类型始终为long
。实际上,P - Q
被计算为((long)(P) - (long)(Q)) / sizeof(T)
。
如果指针算术操作溢出了指针类型的域,则结果将以实现定义的方式被截断,但是不会产生异常。
您可以在指针上加上ѭ1,不会进行任何隐式转换。并且该操作不会溢出指针类型的域。因此不允许截断。
,这里选中和未选中之间的区别实际上是IL中的一个错误,或者仅仅是一些错误的源代码(我不是语言专家,所以我不会评论C#编译器是否为含糊的源代码)。我使用C#编译器的4.0.30319.1版本编译了此测试代码(尽管2.0版本似乎做同样的事情)。我使用的命令行选项为:/ o + /不安全/ 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(在0xFFFFFFFF或-1中为带符号表示法)的符号扩展为0xFFFFFFFFFFFFFFFFFF。然后将2个操作数相加(0x0000000008000000L +(-1L)= 0x0000000007FFFFFFL)。 conv操作码仅出于验证目的而需要,以将本机int转换为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操作码外,它实际上是相同的。对于添加操作码,我们添加了2个“后缀”。第一个是“ .ovf”后缀,其后缀具有明显的含义:检查溢出,但是还需要“启用”第二个后缀:“。un”。 (即,没有\“ add.un \”,只有\“ add.ovf.un \”)。 \“。un \”具有2种效果。最明显的是,累加和溢出检查都像操作数是无符号整数一样进行。从我们的CS类开始,希望大家都记得,由于二进制补码的双重编码,有符号加法和无符号加法是相同的,所以“ .un”实际上只影响溢出检查,对吗?
错误。
请记住,在IL堆栈上,我们没有2个64位数字,我们有一个int32和一个本机int(在归一化之后)。好吧,“。un”意味着从int32到native的转换被视为“ conv.u \”,而不是像上面的默认“ conv.i \”。因此uint.MaxValue为零,扩展为0x00000000FFFFFFFFL。然后加法正确产生0x0000000107FFFFFFL。转换操作码可确保将无符号操作数表示为有符号的int64(可以)。
您的修复程序仅适用于64位。在IL级别,一个更正确的解决方案是将uint32操作数显式转换为native int或unsigned native int,然后32位和64位的check和unchecked都具有相同的表现。
, 请仔细检查您的不安全代码。在分配的内存块之外读取或写入内存会导致“损坏”。
, 我已经解决了这个问题,所以正在回答自己的问题,但是仍然有兴趣阅读有关为什么行为随着ѭ36和ѭ37发生变化的评论。
此代码演示了问题以及解决方法(在添加之前始终将偏移量转换为long
):
public static unsafe void Main(string[] args)
{
// Dummy pointer,never dereferenced
byte* testPtr = (byte*)0x00000008000000L;
uint offset = uint.MaxValue;
unchecked
{
Console.WriteLine(\"{0:x}\",(long)(testPtr + offset));
}
checked
{
Console.WriteLine(\"{0:x}\",(long)(testPtr + offset));
}
unchecked
{
Console.WriteLine(\"{0:x}\",(long)(testPtr + (long)offset));
}
checked
{
Console.WriteLine(\"{0:x}\",(long)(testPtr + (long)offset));
}
}
这将返回(在64位计算机上运行时):
7ffffff
107ffffff
107ffffff
107ffffff
(顺便说一句,在我的项目中,我首先将所有代码编写为托管代码,而没有所有这种不安全的指针算术麻烦,但发现它使用了过多的内存。这只是一个爱好项目;如果被炸毁,唯一会受到伤害的是我。)
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。