某些编译器发出的奇怪的 IL 代码
本文关键字:IL 代码 编译器 | 更新日期: 2023-09-27 17:55:21
我一直在看我挖出的一些旧的(Reflector)反编译源代码。DLL最初是从Visual Basic .NET源代码编译的,使用.NET 2.0 - 除此之外,我不再有关于编译器的信息。
在某个时候,奇怪的事情发生了。代码中有一个分支没有被遵循,即使条件应该成立。确切地说,这是分支:
[...]
if (item.Found > 0)
{
[...]
现在,有趣的部分是如果项目。找到-1
,输入了if
语句的范围。item.Found
的类型是int
.
为了弄清楚发生了什么,我去查找 IL 代码并找到了这个:
ldloc.3
ldfld int32 Info::Found
ldc.i4.0
cgt.un
stloc.s flag3
ldloc.s flag3
brfalse.s L_0024
显然,Reflector在这里是错误的。正确的反编译代码应该是:
if ((uint)item.Found > (uint)0)
{ ... }
到目前为止,上下文还好。现在回答我的问题。
首先,我无法想象有人真正编写了这段代码;IMO没有一个头脑清醒的人会以这种方式区分"-1"和"0" - 这是"发现"可以具有的唯一两个值。
所以,这让我得出结论,编译器做了一些我不理解的事情。
- 为什么编译器会在什么上下文中生成这样的 IL 代码?此检查有什么好处(而不是
ceq
或bne_un
- 这是我所期望的,通常由 C# 生成)? - 相关:原始源代码最有可能是什么?
看起来很古怪,但这与以前版本的Visual Basic有关,这一代以VB6结尾。 它有一个非常不同的布尔类型表示,一个VARIANT_BOOL。 这仍然是 VB.NET 的一个因素,因为它需要支持遗留代码。
True 的值表示形式不同,为 -1。 False 是 0,就像在 .NET 中一样。
虽然这看起来也是一个非常古怪的选择,但任何其他语言都使用 1 来表示 True,这是有充分理由的。 它使逻辑和数学And
和Or
运算符之间的区别消失。 这很好,程序员不必学习的另一件事。 这是一个学习障碍,从大多数 C# 程序员编写的代码类型中很明显,他们盲目地在 if() 语句中应用&&
或||
。 即使这样做不是一个好主意,由于机器代码中所需的短路分支,这些运算符也很昂贵。 如果处理器的分支预测无法很好地预测左操作数,那么由于管道停滞,您很容易丢失一堆 CPU 周期。
不错,但并非没有问题,And
和Or
总是评估左操作数和右操作数。 这有一个跳闸异常的诀窍,有时你确实需要短路。 VB.NET 添加了AndAlso
和OrElse
运算符来解决此问题。
因此,cgt.un
有意义,它可以处理 .NET 布尔值和旧值。 它不在乎 True 值是 -1 还是 1。 并且不在乎变量或表达式实际上是布尔值,在选项严格关闭 VB.NET 允许。
作为实验,我编译了这个VB代码:
Dim test As Boolean
test = True
Dim x As Integer
x = test
If x Then Console.WriteLine("True")
此发行版的 IL 为:
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor()
.entrypoint
.maxstack 2
.locals init (
[0] bool test,
[1] int32 x)
L_0000: ldc.i4.1
L_0001: stloc.0
L_0002: ldloc.0
L_0003: ldc.i4.0
L_0004: cgt.un
L_0006: neg
L_0007: stloc.1
L_0008: ldloc.1
L_0009: ldc.i4.0
L_000a: cgt.un
L_000c: brfalse.s L_0018
L_000e: ldstr "True"
L_0013: call void [mscorlib]System.Console::WriteLine(string)
L_0018: ret
注意使用cgt.un
反射器对 C# 的解释是:
bool test = true;
int x = (int) -(test > false);
if (x > 0x0)
{
Console.WriteLine("True");
}
作为 VB:
Dim test As Boolean = True
Dim x As Integer = CInt(-(test > False))
If (x > &H0) Then
Console.WriteLine("True")
End If
因此,我得出结论,生成的代码与将 VB 布尔值转换为数值有关。
让我们首先考虑您所说的有两种可能的值-1
和0
。有一个问题是,如果42
最终陷入其中,该怎么办;无论这是不可能的(你的陈述是正确的)还是几乎可能(该值就像一个variant_bool,其中-1
是正常的真实值,但所有非零值都应被视为 true),无论哪种方式都值得考虑。对待42
就像对待-1
一样是有道理的;也就是说,将所有非零值视为相同是有意义的。
即使绝对没有其他可能的非零值,除了-1
它仍然推广到"测试是非零",这在其他地方非常常见,所以认为这是"测试是非零"的情况仍然是有意义的。如果编译器不知道-1
是唯一可能的非零值(很可能),则尤其如此。
现在的问题是,是直接对值进行分支(使用 brfalse
、brtrue
等),还是执行布尔运算,然后在结果上进行分支。通常,C# 和 VB.NET 编译器都会生成一个布尔值,然后在调试版本中对其进行分支:
简单代码:
public void TestBool(bool x)
{
if(x)
throw new ArgumentOutOfRangeException();
}
调试 CIL:
nop
ldarg.1
ldc.i4.0
ceq
stloc.0
ldloc.0
brtrue.s NoError
newobj instance void [mscorlib]System.ArgumentOutOfRangeException::.ctor()
throw
NoError:
ret
发布 CIL:
ldarg.1
brfalse.s NoError
newobj instance void [mscorlib]System.ArgumentOutOfRangeException::.ctor()
throw
NoError:
ret
在执行分支之前基本上执行x == true
的额外步骤有助于调试。有时在发布代码中可以看到类似的效果,但频率较低。
因此,出于这个原因,我们在代码中的分支之前进行了比较,而不仅仅是分支。
现在还有另一个问题,我们是否应该测试该值是否为零或测试该值是否为零;两者都等同于:
if(x)
DoSomething();
和
if(!x)
{
}
else
DoSomething();
是等效的。
出于这个原因,可以使用ceq
,随后的分支适用于item.Found
0
的情况。但是,如果有什么更明智的做法,那么使用cne
分支后续适用于item.Found
不0
的情况。
但是没有像cne
这样的CIL指令,或任何可以比较测试某物是否不相等的东西。一般来说,要做"检查不等于",我们做一个序列ceq
、ldc.i4.0
、ceq
;检查两个值是否相等,然后检查该检查的结果是否为 false。
幸运的是,在普通情况下,我们正在检查的东西不等于我们不需要cne
0
因为在这种情况下cgt.un
在逻辑上等价于假设的cne
。这使得cgt.un
成为我们想要测试某些东西不为零时的明显选择。
因此,虽然IYO"没有一个头脑清醒的人会以这种方式区分'-1'和'0'",但总的来说,这是一种非常理智的测试非零的方法。事实上,cgt.un
经常以这样的非零测试出现。
相关:原始源代码最有可能是什么?
If item.Found Then
'More stuff
End If
这相当于 C#
if(item.Found != 0)
{
//More stuff
}