Статьи

Мотивирующий пример сценариев WinDbg для разработчиков .NET

Скрипты WinDbg получают довольно дурную славу — их несколько надуманный синтаксис, странные ограничения и трудные для расшифровки выражения являются распространенными виновниками. Однако в некоторых случаях сценарии WinDbg могут быть очень эффективным и надежным инструментом для извлечения информации из памяти и ее осмысленной обработки.

Этот пост предлагает простой пример, который, надеюсь, будет полезен, когда вы начнете изучать сценарии WinDbg. Для более подробного объяснения и более сложных сценариев обязательно ознакомьтесь с моими прошлыми сообщениями о обходе std :: vector  и  std :: map .

Давайте установим сцену с помощью простого консольного приложения, которое создает несколько объектов кучи и затем ожидает ввода данных пользователем. Это имитирует сервер приложений, возможно, который обрабатывает запросы и сохраняет некоторые из них в памяти, пока они находятся в режиме ожидания.

namespace OrderProcessing
{
    class Order
    {
        public int CustomerId { get; set; }
        public string ProductName { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Order> orders = new List<Order>();
            for (int i = 0; i < 100; ++i)
            {
                orders.Add(new Order
                {
                    CustomerId = i,
                    ProductName = "Product #" + i
                });
            }
            Console.ReadLine();
            GC.KeepAlive(orders);
        }
    }
}

Теперь предположим, что мы хотим создать скрипт, который выводит имена продуктов для всех важных клиентов (чей идентификатор клиента превышает 50), которые в настоящее время находятся в памяти приложения. Делать это с помощью vanilla Visual Studio очень сложно, если вы заранее не знаете, где найти эти объекты. Кроме того, вы хотите сделать это в производственной среде, где у вас не установлена ​​Visual Studio.

Начнем с определения   объектов Order в нашей куче. Это довольно простое упражнение с использованием команды SOS ! Dumpheap  , которая может принимать имя типа:

0:003> .loadby sos clr
0:003> !dumpheap -type OrderProcessing.Order
 Address       MT     Size
028b2354 00c738b0       24     
028b236c 73015738       16     
028b237c 00c73860       16     
028b392c 73015738       32     
028b394c 00c73860       16     
028b399c 00c73860       16     
... truncated for brevity

Statistics:
      MT    Count    TotalSize Class Name
00c738b0        1           24 System.Collections.Generic.List`1[[OrderProcessing.Order, OrderProcessing]]
73015738        7         1120 System.Object[]
00c73860      100         1600 OrderProcessing.Order
Total 108 objects

Обратите внимание, что статистика в конце показывает, что у нас больше, чем просто   экземпляры Order — у нас также отображаются списки и массивы. Если нам нужны только   объекты Order , лучше отфильтровать их по указателю на таблицу методов, например:

0:003> !dumpheap -mt 00c73860       
 Address       MT     Size
028b237c 00c73860       16     
028b394c 00c73860       16     
028b399c 00c73860       16     
028b39ec 00c73860       16     
028b3a3c 00c73860       16     
... truncated for brevity 

Statistics:
      MT    Count    TotalSize Class Name
00c73860      100         1600 OrderProcessing.Order
Total 100 objects

Перед нами уже стоит небольшая задача — в автоматическом сценарии мы действительно не можем полагаться на то, чтобы указатель таблицы методов оставался неизменным между несколькими запусками приложения. Это означает, что нам нужен способ автоматического нахождения указателя таблицы методов, и  здесь  может помочь команда ! Name2EE . Единственная проблема заключается в автоматическом анализе выходных данных, и это задача для команды  .foreach,  которая может пропустить несколько токенов, пока не достигнет того, который нам нужен :

0:003> !Name2EE OrderProcessing!OrderProcessing.Order
Module:      00c72ed4
Assembly:    OrderProcessing.exe
Token:       02000002
MethodTable: 00c73860
EEClass:     00c71338
Name:        OrderProcessing.Order

0:003> .foreach /pS 7 /ps 100 (mt {!Name2EE OrderProcessing!OrderProcessing.Order}) { .echo mt }
00c73860

Как только мы это получим, пришло время изучить отдельный   объект Order, чтобы мы могли автоматически получить из него идентификатор клиента и название продукта. Во-первых, давайте посмотрим на сырую память объекта:

0:003> dd 028b5c30 L8
028b5c30  00c73860 028b5c60 0000005f 00000000
028b5c40  73053b04 0000005f 00000000 730521b4

Первые 4 байта, выделенные жирным шрифтом выше, являются указателями таблицы методов. За ними следуют поля объекта — которые, кажется, в обратном порядке объявления. 5f выглядит как идентификатор клиента, а предыдущие 4 байта выглядят как указатель (как мы знаем, на строку). Теперь предположим, что мы хотим распечатать только идентификатор клиента:

0:003> ? poi(028b5c30+8)
Evaluate expression: 95 = 0000005f

Оператор  poi  выполняет простую разыменование памяти указанного адреса. Действительно, мы получаем 5f (95 в десятичном виде), который является идентификатором этого клиента. А как насчет названия продукта, которое является строкой? Нам просто нужно двойное разыменование. Давайте начнем с просмотра строки в памяти:

0:003> dc 028b5c60 L8
028b5c60  730521b4 0000000b 00720050 0064006f  .!.s....P.r.o.d.
028b5c70  00630075 00200074 00390023 00000035  u.c.t. .#.9.5...

Символы строки хорошо видны, но они не начинаются там, где начинается объект строки. Первые два двойных слова — это  указатель на таблицу методов String и ее длина (0b). Нам нужно пропустить 8 байтов до действительных символов и использовать команду  du  , которая выводит строку Unicode:

0:003> du 028b5c60+8
028b5c68  "Product #95"

У нас есть все движущиеся части, и пришло время соединить их в один сценарий. Мы хотим получить  указатель таблицы методов Order , найти все   объекты Order в куче и для каждого   объекта Order вычислить выражение, которое говорит: «Если идентификатор клиента больше 50, распечатайте название продукта для этого заказа».

Вот полный сценарий, который делает это. Вы можете вставить его в текстовый файл и затем выполнить в отладчике с помощью команды  $$> <scriptfile.txt  .

.foreach /pS 7 /ps 100 (mt {!Name2EE OrderProcessing!OrderProcessing.Order})
{
  r $t0 = mt
}
.foreach (order {!dumpheap -mt @$t0 -short})
{
  .if (poi( order +8)>50)
  {
    du poi( order +4)+8
  }
}

Первая часть устанавливает   псевдорегистр (переменную) $ t0 в таблицу методов   класса Order . Следующая часть перебирает все   объекты Order и оценивает указанное нами условие. Обратите внимание на пробелы вокруг   переменной порядка итерации — они необходимы для отладчика, чтобы успешно выполнить интерполяцию строки.

Надеемся, что этот пример обеспечивает мотивацию для использования сценариев WinDbg в реальном мире. Они могут автоматизировать процессы, которые в противном случае заняли бы минуты или часы, и могут дать вам представление о том, что происходит в вашем приложении, многократно.

Я публикую короткие ссылки и обновления в Твиттере, а также в этом блоге. Вы можете следовать за мной:  @goldshtn