Archive for the ‘Tip’ Category

Tabt i detaljerne

Tuesday, September 7th, 2010

Sådan vasker Microsoft hænderHosstående opslag hænger på herretoilettet hos Microsoft i Hellerup. Det er en detaljeret, illustreret gennemgang af, hvordan man vasker hænder. Hvis vi ser bort fra den reelle risiko for en TL;DR-reaktion, må den minutiøse beskrivelse være tilstrækkelig til at tilgodese selv den mest nidkære bureaukrats behov for information, og ingen skulle således være i tvivl om, hvordan man vasker hænder i Hellerup.

Opslaget er mere underholdende end gavnligt i mine øjne, men det er desværre også et billede på den kode, de fleste af os til tider skriver. Jeg har læst og skrevet masser af kode, der i detaljeringniveau kan måle sig med ovenstående. Jo tættere vi kommer på metallet, desto tyndere bliver vores abstraktioner, og dermed bliver vi nødt til at redegøre for flere detaljer.

Vi kan således ikke helt undgå denne problematik, men jo mere vi kan styre uden om den slags desto bedre.

Problemet er, at det kan være svært at se, hvad målet er. Vi har specificeret, hvad der skal gøres, men det kan være svært at se, hvad det egentlig er, vi gerne vil opnå, og dermed kan det være svært at sige noget konkret om implementeringen. Fungerer den efter hensigten? Er der bivirkninger? Kan den optimeres om nødvendig? Kan den paralleliseres?

Hvad er formålet med instruktionerne i opslaget? Eller sagt på en anden måde: Hvad er det ønskede slutresultat, hvis vi følger proceduren? Er det rene hænder, reduktion af sæbebeholdning eller en sindrig form for håndaerobics i vand? Er det, vi ønsker at opnå, blot en delmængde af det samlede resultat? Hvordan skelner vi i så fald mellem de nødvendige og overflødige trin i processen for at opnå det ønskede?

Det kan være svært at adressere disse spørgsmål, da svarene ligger gemt i detaljerne.

Lad os se på et eksempel mere. Betragt følgende kode.

var numbers1 = new[] { 2, 4, 6, 7, 8, 9 };
var numbers2 = new[] { 1, 2, 3, 4, 5, 6, 9 };
var numbers3 = new[] { 1, 5, 7, 9 };

var result = new List<int>();

foreach (var n1 in numbers1) {
   foreach (var n2 in numbers2) {
      if (n1 == n2) {
         bool found = false;
         foreach (var n3 in numbers3) {
            found |= n2 == n3;
         }
         if (!found) {
            result.Add(n2);
         }
         break;
      }
   }
}

Hvad laver ovenstående? Med mindre man har set konstruktionen et utal af gange, skal de fleste af os nok lige tænke en gang eller to for at gennemskue, hvad formålet med koden er. Problemet er, at der er mange detaljer, så vi er næsten tvunget til at løbe et lille eksempel igennem, før vi kan gennemskue resultatet af koden. Det er ikke et stort problem, men vi skal bruge flere ressourcer på at forstå koden, end vi behøver.

Der er andre nærliggende spørgsmål, der ligeledes kan være svære at svare på her: Kan vi optimere koden, hvis behovet opstår? Hvor mange uafhængige operationer er der i koden? Kan disse paralleliseres?

Som du sikkert har gennemskuet, er indholdet af result alle de elementer, der er i både numbers1 og numbers2, men ikke i numbers3.

Det lugter lidt af mængdeoperationer, og med den erkendelse kan vi pludselig tale om problemet i domænespecifikke termer. Vi kan tilmed omskrive koden til at bruge .NETs mængdeoperationer. Ovenstående kan således udtrykkes som:

result = numbers1.Intersect(numbers2).Except(numbers3).ToList();

Vi kan selvfølgelig glæde os over, at dette er meget kortere, men det er kun en af gevinsterne her. Ved at bruge domænespecifikke abstraktioner kan vi kommunikere i et sprog, der giver mening i forhold til problemet, og vi er fri for at bekymre os om, hvad der skal til for at opnå det ønskede resultat. Vores kode er således mere deklarativ. Vi specificerer, hvad vi vil have, fremfor hvordan vi vil finde frem til resultatet i et sprog, der relaterer til vores problemdomæne.

Vi kan også let identificere antallet af nødvendige operationer, og derved har vi bedre mulighed for at identificere overflødige trin. I og med at vi arbejder med abstraktioner, er det derimod svært at sige noget om hvorvidt disse er implementeret optimalt, men med mindre vores målinger viser, at koden ikke kører optimalt, er vi sikkert bedre tjent med at bruge kode, der er testet og dokumenteret fremfor vores egen hjemmebryggede kode.

