Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Possible bug in C# JIT optimizer?

Tags:

Working on a SQLHelper class to automate stored procedures calls in a similar way to what is done in the XmlRpc.Net library, I have hit a very strange problem when running a method generated manually from IL code.

I've narrowed it down to a simple generated method (probably it could be simplified even more). I create a new assembly and type, containing two methods to comply with

public interface iTestDecimal {     void TestOk(ref decimal value);     void TestWrong(ref decimal value); } 

The test methods are just loading the decimal argument into the stack, boxing it, checking if it's NULL, and if it is not, unboxing it.

The generation of TestOk() method is as follows:

static void BuildMethodOk(TypeBuilder tb) {     /* Create a method builder */     MethodBuilder mthdBldr = tb.DefineMethod( "TestOk", MethodAttributes.Public | MethodAttributes.Virtual,       typeof(void), new Type[] {typeof(decimal).MakeByRefType() });      ParameterBuilder paramBldr = mthdBldr.DefineParameter(1,  ParameterAttributes.In | ParameterAttributes.Out, "value");     // generate IL     ILGenerator ilgen = mthdBldr.GetILGenerator();      /* Load argument to stack, and box the decimal value */     ilgen.Emit(OpCodes.Ldarg, 1);      ilgen.Emit(OpCodes.Dup);     ilgen.Emit(OpCodes.Ldobj, typeof(decimal));     ilgen.Emit(OpCodes.Box, typeof(decimal));      /* Some things were done in here, invoking other method, etc */     /* At the top of the stack we should have a boxed T or null */      /* Copy reference values out */      /* Skip unboxing if value in the stack is null */     Label valIsNotNull = ilgen.DefineLabel();     ilgen.Emit(OpCodes.Dup);      /* This block works */     ilgen.Emit(OpCodes.Brtrue, valIsNotNull);     ilgen.Emit(OpCodes.Pop);     ilgen.Emit(OpCodes.Pop);     ilgen.Emit(OpCodes.Ret);     /* End block */      ilgen.MarkLabel(valIsNotNull);     ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));      /* Just clean the stack */     ilgen.Emit(OpCodes.Pop);     ilgen.Emit(OpCodes.Pop);     ilgen.Emit(OpCodes.Ret); } 

The building for TestWrong() is nearly identical:

static void BuildMethodWrong(TypeBuilder tb) {     /* Create a method builder */     MethodBuilder mthdBldr = tb.DefineMethod("TestWrong", MethodAttributes.Public | MethodAttributes.Virtual,     typeof(void), new Type[] { typeof(decimal).MakeByRefType() });      ParameterBuilder paramBldr = mthdBldr.DefineParameter(1,  ParameterAttributes.In | ParameterAttributes.Out, "value");      // generate IL     ILGenerator ilgen = mthdBldr.GetILGenerator();      /* Load argument to stack, and box the decimal value */     ilgen.Emit(OpCodes.Ldarg, 1);     ilgen.Emit(OpCodes.Dup);     ilgen.Emit(OpCodes.Ldobj, typeof(decimal));     ilgen.Emit(OpCodes.Box, typeof(decimal));      /* Some things were done in here, invoking other method, etc */     /* At the top of the stack we should have a boxed decimal or null */      /* Copy reference values out */      /* Skip unboxing if value in the stack is null */     Label valIsNull = ilgen.DefineLabel();     ilgen.Emit(OpCodes.Dup);      /* This block fails */     ilgen.Emit(OpCodes.Brfalse, valIsNull);     /* End block */      ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal));     ilgen.MarkLabel(valIsNull);      /* Just clean the stack */     ilgen.Emit(OpCodes.Pop);     ilgen.Emit(OpCodes.Pop);     ilgen.Emit(OpCodes.Ret); } 

The only difference is I'm using BrFalse instead of BrTrue to check if the value in the stack is null.

Now, running the following code:

iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create();  decimal dectest = 1; testiface.TestOk(ref dectest); Console.WriteLine(" Dectest: " + dectest.ToString()); 

The SimpleCodeGen.Create() is creating a new assembly and type, and calling the BuildMethodXX above to generate the code for TestOk and TestWrong. This works as expected: does nothing, value of dectest is not changed. However, running:

iTestDecimal testiface = (iTestDecimal)SimpleCodeGen.Create();  decimal dectest = 1; testiface.TestWrong(ref dectest); Console.WriteLine(" Dectest: " + dectest.ToString()); 

the value of dectest is corrupted (sometimes it gets a big value, sometimes it says "invalid decimal value", ...) , and the program crashes.

May this be a bug in the JIT, or am I doing something wrong?

Some hints:

  • In debugger, it happens only when "Suppress JIT optimizations" is disabled. If "Suppress JIT optimizations" is enabled, it works. This makes me think the problem must be in the JIT optimized code.
  • Running the same test on Mono 2.4.6 it works as expected, so this is something specific for Microsoft .NET.
  • Problem appears when using datetime or decimal types. Apparently, it works for int, or for reference types (for reference types, the generated code is not identical, but I'm omiting that case as it works).
  • I think this link, reported long time ago, might be related.
  • I've tried .NET framework v2.0, v3.0, v3.5 and v4, and behavior is exactly the same.

I'm omitting the rest of the code, creating the assembly and type. If you want the full code, just ask me.

Thanks very much!

Edit: I'm including the rest of the assembly and type creation code, for completion:

class SimpleCodeGen {     public static object Create()     {         Type proxyType;          Guid guid = Guid.NewGuid();         string assemblyName = "TestType" + guid.ToString();         string moduleName = "TestType" + guid.ToString() + ".dll";         string typeName = "TestType" + guid.ToString();          /* Build the new type */         AssemblyBuilder assBldr = BuildAssembly(typeof(iTestDecimal), assemblyName, moduleName, typeName);         proxyType = assBldr.GetType(typeName);         /* Create an instance */         return Activator.CreateInstance(proxyType);     }      static AssemblyBuilder BuildAssembly(Type itf, string assemblyName, string moduleName, string typeName)     {         /* Create a new type */         AssemblyName assName = new AssemblyName();         assName.Name = assemblyName;         assName.Version = itf.Assembly.GetName().Version;         AssemblyBuilder assBldr = AppDomain.CurrentDomain.DefineDynamicAssembly(assName, AssemblyBuilderAccess.RunAndSave);         ModuleBuilder modBldr = assBldr.DefineDynamicModule(assName.Name, moduleName);         TypeBuilder typeBldr = modBldr.DefineType(typeName,           TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Public,            typeof(object), new Type[] { itf });          BuildConstructor(typeBldr, typeof(object));         BuildMethodOk(typeBldr);         BuildMethodWrong(typeBldr);         typeBldr.CreateType();         return assBldr;     }      private static void BuildConstructor(TypeBuilder typeBldr, Type baseType)     {         ConstructorBuilder ctorBldr = typeBldr.DefineConstructor(           MethodAttributes.Public | MethodAttributes.SpecialName |           MethodAttributes.RTSpecialName | MethodAttributes.HideBySig,           CallingConventions.Standard,           Type.EmptyTypes);          ILGenerator ilgen = ctorBldr.GetILGenerator();         //  Call the base constructor.         ilgen.Emit(OpCodes.Ldarg_0);         ConstructorInfo ctorInfo = baseType.GetConstructor(System.Type.EmptyTypes);         ilgen.Emit(OpCodes.Call, ctorInfo);         ilgen.Emit(OpCodes.Ret);     }      static void BuildMethodOk(TypeBuilder tb)     {         /* Code included in examples above */     }      static void BuildMethodWrong(TypeBuilder tb)     {         /* Code included in examples above */                } } 
like image 245
Álvaro Iradier Avatar asked May 11 '11 08:05

Álvaro Iradier


People also ask

What are bugs in C?

In computer technology, a bug is a coding error in a computer program. (We consider a program to also include the microcode that is manufactured into a microprocessor.) The process of finding bugs -- before users do -- is called debugging.

What are the 3 types of error in programming?

When developing programs there are three types of error that can occur: syntax errors. logic errors. runtime errors.

What are syntax errors in C?

Syntax errors: Errors that occur when you violate the rules of writing C/C++ syntax are known as syntax errors. This compiler error indicates something that must be fixed before the code can be compiled. All these errors are detected by compiler and thus are known as compile-time errors.


Video Answer


1 Answers

Look at this part of your code:

ilgen.Emit(OpCodes.Dup); ilgen.Emit(OpCodes.Brfalse, valIsNull); ilgen.Emit(OpCodes.Unbox_Any, typeof(decimal)); ilgen.MarkLabel(valIsNull); 

After the first line, the top of the stack will contain two object references. You then conditionally branch, removing one of the references. The next line unboxes the reference to a decimal value. So where you mark your label, the top of the stack is either an object reference (if the branch was taken) or a decimal value (if it wasn't). These stack states are not compatible.

EDIT

As you point out in your comment, your IL code following this would work if the stack state has a decimal on top or if it has an object reference on top, since it just pops the value off of the stack either way. However, what you're trying to do still won't work (by design): there needs to be a single stack state at each instruction. See section 1.8.1.3 (Merging stack states) of the ECMA CLI spec for more details.

like image 71
kvb Avatar answered Nov 10 '22 01:11

kvb