Pull to refresh

Отладка приложений в .NET Framework 2.0 и выше

Reading time12 min
Views5.7K
Хочу начать серию статей, посвящённых отладке ваших .NET приложений на стороне заказчика, а также оптимизации вашего кода. В связи с этим понадобиться немного подготовить вашу систему. В этой статье мы ознакомимся с различными инструментами для отладки приложений, немного углубимся в описание CLR, где это будет необходимо.

Утилиты


Мне удобно, когда это всё установлено на отдельную виртуальную машину, и не засоряет основную ОС, которую я использую только для разработки приложений на .NET и развлечения.

Отладка приложений .NET


При разработке приложений разработчики обычно работают на машинах, над которыми у них есть полный контроль, и они могут использовать различные утилиты. Для отладки у них есть среды разработки, с возможностью пошагового выполнения кода, изменения его и компиляции. Без всяких проблем можно в любое время приостановить приложение, запустить его снова. Однако на серверах заказчика, приостанавливать приложение для пошаговой отладки практически невозможно без того, что пользователи системы будут затронуты. Вообще, отладка приложений на сервере заказчика означает обычно, что проблема должна быть решена в кратчайшие сроки. При простое сервиса заказчик теряет деньги, уходят клиенты, идёт простой производственного цикла, возможны и другие проблемы. При возникновении проблемы, крайне желательно предпринять меры для её временного решения. Это может быть перезагрузка приложения, ограничение функциональности и другие решения по ситуации.
Может случиться несколько проблем, это: зависание приложения, взаимные блокировки, необрабатываемые или фатальные исключения, потеря данных, проблемы с производительностью, утечки ресурсов и памяти, некорректная функциональность. Все эти ситуации могут затрагивать как всех пользователей системы, так и конкретных пользователей.
Основные причины, которые приводят неполадкам в приложении, это различие окружения (аппаратного, программного, различие в настройке), а также взаимодействие с другими системами.
Чем так сложна отладка приложений на стороне заказчика? Нельзя использовать стандартные средства, сложно или невозможно получить доступ к серверу, проблема может появляться очень редко или только на стороне заказчика.
В зависимости от типа вашего приложения, вы можете выполнить следующие шаги перед тем, как приложение будет установлено у заказчика.
Серверное приложение:
  • Установить пакет отладки на сервер заказчика;
  • Установить сервер символов на вашей стороне;
  • Добавить логирование и средства мониторинга.

Клиентское приложение:
  • Установить сервер символов на вашей стороне;
  • Добавить логирование и средства мониторинга.

Чтобы свести риски к минимуму, лучше находить и исправлять ошибки до того, как они появятся у заказчика, отрабатывать различные ситуации, делать автоматизированные тестирования, как стандартные в Visual Studio, так и сторонние — NUnit, NCover.
При возникновении проблемы важно собрать, как можно больше информации, которая будет полезна, для изучения и исправления ошибки.

Структура управляемой ассемблерной сборки


Я не буду сильно вдаваться в подробности. Они легкодоступны в различных вариантах, я лишь немного остановлюсь на том, что мы дальше будем использовать, и отмечу дополнительные источники информации.
Исполняемый файл для .NET начинается с обычных заголовков для MS-DOS и COFF-заголовка, стандартных в отношении всех Windows приложений.
В первой секции данных находятся CLR-заголовок и данные.
ildasm.exe
Отметим, что первая секция имеет атрибуты CNT_CODE, MEM_EXECUTE, MEM_READ, указывающие загрузчику на то, что в секции содержится код, который будет выполняться средой выполнения. В таблице импорта можно увидеть вызов “_CorExeMain” в случае, если у вас выполняемое приложение. Она реализована в mscoree.dll – в основном модуле среды выполнения.
Методы импорта mscroree.dll
Дополнительную информацию вы можете получить из статьи Unmanaged API Reference.
За этими сегментами следует сам IL код вашего приложения. Результатом компиляции является код на промежуточном языке (MSIL). Это процессорно независимый язык – язык более высокого уровня, чем native код. Спецификации вы можете найти здесь.