Hvis vi forestiller os, at result i stedet repræsenterer listen af kunder, der skal have et specielt tilbud. Således kunne numbers1 repræsentere de kunder, der interesserer sig for hi-fi, numbers2 kunne være listen af kunder, der interesserer sig for vvs-artikler, og numbers3 kunne holde styr på de kunder, der ikke har betalt til tiden. I så fald kunne vi bruge vores mængdeoperationer til at sende brev til relevante kunder omkring et nyt spabad med surround sound (hvis det ikke findes, skal det nok komme).

Koden ville essentielt være den samme, men den nuværende navngivning er helt i skoven. Koden taler om tal og mængder, og vi har således ingen forbindelse mellem koden og domænet. Så selvom .NET tilbyder os de nødvendige operationer, ville vi med fordel kunne indkapsle mængdeoperationerne i en eller flere metoder, hvis navne kan bygge bro til domænet.

Så hvad er lektien her?

Undgå udpenslende kode. Se om problemet kan udtrykkes på en måde, så vi kan bruge eksisterende funktionalitet. Hvis det ikke er tilfældet, så skab de nødvendige abstraktioner. I begge tilfælde bør abstraktioner navngives (evt. indpakkes) i konstruktioner, der giver mening i problemdomænet.

Følger vi disse trin, får vi kode, der er lettere at læse. Vi får muligvis mere kode, men hver del bliver kort, præcis og formuleret i et sprog, der passer til problemet. Vi får lettere ved at ræsonnere om kodens egenskaber, da den er lettere at overskue. Kode, der er let at forstå, er også lettere at vedligeholde og udvide.

Abstraktioner som typer og metoder er blandt af de vigtigste værktøjer i vores værktøjskasse, fordi de giver os mulighed for at opsplitte kompleksitet i overkommelige størrelser og navngive disse i et sprog, der afspejler vores problemdomæne. Brug dem!

En overraskelse og 1000+ doubles

Friday, July 23rd, 2010

Dette indlæg rummer egentlig ingen ny viden, men på trods af at jeg efterhånden har brugt lang tid på finurlige detaljer omkring .NETs afviklingsmiljø, er jeg først nu stødt på denne detalje, så set i det lys vil jeg tillade mig at betegne emnet for dette indlæg som esoterisk og gentage noget, der muligvis ikke er nyt for alle.

Som det forhåbentlig er kendt for læserne af denne blog, foregår dynamisk allokering af hukommelse i .NET enten i generation 0 af heapen eller på Large Object Heap (LOH) i fald den allokerede instans er på 85.000 bytes eller mere.

Derfor vil small pege på et array allokeret i generation 0 og large pege på et array allokeret på LOH i nedenstående eksempel.

var small = new byte[1000];
var large = new byte[85000];

Det kan vi verificere via WinDbg eller ved at kalde GC.GetGeneration(), der som forventet returnerer henholdsvis 0 og 2 for de to referencer (bemærk, at metoden ikke skelner mellem generation 2 og LOH, da de i forhold til garbage collection behandles i samme ombæring). Bruger vi debuggeren, får vi lidt mere nøjagtig information, og her kan vi se, at large faktisk peger på en instans på LOH.

Gentager vi øvelsen for double[], burde vi igen kunne forudsige placeringen af de enkelte instanser. En double fylder 8 bytes, så for at komme over den magiske grænse, skal vi have et array med lidt over 10.000 elementer, lad os bare sige 11.000. Med det in mente burde vi altså kunne konkludere at small og large endnu en gang peger på instanser i henholdsvis generation 0 og på LOH.

var small = new double[1000];
var large = new double[11000];

Det er bare ikke sådan det forholder sig. Begge arrays bliver allokeret på LOH!

CLRen benytter nemlig en forholdsvis esoterisk optimering i dette tilfælde. Arrays af double med 1000 eller flere elementer bliver mod forventning altid allokeret på LOH. Det er nyt for mig, men hvis man nærlæser kommentarerne til dette gamle blogindlæg, kan man se, at det er ”by design”. Det er altså ikke en fejl, det er en feature.

Argumentationen er, at objekter på LOH altid ligger på 8 bytes skel, og derfor giver bedre performance ved opslag af elementerne. Jeg har ikke kunne finde nogen forklaring på, hvorfor grænsen på 1000 elementer er valgt, men sådan forholder det sig nu engang.

