某些编译器发出的奇怪的 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 代码?此检查有什么好处(而不是 ceqbne_un - 这是我所期望的,通常由 C# 生成)?
  • 相关:原始源代码最有可能是什么?

某些编译器发出的奇怪的 IL 代码

看起来很古怪,但这与以前版本的Visual Basic有关,这一代以VB6结尾。 它有一个非常不同的布尔类型表示,一个VARIANT_BOOL。 这仍然是 VB.NET 的一个因素,因为它需要支持遗留代码。

True 的值表示形式不同,为 -1。 False 是 0,就像在 .NET 中一样。

虽然这看起来也是一个非常古怪的选择,但任何其他语言都使用 1 来表示 True,这是有充分理由的。 它使逻辑和数学AndOr运算符之间的区别消失。 这很好,程序员不必学习的另一件事。 这是一个学习障碍,从大多数 C# 程序员编写的代码类型中很明显,他们盲目地在 if() 语句中应用&&||。 即使这样做不是一个好主意,由于机器代码中所需的短路分支,这些运算符也很昂贵。 如果处理器的分支预测无法很好地预测左操作数,那么由于管道停滞,您很容易丢失一堆 CPU 周期。

不错,但并非没有问题,AndOr总是评估左操作数和右操作数。 这有一个跳闸异常的诀窍,有时你确实需要短路。 VB.NET 添加了AndAlsoOrElse运算符来解决此问题。

因此,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 布尔值转换为数值有关。

让我们首先考虑您所说的有两种可能的值-10。有一个问题是,如果42最终陷入其中,该怎么办;无论这是不可能的(你的陈述是正确的)还是几乎可能(该值就像一个variant_bool,其中-1是正常的真实值,但所有非零值都应被视为 true),无论哪种方式都值得考虑。对待42就像对待-1一样是有道理的;也就是说,将所有非零值视为相同是有意义的。

即使绝对没有其他可能的非零值,除了-1它仍然推广到"测试是非零",这在其他地方非常常见,所以认为这是"测试是非零"的情况仍然是有意义的。如果编译器不知道-1是唯一可能的非零值(很可能),则尤其如此。

现在的问题是,是直接对值进行分支(使用 brfalsebrtrue 等),还是执行布尔运算,然后在结果上进行分支。通常,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.Found0的情况。

但是没有像cne这样的CIL指令,或任何可以比较测试某物是否不相等的东西。一般来说,要做"检查不等于",我们做一个序列ceqldc.i4.0ceq;检查两个值是否相等,然后检查该检查的结果是否为 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
}