Jasica.Net

C#, .Net, SQL i nie tylko

Corupted State Exception

.Net w wersji 4.0 wprowadził pojęcie Corupted State Exception. W odróżnieniu od pozostałych wyjątków, nie są one wyłapywane przez bloki catch/finally. Takie zachowanie ma jak najbardziej sens, gdyż w większości sytuacji stanowią one sygnał, że w programie stało się coś złego i dalsze wykonywanie może nie mieć sensu. Występują one głównie przy współpracy z natywnymi bibliotekami. Poniższe wyjątki mogą być traktowane jako CSE:

  • STATUS_ACCESS_VIOLATION
  • STATUS_STACK_OVERFLOW
  • EXCEPTION_ILLEGAL_INSTRUCTION
  • EXCEPTION_IN_PAGE_ERROR
  • EXCEPTION_INVALID_DISPOSITION
  • EXCEPTION_NONCONTINUABLE_EXCEPTION
  • EXCEPTION_PRIV_INSTRUCTION
  • STATUS_UNWIND_CONSOLIDATE

Przeglądając tą listę możemy zauważyć, że w większości wypadków sugerują one błędy w kodzie natywnym. Poniższy kod C++ generuje błędne odwołanie do fragmentu pamięci, które posłuży jako przykład:

extern "C" __declspec(dllexport) 
int Add(int a, int b)
{
    int * t = 0;
    *t = 1;
    return a + b;
}

Kod tej funkcji w C# można wywołać stosując atrybut DllImport:

[DllImport("test.dll")]
public static extern int Add(int a, int b);

static void Main(string[] args)
{
     try
     {
         int result = Add(1, 2);
         Console.WriteLine(result);
     }
     catch (Exception ex)
     {
         Console.WriteLine("Cath: " + ex.Message + ex.StackTrace);
     }
     finally
     {
         Console.WriteLine("Finally");
     }
     Console.WriteLine("End");
}

Wynik działania powyższego kodu będzie różny w zależności od wybranej wersji CLR. W 4.0 nie wykona się zarówno blok catch jak i finally, natomiast w wcześniejszych wersjach wyjątek zostanie złapany, a program wykona się do końca. Wyjątki tego typu można wyłapywać CLR 4.0 o ile programista świadomie oznaczy metodę atrybutem HandleProcessCorruptedStateExceptions. Może to mieć znaczenie, gdy biblioteka zawiera dobrze znany błąd i nie da się go wyeliminować w danej chwili inaczej. Zachowanie to można wyłączyć także dla całego kodu, bez dokonywania jakikolwiek zmian w nim. Zachowanie kompatybilności wstecz można wymusić dodając wpis w pliku konfiguracyjnym:

<configuration>
    <runtime>
        <legacyCorruptedStateExceptionsPolicy enabled="true"/>
    </runtime>
</configuration>

Corupted State Exception chronią mniej świadomych programistów przed kontynuowaniem wykonywania kodu, gdy aplikacja znalazła się w złym stanie i dalsze operacje mogą prowadzić do utraty bądź uszkodzenia ważnych danych. Niestety, często w aplikacjach znajdują się zbyt ogólne bloki catch, łapiące wyjątki każdego typu, co zamiast zapewniać stabilność aplikacji, mogą prowadzić do nieoczekiwanych rezultatów. CAS chronią przed tego typu błędami i jednocześnie pozwalają wymusić wyłapywanie ich o ile zachodzi taka konieczność przy pomocy wspomnianego wcześniej atrybutu- HandleProcessCorruptedStateExceptions.

MSDeploy(aka WebDeploy) - problem po aktualizacji do .Net 4.5

Migracja do .Net 4.5, mimo iż miał być bezproblemowa, czasem stwarza problemy. Jednym z narzędzi jakie nie chciało od razu współpracować z .Net 4.5 jest MSDeploy(aka WebDeploy).

Po migracji projektu do .Net 4.5 MSDeploy podczas publikacji paczki na IIS, zgłaszał błąd:

Error Code: 51
More Information: The application pool that you are trying to use has the
'managedRuntimeVersion' property set to 'v4.0'. This application requires 'v4.5'.
Error count: 1.

Mało eleganckim obejściem jest oszukanie MSDeploy'a poprzez zmianę wersji .Net przed publikacją przy pomocy przełącznika preSync:

-preSync:runCommand="%systemroot%\system32\inetsrv\APPCMD stop apppool P  
 & %systemroot%\system32\inetsrv\APPCMD set apppool P /managedRuntimeVersion:v4.5"

Wykonanie tego polecenia wprowadzi zmiany w ustawieniach puli, a nawet nowa wersja frameworka będzie dostępna do wyboru na liście:

 

Oczywiście, tak ustawiona pula nie będzie działać poprawnie, więc należy przywrócić ustawienia pierwotne, prosząc MSDeploy'a by wykonał na zakończenie porządki:

-postSync:runCommand="%systemroot%\system32\inetsrv\APPCMD set apppool P /managedRuntimeVersion:v4.0  
 & %systemroot%\system32\inetsrv\APPCMD start apppool P

Zna ktoś inne rozwiązanie tego problemu?

BadImageFormatException - problem z 32 i 64 bitowymi bibliotekami po raz N-ty?