Det er fristende at prøve, om vi kan eftervise effekten af denne optimering, men det er ikke let i praksis, da vi har meget begrænset kontrol over og indsigt i allokering og adresselæsninger i managed code, så derfor må vi tage Microsofts ord for pålydende her.

Konsekvenserne af denne optimering kan være mange og ikke alle nødvendigvis til vores fordel. Hvis vi antager, at optimeringen har sin berettigelse, så må vi gå ud fra, at læsning af elementerne i et double[], nyder gavn af den gunstige placering i hukommelsen.

Til gengæld er disse arrays pludselig dyrere at allokere, eftersom allokering på LOH benytter en free list, hvilket kan være mange gange langsommere end den simple pointeroperation, der skal til ved allokering i generation 0. Ligeledes vil levetiden af disse arrays stige markant. Er der tale om midlertidige arrays, går de fra hurtig oprydning i generation 0 til en lang levetid på LOH. Det kan have indflydelse på hvor stor belastning garbage collection lægger på applikationen, ligesom det kan give yderligere problemer i form af fragmentering af LOH.

Konsekvenserne afhænger af den konkrete applikations forbrug af hukommelse, men givet er det, at det kan være en særdeles vigtig detalje i nogle scenarier. Så med mange års forsinkelse gør jeg hermed mit til at udbrede kendskabet til en godt bevaret hemmelighed.

Visual Studio hosting process

Wednesday, March 24th, 2010

Som udgangspunkt sætter Visual Studio C#-projekter til at benytte en dedikeret værtsproces (vshost.exe) i forbindelse med debugging. Det forbedrer angiveligt hastigheden, giver mulighed for debugging af partial trust-applikationer, og tillader evaluering af kode på designtidspunktet.

Personligt har jeg aldrig kunne konstatere at vshost gør en forskel i forhold til hastigheden, men jeg vil da ikke udelukke, at der kan være tilfælde, hvor den har en positiv effekt.

Jeg må ligeledes indrømme, at jeg heller ikke rigtig har haft brug for at køre kode på designtidspunktet, når det kommer til C#. Bevares, jeg kan da sagtens se fidusen, men C# lægger ikke rigtig op til det, synes jeg.

Ergo er der kun debugging af partial trust-applikatikoner tilbage på vshosts featureliste. Det er heller ikke noget, jeg rigtig gør mig i, så for at gøre en lang historie kort, har vshost ikke rigtig noget at tilbyde mig. Det kan meget vel være, at du har andre behov, men hvis du ligesom jeg ikke rigtig får glæde af vshost, kommer her et par råd.

Heldigvis kan vi let slå denne feature fra: Gå ind under Project Properties, vælg Debug og fjern derefter hakket i Enable the Visual Studio hosting process.

Desværre gemmes denne indstilling per projekt, så hvis vi har mange projekter, kan det blive til en masse klikken med musen. I Visual Studio er projektdefinitioner blot XML-filer, så derfor er det ikke svært at ændre denne opsætning for mange projekter på en gang med en ordentlig editor eller lidt scripting.

Hver projektdefinition indeholder en PropertyGroup-sektion for hver build-konfiguration. For at slå brug af værtsprocessen fra, skal følgende indsættes for de relevante PropertyGroup-sektioner.

<UseVSHostingProcess>false</UseVSHostingProcess>

Det er desværre heller ikke muligt, at slå det fra for alle fremtidige projekter via Visual Studio, men da projekter baseres på en række skabeloner, kan vi rette disse og slå hosting processen fra for de relevante projekttyper en gang for alle. I Visual Studio 2008 ligger disse under

\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\ProjectTemplatesCache\CSharp\Windows\1033

Visual Studio 2010 følger samme fremgangsmåde (men versionsnummeret i stien er 10.0 i stedet for 9.0)

I denne mappe finder vi en undermappe per projektkategori og under disse ligger de enkelte skabelonfiler. På min maskine ser det ud som følger:

03-01-2010  01:30    <DIR>          ClassLibrary.zip
03-01-2010  01:31    <DIR>          ConsoleApplication.zip
03-01-2010  01:30    <DIR>          EmptyProject.zip
03-01-2010  01:31    <DIR>          WindowsApplication.zip
03-01-2010  01:31    <DIR>          WindowsControlLibrary.zip
03-01-2010  01:30    <DIR>          WindowsService.zip
03-01-2010  01:30    <DIR>          WPFApplication.zip
03-01-2010  01:31    <DIR>          WPFBrowserApplication.zip
03-01-2010  01:30    <DIR>          WPFControlLibrary.zip
03-01-2010  01:30    <DIR>          WPFCustomControl.zip

