Post-mortem debugging s CLR V4 a Visual Studiem 2010

Václav Dajbych       21.12.2009       .NET       12924 zobrazení

Nový Common Language Runtime v .NET 4 umožňuje v případě pádu aplikace uložit její stav do zvláštního souboru nazývaného dump. Ten může vývojář použít k přesné analýze příčiny pádu aplikace. Vidí totiž obsahy proměnných a datových struktur stejně, jako když aplikaci ladí. Článek popisuje jak konfigurovat prostředí tak, aby pády aplikace generovaly dump soubor, který se pak odešle na podnikový server.

Pokud aplikaci ladí vývojář, běží mu pod aplikací Visual Studio, takže má velmi dobrý přehled o tom co se děje. Testuje-li aplikaci tester, je to už o něco horší. Popíše postup, jak se k chybě došlo a v lepším případě dokonce poukáže na možnou příčinu, která se později neukáže jako zcela zcestná. Používá-li aplikaci zákazník v divočejší míře než s tím počítal vývojář, dostane se zpět vývojáři v nejlepším případě snímek obrazovky. Ten má zřídka kdy vypovídající hodnotu. Výpis z logu bývá sice užitečný, je to ale opět jen návod k rekonstrukci chyby. Visual Studio 2010 a .NET 4 nám dává velice elegantní řešení, které přinese vývojáři tolik informací, že jediný rozdíl mezi laděním ve vývojovém a produkčním prostředí je v tom, nemůžeme pokračovat v běhu programu.

JIT, což je kompilátor z MSIL do strojového kódu, a Windows jdou nastavit tak, aby hlídali určitou aplikaci a pokud v ní dojde k neošetřené výjimce, připojí k ní debugger, který zapíše na disk výpis její paměti. Tomu se říká minidump. Neobsahuje totiž výpis úplně celé paměti procesu, ale jen těch částí, o kterých se předpokládá, že nás budou zajímat. K nastavení automatického generování minidump souboru stačí zapsat několik klíčů do registru Windows a nainstalovat debugger, který je součástí Debugging Tools for Windows. Výpisy paměti se ukládají jen do složky, kterou určíme. Tuto složku může sledovat nějaký další program, který soubor odešle na podnikový server. Vývojář si ho už jen otevře ve Visual Studiu 2010 a vidí stav programu stejně dobře, jako kdyby chyba nastala u něj. Aplikace ale musí být v .NET 4 a zkompilovaná v Debug režimu. Soubor pdb (program debug database) není k vygenerování minidump souboru zapotřebí.

Následující obsah reg souboru po importu do registru nastaví ladění a vygenerování minidump souboru pro aplikaci s názvem HelloWorld.exe:

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework]
"DbgManagedDebugger"="\"C:\\Program Files\\Debugging Tools for Windows (x64)\\cdb.exe\"
-pv -p %ld -c \".dump /u /ma c:\\crash_dumps\\crash.dmp;.kill;qd\""
"DbgJITDebugLaunchSetting"=dword:00000000
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug]
"Debugger"="\"C:\\Program Files\\Debugging Tools for Windows (x64)\\cdb.exe\"
-pv -p %ld -c \".dump /u /ma c:\\crash_dumps\\crash.dmp;.kill;qd\""
"Auto"="0"
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug]
"Debugger"="\"C:\\Program Files (x86)\\Debugging Tools for Windows (x86)\\cdb.exe\"
-pv -p %ld -c \".dump /u /ma c:\\crash_dumps\\crash.dmp;.kill;qd\""
"Auto"="0"
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework]
"DbgManagedDebugger"="\"C:\\Program Files (x86)\\Debugging Tools for Windows (x86)\\cdb.exe\"
-pv -p %ld -c \".dump /u /ma c:\\crash_dumps\\crash.dmp;.kill;qd\""
"DbgJITDebugLaunchSetting"=dword:00000000
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\DebugApplications]
"HelloWorld.exe"=dword:00000001

Toto je nejsložitější příklad, který funguje na x64 stoji jak pro x64 tak i pro x86 aplikaci. K odpovídajícímu typu aplikace je nutné připojit odpovídající debugger. V uzlu Wow6432Node je nastavení pro x86 aplikace na x64 stroji. Ostatní nastavení je pro x64. Na x86 stroji nejsou záznamy Wow6432Node platné a u ostatních se jen upraví cesty na x86 debugger. Naopak máme-li všechny aplikace na x64 stoji čistě x64, nejsou záznamy v uzlu Wow6432Node potřeba. Nainstalujeme také Debugging Toos for Windows v potřebné bitové verzi. K samotnému vygenerování minidumpu není potřeba celý obsah instalátoru, stačí pouze aplikace cdb.exe. V poslední větvi se nastavují aplikace, které se mají po pádu ladit. Takto lze nastavit i několik aplikací.

Pojďme si ukázat, jak to vypadá v praxi. Jako příklad nám poslouží jednoduchá konzolová aplikace. Metoda Main obsahuje řetězec a spojový seznam, který znázorňuje nějakou hlubší strukturu. Nejprve si nainicializujeme proměnné a poté zavoláme metodu MyCode, která způsobí pád aplikace.