За IL кодом идётут заголовки метаданных, сами метаданные и дополнительные сегменты, содержащие ресурсы приложения, native код приложения, если приложение было разработано на управляемом c++.
Информацию о структуре исполняемого файла можно получить с помощью стандартной утилиты ildasm.exe.

Изучаем возможности WinDbg+SOS, adplus.vbs


На этом этапе я надеюсь вы уже установили все нужные приложения и настроили их (добавьте ещё директорию Debugging Tools в переменную окружения PATH). Я буду делать дальнейшее исследование на этой тестовой программе:
Исходные коды приложения
Вы можете (и даже нужно) написать своё приложение, которое будет выполнять какую-нибудь чушь. Что важного в этой программе (о__О, меня от таких мыслей перекосило), а важного в ней то, что мы создаём несколько объектов в хипе, вызываем несколько методов, и в конце случайно генерируем исключение. Создадим debug версию приложения с символами.
Запустим наше приложение. И введём такую команду в директории с приложением
adplus -quiet -crash -fullonfirst -pn ApplicationForWinDbg.exe
ApplicationForWinDbg.exe — ваш скомпилённый exe.

В консольном приложении нажмём любую клавишу. Если вы всё сделали правильно, то в директории, куда были установлены Debugging Tools появится папка Crash_Mode__Date_[DATE]__Time_[TIME]. Где [DATE] и [TIME] – дата и время создания дампа.
Мы создали дамп, попробуем его исследовать, а заодно разобраться во внутренностях CLR. Детальных объяснений в данной статье я не буду давать, для нас главное научиться использовать WinDbg + SOS. Более детально в других статьях.
Открываем дамп в WinDbg. У вас должно получиться что-то на подобие этого:
WinDbg с загруженным дампом
Загружаем расширение WinDbg – SOS, выполнив команду:
0:000>.loadby sos mscorwks
Чтобы проверить, как прошла загрузка, выполним следующую команду:
0:000> !eeversion
2.0.50727.3053 retail
Workstation mode
SOS Version: 2.0.50727.3053 retail build

Мы убедились, что расширение загружено, нажимаем ctrl+s и добавляем путь до нашего приложения, чтобы WinDbg подгрузил символы. Теперь можем приступить к исследованию. Посмотрим на потоки, которые есть у нас в приложении:
0:000> ~*
.  0  Id: e8c.11bc Suspend: 1 Teb: 7ffde000 Unfrozen
      Start: ApplicationForWinDbg!COM+_Entry_Point <PERF> (ApplicationForWinDbg+0x284e) (0011284e) 
      Priority: 0  Priority class: 32  Affinity: 3
   1  Id: e8c.13f0 Suspend: 1 Teb: 7ffdd000 Unfrozen
      Start: mscorwks!DebuggerRCThread::ThreadProcStatic (7243237f) 
      Priority: 0  Priority class: 32  Affinity: 3
   2  Id: e8c.130c Suspend: 1 Teb: 7ffdc000 Unfrozen
      Start: mscorwks!Thread::intermediateThreadProc (724c1fcf) 
      Priority: 2  Priority class: 32  Affinity: 3

Посмотрим на управляемые потоки:
0:000> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                      PreEmptive   GC Alloc           Lock
       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
   0    1 11bc 00529188      a020 Disabled 01c903cc:01c91fe8 00524c40     0 MTA
   2    2 130c 00537020      b220 Enabled  00000000:00000000 00524c40     0 MTA (Finalizer)