Hvis jeg vil ændre projektskabelonen for Windows Applications, skal jeg rette csproj-filen under WindowsApplications.zip som beskrevet ovenfor. Voila! Ikke mere værtsproces.

Omkostningerne ved værtsprocessen

Det er jo meget fint, at vi kan ændre dette, men der er vel en grund til, at denne proces er der, og hvis den ikke er til nogen skade, hvorfor så bruge tid på disse besværgelser? Som nævnt har værtsprocessen sin berettigelse i visse tilfælde.

Desværre har den også sine omkostninger. I nogle tilfælde har den dog ingen nævneværdig konsekvens. I de tilfælde er der ikke grund til at gøre noget.

I andre tilfælde kan værtsprocessen ligefrem stå i vejen for afvikling af applikationen fra Visual Studio.

Når vi kører vores applikation via værtsprocessen, kommer vi til at slæbe rundt på, hvad den end må have brug for. Den indlæser en bunke assemblies og starter indtil flere tråde. Det bruger ekstra hukommelse og mudrer generelt billedet af, hvad der sker, når applikationen kører.

I nogle tilfælde kan det tilmed være nok til at forhindre afvikling af applikationen via Visual Studio, som illustreret af dette spørgsmål på StackOverflow. I de tilfælde er det rart at kunne komme af med værtsprocessen.

WinDbg Q&A: Returværdier i WinDbg (anden del)

Friday, March 12th, 2010

I forrige indlæg viste jeg, hvordan vi får returværdien fra en metode ved hjælp af WinDbg. Denne gang skal vi se på en lidt mere kompliceret situation. Hvordan finder vi returværdien fra en anonym metode?

Givet nedenstående kode:

Func<int, int> f = x => x + 1;
Console.WriteLine(f.Invoke(1));

Hvordan finder vi returværdien for den metode, f repræsenterer?

Faktisk er problemstillingen ikke så kompliceret, så man måske skulle tro. Anonyme metoder er nemlig ikke spor anonyme, når det kommer til stykket. De er blot pakket ind i lidt compilerhekseri, så den første opgave er, at finde ud af hvilket navn den anonyme metode gemmer sig under.

Det kan vi læse ud af IL-koden, så lad os se nærmere på den for den omsluttende metode. I det her tilfælde er det Main(), så lad os se på den. For at se på IL-koden via WinDbg, har vi brug for Main()s MethodDesc. Den kan vi finde med !name2ee:

0:000> !name2ee * TestBench.Program.Main
Module: 6db11000 (mscorlib.dll)
--------------------------------------
Module: 00162c5c (TestBench.exe)
Token: 0x06000001
MethodDesc: 00163010
Name: TestBench.Program.Main()
JITTED Code Address: 00270070

MethodDesc for Main() er altså 00163010. Lad os se på IL for metoden.

0:000> !dumpil 00163010
ilAddr = 013c2064
IL_0000: nop
IL_0001: ldstr "press enter"
IL_0006: call System.Console::WriteLine
IL_000b: nop
IL_000c: call System.Console::ReadLine
IL_0011: pop
IL_0012: ldsfld TestBench.Program::CS$<>9__CachedAnonymousMethodDelegate1
IL_0017: brtrue.s IL_002c
IL_0019: ldnull
IL_001a: ldftn TestBench.Program::<Main>b__0
IL_0020: newobj class [System.Core]System.Func`2<int32,int32>::.ctor
IL_0025: stsfld TestBench.Program::CS$<>9__CachedAnonymousMethodDelegate1
IL_002a: br.s IL_002c
IL_002c: ldsfld TestBench.Program::CS$<>9__CachedAnonymousMethodDelegate1
IL_0031: stloc.0
IL_0032: ldloc.0
IL_0033: ldc.i4.1
IL_0034: callvirt class [System.Core]System.Func`2<int32,int32>::Invoke
IL_0039: call System.Console::WriteLine
IL_003e: nop
IL_003f: ret

Læg mærke til de spøjse navne. Det er navnene på henholdsvis den generede delegate type og den faktiske metode, der implementerer vores anonyme metode. Vores metode hedder altså TestBench.Program::

b__0. Lad os se nærmere på den:

0:000> !name2ee * TestBench.Program.<Main>b__0
Module: 6db11000 (mscorlib.dll)
--------------------------------------
Module: 00152c5c (TestBench.exe)
Token: 0x06000003
MethodDesc: 00153024
Name: TestBench.Program.<Main>b__0(Int32)
Not JITTED yet. Use !bpmd -md 00153024 to break on run.

