Archive for the ‘WinDbg Q&A’ Category

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.

WinDbg Q&A: Hvorfor er der så mange instanser af string og object[] på heapen?

Wednesday, October 28th, 2009

Når vi debugger .NET applikationer med WinDbg, vil vi almindeligvis have brug for at inspicere heapen med !dumpheap-kommandoen. Den viser antallet af instanser af de forskellige typer på heapen. Listen er sorteret efter summen af størrelsen for instanserne af de enkelte typer på heapen. Dog skal det bemærkes, at der er tale om selve typernes størrelse, så størrelsen af en type med referencer inkluderer altså ikke størrelsen på de refererede objekter.

Det vil typisk sige, at typer med mange instanser vil være at finde mod slutningen af listen, og blandt dem finder vi meget ofte string og object[], men hvorfor er der så mange af disse i stort set alle .NET applikationer?

String

Lad os begynde med string, for her er forklaringen oplangt. For det første opretter CLRen en hel del strings til diverse konstanter, stier og så videre og jo flere assemblies vi refererer, desto flere af disse får vi med i bagagen. Dertil kommer at instanser af string som bekendt er immutable, så hvis applikationen laver en eller anden form for string-behandling, skaber det hurtig mange instanser. Husk at alle konstante tekster samt alle konstruerede tekster havner på heapen, så derfor løber det hurtig op. Med mindre der er tale om meget store instanser, er det sjældent værd at bekymre sig om.

Object[]

Antallet af string-instanser kan virke overvældende, men når vi tænker over det, er det i virkeligheden ikke så underligt, men hvad med object[]?

Var vi ikke lige blevet enige om, at generics og typestærke collections er sådan en god ide? Hvorfor skulle vi så bruge object[]? Og hvis det ikke er vores applikation, der bruger alle disse forældede collections, er det så Microsofts klasser, der endnu ikke er blevet opdateret?

Forklaringen er faktisk mere subtil end som så. Den underliggende type af List<T> er faktisk object[]. Hvordan kan det passe? Når vi fyrer op under Reflector, kan vi jo tydelig se, at List<T> benytter T[] til opbevaring af elementerne. Så hvis vi har List<string>, får vi vel string[] som den underliggende type. Ja, og nej.

Den underliggende type af List<string> er ganske rigtig string[], men måden CLRen implementerer arrays for referencetyper er ved hjælp af object[]. Lad os se et eksempel. Hvis vi har følgende i Main(), kan vi let finde den aktuelle List<string>-instans ved hjælp af !dso.

var Simpsons = new List<string> {
   "Homer", "Marge", "Bart", "Lisa", "Maggie"
};
0:000> !dso
OS Thread Id: 0x1390 (0)
ESP/REG  Object   Name
0029ed28 01ea2b78 Microsoft.Win32.SafeHandles.SafeFileHandle
0029ed38 01ea2b78 Microsoft.Win32.SafeHandles.SafeFileHandle
0029ed6c 01ea42ac System.Byte[]
0029ed70 01ea2b8c System.IO.__ConsoleStream
0029ed90 01ea4034 System.IO.StreamReader
0029ed94 01ea4034 System.IO.StreamReader
0029edac 01ea4034 System.IO.StreamReader
0029edb0 01ea45c4 System.IO.TextReader+SyncTextReader
0029edd0 01ea45c4 System.IO.TextReader+SyncTextReader
0029ede0 01ea2af4 System.Collections.Generic.List`1[[System.String, mscorlib]] <=== HER!
0029eea4 01ea2a38 System.Object[]    (System.String[])
0029f050 01ea2a38 System.Object[]    (System.String[])
0029f078 01ea2a38 System.Object[]    (System.String[])

Vores instans findes på adressen 01ea2af4, så lad os se nærmere på den.

0:000> !do 01ea2af4
Name: System.Collections.Generic.List`1[[System.String, mscorlib]]
MethodTable: 5f5f58c8
EEClass: 5f3abf0c
Size: 24(0x18) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5f5c4eec  40009d8        4      System.Object[]  0 instance 01ea2b3c _items
5f5eab0c  40009d9        c         System.Int32  1 instance        5 _size
5f5eab0c  40009da       10         System.Int32  1 instance        5 _version
5f5e84dc  40009db        8        System.Object  0 instance 00000000 _syncRoot
5f5c4eec  40009dc        0      System.Object[]  0   shared   static _emptyArray