Wystąpienie BadImageFormatException, wśród doświadczonych developerów, rodzi pytanie czy przez przypadek nie została załadowana zewnętrzna biblioteka w złej wersji- 32 lub 64 bitowej. Nie jest to jednak jedyna przyczyna występowania tego wyjątku. Na początek jednak przyjrzyjmy się kawałkowi prostego kodu:

using System;
using System.Diagnostics;

namespace BadImageFormatExceptionSample
{
    public class Data
    {
        public string Text { get; set; }
    }

    public class Base<T> where T : Data
    {
        public virtual T Print(T t)
        {
            Log(() =>
            {
                Console.WriteLine(t.Text);
            });
            return t;
        }

        protected virtual void Log(Action action)
        {
            var stackTrace = new StackTrace();
            var method = stackTrace.GetFrame(1).GetMethod();
            Console.WriteLine("Type: {0}, Method: {1}; before", 
                method.DeclaringType.Name, method.Name);
            action();
            Console.WriteLine("Type: {0}, Method: {1}; after", 
                method.DeclaringType.Name, method.Name);
        }
    }

    public class TestClass : Base<Data>
    {
        public override Data Print(Data data)
        {
            Log(() =>
            {
                data = base.Print(data);
            });

            return data;
        }
    }

    class Program
    {
        static void Main()
        {
            var z = new TestClass();
            z.Print(new Data() { Text = "Test" });
        }
    }
}

Kod jest stosunkowo prosty. Można by dać go jako rozgrzewkę osobie na rozmowie kwalifikacyjnej i spytać co program wypisze na ekranie. Jako wyjście spodziewalibyśmy się tekstu podobnego do:

Type: TestClass, Method: Print; before
Type: Base`1, Method: Print; before
Test
Type: Base`1, Method: Print; after
Type: TestClass, Method: Print; after

Niestety, wbrew wszelkim oczekiwaniom pojawi się tu wyjątek, który jest bohaterem tego artykułu. Dla dopełnienia prezentuję początek wyjścia z programu:

Type: TestClass, Method: Print; before

Unhandled Exception: System.BadImageFormatException: An attempt was made to load 
    a program with an incorrect format. (Exception from HRESULT: 0x8007000B)

Na pierwszy rzut oka ciężko wskazać co jest nie tak, jednak przy dłuższej analizie zdekompilowanego kodu można dojść do wniosku, że coś jest nie w porządku. Problem da się łatwo obejść; jeśli wystąpił wystarczy użyć mechanizmu delegatów i wszystko będzie działać jak na począku zakłądaliścy:

public override Data Print(Data data)
{
    var print = new Func<Data, Data>(base.Print);
    Log(() =>
    {
        data = print(data);
    });
    return data;
}

Całe szczęście problem nie występuje często - trzeba użyć kilku bardziej zaawansowanych mechanizmów języka by problem wystąpił. Problem został naprawiony w kompilatorze nadchodzącym z wersją 2012 Visual Studio, więc możemy spać spokojnie i dalej śmiało używać wyrażeń lambda, jak tylko wyobraźnia nam pozwoli. Dociekliwym przedstawiam jeszcze kod IL wygenerowany przez Visual Studio 2012:

 

.method public hidebysig instance void  '<Print>b__0'() cil managed
{
  // Code size       25 (0x19)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldarg.0
  IL_0003:  ldfld      class BadImageFormatExceptionSample.TestClass BadImageFormatExceptionSample.TestClass/'<>c__DisplayClass1'::'<>4__this'
  IL_0008:  ldarg.0
  IL_0009:  ldfld      class BadImageFormatExceptionSample.Data BadImageFormatExceptionSample.TestClass/'<>c__DisplayClass1'::data
  IL_000e:  call instance class BadImageFormatExceptionSample.Data BadImageFormatExceptionSample.TestClass::'<>n__FabricatedMethod3'(class BadImageFormatExceptionSample.Data)
  IL_0013:  stfld      class BadImageFormatExceptionSample.Data BadImageFormatExceptionSample.TestClass/'<>c__DisplayClass1'::data
  IL_0018:  ret
} // end of method '<>c__DisplayClass1'::'<Print>b__0'

Oraz kłopotliwa wersja z 2010 dla porównania(różnica w lini IL_000e):

.method public hidebysig instance void  '<Print>b__0'() cil managed
{
  // Code size       25 (0x19)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldarg.0
  IL_0003:  ldfld      class BadImageFormatExceptionSample.TestClass BadImageFormatExceptionSample.TestClass/'<>c__DisplayClass1'::'<>4__this'
  IL_0008:  ldarg.0
  IL_0009:  ldfld      class BadImageFormatExceptionSample.Data BadImageFormatExceptionSample.TestClass/'<>c__DisplayClass1'::data
  IL_000e:  call instance !0 BadImageFormatExceptionSample.TestClass::'<>n__FabricatedMethod3'(!0)
  IL_0013:  stfld      class BadImageFormatExceptionSample.Data BadImageFormatExceptionSample.TestClass/'<>c__DisplayClass1'::data
  IL_0018:  ret
} // end of method '<>c__DisplayClass1'::'<Print>b__0'

Na koniec rodzi się jeszcze jedna myśl- czy nie za bardzo ufamy narzędziom z których korzystamy? Kompilatory tworzą też ludzie i mogą popełniać błędy, więc do każdego kodu należy podchodzić z pewną rezerwą- mniejszą bądź większą, w zależności od źródła. Taka programistyczna zasada ograniczonego zaufania.