Som det fremgår, er den endnu ikke oversat, så vi er nødt til at gå via et breakpoint på MD som foreslået i udskriften.

 0:000> !bpmd -md 00153024
MethodDesc = 00153024
Adding pending breakpoints...

Så er det blot at køre videre, til vi rammer vores breakpoint.

0:000> g
(1ad0.ecc): CLR notification exception - code e0444143 (first chance)
JITTED TestBench!TestBench.Program.<Main>b__0(Int32)
Setting breakpoint: bp 00260118 [TestBench.Program.<Main>b__0(Int32)]
Breakpoint 0 hit
eax=00153024 ebx=0038efdc ecx=00000001 edx=00000001 esi=006255f0 edi=00000000
eip=00260118 esp=0038efa0 ebp=0038efb0 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
00260118 55              push    ebp

Metoden er JIT-oversat, og vi er stoppet i begyndelsen af den. Som i første eksempel skal vi altså blot køre til slutningen og inspicere det korrekte register.

0:000> g $ra
eax=00000002 ebx=0038efdc ecx=00000001 edx=00000000 esi=006255f0 edi=00000000
eip=002600f0 esp=0038efa4 ebp=0038efb0 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
002600f0 8945fc          mov     dword ptr [ebp-4],eax ss:002b:0038efac=0038f1d8

0:000> ? $retreg
Evaluate expression: 2 = 00000002

Og der har vi vores returværdi. Havde metoden returneret long i stedet for int, skulle vi blot bruge $retreg64 i stedet.

WinDbg Q&A: Returværdier i WinDbg (første del)

Friday, March 12th, 2010

Et tilbagevendende spørgsmål fra min sidemand er ”hvordan ser man returværdien fra en funktion under debugging?” og kort tid efter kommer kommentaren, ”det kunne man da i C++”. Derefter følger lidt mumlen og banden.

Desværre er der ingen let måde at gøre det på, når det kommer til managed code. I hvert fald ikke fra Visual Studio. I WinDbg er det derimod ikke så svært, så lad os se på hvad, der skal til.

Returværdier afleveres i et register. Desværre er dette platformafhængig, men til alt held har WinDbg et såkaldt pseudoregister, der indkapsler denne forskellighed (faktisk er der to, et til 32 bit værdier og et til 64 bit værdier). For at se returværdien skal vi altså blot inspicere dette register på det rette tidspunkt.

Det relevante register sættes lige inden funktionen returnerer, og derfor er vi nødt til at stoppe afviklingen på dette tidspunkt. Heldigvis er der endnu et pseudoregister, der kan hjælpe os i den situation. Lad os se på et eksempel.

Betragt nedenstående klasse. Vi har en instansmetode, SomeMethod(), der returnerer en string eller mere specifikt en reference til en instans af string.

public sealed class SomeType {
   public string SomeMethod() {
      var date = DateTime.Now.ToShortDateString();
      var buffer = new StringBuilder();
      var line = new string('-', date.Length);
      buffer.AppendLine(line);
      buffer.AppendLine(date);
      buffer.AppendLine(line);
      return buffer.ToString();
   }
}

Vi desuden har følgende kode:

var st = new SomeType();
Console.WriteLine(st.SomeMethod());

og vi er interesseret i, at finde ud af hvad SomeMethod() returnerer. Lad os sætte WinDbg på sagen, og lad os desuden sige, at vi endnu ikke har kørt SomeMethod() andetsteds i koden, så den er endnu ikke blevet JIT-oversat.

Det første, vi skal gøre, er at finde method tabel (MT) for SomeType. Det kan vi gøre !name2ee eller via !dumpheap -type. Sidstnævnte kræver lidt mindre tastearbejde, så den snupper jeg.

0:003> !dumpheap -type SomeType
 Address       MT     Size
02652a78 002a3080       12
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
002a3080        1           12 TestBench.SomeType
Total 1 objects

Som forventet finder vi en instans. Af ovenstående kan vi se, at den relevante MT er 002a3080. Via den kan vi se metoder på vores type.

