Скрипты 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