Over the last few days I’ve been experimenting a bit with variations of conditions to see their advantages and disadvantages. In this context I got a surprise when analyzing the source with a decompiler. Let’s look at the source code to be examined. We have an array of boolean variables that are evaluated using different procedures. On the one hand via a nested sequence of If statements and on the other hand via a non-nested sequence of If statements.
//-Begin----------------------------------------------------------------
using System;
using System.Reflection;
public class ConditionTest {
public struct conditions {
public bool Condition1;
public bool Condition2;
public conditions(bool Condition1, bool Condition2) {
this.Condition1 = Condition1;
this.Condition2 = Condition2;
}
}
public static void Main(string[] args) {
conditions[] CollectionOfConditions = new conditions[] {
new conditions(true, false),
new conditions(false, true)
};
//-Variant1---------------------------------------------------------
foreach (conditions Conditions in CollectionOfConditions) {
if (Conditions.Condition1) {
if (Conditions.Condition2) {
Console.WriteLine("Condition2");
} else {
continue;
}
} else {
continue;
}
}
//-Variant2---------------------------------------------------------
foreach (conditions Conditions in CollectionOfConditions) {
if (!Conditions.Condition1) {
continue;
}
if (!Conditions.Condition2) {
continue;
} else {
Console.WriteLine("Condition2");
}
}
}
}
//-End------------------------------------------------------------------
After compiling this source code and decompiling it again for analysis purposes, I got a surprise. The Main procedure looks like this:
// ConditionTest
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
public static void Main(string[] args)
{
ConditionTest.conditions[] CollectionOfConditions = new ConditionTest.conditions[]
{
new ConditionTest.conditions(true, false),
new ConditionTest.conditions(false, true)
};
foreach (ConditionTest.conditions Conditions in CollectionOfConditions)
{
if (Conditions.Condition1)
{
if (Conditions.Condition2)
{
Console.WriteLine("Condition2");
}
}
}
foreach (ConditionTest.conditions Conditions in CollectionOfConditions)
{
if (Conditions.Condition1)
{
if (Conditions.Condition2)
{
Console.WriteLine("Condition2");
}
}
}
}
No difference between Variant1 and Variant2.
Then I looked at the compiled code with the Intermediate Language Disassembler (IL DASM).
Hint: I did the indentations manually.
The branches (opcode br) with their jump targets can be seen very good.
Variant1:
IL_003c: ldloc.0
IL_003d: stloc.3
IL_003e: ldc.i4.0
IL_003f: stloc.s CS$7$0002
IL_0041: br.s IL_0092
IL_0043: ldloc.3
IL_0044: ldloc.s CS$7$0002
IL_0046: ldelema ConditionTest/conditions
IL_004b: ldobj ConditionTest/conditions
IL_0050: stloc.1
IL_0051: nop
IL_0052: ldloca.s Conditions
IL_0054: ldfld bool ConditionTest/conditions::Condition1
IL_0059: ldc.i4.0
IL_005a: ceq
IL_005c: stloc.s CS$4$0003
IL_005e: ldloc.s CS$4$0003
IL_0060: brtrue.s IL_0088
IL_0062: nop
IL_0063: ldloca.s Conditions
IL_0065: ldfld bool ConditionTest/conditions::Condition2
IL_006a: ldc.i4.0
IL_006b: ceq
IL_006d: stloc.s CS$4$0003
IL_006f: ldloc.s CS$4$0003
IL_0071: brtrue.s IL_0082
IL_0073: nop
IL_0074: ldstr "Condition2"
IL_0079: call void [mscorlib]System.Console::WriteLine(string)
IL_007e: nop
IL_007f: nop
IL_0080: br.s IL_0085
IL_0082: nop
IL_0083: br.s IL_008c
IL_0085: nop
IL_0086: br.s IL_008b
IL_0088: nop
IL_0089: br.s IL_008c
IL_008b: nop
IL_008c: ldloc.s CS$7$0002
IL_008e: ldc.i4.1
IL_008f: add
IL_0090: stloc.s CS$7$0002
IL_0092: ldloc.s CS$7$0002
IL_0094: ldloc.3
IL_0095: ldlen
IL_0096: conv.i4
IL_0097: clt
IL_0099: stloc.s CS$4$0003
IL_009b: ldloc.s CS$4$0003
IL_009d: brtrue.s IL_0043
Variant2:
IL_00ab: ldloc.0
IL_00ac: stloc.3
IL_00ad: ldc.i4.0
IL_00ae: stloc.s CS$7$0002
IL_00b0: br.s IL_00f5
IL_00b2: ldloc.3
IL_00b3: ldloc.s CS$7$0002
IL_00b5: ldelema ConditionTest/conditions
IL_00ba: ldobj ConditionTest/conditions
IL_00bf: stloc.1
IL_00c0: nop
IL_00c1: ldloca.s Conditions
IL_00c3: ldfld bool ConditionTest/conditions::Condition1
IL_00c8: stloc.s CS$4$0003
IL_00ca: ldloc.s CS$4$0003
IL_00cc: brtrue.s IL_00d1
IL_00ce: nop
IL_00cf: br.s IL_00ef
IL_00d1: ldloca.s Conditions
IL_00d3: ldfld bool ConditionTest/conditions::Condition2
IL_00d8: stloc.s CS$4$0003
IL_00da: ldloc.s CS$4$0003
IL_00dc: brtrue.s IL_00e1
IL_00de: nop
IL_00df: br.s IL_00ef
IL_00e1: nop
IL_00e2: ldstr "Condition2"
IL_00e7: call void [mscorlib]System.Console::WriteLine(string)
IL_00ec: nop
IL_00ed: nop
IL_00ee: nop
IL_00ef: ldloc.s CS$7$0002
IL_00f1: ldc.i4.1
IL_00f2: add
IL_00f3: stloc.s CS$7$0002
IL_00f5: ldloc.s CS$7$0002
IL_00f7: ldloc.3
IL_00f8: ldlen
IL_00f9: conv.i4
IL_00fa: clt
IL_00fc: stloc.s CS$4$0003
IL_00fe: ldloc.s CS$4$0003
IL_0100: brtrue.s IL_00b2
It can be clearly seen that the different variants are also implemented in the compiled code as they were coded in the source.
Conclusion
As we can see, in this approach the source code is implemented in the compilation as it was programmed. The decompiler builds source code from the compiled code. The reverse engineered source code is functional, but does not correspond to the actual source code. When a decompiler is used, the result is only a functional code representation and does not have to correspond to what the programmer actually coded.As we can see from this example, we cannot believe everything we see.