code

Po spuštění aplikace se vygeneruje minidump soubor. Následující obrázek zachycuje Visual Studio 2010 po jeho otevření. Vedle základních informací máme k dispozici seznam načtených dynamických knihoven. Pro ladění v managed režimu zvolíme Debug with Mixed.

summary

Pokud máme k dispozici projekt, ze kterého se přeložila původní aplikace, zobrazí se její zdrojový kód a zvýrazní se řádek, na kterém došlo k vyjímce. Vidíme seznam proměnných a jejich obsah v aktuálním stacku. Můžeme se libovolně přípínat do rodičovského stacku, kde máme opět přístupné všechny proměnné.

line

Po přepnutí do rodičovského stacku vidíme onen spojový seznam i řetězec. Spojový seznam můžeme procházet libovolně do hloubky. Obsah globálních proměnné nevidíme, protože na ní neukazuje proměnná v žádném stacku.

locals

Jediné potíže, na které jsem narazil a které znemožňovaly toto pohodlné ladění, spočívaly v tom, že Visual Studio neumí zpracovat minidump zachycující vyjímku, která nastala v konstruktoru WPF okna. Nesnáze může také způsobovat pomalá linka zákazníka, protože minidump soubory mají řádově desítky megabajtů.

Zbytek článku ukazuje upload minidump souboru na server. Následující kód si hlídá adresář C:\crash_dumps a pokud zjistí nový dmp soubor, odešle jej na server:

  FileSystemWatcher fsw = new FileSystemWatcher(@"C:\crash_dumps", "*.dmp");
  fsw.Created += new FileSystemEventHandler(fsw_Created);

  void fsw_Created(object sender, FileSystemEventArgs e) {
    string filename = e.FullPath;

    ServiceClient client = new ServiceClient();
    FileStream fs = File.Open(filename, FileMode.Open);
    string serverHash = client.UploadDump(fs);

    fs.Seek(0, SeekOrigin.Begin);

    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
    byte[] hash = md5.ComputeHash(fs);
    string localHash = BitConverter.ToString(hash);

    fs.Close();

    if (localHash == serverHash) File.Delete(filename);
  }

Služba přijímá stream a ukládá ho do souboru po 4kB blocích. Na konci spočítá MD5 hash a vrátí ho klientovi:

  [ServiceContract]
  public interface IService {
    [OperationContract]
    string UploadDump(Stream content);
  }

  public class Service : IService {
    string IService.UploadDump(Stream content) {
      string file = null;
      try {
        file = Path.GetTempFileName();

        FileStream fs = File.Open(file, FileMode.Create, FileAccess.Write);

        const int bufferLen = 4096;
        byte[] buffer = new byte[bufferLen];
        int count = 0;
        while ((count = content.Read(buffer, 0, bufferLen)) > 0) fs.Write(buffer, 0, count);
        fs.Close();
        content.Close();

        fs = File.Open(file, FileMode.Open, FileAccess.Read);
        MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
        byte[] hash = md5.ComputeHash(fs);
        fs.Close();

        File.Move(file, Path.Combine(@"C:\Client Crash Dumps\HelloWorld",
          Path.GetFileNameWithoutExtension(file) + ".dmp"));

        return BitConverter.ToString(hash);
      } catch {
        if (file != null &amo;&amo; File.Exists(file)) File.Delete(file);
        return null;
      }
    }
  }

Odpovídající web.config by se ale aplikovat jen pro tuto službu. Její endpoint nebudeme nikde zveřejňovat, protože případným DoS útokům otevírá dveře.

  <configuration>
    <system.web>
      <httpRuntime maxRequestLength="1048576" executionTimeout="3600"/>
      <!-- 1 GB   1 hod -->
    </system.web>
    <system.serviceModel>
      <behaviors>
        <serviceBehaviors>
          <behavior name="ServiceUpload.ServiceBehavior">
            <serviceMetadata httpGetEnabled="true"/>
            <serviceDebug includeExceptionDetailInFaults="false"/>
          </behavior>
        </serviceBehaviors>
      </behaviors>
      <services>
        <service behaviorConfiguration="ServiceUpload.ServiceBehavior" name="ServiceUpload.Service">
          <endpoint address="" binding="basicHttpBinding"

            bindingConfiguration="HttpStreaming" contract="ServiceUpload.IService"/>
          <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
        </service>
      </services>
      <bindings>
        <basicHttpBinding>
          <binding name="HttpStreaming" maxReceivedMessageSize="1073741824" transferMode="Streamed"/>
          <!-- 1 GB -->
        </basicHttpBinding>
      </bindings>
    </system.serviceModel>
  </configuration>

Služba je typu basic http binding, takže komunikace neprobíhá po zabezpečeném kanále, pokud si stream sami nezašifrujeme. Výhodou je, že se soubor posílá jako stream a http bindingu nestojí v cestě firewally. Metoda implementující zpracování streamu nemůže mít žádný další parametr, což by v tomto případě nemělo vadit.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

 

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback