Как стать автором
Обновить

Скрозь тернии к велосипедам, часть первая: изучаем основы кастомизации отладчика Visual Studio с помощью плагинов

Время на прочтение9 мин
Количество просмотров4.6K
Одним из нововведений Visual Studio 2012 сопутствовало явление народу нового кастомизируемого отладчика под названием «Concord». Его компонентная система позволяет VSIX-плагинам подстраивать под себя поведение отладчика и писать новые, контекстно-зависимые, инструменты, которые могут эксплуатировать отладчик для своих нужд. Его API предоставляет множество QOL фич, таких как маршалинг между управляемым/неуправляемым кодом, бесшовная интеграция с удалённо/локально отлаживаемым процессом, и не только. По сути, практически всё, что можно сделать в IDE, можно сделать программно, используя Concord API! Менять на лету значения конкретных переменных, вызывать по заказу функции (или специально заставлять программу пропускать вызовы оных!), плагинам доступен поиск по PDB (!), пошаговый обход и даже модификация кода! Открой кат, и ты узнаешь об этих малоизвестных инновациях в области велосипедостроения.

Начать следует, наверное, с начала. Отладчик обнаруживает компоненты, читая информацию из vsdconfig файла, на которой ссылается манифест VSIX-плагина. В свою очередь, vsdconfig указывает на то, какие интерфейсы реализованы компонентами плагина, и как эти компоненты найти (ссылка на .dll-файл, с указанием класса или, в случае нативной реализации, с указанием CLSID. Я буду приводить примеры на C#). Так-же, указывается уникальный идентификатор (GUID) каждого компонента, равно как и его «уровень». Уровень — это то, что определяет, в какой последовательности будут обрабатываться плагины, а так-же то, в контекст какого процесса эта реализация будет загружена — в процесс IDE или в процесс отлаживаемого приложения. Связано это с тем, что некоторый функционал может работать только в контексте IDE, и наоборот — только в контексте отлаживаемого процесса. Некоторые функции API работают одинаково и там и там. Так-же, у ряда компонентов есть свои правила расположения, ибо они могут зависеть от существующих элементов отладчика, расположенные на своих фиксированных уровнях. Чтобы избежать казусов, рекомендую RTFM (https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.debugger.componentinterfaces?view=visualstudiosdk-2017) и самостоятельные эксперименты в отдельной песочнице, которую будет не жалко удалять в случае чего (связано это с, опять таки, таким нюансом — в ряде случаев даже не понятно почему не загружается сборка или тип, ибо я так и не нашёл где бы появлялись логи, которые стабильно сигнализировали бы о проблеме. Ошибка, связанная, к примеру, со ссылкой на зависимость, которая не может быть загружена в целевой процесс, может как появиться в output-е студии, так и нет. Будьте акуратны, делайте частые коммиты, и не садитесь пьяными за руль).

Список уровней выглядит следующим образом (я приведу текст на английском, чтобы читатель не имел казусов при совершении актов RFTM):

Уровни компонентов IDE (значения > 100,000):
Component Component Level
AD7 AL 1,000,000,000
Disassembly Provider 9,998,000
Stack Query – Components that wish to query the call stack 9,997,000
Stack Provider 9,996,000
Stack Filter — level where stacks can be filtered and annotated 9,995,000
Breakpoint Manager 9,994,000
IDE Expression Evaluation 9,992,000
Symbol Stack Walkers – stack walkers that need access to symbols 9,991,000
IDE SymbolProvider — Components which provide symbol information to rest of the debugger. The symbol path should not be used below this level. 1,999,000

Уровни компонентов целевого процесса ( значения < 99,999):
Component Component Level
Monitor Symbol Provider – Symbol providers when the symbolic state is built on the target computer (ex: interpreter, dynamically compiled/emitted code) 75,000
Breakpoint Condition Processor — This level is for processing breakpoint conditions such as condition expressions and hit counts. Below this point all physical breakpoint events will be visible regardless of whether or not they have false conditions. 70,000
Monitor Task Provider – This is the level for task data mining in the target process 65,500
Monitor Expression Evaluator 65,000
Monitor Coordination – Components which arbitrate between the various monitors for stepping, breakpoints by native address, stack walk, etc. 60,000
Monitor Stack Walkers 55,000
Custom Debug Monitor – Reserved for third party debug monitors which wish to make use of services provided by the standard debug monitors. 40,500
Runtime Debug Monitor — Provides data inspection and execution control for managed/native/script code 40,000
Base Debug Monitor 10,000
Base Debug Monitor Services – provides utility services to base debug monitors (ex: process creation) as well as pre-debugging services (ex: process enumeration) 1,000

Дальше у нас по порядку, процесс создания проекта. Не будь тут важных нюансов, я бы мог бы описать этот процесс минимально или вообще пропустить, но реалии совсем иные — нам понадобится ряд библиотечных зависимостей, равно как и инструмент для создания конфигурационных файлов, который почему-то не распространяется вместе с VisualStudio, а доступен только с nuget. По сути, сейчас надо переходить к сути. Процесс создания и настройки проекта строится следующим образом:

  1. Открываем Visual Studio. В моём случае, 2017 Community Edition
  2. VSIX Project ( вкладка Visual C# -> Extensibility, или через поиск). Назовём его «HelloVSIX»
  3. Добавляем новый class library проект в солюшен, и называем его «DebuggeePlugin»
  4. Ставим референс на проект «DebuggeePlugin» в проекте «HelloVSIX»
  5. Добавим в DebuggeePlugin референс на nuget-plugin «Microsoft.VisualStudio.Debugger.Engine».
    Ставим референс на сборку «Microsoft.VisualStudio.Debugger.Engine» в проекте DebuggeePlugin
    Добавляем в проект «DebuggeePlugin» референс на nuget пакет Microsoft.VSSDK.Debugger.VSDConfigTool. Это наш инструмент для генерации VSD-конфигов

Теперь, мы готовы для того, чтобы заставить наш плагин сделать что-то полезное. Давайте сделаем самое простое что можно сделать — пусть он отображает MessageBox с надписью «Hello VSIX» тогда, когда целевой процесс натыкается на точку входа. Для этого, нам понадобится создать класс, который реализует интерфейс IDkmEntryPointNotification, а так-же заполнить несколько конфигурационных файлов. Добавим новый публичный класс с названием DkmEntryPointNotificationService, и унаследуем интерфейс IDkmEntryPointNotification, и оставим пока реализацию по-умолчанию:
using Microsoft.VisualStudio.Debugger;
using Microsoft.VisualStudio.Debugger.ComponentInterfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DebuggeePlugin
{
    class DkmEntryPointNotificationService : IDkmEntryPointNotification
    {
        public void OnEntryPoint(DkmProcess process, DkmThread thread, DkmEventDescriptor eventDescriptor)
        {
            throw new NotImplementedException();
        }
    }
}

Теперь интересный момент. У нас, на самом деле, есть не все необходимые инструменты для построения солюшена. Нам ещё необходимо добавить референс на nuger-пекедж «Microsoft.VSSDK.Debugger.VSDC» в проекте DebuggeePlugin, иначе конфигурационные файлы не будут правильно обрабатываться!
Добавим в проект «DebuggeePlugin» файл «DkmEntryPointNotificationService.vsdconfigxml». Для каждого объявленного класса, который должен получать уведомления через реализации интерфейсов неймспейса «Microsoft.VisualStudio.Debugger.ComponentInterfaces», следует иметь такой файл. Кстати говоря, можно реализовывать сразу несколько таких интерфейсов одним классом. Теперь надо изменить build action у нашего ".vsdconfigxml" файла. Для этого придётся вручную подредактировать проектный файл (я серьёзно). Выгружаем проект «DebuggeePlugin», и открываем редактором студии. Нам необходимо найти следующий XLM-тег:

<None Include="DkmEntryPointNotificationService.vsdconfigxml" />

и переместить этот тег в свою собственную ItemGroup, изменив тип с None на VsdConfigXmlFiles:
<ItemGroup>
    <VsdConfigXmlFiles Include="DkmEntryPointNotificationService.vsdconfigxml" />
  </ItemGroup>

Можно сохранять и перезагружать проект.

После этого, нужно убедиться в том, что свойство copy to output directory установлено в copy always или copy if newer у *.vsdconfigxml файла. Иначе, опять таки, никакого *.vsdconfig файла в аутпуте не будет. Теперь, переходим к конфигам. Первое, что нужно сделать: если в проект «DebuggeePlugin» добавился файл vsdconfig.xsd, то его следует удалить. Мы его сейчас заменим, ибо с сырым текстом работать проще. Откроем DkmEntryPointNotificationService.vsdconfigxml и заменим текст на следующий:

<?xml version="1.0" encoding="utf-8"?>
<Configuration xmlns="http://schemas.microsoft.com/vstudio/vsdconfig/2008">
  <ManagedComponent
    ComponentId="422413E1-450E-40A6-AE24-7E80A81CC668"
    ComponentLevel="99950"
    AssemblyName="DebuggeePlugin">
    <Class Name="DebuggeePlugin.DkmEntryPointNotificationService">
      <Implements>
        <InterfaceGroup>
          <NoFilter/>
          <Interface Name="IDkmEntryPointNotification"/>
        </InterfaceGroup>
      </Implements>
    </Class>
  </ManagedComponent>
</Configuration>


В любом таком файле от нас потребуется указать следующие вещи:

  1. ComponentId — это значение можно сгенерировать с помощью инструмента генерации GUID (Tools -> CreateGUID)
  2. ComponentLevel — уровень нашего компонента в иерархии. Смотрите таблицу выше и справочную информацию на MSDN, чтобы выбрать нужный диапазон значений.
  3. Assemblyname — название нашей сборки (не солюшена!). В данном случае, будет DebuggeePlugin
  4. Class Name — следует указывать включая название неймспейса, в котором находится класс. В данном случае, DebuggeePlugin.DkmEntryPointNotificationService
  5. массив InterfaceGroup — каждая запись в нём указывает на реализованый данным компонентом интерфейс. Внутри каждой InterfaceGroup ноды должна идти суб-нода с указанием общего для всех, в данной группе, интерфейсов, фильтра, но о фильтрах потом. Сейчас у нас Interface нода только одна, и несёт в себе имя интерфейса IdkmEntryPointNotification. Будь у нас несколько интерфейсов, нод Interface было бы несколько.

Напомню, что для каждого класса, который должен получать уведомления от отладчика, должен иметься такой файл. Но на этом веселье не заканчивается. Каждый такой файл, в последствии, собирается в .vsdconfig файл в output директории проекта. И на них следует сделать ссылку в манифесте плагина. Делается это следующим образом:

  1. После того, как мы сформировали ".vsdconfigxml" файл, мы должны… собрать один раз солюшен, иначе у нас не будет никакого .vsdconfig файла в output директории проекта)
  2. После чего, открыть текстовый редактор для файла source.extension.vsixmanifest и перед закрывающим тегом PackageManifest, добавить следующий код:

 <Assets>
    <Asset Type="DebuggerEngineExtension" d:Source="File" Path="DebuggeePlugin.vsdconfig" />
  </Assets>

Если после совершённых действий, появится файл «DebuggeePlugin.vsdconfig» в проекте HelloVSIX, его следует УДАЛИТЬ ИЗ ПРОЕКТА и собрать солюшен снова, иначе он не будет обновляться. В случае, если что-то идёт не так, можно попробовать очистить экспериментальный экземпляр Visual Studio следующей командой:

%VSSDK installation%/VisualStudioIntegration/Tools/Bin/CreateExpInstance.exe /Reset /VSInstance=%version% /RootSuffix=Exp && PAUSE

Подготовительные работы окончены! Можно запускать отладку нашего плагина. Делается это путём запуска экспериментального экземпляра VisualStudio (у VSIX проектов это дефолтный debug target, так что никаких дополнительных действий производить не нужно). Собственно, нажимаем Debug->StartDebugging и видим экспериментальный экземпляр VisualStudio. В нём, по умолчанию, уже должен быть установлен наш плагин. Удостовериться в этом можно через меню Tools->Extensions and updates.

Ввиду того, что мы реализовывали интерфейс «IDkmEntryPointNotification», нам придётся создавать тестовый проект в экспериментальном экземпляре VisualStudio. Собственно, создаём новый проект, выбираем C++ -> Console Application (выбирайте C++, потому-что следующие примеры будут содержать C++ специфику), называем его VSIXTestApp, без изменений запускаем, собираем и видим что наш экспериментальный инстанс остановился на вбросе исключения внутри метода DebuggeePlugin.DkmEntryPointNotificationService.OnEntryPoint. Отлично! Теперь нужно показать MessageBox. Для того, чтобы это сделать, необходимо к проекту DebuggeePlugin добавить следующие референсы:

  • Microsoft.VisualStudio.Shell.15.0
  • Microsoft.VisualStudio.Shell.Interop
  • Microsoft.VisualStudio.Shell.Interop.8.0
  • Microsoft.VisualStudio.OLE.Interop
  • Microsoft.VisualStudio.Shell.Framework

Добавить два using'а в начале файла DkmEntryPointNotificationService.cs:

using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

И добавить вызов к методу VsShellUtilities.ShowMessageBox в методе DkmEntryPointNotificationService.OnEntryPoint:

using Microsoft.VisualStudio.Debugger;
using Microsoft.VisualStudio.Debugger.ComponentInterfaces;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DebuggeePlugin
{
    class DkmEntryPointNotificationService : IDkmEntryPointNotification
    {
        public void OnEntryPoint(DkmProcess process, DkmThread thread, DkmEventDescriptor eventDescriptor)
        {
            VsShellUtilities.ShowMessageBox(Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider, "Hello VSIX", "Hello VSIX", OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
        }
    }
}

Пересобираем, запускаем экспериментальный экземпляр студии, запускаем тестовый проект и вуаля!

Мы видим что тестовый инстанс студии создал MessageBox!



А какой, собственно, benefit?

Здесь мы научились настраивать VSIX-проект, содержащий плагин для отладчика Visual Studio, учитывая большинство нюансов, которые попались на пути к результату. Это является отправной точкой для более детальной работы. В следующей статье, я покажу ещё один важный момент: каким образом совершается коммуникация между IDE и Debug target компонентами.

Для дальнейшей справки по использованию Concord API, можно обратиться не только к MSDN, но и к следующим репозиториям Microsoft на гитхабе:
github.com/microsoft/PTVS
github.com/Microsoft/ConcordExtensibilitySamples
Теги:
Хабы:
+6
Комментарии5

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн