为什么包含不可能的条件分支会更改此方法的返回值?



我有以下手动编写的 IL 方法,它将无符号 32 位整数转换为有符号 64 位整数:

.method public hidebysig static int64 ToInt64(uint32 'value') cil managed
{
.maxstack 2
ldarg.0
ldc.i4.0
brtrue.s return // Never taken.
conv.u8
return:
ret
}

请注意,由于评估堆栈顶部的值始终为 0,因此永远不会采用条件分支。

当我将值 4294967295(uint32的最大值(传递给方法时,它返回 -1 而不是预期的4294967295。对我来说,这表明conv.u8正在被跳过,并且正在发生将转换扩展到int64的迹象。

但是,如果我将相同的参数传递给一个修改后的方法,该方法删除了不可能的条件分支......

.method public hidebysig static int64 ToInt64(uint32 'value') cil managed
{
.maxstack 1
ldarg.0
conv.u8
ret
}

。它返回预期的4294967295。

更有趣的是,如果我不是删除分支,而是在conv.u8指令之前添加一个conv.u4指令......

.method public hidebysig static int64 ToInt64(uint32 'value') cil managed
{
.maxstack 2
ldarg.0
ldc.i4.0
brtrue.s return
conv.u4
conv.u8
return:
ret
}

。它还返回4294967295。

我终生无法弄清楚为什么包含总是错误的条件分支会改变方法的结果,也无法弄清楚为什么对已经是 32 位整数的值进行操作的conv.u4指令会影响方法的执行。根据我对 CIL 和 CLR 的(有限(理解,该方法的所有变体在 CLR 眼中都应该有效(但可能无法验证(,并且应该产生相同的结果。

我是否缺少 IL 执行方式的某些方面?还是我偶然发现了某种运行时错误?

我注意到的一件事是 ECMA-335 的 III.1.7.5 节("后向分支约束"(陈述如下:

对于任何方法,通过CIL指令流的单次前向传递,都可以推断每条指令的评估堆栈的确切状态(其中"状态"是指评估堆栈上每个项目的数量和类型(。

假设当方法返回时,评估堆栈可以保存 32 位整数或 64 位整数,具体取决于是否采用分支,这是否可以理解为暗示该方法无效 CIL,并且运行时只是尽力处理它?还是本节中提到的约束不适用,因为这是一个前向分支?


下面是面向重现行为的netcoreapp2.2运行时的示例控制台应用。

.assembly extern System.Runtime
{
.publickeytoken = ( B0 3F 5F 7F 11 D5 0A 3A )
.ver 4:2:1:0
}
.assembly extern System.Console
{
.publickeytoken = ( B0 3F 5F 7F 11 D5 0A 3A )
.ver 4:1:1:0
}
.assembly ConsoleApp1
{
// [assembly: CompilationRelaxations(CompilationRelaxations.NoStringInterning)]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(valuetype [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxations) = (
01 00 08 00 00 00 00 00 )
// [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = (
01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78
63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 )
// [assembly: Debuggable(DebuggingModes.IgnoreSymbolStoreSequencePoints)]
.custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = (
01 00 02 00 00 00 00 00 )
// [assembly: TargetFramework(".NETCoreApp,Version=v2.2", FrameworkDisplayName = "")]
.custom instance void [System.Runtime]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = (
01 00 18 2e 4e 45 54 43 6f 72 65 41 70 70 2c 56
65 72 73 69 6f 6e 3d 76 32 2e 32 01 00 54 0e 14
46 72 61 6d 65 77 6f 72 6b 44 69 73 70 6c 61 79
4e 61 6d 65 00 )
.hash algorithm 0x00008004 // SHA1
.ver 1:0:0:0
}
.module ConsoleApp1.dll
.imagebase      0x10000000
.file alignment 0x00000200
.stackreserve   0x00100000
.subsystem      0x0003     // IMAGE_SUBSYSTEM_WINDOWS_CUI
.corflags       0x00000001 // COMIMAGE_FLAGS_ILONLY
.class public auto ansi abstract sealed beforefieldinit ConsoleApp1.Program extends [System.Runtime]System.Object
{
.method public hidebysig static int64 ToInt64(uint32 'value') cil managed
{
.maxstack 1
ldarg.0
conv.u8
ret
}
.method public hidebysig static int64 ToInt64_Branch(uint32 'value') cil managed
{
.maxstack 2
ldarg.0
ldc.i4.0
brtrue.s return
conv.u8
return:
ret
}
.method public hidebysig static int64 ToInt64_ConvU4(uint32 'value') cil managed
{
.maxstack 1
ldarg.0
conv.u4
conv.u8
ret
}
.method public hidebysig static int64 ToInt64_Branch_ConvU4(uint32 'value') cil managed
{
.maxstack 2
ldarg.0
ldc.i4.0
brtrue.s return
conv.u4
conv.u8
return:
ret
}
.method public hidebysig static void Main() cil managed
{
.maxstack 2
.entrypoint
ldstr "ToInt64(uint.MaxValue): {0}"
ldc.i4.m1
call int64 ConsoleApp1.Program::ToInt64(uint32)
box [System.Runtime]System.Int64
call void [System.Console]System.Console::WriteLine(string, object)
ldstr "ToInt64_Branch(uint.MaxValue): {0}"
ldc.i4.m1
call int64 ConsoleApp1.Program::ToInt64_Branch(uint32)
box [System.Runtime]System.Int64
call void [System.Console]System.Console::WriteLine(string, object)
ldstr "ToInt64_ConvU4(uint.MaxValue): {0}"
ldc.i4.m1
call int64 ConsoleApp1.Program::ToInt64_ConvU4(uint32)
box [System.Runtime]System.Int64
call void [System.Console]System.Console::WriteLine(string, object)
ldstr "ToInt64_Branch_ConvU4(uint.MaxValue): {0}"
ldc.i4.m1
call int64 ConsoleApp1.Program::ToInt64_Branch_ConvU4(uint32)
box [System.Runtime]System.Int64
call void [System.Console]System.Console::WriteLine(string, object)
ret
}
}

当使用具有发布配置的 Microsoft.NETCore.ILAsm 编译并运行时,它会将以下内容输出到控制台:

ToInt64(uint.MaxValue): 4294967295
ToInt64_Branch(uint.MaxValue): -1
ToInt64_ConvU4(uint.MaxValue): 4294967295
ToInt64_Branch_ConvU4(uint.MaxValue): 4294967295

您应该检查第 III.3.18 节 (brtrue

(:

可验证代码需要堆栈的类型一致性、局部变量和参数,用于到达目标指令的每个可能路径

然后:

满足正确性要求但无法验证的 CIL 序列的操作可能会违反类型安全

正如您自己所观察到的,您没有确保堆栈在到达return:的所有路径上的完整性。

相关内容

  • 没有找到相关文章

最新更新