0:003> !dumpmt -md 002a3080
EEClass: 002a138c
Module: 002a2c5c
Name: TestBench.SomeType
mdToken: 02000004  (C:\dev\TestBench\TestBench\bin\x86\Debug\TestBench.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6dcd6a90   6db51248   PreJIT System.Object.ToString()
6dcd6ab0   6db51250   PreJIT System.Object.Equals(System.Object)
6dcd6b20   6db51280   PreJIT System.Object.GetHashCode()
6dd474c0   6db512a4   PreJIT System.Object.Finalize()
003e00d8   002a3078      JIT TestBench.SomeType..ctor()
002ac03d   002a306c     NONE TestBench.SomeType.SomeMethod()

Der har vi SomeMethod(). Bemærk, at JIT-kolonnen siger NONE, hvilket vil sige, at metoden ikke er blevet JIT-oversat. Det kan skyldes, at den enten ikke har været kaldt endnu eller at den er blevet inlined. I vores tilfælde er det fordi den endnu ikke er blevet kaldt, og derfor er den ikke blevet oversat.

Læg mærke til Entry og MethodDesc-kolonnerne. Hvis metoden er oversat, står adressen på den oversatte version i Entry-kolonnen. Er den ikke oversat, skal vi i stedet sætte et breakpoint via MethodDesc. Da vores metode ikke er oversat, er det fremgangsmåden i dette tilfælde. Det gøres som følger:

0:003> !bpmd -md 002a306c
MethodDesc = 002a306c
Adding pending breakpoints...

Herefter kører vi videre, indtil vi rammer vores breakpoint.

0:003> g
(1638.fc4): CLR notification exception - code e0444143 (first chance)
JITTED TestBench!TestBench.SomeType.SomeMethod()
Setting breakpoint: bp 003E0110 [TestBench.SomeType.SomeMethod()]
Breakpoint 0 hit
eax=002a306c ebx=001af3ac ecx=02652a78 edx=00000000 esi=005855f0 edi=00000000
eip=003e0110 esp=001af370 ebp=001af380 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
003e0110 55              push    ebp

Applikationen er nu stoppet ved indgangen til SomeMethod(), og hvis vi kører !dumpmt igen som ovenfor, kan vi se, at metoden ganske rigtig er blevet JIT-oversat.

Hvis vi blot er interesseret i returværdien, er det eneste, vi behøver at gøre, at fortsætte afviklingen til vi rammer returadressen. Herefter vil vores returværdi være i det relevante register.

Vi kan fortsætte afviklingen og stoppe ved returadressen via g-kommandoen og pseudoregisteret $ra, som indeholder returadressen. Altså:

0:000> g $ra
C:\Windows\assembly\NativeImages_v2.0.50727_32\mscorlib\8c1770d45c63cf5c462eeb945ef9aa5d\mscorlib.ni.dll
eax=02655d04 ebx=001af3ac ecx=00000001 edx=00000000 esi=005855f0 edi=00000000
eip=003e00b7 esp=001af374 ebp=001af380 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
003e00b7 8945f4          mov     dword ptr [ebp-0Ch],eax ss:002b:001af374=001af390

Så er vi ved udgangen af metoden, og skal altså blot udskrive returværdien. Vi ved, at det er en string, så returværdien er en reference til et objekt. Lad os skrive det ud:

0:000> !do $retreg
Name: System.String
MethodTable: 6dd788c0
EEClass: 6db3a498
Size: 146(0x92) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: ----------
12-03-2010
----------

Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6dd7ab0c  4000096        4         System.Int32  1 instance       65 m_arrayLength
6dd7ab0c  4000097        8         System.Int32  1 instance       36 m_stringLength
6dd795a0  4000098        c          System.Char  1 instance       2d m_firstChar
6dd788c0  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00582618:02651198 <<
6dd794f0  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00582618:02651768 <<

Voila! Et styk string fra vores metode.

Hallo debugger! Er du der?

Tuesday, March 9th, 2010

Debugger-klassen i System.Diagnostics, har en property ved navn IsAttached, der ikke overraskende kan fortælle os, om vores proces kører under en debugger. Hjælpeteksten nævner dog ikke noget om, at der skal være tale om en managed debugger. Det vil sige, at IsAttached f.eks. ikke opdager tilstedeværelsen af WinDbg.

Heldigvis er der et Win32-kald, der kan fortælle os om en native debugger er til stede. For at få svar på det, skal følgende kodestump kaldes:

[DllImport("kernel32.dll")]
static extern bool IsDebuggerPresent()

Desværre insisterer dette kald på, at det skal være en native debugger, så den opdager ikke Visual Studio, hvis der er tale om debugging af en .NET applikation.

Flere gode debugging-nyheder i CLR 4

Wednesday, February 24th, 2010

Læser man hjælpen til den nye version af SOS, er det første man lægger mærke til nok de mange nye kommandoer, men der er faktisk også meget interessante opdateringer til den eksisterende funktionalitet.

Hjælpeteksten til !u-kommandoen er således blevet opdateret med følgende lille kommentar:

If the debugger has the option SYMOPT_LOAD_LINES specified (either by the.lines or .symopt commands), and if symbols are available for the managed module containing the method being examined, the output of the command will include the source file name and line number corresponding to the disassembly.

Vi har med andre ord fået en smule af funktionaliteten fra den legendariske version 6.7.5 tilbage. Det er desværre ikke en komplet løsning, men det er i hvert fald et seriøst skridt i den rigtige retning.

Heldigvis er det ikke kun !u, der er blevet opdateret. !clrstack viser nu også linjenumre, hvis ovenstående betingelser er på plads. Vi har således fået meget bedre muligheder, for at illustrere sammenhængen den kørende applikation og kildeteksten. Lad os se på et eksempel.

static void SomeMethod(int i) {
    SomeSubMethod(i + 1);
}

static void SomeSubMethod(int i) {
    var text = string.Format("The answer is {0}", i);
    Console.WriteLine(text);
    Console.ReadLine();
}

static void Main() {
    SomeMethod(41);
}

Sætter vi WinDbg på ved Console.ReadLine(), og udskriver stakken for den relevante tråd, får vi følgende:

0:000> !clrstack
OS Thread Id: 0x1b00 (0)
Child SP         IP               Call Site
000000000019e648 00000000772c00da [NDirectMethodFrameStandalone: 000000000019e648] System.IO.__ConsoleStream.ReadFile(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
000000000019e5f0 000007feeb1d6311 DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30128_64\mscorlib\efe2adda88bca3a9f9bf9cc89514729b\mscorlib.ni.dll

000000000019e710 000007feeb980e7a System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Int32, Int32 ByRef)
000000000019e780 000007feeb980ce2 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
000000000019e7e0 000007feeb19601c System.IO.StreamReader.ReadBuffer()
000000000019e830 000007feeb194c40 System.IO.StreamReader.ReadLine()
000000000019e890 000007feeb988d90 System.IO.TextReader+SyncTextReader.ReadLine()
000000000019e8f0 000007ff0015023e TestApp.Program.SomeSubMethod(Int32)
 [C:\dev2010\TestApp\TestApp\Program.cs @ 18]
000000000019e950 000007ff00150199 TestApp.Program.SomeMethod(Int32) [C:\dev2010\TestApp\TestApp\Program.cs @ 12]
000000000019e980 000007ff00150144 TestApp.Program.Main() [C:\dev2010\TestApp\TestApp\Program.cs @ 22]
000000000019ed70 000007feec05cc74 [GCFrame: 000000000019ed70]

Læg mærke til fil- og linjeangivelserne for Main(), SomeMethod() og SomeSubMethod().

Via instruktionspointeren, kan vi vise den oversatte kode for metoderne. Lad os se nærmere på SomeSubMethod().

0:000> !U  000007ff0015023e
Normal JIT generated code
TestApp.Program.SomeSubMethod(Int32)
Begin 000007ff001501c0, size 8b

C:\dev2010\TestApp\TestApp\Program.cs @ 15:
000007ff`001501c0 894c2408        mov     dword ptr [rsp+8],ecx
000007ff`001501c4 4883ec58        sub     rsp,58h
000007ff`001501c8 48c744242000000000 mov   qword ptr [rsp+20h],0
000007ff`001501d1 48b8d8340300ff070000 mov rax,7FF000334D8h
000007ff`001501db 8b00            mov     eax,dword ptr [rax]
000007ff`001501dd 85c0            test    eax,eax
000007ff`001501df 7405            je      000007ff`001501e6
000007ff`001501e1 e88ada33ec      call    clr!JIT_DbgIsJustMyCode (000007fe`ec48dc70)
000007ff`001501e6 90              nop

C:\dev2010\TestApp\TestApp\Program.cs @ 16:
000007ff`001501e7 48b85030591200000000 mov rax,12593050h
000007ff`001501f1 488b00          mov     rax,qword ptr [rax]
000007ff`001501f4 4889442430      mov     qword ptr [rsp+30h],rax
000007ff`001501f9 8b442460        mov     eax,dword ptr [rsp+60h]
000007ff`001501fd 89442428        mov     dword ptr [rsp+28h],eax
000007ff`00150201 488d0d888a15eb  lea     rcx,[mscorlib_ni+0x4f8c90 (000007fe`eb2a8c90)]
000007ff`00150208 488d542428      lea     rdx,[rsp+28h]
000007ff`0015020d e89e29edeb      call    clr!JIT_BoxFastMP_InlineGetThread (000007fe`ec022bb0)
000007ff`00150212 488bd0          mov     rdx,rax
000007ff`00150215 488b4c2430      mov     rcx,qword ptr [rsp+30h]
000007ff`0015021a e8116a01eb      call    mscorlib_ni+0x3b6c30 (000007fe`eb166c30) (System.String.Format(System.String, System.Object), mdToken: 0000000001C9C090)
000007ff`0015021f 4889442438      mov     qword ptr [rsp+38h],rax
000007ff`00150224 488b442438      mov     rax,qword ptr [rsp+38h]
000007ff`00150229 4889442420      mov     qword ptr [rsp+20h],rax

C:\dev2010\TestApp\TestApp\Program.cs @ 17:
000007ff`0015022e 488b4c2420      mov     rcx,qword ptr [rsp+20h]
000007ff`00150233 e8f80903eb      call    mscorlib_ni+0x3d0c30 (000007fe`eb180c30) (System.Console.WriteLine(System.String), mdToken: 0000000001C9C090)
000007ff`00150238 90              nop

C:\dev2010\TestApp\TestApp\Program.cs @ 18:
000007ff`00150239 e862886beb      call    mscorlib_ni+0xa58aa0 (000007fe`eb808aa0) (System.Console.ReadLine(), mdToken: 0000000001C9C090)
>>> 000007ff`0015023e 4889442440      mov     qword ptr [rsp+40h],rax
000007ff`00150243 90              nop

C:\dev2010\TestApp\TestApp\Program.cs @ 19:
000007ff`00150244 eb00            jmp     000007ff`00150246
000007ff`00150246 4883c458        add     rsp,58h
000007ff`0015024a c3              ret

Som sædvanlig får vi den JIT-oversatte kode tilsat CLR-kommentarer, men læg mærke til, at de enkelte kodeblokke nu indledes med en reference til den relevante linje i kildeteksten. Det gør det meget lettere, at skabe sammenhæng mellem den JIT-oversatte maskinekode og den oprindelige kildetekst.

Men vent! Der er mere!

Yderligere granskning af hjælpeteksten opsporer følgende lille, interessante notits i slutningen af FAQ-sektionen.

Does SOS support DML?
Yes. SOS respects the .prefer_dml option in the debugger. If this setting is turned on, then SOS will output DML by default. Alternatively, you may leave it off and add /D to the beginning of a command to get DML based output for it. Not all SOS commands support DML output.

Så SOS understøtter nu DML. Fint, men hvad er pokker DML?

DML står for debugger markup language. Det er ikke en ny feature, den har været der siden version 6.6.07, men det er en forholdsvis velbevaret hemmelighed. Således er der kun en enkelt reference til DML i hjælpeteksten og ingen uddybning. Den eneste information, jeg har fundet, er et dokument ved navn dml.doc i WinDbg installationsbibliotek.

Nyheden er altså ikke DML i sig selv, men at SOS nu understøtter DML.

Heldigvis kræver DML ikke den store forklaring. I en nøddeskal er DML et simpelt markup-sprog, der kan bruges til at tilføre yderligere information til resultatet af debugger-kommandoer. Som FAQen nævner, er flere af SOS-kommandoerne blevet udstyret med DML, så hvis vi slår DML-visning til, får vi altså yderligere funktionalitet i form af links til relevante kommandoer i forhold til det aktuelle output.

Kører vi eksempelvis !dumpobj, får vi, som vist nedenfor, links til flere relaterede opslag. Det gør en hel del arbejdsgange meget lettere.

DML i SOS

Effekten af de enkelte links kan aflures nederst til venstre i WinDbgs statusbar.

Udokumenterede kommandoer og forkortelser

Udover den umiddelbare reduktion af tastearbejdet giver DML også indblik i yderligere features. Jeg opdagede således .extmatch-kommandoen, da jeg kørte .chain med DML slået til.

.extmatch viser alle eksporterede kommandoer fra en given extension. Kører vi den på SOS, dukker der adskillige udokumenterede kommandoer op som f.eks. HandleCLRN, SOSFlush, VerifyStackTrace og WatsonBuckets. Uden dokumentation er det svært, at få noget fornuftig ud af disse. SOSFlush er godt nok beskrevet på MSDN, men det er ikke fordi, jeg blev meget klogere af det.

Derudover fandt jeg også en del udokumenterede forkortelser, jeg ikke kendte. De er som følger:

!da for !dumparray
!hof for !histobjfind
!t for !threads
!tp for !threadpool
!vh for !verifyheap
!vo for !verifyobj

Det eneste problem, jeg har opdaget med DML indtil videre, er, at mdTokens af en eller anden grund linker til !u med et ugyldigt -md-parameter. Jeg håber, at det bliver rettet inden release.