我有一些不安全的C#代码,它byte*在64位机器上运行的大型内存块上执行指针算法.它在大多数情况下都能正常工作,但是当事情变得很大时,我常常会在指针不正确的情况下出现某种腐败现象.
奇怪的是,如果我打开"检查算术溢出/下溢"一切正常.我没有任何溢出异常.但由于性能大,我需要在没有此选项的情况下运行代码.
什么可能导致这种行为上的差异?
小智 6
这里检查和未检查之间的区别实际上是IL中的一个错误,或者只是一些不良的源代码(我不是语言专家所以我不会评论C#编译器是否为该ambigious生成正确的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)
Run Code Online (Sandbox Code Playgroud)
在IL偏移11处,add获得2个操作数,一个是字节*,另一个是uint32.根据CLI规范,这些实际上分别归一化为native int和int32.根据CLI规范(准确的分区III),结果将是native int.因此,必须将secodn操作数提升为native int类型.根据规范,这是通过符号扩展来完成的.所以uint.MaxValue(带符号表示法为0xFFFFFFFF或-1)符号扩展为0xFFFFFFFFFFFFFFFF.然后添加2个操作数(0x0000000008000000L +( - 1L)= 0x0000000007FFFFFFL).转换操作码仅用于验证目的,以将原生int转换为int64,在生成的代码中为int.
现在对于已检查的块,我们有这个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)
Run Code Online (Sandbox Code Playgroud)
除了add和conv操作码之外,它实际上是相同的.对于添加操作码,我们添加了2个'后缀'.第一个是".ovf"后缀,它有一个明显的含义:检查溢出,但它也需要'启用第二个后缀:".un".(即没有"add.un",只有"add.ovf.un").".un"有2个效果.最明显的一点就是完成了额外的溢出检查,就好像操作数是无符号整数一样.从我们的CS类回来的时候,希望我们都记得,由于二进制补码二进制编码,有符号加法和无符号加法是相同的,所以".un"真的只影响溢出检查,对吧?
错误.
请记住,在IL堆栈上我们没有2个64位数字,我们有一个int32和一个native int(在规范化之后).那么".un"意味着从int32到native的转换被视为"conv.u"而不是上面的默认"conv.i".因此uint.MaxValue为零扩展为0x00000000FFFFFFFFL.然后正确添加产生0x0000000107FFFFFFL.conv操作码确保无符号操作数可以表示为有符号的int64(它可以).
你的修复只能找到64位.在IL级别,更正确的修复方法是将uint32操作数显式转换为native int或unsigned native int,然后检查和取消选中对于32位和64位都是相同的.