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

时间:2015-06-11 07:35:37

标签: c# vb.net decompiling il

我一直在查看我挖出的一些旧的(Reflector)反编译源代码。该DLL最初是使用.NET 2.0从Visual Basic .NET源代码编译的 - 除此之外我还没有关于编译器的信息。

在某些时候发生了一些奇怪的事情。代码中有一个分支没有被跟踪,即使条件应该已经存在。确切地说,这是分支:

[...]
if (item.Found > 0)
{
    [...]

现在,有趣的是,如果item.Found为-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#生成的?
  • 相关:最有可能的原始源代码是什么?

3 个答案:

答案 0 :(得分:4)

看起来很古怪,但这与以前版本的Visual Basic相关,后者以VB6结束。它有一个非常不同的布尔类型表示a 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中允许使用Option Strict Off。

答案 1 :(得分:3)

作为一个实验,我编译了这个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

Reflector对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布尔值转换为数值有关。

答案 2 :(得分:1)

让我们首先考虑有两个可能的值-10。如果42在那里结束,那么应该怎么做;这是不可能的(你的陈述中是正确的)还是只是可能的(这个值就像一个variant_bool,其中-1是正常的真值,但是所有的非零都应该被视为真)这是值得考虑的无论哪种方式。将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.Found0的情况。但是如果使用cne并且随后的分支适用于item.Found不是0的情况,则更合理。

但是没有像cne这样的CIL指令,或任何可以比较测试某些东西是否相等的指令。通常要做“检查不相等”,我们会执行序列ceqldc.i4.0ceq;检查两个值是否相等,然后检查该检查的结果是否为假。

幸运的是,在常见的情况下,我们正在检查的内容不等于0我们不需要cne,因为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
}