为什么包含不可能的条件分支会更改此方法的 return 值?
Why does the inclusion of an impossible conditional branch change the return value of this method?
我有以下手动编写的 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
的最大值)传递给方法时,它是 returns -1 而不是预期的 4294967295。对我来说,这表明 conv.u8
被跳过,一个扩展到 int64
的符号正在发生。
但是,如果我将相同的参数传递给修改后的方法,该方法删除了不可能的条件分支...
.method public hidebysig static int64 ToInt64(uint32 'value') cil managed
{
.maxstack 1
ldarg.0
conv.u8
ret
}
...它 returns 预期的 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
}
...它也 returns 4294967295.
我终生无法弄清楚为什么包含始终为假的条件分支会改变方法的结果,也无法弄清楚为什么 conv.u4
指令对已经是 32 位的值进行操作整数会影响方法的执行。就我对 CIL 和 CLR 的(有限)理解而言,该方法的所有变体在 CLR 眼中都应该是有效的(但可能无法验证)并且应该产生相同的结果。
关于 IL 的执行方式,我是否遗漏了某些方面?还是我偶然发现了某种 运行 时间错误?
我注意到的一件事是 ECMA-335 ("Backward branch constraints") 的第 III.1.7.5 节规定如下:
It shall be possible, with a single forward-pass through the CIL instruction stream for any method, to infer the exact state of the evaluation stack at every instruction (where by “state” we mean the number and type of each item on the evaluation stack).
鉴于当方法 returns 时,评估堆栈可以保存 32 位整数或 64 位整数,具体取决于是否采用分支,这是否可以被视为暗示该方法是无效的 CIL 并且 运行time 只是尽力处理它吗?或者本节中提到的限制是否不适用,因为这是前向分支?
下面是一个示例控制台应用程序,目标是 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
):
Verifiable code requires the type-consistency of the stack, locals and arguments for every possible path to the destination instruction
然后:
The operation of CIL sequences that meet the correctness requirements, but which are not verifiable, might violate type safety
正如您自己观察到的,您没有确保到达 return:
.
的所有路径上堆栈的完整性
我有以下手动编写的 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
的最大值)传递给方法时,它是 returns -1 而不是预期的 4294967295。对我来说,这表明 conv.u8
被跳过,一个扩展到 int64
的符号正在发生。
但是,如果我将相同的参数传递给修改后的方法,该方法删除了不可能的条件分支...
.method public hidebysig static int64 ToInt64(uint32 'value') cil managed
{
.maxstack 1
ldarg.0
conv.u8
ret
}
...它 returns 预期的 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
}
...它也 returns 4294967295.
我终生无法弄清楚为什么包含始终为假的条件分支会改变方法的结果,也无法弄清楚为什么 conv.u4
指令对已经是 32 位的值进行操作整数会影响方法的执行。就我对 CIL 和 CLR 的(有限)理解而言,该方法的所有变体在 CLR 眼中都应该是有效的(但可能无法验证)并且应该产生相同的结果。
关于 IL 的执行方式,我是否遗漏了某些方面?还是我偶然发现了某种 运行 时间错误?
我注意到的一件事是 ECMA-335 ("Backward branch constraints") 的第 III.1.7.5 节规定如下:
It shall be possible, with a single forward-pass through the CIL instruction stream for any method, to infer the exact state of the evaluation stack at every instruction (where by “state” we mean the number and type of each item on the evaluation stack).
鉴于当方法 returns 时,评估堆栈可以保存 32 位整数或 64 位整数,具体取决于是否采用分支,这是否可以被视为暗示该方法是无效的 CIL 并且 运行time 只是尽力处理它吗?或者本节中提到的限制是否不适用,因为这是前向分支?
下面是一个示例控制台应用程序,目标是 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
):
Verifiable code requires the type-consistency of the stack, locals and arguments for every possible path to the destination instruction
然后:
The operation of CIL sequences that meet the correctness requirements, but which are not verifiable, might violate type safety
正如您自己观察到的,您没有确保到达 return:
.