Расширение отладки SOS или получаем больше сведений о внутренних структурах данных CLR используя Visual Studio

Существуют мощные инструменты и возможности, которые способны сильно облегчить, если не разработку, то хотя бы отладку, причём они не очень широко афишируются. При этом помогают получать более полное представление о том, с чем имеешь дело. Последняя косвенно влияет именно на разработку. Небольшая демонстрация одной из таких возможностей является целью данной статьи. А именно - использование расширения SOS в Visual Studio. Постараюсь насколько возможно просто продемонстрировать это на примере. Чтобы получить полные сведения о наборе комманд и более детальное описание советую посмотреть соответствующий раздел в MSDN. И так приступим. Создадим простое консольное приложение на C# в Visual Studio 2012. Добавим туда следующий код для демонстрации.
using System;

namespace TestSos
{
  class Program
  {
    static void Main(string[] args)
    {
      Test test = new Test();
      TestBase test1 = new TestBase();
      TestDerived test2 = new TestDerived();
      test2.TestMethod(1, 2);
      Console.ReadLine();
    }
  }
  public class Test { }
  public class TestBase
  {
    protected double FieldBase = 117;
  }
  public class TestDerived : TestBase
  {
    public TestDerived()
    {
      FieldBase = 117;
    }
    private int Field1 = 1;
    private string Field2 = "a";
    public int TestMethod(int a, int b)
    {
      int result = a + b;
      return result;
    }
  }
}
Чтобы иметь возможность загрузить расширения отладки SOS нужно в свойствах проекта включить отладку неуправляемого кода.
 

Запускаем приложение в режиме отладки. Чтобы загрузить расширения отладки нужно в окне Интерпретация (Immediate window) выполнить следующую комманду.
.load sos.dll 
Чтобы не получить ошибку следующего содержания,
.load sos.dll
Error during command: 
extension C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll could not load (error 193)
нужно убедиться что приложение запускается в режиме x86, а не x86-64, если конечно у вас 64-разрядная версия Windows. Чтобы использовать в полной мере 64-разрядную версию SOS нужен WinDbg, так как Visual Studio 32-разрядная. Но для наших исследований это не критично. Если всё правильно сделать, то получим примерно следующее.



И ещё стоит упомянуть, что загружать SOS нужно во время останова отладчика, иначе получим следующую ошибку (возможности WinDbg намного больше в данном плане):
.load sos.dll
The expression cannot be evaluated while in run mode. 
Одна из самых полезных комманд: !DumpHeap, которая без параметров выведет содержимое кучи. Чтобы показать только нужные нам типы нужно указать имя типа с параметром [-type имя_типа].
.load sos.dll
extension C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll loaded
!DumpHeap -type Test
PDB symbol for clr.dll not loaded
 Address       MT     Size
02b02450 00183814       12     
02b0245c 00183880       16     
02b0246c 00183914       24     

Statistics:
      MT    Count    TotalSize Class Name
00183814        1           12 TestSos.Test
00183880        1           16 TestSos.TestBase
00183914        1           24 TestSos.TestDerived
Total 3 objects
Данная комманда ищет типы с наличием строки в названии, указанной в параметре, поэтому кроме типа Test выводятся и остальные два тоже. Как видно из вывода приведённого высше, в куче имеется один объект типа Test и размером 12 байт. Но ведь мы туда ничего не писали, почему он имеет такой размер? Дело в том, что структура объекта содержит несколько секций используемой самой CLR (такие как Syncblk, TypeHandle, указатель на MethodTable и т.п.). Вот экземпляр класса TestBase имеет размер 16 байт. Всё потому, что мы добавили туда поле типа double, который имеет в CLR размер в 64 бита (или 8 байт). Ниже показан результат данной комманды и окно Память, с содержимым значений по этим адресам (значения сгруппированы по столбцам по 4 байта).



Ещё одна полезная комманда: !DumpObj адрес_объекта, выодящая сведения об объекте, по конкретному адресу. Выведем детальные данные о наших объектах, взяв их адреса из полученной нами информации (естественно адреса в памяти разные при каждом запуске программы, поэтому они верны только для текущего сеанса).
!DumpObj 02b02450
Name:        TestSos.Test
MethodTable: 00183814
EEClass:     00181300
Size:        12(0xc) bytes
File:        C:\Users\Yatajga\documents\visual studio 2012
\Projects\TestSos\TestSos\bin\Debug\TestSos.exe
Fields:
None

!DumpObj 02b0245c
Name:        TestSos.TestBase
MethodTable: 00183880
EEClass:     00181354
Size:        16(0x10) bytes
File:        C:\Users\Yatajga\documents\visual studio 2012
\Projects\TestSos\TestSos\bin\Debug\TestSos.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
56c072ec  4000001        4        System.Double  1 instance 117.000000 FieldBase

!DumpObj 02b0246c
Name:        TestSos.TestDerived
MethodTable: 00183914
EEClass:     001813a8
Size:        24(0x18) bytes
File:        C:\Users\Yatajga\documents\visual studio 2012
\Projects\TestSos\TestSos\bin\Debug\TestSos.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
56c072ec  4000001        4        System.Double  1 instance 117.000000 FieldBase
56c0c770  4000002       10         System.Int32  1 instance        1 Field1
56c0afb0  4000003        c        System.String  0 instance 02b02484 Field2
Как видно строковой литерал представлен в виде отдельного объекта, ссылка на который присутствует в TestDerived. Некоторую информацию о таблице методов и о применении SOS, можно найти в данной статье. На самом деле не всё так просто организовано, просто я постарался максимально упростить. Надеюсь данная статья будет хорошей отправной точкой для дальнейшего изучения возможностей SOS, т.к. тут были описаны всего две простых инструкции. В следующей статье я покажу применение SOS для подключения к процессу w3wp и получения сведений о работающем приложении ASP.NET.

atari_87
27.04.2013 23:03
На мой взгляд не совсем верно расставлены границы между объектами. Например класс Test должен начинаться с адреса 0x029F244С. Потому, что SyncBlock идет первым (4 байта), далее за ним идет TypeHandle (4 байта), а затем поля экземпляра (если в классе не определены никакие поля экземпляра, в класс добавляется 4 байта). Ссылка на объект указывает не на начало "блока" объекта, а по смещению 4 байта (то есть на TypeHandle).
И поясните пожалуйста, откуда взялось число 117? И если следовать этой теории, то почему в классе TestBase не хватает 4 байт?
28.04.2013 11:15
Спасибо за комментарий. Да, вы правы границы выставлены неточно. Сначала сделал всё преднамеренно, чтобы лишний раз не вводить заблуждение начёт смещения, хотел чтобы нагляднее было, а потом подумал, что это неправильно и хотел исправить. Да только всё возможности не было изменить рисунок, так как оригинальный файл с картинкой на другой машине и в настоящее время к нему доступа нет, а менять все адреса в листингах времени не было. А что касается значения 117, если преобразовать HEX представление 0x405d400000000­000 в double то получим 117.0. Просто и тут не исправил код в листинге.