Из этого мы можем заметить, что у нас в приложении 2 управляемых потока – основной управляемый поток, в котором выполняется наше приложение, и поток Finalizer. И ещё есть один неуправляемый поток mscorwks!DebuggerRCThread::ThreadProcStatic – это поток Debugger'а. Спецификации общения закрыты, предоставляет отладочную информацию, возможности пошаговой отладки и др. Этот поток существует вне зависимости от того, создаёте ли вы Release версию вашего приложения или Debug версию.
Посмотрим на AppDomain'ы нашего приложения:
0:000> !dumpdomain
--------------------------------------
System Domain: 728ed058
LowFrequencyHeap: 728ed07c
HighFrequencyHeap: 728ed0c8
StubHeap: 728ed114
Stage: OPEN
Name: None
--------------------------------------
Shared Domain: 728ec9a8
LowFrequencyHeap: 728ec9cc
HighFrequencyHeap: 728eca18
StubHeap: 728eca64
Stage: OPEN
Name: None
Assembly: 0051c920
--------------------------------------
Domain 1: 00524c40
LowFrequencyHeap: 00524c64
HighFrequencyHeap: 00524cb0
StubHeap: 00524cfc
Stage: OPEN
SecurityDescriptor: 00526198
Name: ApplicationForWinDbg.exe
Assembly: 0051c920 [C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 0051c990
SecurityDescriptor: 005384a8
  Module Name
70da1000 C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll

Assembly: 0051ca70 [D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe]
ClassLoader: 0051cae0
SecurityDescriptor: 0053d088
  Module Name
00202c5c D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe

Можно увидеть, что кроме домена, в котором выполняется наш код, у нас есть ещё System Domain, Shared Domain. Первый отвечает за загрузку Shared Domain, а также за загрузку и выгрузку всех пользовательских доменов. Второй выступает как хранилище для доменно-нейтральных ассемблерных сборок. Код не выполняется в Shared Domain, потому что код может быть только выполнен в пользовательских доменах.
В пользовательском домене мы видим модули – выполняемые файлы dll, exe и другие. Ассемблерная сборка может содержать несколько модулей, это не только выполняемые файлы, но и файлы ресурсов. Возьмём из предыдущего примера адрес нашей ассемблерной сборки 0051ca70, и получим дополнительную информацию:
0:000> !dumpassembly 0x0051ca70
Parent Domain: 00524c40
Name: D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe
ClassLoader: 0051cae0
SecurityDescriptor: 002bed20
  Module Name
00202c5c D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe

Посмотрим на информацию о модуле 00202c5c:
0:000> !dumpmodule 0x00202c5c
Name: D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe
Attributes: PEFile 
Assembly: 0051ca70
LoaderHeap: 00000000
TypeDefToMethodTableMap: 00200038
TypeRefToMethodTableMap: 00200048
MethodDefToDescMap: 002000a0
FieldDefToDescMap: 002000b8
MemberRefToDescMap: 002000bc
FileReferencesMap: 00200118
AssemblyReferencesMap: 0020011c
MetaData start address: 001120e4 (1668 bytes)

Если мы используем параметр –mt, то мы также получим информацию о типах, в нашем модуле:
0:000> !dumpmodule -mt 0x00202c5c
Name: D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe
Attributes: PEFile 
Assembly:thodTableMap: 00200038
TypeRefToMethodTableMap: 00200048
MethodDefToDescMap: 002000a0
FieldDefToDescMap: 002000b8
MemberRefToDescMap: 002000bc
FileReferencesMap: 00200118
AssemblyReferencesMap: 0020011c
MetaData start address: 001120e4 (1668 bytes)

Types defined in this module

      MT    TypeDef Name
------------------------------------------------------------------------------
00203080 0x02000002 ApplicationForWinDbg.Class1
0020300c 0x02000003 ApplicationForWinDbg.Program

Types referenced in this module

      MT    TypeRef Name
------------------------------------------------------------------------------
71010508 0x01000001 System.Object
710108ec 0x01000012 System.String
71014258 0x01000013 System.Console
71012b38 0x01000015 System.Int32
 0051ca70
LoaderHeap: 00000000
TypeDefToMe

Мы обнаружили наши типы, определённые в коде, это ApplicationForWinDbg.Class1 и ApplicationForWinDbg.Program. Информация об этих классах хранится во внутренних структурах EEClass. Эта стуктура содержит информацию о количестве интерфейсов, методов, полей, а также их структуры и многое другое.
Посмотрим, что у нас твориться в хипе:
0:000> !dumpheap
 Address       MT     Size
01c71000 0052ccc8       12 Free
01c7100c 0052ccc8       12 Free
01c71018 0052ccc8       12 Free
01c71024 71010b10       72     
01c7106c 71010ba0       72     
01c710b4 71010c30       72     
01c710fc 71010cc0       72     
01c71144 71010cc0       72     
01c7118c 71010508       12     
01c71198 710108ec       20     
01c711ac 71010fb8       28     
01c711c8 710108ec      160     
01c71268 710108ec      216     
01c71340 710110cc      100     
01c713a4 710113d8       44     
01c713d0 70fe40bc       80     
01c71420 710108ec       28     
01c7143c 710108ec       32     
01c7145c 710108ec       20     
01c71470 710108ec       52     
01c714a4 710108ec       40     
…………………………………………
01c90398 7101151c       24     
01c903b0 710108ec       28     
02c71000 0052ccc8       16 Free
02c71010 70fe40bc     4096     
02c72010 0052ccc8       16 Free
02c72020 70fe40bc      528     
02c72230 0052ccc8       16 Free
02c72240 70fe40bc     4096     
02c73240 0052ccc8       16 Free
total 5361 objects
Statistics:
      MT    Count    TotalSize Class Name
71013dc0        1           12 System.Text.DecoderExceptionFallback
71013d7c        1           12 System.Text.EncoderExceptionFallback
71013ae4        1           16 System.Text.DecoderReplacementFallback
71013a94        1           16 System.Text.EncoderReplacementFallback
71014808        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
710147b0        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
71014724        1           20 System.Text.InternalEncoderBestFitFallback
71012e50        1           20 System.Security.PermissionToken
7100e8b8        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle
71014770        1           24 System.Text.InternalDecoderBestFitFallback
710143c0        1           24 System.IO.TextWriter+SyncTextWriter
710127cc        2           24 System.Security.Permissions.SecurityPermission
71012510        1           24 System.OperatingSystem
71014290        1           28 Microsoft.Win32.Win32Native+InputRecord
71013f24        1           28 System.Text.EncoderNLS
71013c34        1           28 System.IO.Stream+NullStream
71013990        1           28 System.Text.UTF8Encoding
71010fb8        1           28 System.SharedStatics
71013ea4        1           32 System.Text.UTF8Encoding+UTF8Encoder
71012efc        1           32 System.Security.PermissionTokenFactory
710122cc        1           32 Microsoft.Win32.Win32Native+OSVERSIONINFO
710142e4        1           36 System.IO.__ConsoleStream
71012eb0        1           36 System.Security.Util.TokenBasedSet
71012d18        1           36 System.Security.Permissions.FileIOPermission
710120c8        1           36 System.Int64[]
710123d4        1           40 Microsoft.Win32.Win32Native+OSVERSIONINFOEX
7101278c        1           44 System.Security.FrameSecurityDescriptor
710113d8        1           44 System.AppDomainSetup
71012874        3           48 System.Security.Permissions.FileIOAccess
71012494        2           48 System.Version
71011e38        2           48 System.Reflection.Assembly
7100e6f4        4           48 System.UInt16
71012c14        2           56 System.Collections.ArrayList+ArrayListEnumeratorSimple
71010ec0        1           56 System.Threading.Thread
71013714        1           68 System.Globalization.CultureTable
710128bc        3           72 System.Security.Util.StringExpressionSet
710125b0        2           72 System.Security.PermissionSet
71010c30        1           72 System.ExecutionEngineException
71010ba0        1           72 System.StackOverflowException
71010b10        1           72 System.OutOfMemoryException
71014608        1           76 System.Text.SBCSCodePageEncoding
710137a8        5          100 System.Globalization.CultureTableItem
710110cc        1          100 System.AppDomain
0052ccc8        7          100      Free
71012b38        9          108 System.Int32
710140d4        2          112 System.IO.StreamWriter
71010a28        6          120 System.Text.StringBuilder
71010508       10          120 System.Object
710136c4        3          144 System.Globalization.CultureTableRecord
7101291c        6          144 System.Collections.ArrayList
71010cc0        2          144 System.Threading.ThreadAbortException
71011a6c        8          160 System.RuntimeType
71011fe4        3          192 System.IO.UnmanagedMemoryStream
710134e8        3          204 System.Globalization.CultureInfo
71013850        2          256 System.Globalization.NumberFormatInfo
71012f40        7          392 System.Collections.Hashtable
7101335c        3          684 System.Byte[]
7101303c        7         1008 System.Collections.Hashtable+bucket[]
71012a88       14         1536 System.Int32[]
70fe40bc       28         9564 System.Object[]
00203080     1000        12000 ApplicationForWinDbg.Class1
7101151c     1022        25376 System.Char[]
710108ec     3160        82600 System.String
Total 5361 objects

Попробуйте ответить, сколько строк, находится сейчас в хипе?
Получим информацию о нашем классе ApplicationForWinDbg.Class1 используя указатель на метаданные:
0:000> !dumpmt 0x00203080
EEClass: 0020135c
Module: 00202c5c
Name: ApplicationForWinDbg.Class1
mdToken: 02000002  (D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 7

Теперь нам доступен указатель на его EEClass, с его описанием, смотрим:
0:000> !dumpclass 0x0020135c
Class Name: ApplicationForWinDbg.Class1
mdToken: 02000002 (D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe)
Parent Class: 70da3ef0
Module: 00202c5c
Method Table: 00203080
Vtable Slots: 4
Total Method Slots: 5
Class Attributes: 100000  
NumInstanceFields: 0
NumStaticFields: 0

Посмотрим на список всех методов, у нашего класса, по его указателю на таблицу методов 00203080:
0:000> !dumpmt -md 0x00203080
EEClass: 0020135c
Module: 00202c5c
Name: ApplicationForWinDbg.Class1
mdToken: 02000002  (D:\#Projects\#Active\TestApplicationNet\ApplicationForWinDbg\bin\Debug\ApplicationForWinDbg.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 7
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
70f66a70   70de4934   PreJIT System.Object.ToString()
70f66a90   70de493c   PreJIT System.Object.Equals(System.Object)
70f66b00   70de496c   PreJIT System.Object.GetHashCode()
70fd72f0   70de4990   PreJIT System.Object.Finalize()
004b0150   00203078      JIT ApplicationForWinDbg.Class1..ctor()
004b0188   00203060      JIT ApplicationForWinDbg.Class1.Method1(System.String)
004b01d8   0020306c      JIT ApplicationForWinDbg.Class1.Method2(Int32)

И для примера посмотрим на информацию о ApplicationForWinDbg.Class1.Method1 и его скомпилированный код:
0:000> !dumpmd 0x00203060
Method Name: ApplicationForWinDbg.Class1.Method1(System.String)
Class: 0020135c
MethodTable: 00203080
mdToken: 06000001
Module: 00202c5c
IsJitted: yes
CodeAddr: 004b0188

0:000> !u 0x004b0188
Normal JIT generated code
ApplicationForWinDbg.Class1.Method1(System.String)
Begin 004b0188, size 3a
>>> 004b0188 55              push    ebp
004b0189 8bec            mov     ebp,esp
004b018b 83ec0c          sub     esp,0Ch
004b018e 894dfc          mov     dword ptr [ebp-4],ecx
004b0191 8955f8          mov     dword ptr [ebp-8],edx
004b0194 833d142e200000  cmp     dword ptr ds:[202E14h],0
004b019b 7405            je      004b01a2
004b019d e8dfa21472      call    mscorwks!JIT_DbgIsJustMyCode (725fa481)
004b01a2 90              nop
004b01a3 8b153020c702    mov     edx,dword ptr ds:[2C72030h] ("$")
004b01a9 8b4df8          mov     ecx,dword ptr [ebp-8]
004b01ac e86feaaa70      call    mscorlib_ni+0x1bec20 (70f5ec20) (System.String.Concat(System.String, System.String), mdToken: 060001c9)
004b01b1 8945f4          mov     dword ptr [ebp-0Ch],eax
004b01b4 8b4df4          mov     ecx,dword ptr [ebp-0Ch]
004b01b7 e81c36fc70      call    mscorlib_ni+0x6d37d8 (714737d8) (System.Console.WriteLine(System.String), mdToken: 060007c8)
004b01bc 90              nop
004b01bd 90              nop
004b01be 8be5            mov     esp,ebp
004b01c0 5d              pop     ebp
004b01c1 c3              ret

На этом пожалуй закончу, так как статья получилась длинная из за всех листингов. Ждите продолжения. И более подробных объяснений назначения всех полей, а также в следующей статье мы поговорим о других коммандах.
Tags:
Hubs:
+39
Comments27

Articles