WinDbg reporterer korrekt, at vi har fat i List<string>, men læg også mærke til, at vi blandt Fields finder _items af typen Object[]. Er det vores data? Lad os se nærmere på indholdet af _items.

0:000> !dumparray -details -nofields 01ea2b3c
Name: System.String[]
MethodTable: 5f5c4eec
EEClass: 5f3aa8a0
Size: 48(0x30) bytes
Array: Rank 1, Number of elements 8, Type CLASS
Element Methodtable: 5f5e88c0
[0] 01ea2a64
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 28(0x1c) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Homer
[1] 01ea2a80
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 28(0x1c) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Marge
[2] 01ea2a9c
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 26(0x1a) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Bart
[3] 01ea2ab8
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 26(0x1a) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Lisa
[4] 01ea2ad4
    Name: System.String
    MethodTable: 5f5e88c0
    EEClass: 5f3aa498
    Size: 30(0x1e) bytes
     (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    String:     Maggie
[5] null
[6] null
[7] null

Der er vist ingen tvivl om, at det object[] indeholder navnene på vores gule familie, men hvor er sammenhængen mellem List<string> og det underliggende object[]? Hvordan ved CLRen, at vores array indeholder instanser af string?

Lad os først få en ting på det rene. Referencer er referencer, så der er ingen forskel på en reference til en instans af object og en reference til en instans af string. Det er blot referencer, så der er intet galt i, at CLRen bruger et object[] til at gemme referencerne, men hvordan holder den styr på det faktiske indhold?

Ovenstående dump giver ikke nogen svar på dette spørgsmål. SOS kan give indtrykket af, at vi ser det hele, men faktisk pakker SOS et par implementeringsdetaljer ind og viser os dem på anden vis. Vi er altså nødt til at gå uden om SOS, for at få svar på det spørgsmål.

Dumper vi hukommelsen for vores object[], får vi de manglende brikker.

0:000> dd 01c62b3c -4 01c62b3c -4 +0x30 -1
01c62b38  00000000 5f5c4eec 00000008 5f5e88c0
01c62b48  01c62a64 01c62a80 01c62a9c 01c62ab8
01c62b58  01c62ad4 00000000 00000000 00000000

Den noget kryptiske kommando kalder vist på en forklaring. Den instansadresse SOS reporterer, er faktisk forskudt med et enkelt word. Det vil sige, at data for en instans begynder 4 bytes før den viste adresse (på 32 bit forstås). Så for at udskrive indholdet af instansen er vi nødt til at gå 4 bytes tilbage og udskrive til og med instansadressen, minus de fire bytes, plus størrelsen af instansen minus en. Det giver os data for vores object[].

Det første word er instansens SyncBlock, derefter følger instansen egen Method Table, så kommer længden at arrayet (8 i dette tilfælde), og herefter kommer det interessante: Hvad gemmer der sig bag 5f5e88c0? At dømme på værdien kunne det være en Method Table, så lad os se om det skulle være tilfældet.

0:000> !dumpmt 5f5e88c0
EEClass: 5f3aa498
Module: 5f381000
Name: System.String
mdToken: 02000024  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 7
Slots in VTable: 196

Det er en Method Table for string og dermed har vi kædet vores object[] sammen med den aktuelle type. De efterfølgende otte words er referencerne til vores fem string-instanser, samt tre tommer pladser til yderligere string-referencer.

Det efterlader blot et spørgsmål: Hvis object[] i realiteten er T[], hvad indeholder det mystiske felt så for et rigtig object array? Method Table for object naturligvis og så passer pengene.

WinDbg Q&A: Hvordan udtrækker man kode fra en dumpfil?

Sunday, October 4th, 2009

Under normale omstændigheder har vi adgang til såvel kildetekst som oversatte moduler, når vi debugger, men i nogle situationer er det eneste, vi kan få adgang til, en dumpfil.

Har vi brug for at sammenholde dumpfilen med kode, kan det derfor se lidt sort ud, men eftersom et full dump jo indeholder en komplet kopi af processens adresserum, rummer den også den nødvendige MSIL. Den kan vi inspicere ved hjælp af WinDbg og SOS.

Dump af IL

Den letteste måde at se kode fra en dumpfil er via !dumpil-kommandoen. Den kræver, at vi har en reference til en MethodDesc for den relevante metode. Så hvis vi f.eks. har en exception som vist nedenfor, kan vi bruge !dumpil, til at se koden, der smider denne exception.

0:000> !pe
Exception object: 01b06064
Exception type: System.NotSupportedException
Message: Specified method is not supported.
InnerException: <none>
StackTrace (generated):
    SP       IP       Function
    0028F130 00750126 ClassLibrary1!ClassLibrary1.Class1.Foo()+0x36
    0028F13C 007500B1 TestBench!TestBench.Program.Main(System.String[])+0x41
StackTraceString: <none>
HResult: 80131515

Ifølge ovenstående er det Foo()-metoden i Class1, der smider en NotSupportedException. For at udskrive implementeringen af Foo(), har vi brug for en MethodDesc for metoden. Den kan vi finde med !name2ee som følger.

0:000> !name2ee ClassLibrary1!ClassLibrary1.Class1.Foo
Module: 001630bc (ClassLibrary1.dll)
Token: 0x06000001
MethodDesc: 00163450
Name: ClassLibrary1.Class1.Foo()
JITTED Code Address: 007500f0

Her kan vi se, at den relevante MethodDesc er 00163450. Læg også mærke til, at metoden er JIT-oversat, så hvis vi har brug for at inspicere den oversatte kode, kan vi disassemble fra ovenstående adresse. Det har vi dog ikke brug for i denne sammenhæng, så lad os springe det over.

Ved at kalde !dumpil med Foo()s MethodDesc, får vi følgende:

0:000> !dumpil 00163450
ilAddr = 73532050
IL_0000: ldstr "Foo"
IL_0005: call System.Console::WriteLine
IL_000a: newobj System.NotSupportedException::.ctor
IL_000f: throw

I dette tilfælde er MSIL-koden såre simpel. Den udskriver Foo og kaster derefter en exception. Desværre er kode sjældent så simpel, og hvis man ikke er stærk i MSIL, er det måske ikke den bedste mulighed. Fremgangsmåden er heller ikke særlig fleksibel, hvis vi har brug for at se på mange metoder.

IL for hele modulet

Hvis vi gerne vil have vist MSIL som C#, kan vi som bekendt bruge Reflector, men kræver det ikke, at vi har det eller de relevante assemblies? Jo, det gør det, men det har vi jo også, eftersom de er indlæst i vores proces.

Derfor skal vi blot pille den relevante del af vores dump ud, gemme det som et DLL og indlæse dette i Reflector. For at gøre det, skal vi først finde ud af, hvor det relevante modul befinder sig i processen. Det kan vi gøre med lm-kommandoen som følger:

0:000> lm vm ClassLibrary1
start    end        module name
73530000 73538000   ClassLibrary1   (deferred)
    Image path: c:\Dev\TestBench\TestBench\bin\Release\ClassLibrary1.dll
    Image name: ClassLibrary1.dll
    Has CLR image header, track-debug-data flag not set
    Timestamp:        Sun Oct 04 13:39:23 2009 (4AC8896B)
    CheckSum:         00000000
    ImageSize:        00008000
    File version:     1.0.0.0
    Product version:  1.0.0.0
    File flags:       0 (Mask 3F)
    File OS:          4 Unknown Win32
    File type:        2.0 Dll
    File date:        00000000.00000000
    Translations:     0000.04b0
    ProductName:      ClassLibrary1
    InternalName:     ClassLibrary1.dll
    OriginalFilename: ClassLibrary1.dll
    ProductVersion:   1.0.0.0
    FileVersion:      1.0.0.0
    FileDescription:  ClassLibrary1
    LegalCopyright:   Copyright ©  2009

Læg mærke til startadressen øverst. Det er den, vi skal bruge. Med den kan vi nemlig gemme modulet som følger:

0:000> !savemodule 73530000 c:\temp\ClassLibrary1.dll
3 sections in file
section 0 - VA=2000, VASize=6c4, FileAddr=200, FileSize=800
section 1 - VA=4000, VASize=328, FileAddr=a00, FileSize=400
section 2 - VA=6000, VASize=c, FileAddr=e00, FileSize=200

Og nu kan vi så indlæse vores DLL i Reflector og se den tilhørende MSIL som C#. Voila!

public class Class1
{
    // Methods
    public void Foo()
    {
        Console.WriteLine("Foo");
        throw new NotSupportedException();
    }
}

Jeg indrømmer gerne, at koden i dette tilfælde er ret uinteressant, men den fungerer fint til at illustrere teknikken. Pointen er, at hvis vi har et dump, har vi gode muligheder for at inspicere såvel kode som data.

WinDbg Q&A: Brug WinDbg med andre værktøjer

Tuesday, August 18th, 2009

Der er en del WinDbg-kommandoer, der producerer et ganske omfattende output. Nogle kommandoer understøtter diverse filtreringsmuligheder, men der er desværre ingen standard på tværs af kommandoerne. Heldigvis er det dog let at lade andre værktøjer som grep, Perl eller deslige komme på banen, og derved få udvidet værktøjskassen ganske betragteligt. I dette indlæg skal vi se på hvordan.

grep i output

!dumpheap giver os mulighed for at lokalisere specifikke typer ved hjælp af -type. Desværre understøttes wild cards ikke og sammenligningen er tilmed case sensitive. Har vi brug for lidt mere fleksibel filtrering, ville det således være en ide at sende output fra !dumpheap videre til en passende grep.

.shell -ci kommandoen udfører en eller flere WinDbg-kommandoer og sender resultatet af disse videre til en ekstern proces, der derefter kan gøre behandle data. Output fra den eksterne proces dirigeres tilbage til Command-vinduet i WinDbg. Vi kan således bruge denne konstruktion til at filtrere output fra !dumpheap ved hjælp af grep.

Lad os sige, at vi leder efter alle typer, hvor ordet Set indgår, og vi er ligeglade med, om Set er med stort S eller ej. Det kan gøres med følgende kommando (forudsat at grep er tilgængelig).

0:003> .shell -ci "!dumpheap -stat" grep -i set

Læg mærke til at WinDbg-kommandoen er i citationstegn, og der er ikke noget pipe-tegn, som det kendes fra diverse shells, men ellers er syntaksen let gennemskuelig.

Et mere avanceret eksempel

For nylig havde jeg brug for at finde den samlede størrelse af alle instanser af en bestemt type. Kalder vi !objsize uden argumenter, udskriver den størrelsen på alle de objekter, der er direkte refereret af managed tråde. I nogle tilfælde vil det være nok, men i mit tilfælde havde jeg brug for at finde instanser af en type, der kun var indirekte refereret og dermed ikke inkluderet i output fra !objsize.

Opgaven bestod derfor i at kalde !objsize på hver enkelt adresse for de mange instanser. Kalder vi !dumpheap med -short, reduceres det normale output til en lang liste af adresser for de listede instanser, og med .foreach-kommandoen kan vi få kaldt !objsize for hver af disse. Den indledende øvelse så derfor ud som følger (jeg havde i forvejen fundet den ønskede mt):

.foreach ( x { !dumpheap -mt 00364ea8 -short } ) { !objsize x }

.foreach erklærer en variabel, som jeg i dette tilfælde har kaldt x. Variablen løber igennem resultaterne af den første kommando i krøllede parenteser, og vi kan efterfølgende referere til indholdet af variablen i den anden kommando (ligeledes i krøllede parenteser). Ovenstående finder derfor alle instanser med den angivne method table og bruger disse som input til !objsize. Resultatet er en lang liste af størrelser som vist i uddrag nedenfor:

sizeof(01961ad8) =         8240 (      0x2030) bytes (System.Linq.Set`1[[System.Int32, mscorlib]])
sizeof(01961b3c) =         8240 (      0x2030) bytes (System.Linq.Set`1[[System.Int32, mscorlib]])
sizeof(01961b58) =         8240 (      0x2030) bytes (System.Linq.Set`1[[System.Int32, mscorlib]])
sizeof(01961b74) =         8240 (      0x2030) bytes (System.Linq.Set`1[[System.Int32, mscorlib]])
…

Størrelsen er angivet i decimal efter lighedstegnet og i hex i den efterfølgende parentes, så for at finde summen, skal vi blot grave værdierne ud og lægge dem sammen.

Desværre har WinDbg ikke indbygget tekstsøgning, så derfor er vi nødt til at bruge andre værktøjer. En opgave som denne er jo ingen sag for et lille script. Jeg bruger som regel Perl til den slags, men Awk, Python og mange andre ville være lige så gode til formålet.

Som vi allerede har set, er det let at sende output fra en WinDbg-kommando videre til en ekstern applikation. Jeg vil derfor gerne kunne gøre noget i retning af:

.shell -ci ".foreach ( x { !dumpheap -mt 00364ea8 -short } ) { !objsize x }" objsize_sum

Hvor objsize_sum er et Perl-script. For at kunne gøre det, er vi nødt til at have et par ting på plads.

Først og fremmest skal Perl naturligvis være installeret på maskinen, og både Perls bin directory og placeringen af scriptet skal være i vores PATH. For at kalde Perl-scripts på denne måde, skal en passende Perl extension være registreret i listen af extensions for filer, der kan afvikles. Vi skal med andre ord have .pl tilføjet til PATHEXT.

Så mangler vi bare et lille Perl-script til at gøre det egentlige arbejde. Mit ser ud som følger:

c:\scripts>cat objsize_sum.pl

while(<>) {
   ($size) = /=\s*(\d+) /;
   $sum += $size;
   $count++;
}
printf("Total size of %d instances is %.2f MB.\n\n", $count, $sum / 1024 / 1024);

Herefter kan jeg kalde mit script som vist ovenfor og som forventet, får jeg den samlede størrelse udskrevet i WinDbg Command-vinduet.

Ovenstående synes måske en anelse omfattende, men størstedelen af arbejdet skal kun gøres første gang. Herefter er den nødvendige struktur på plads, og vi kan løbende tilføje nye scripts til vores WinDbg-værktøjskasse ved blot at placere dem i den korrekte mappe og kalde dem via .shell -ci.

WinDbg Q&A: Hvordan finder jeg instanser af en bestemt referencetype i hukommelsen?

Tuesday, May 12th, 2009

Dette er første indlæg i en serie, jeg kalder WinDbg Q&A. Ideen er at tage udgangspunkt i små, konkrete opgaver og beskrive, hvordan vi løser dem i WinDbg. Formålet er således ikke at beskrive komplette debug-scenarier, men i stedet at opbygge en samling af teknikker, der kan bruges på forskellige problemstillinger. 

Jeg har et par spørgsmål under forberedelse, men jeg modtager meget gerne forslag til yderligere, så hvis der er noget, du godt kunne tænke dig at få lidt hjælp til, er det bare at råbe op. 

Så uden yderligere sniksnak vil jeg tage hul på det første spørgsmål:

Hvordan finder jeg instanser af en bestemt referencetype i hukommelsen?

Det første, vi skal gøre os klart i den sammenhæng, er, at .NET opererer med to former for typer: referencetyper og værdityper. Den grundlæggende forskel er, at variable til referencetyper indeholder en reference til den egentlige datainstans, det vi typisk omtaler som objekter, mens variable til værdityper lagrer den egentlige værdi. Den væsentlige forskel i denne sammenhæng er dog, at referencetyper altid placeres på heapen, mens værdityper kun placeres på heapen, hvis de er en del af en referencetype. I alle andre tilfælde placeres de på stakken for den relevante tråd. I dette spørgsmål ser vi på referencetyper, da det typisk er dem, vi har brug for at undersøge i forbindelse med debugging. 

For at illustrere hvordan referencetyper opfører sig under afvikling, bruger vi følgende, lille stump kode. Jeg har indsat en lille pause, så vi har et passende sted at attache WinDbg, og læg også mærke til, at jeg refererer rt efter pausen. Derved forhindrer vi release modes aggressive oprydning i at nedlægge referencen til vores instans. 

class Program {
      static void Main(string[] args) {
         var rt = new ReferenceType("hello world", 42);

         Console.WriteLine("Press enter");
         Console.ReadLine();
         Console.WriteLine(rt);
      }
   }

   public sealed class ReferenceType {
      private string Text;
      private int Number;

      public ReferenceType(string t, int n) {
         Text = t;
         Number = n;
      }

      public override string ToString() {
         return string.Format("text: {0}, number: {1}", Text, Number);
      }
   }

Referencetyper opbevares som nævnt altid på heapen, så ved at gennemsøge denne, kan vi finde den eller de instanser, vi leder efter. Vi kan udskrive indholdet af heapen med sos-kommandoen !dumpheap

Kører vi !dumpheap uden argumenter, lister WinDbg alle objekter på heapen samt en oversigt over, hvordan disse fordeler sig på forskellige typer. For hver instans udskrives adressen i hukommelsen, den såkaldte method table (kaldet MT) og typens størrelse. Oversigten for neden viser hvilke typer, der er repræsenteret på heapen, hvor mange instanser der er af hver, samt hvad typerne fylder tilsammen. 

Desværre er heapen ganske omfattende selv for en simpel applikation. Det skyldes, at ud over de objekter vores applikation bruger og derfor gemmer på heapen, anvender CLRen ligeledes heapen til et væld af instanser, så heapen er aldrig “tom”. Vi har derfor brug for at kunne indsnævre vores søgning. 

For at liste alle instanser af en bestemt type, har vi brug for typens method table (MT). Den kan vi finde ved et opslag med !name2ee, men det er typisk lettere at bruge !dumpheap med flaget -type, der tillader os at filtrere på typer, hvis navn matcher en given tekst. Vi kan derfor køre nedenstående kommando for at finde vores instans af ReferenceType på heapen.

0:003> !dumpheap -type ReferenceType
 Address       MT     Size
02802d80 00913130       16     
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
00913130        1           16 WinDbgQnA.ReferenceType
Total 1 objects

Øverst ser vi addresserne på alle instanser af typer, der matcher vores type, og nederst finder vi oversigten. I dette tilfælde har vi indsnævret det til, at vi kun får udskrevet en type (og der er tilmed kun en instans), men ellers vil udskriftet almindeligvis være mere omfattende. 

Vi behøver ikke at skrive typens fulde navn. !dumpheap udskriver alle typer, hvis fulde navn indeholder den tekst, vi angiver. I C# termer svarer det til, at WinDbg laver en string.Contains() med vores tekst på typens fulde navn. 

Af ovenstående kan vi se, at MT for ReferenceType er 00913130. Vi kan også se, at der kun er en instans på heapen i øjeblikket, hvilket jo passer fint med ovenstående kode. Adressen på denne instans er 02802d80

Vi kan udskrive indholdet af vores instans via !dumpobj (eller den lidt kortere udgave !do). 

0:003> !do 02802d80 
Name: WinDbgQnA.ReferenceType
MethodTable: 00913130
EEClass: 0091149c
Size: 16(0x10) bytes
 (C:\Dev\TestApp\Sandbox2\bin\Release\Sandbox2.exe)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
00a1911c  4000001        4        System.String  0 instance 02802d30 Text
01c5edcc  4000002        8         System.Int32  1 instance       42 Number

Læg mærke til, at vi kan se typen og værdien af de enkelte felter. Står der 1 i VT-kolonnen, er feltet en værditype, og vi kan således aflæse den konkrete værdi for feltet. I dette tilfælde finder vi 42 helt som forventet. 

For Text-feltet, står der 0 i VT, hvilket fortæller os, at det er en referencetype. Værdien af feltet er således ikke indholdet af vores string, men derimod en reference til vores string-instans. 

Vi kan udskrive indholdet af denne ved at køre !do på adressen i Text-feltet.

0:003> !do 02802d30 
Name: System.String
MethodTable: 00a1911c
EEClass: 01bf2720
Size: 40(0x28) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: hello world
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
01c5edcc  4000096        4         System.Int32  1 instance       12 m_arrayLength
01c5edcc  4000097        8         System.Int32  1 instance       11 m_stringLength
00a19840  4000098        c          System.Char  1 instance       68 m_firstChar
00a1911c  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00267328:02801740 <<
01c54620  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00267328:02801754 <<

Dette lister hele string-objektet, men læg mærke til feltet String i oversigten. Her finder vi indholdet af vores string, og dermed har vi fundet indholdet af vores ReferenceType-instans. 

I kommende indlæg skal vi se på, hvordan vi kan finde den eller de relevante instanser ud af en større samling.