Глеб Балыков — Open Source .NET Interop Debugger

Информация о загрузке и деталях видео Глеб Балыков — Open Source .NET Interop Debugger
Автор:
DotNext — конференция для .NET‑разработчиковДата публикации:
16.08.2024Просмотров:
962Описание:
Подробнее о конференции DotNext: — — Скачать презентацию с сайта DotNext — Глеб рассказал про open source .NET debugger под названием NetCoreDbg и про его новую функциональность для interop-дебага. Режим interop-дебага позволяет отлаживать не только managed-код, но и нативный код в одной и той же сессии. Подобная функциональность есть в Visual Studio Debugger, но он поддерживает только Windows x86/x64/arm64. NetCoreDbg поддерживает операционную систему Linux и архитектуры x86/x64/arm/arm64. Без подобной функциональности сложно отлаживать переходы из managed-кода в нативный код и обратно, а NetCoreDbg решает данную проблему. Спикер рассказал про то, как устроен дебаггер, и покажет примеры использования NetCoreDbg в различных режимах, в том числе через интерфейс командной строки как в gdb. Будет интересно .NET-разработчикам, которые используют P/Invoke (то есть нативный код), и хоть раз встречались с проблемами взаимодействия с нативным кодом.
Транскрибация видео
Я рад представить нашего следующего спикера, Глеба Балыкова, который уже шестой год контрибьютит в .NET и участвует в разработке единственного опенсурсного дебаггера для .NET, это .NET Interop Debugger.
Сейчас будет хардкорный доклад, то, что мы любим.
Прошу.
Всем привет, меня зовут Греб Балыков.
Как уже сказали, я периодически делаю какие-то коммиты в сам .NET, в рантайм, какие-то оптимизации, ну и участвую в разработке open-source .NET-дебаггера .NET Core DBG.
Возможно, кто-то про него слышал, возможно, даже кто-то его использовал.
Ну и сегодня я расскажу про новый режим .NET Core DBG, который нужен для interop-дебага, для оплатки, собственно, нативного image-кода в одной и той же сессии.
Для начала план моего сегодняшнего выступления.
Сначала расскажу, в принципе, что такое дебаггеры, познакомлю тех, кто может с таким понятием еще не знаком.
Затем расскажу, что такое Netcore DBG, как он появился, почему он появился, покажу, какие у него есть фичи, какая у него архитектура.
Ну и также покажу, как использовать обычный Managed Netcore DBG.
Ну а затем мы перейдем к самой интересной части, к Interop Debugging.
Я расскажу, что это такое, что предоставляет Microsoft в своих решениях.
Мы посмотрим, какие есть фичи у Interop режима в Netcore DBG.
Посмотрим, как его использовать на примерах.
Ну и какие-то дальнейшие направления, куда можно этот Interop режим развивать.
Для начала, что такое дебаггер?
Дебаггер – это, по сути, программа для отладки других программ, иными словами, отладчик.
Под это определение попадает широкий набор программ, которые так или иначе помогают решать какие-то проблемы, находить их, делать это быстрее, тем самым повышая эффективность разработчиков.
Ну а фичи классических дебаггеров – это показ бэктрейса в какой-то точке программы с полным путем до этой точки со всеми фреймами и строчками кода, остановка на брейкпойнтах, также в какой-то строчке, например, кода или на какой-то функции, эволюция экспрессионов, например, с вычислением каких-то выражений арифметических, вызовом функций и так далее, ну и степпинг по коду программы.
Наверняка разработчики на Linux очень хорошо знакомы с дебагерами GDB и LDB.
Так или иначе, разработчики на Windows наверняка хорошо знакомы с Visual Studio дебагером.
Ну и, возможно, еще с WinDBG.
Для Android-разработчиков наверняка знакома Android Studio.
Ну и для Web, например, Firefox JavaScript Debugger.
В принципе, дебагеров достаточно много.
Я здесь какие-то основные, наверное, примеры привел.
Возможно, после моего выступления кто-то захочет написать еще один свой.
Ну и давайте посмотрим на простом примере, на архитектуру дебаггера, на примере консольного дебаггера GDB.
Значит, у нас есть отлаживаемый процесс, в котором работает какое-то нативное приложение, и есть процесс отладчика, в котором работает в данном примере отладчик GDB, ну и он как-то взаимодействует с отлаживаемым процессом.
Разработчик здесь взаимодействует с интерфейсом командной сырки GDB, соответственно, передает какие-то команды, и GDB их уже выполняет.
В случае использования IDE ситуация чуть сложнее, но у нас также есть отлаживаемый процесс, также есть процесс отладчика, еще есть процесс, ну или группа процессов от самой IDE.
Там работает пользовательский интерфейс, ну и некоторый плагин.
Плагин с одной стороны взаимодействует с интерфейсом IDE, а с другой стороны взаимодействует с дебаггером по некоторому протоколу.
Протоколы могут быть разные, я еще скажу, как и поддерживает Netcore DBG, посмотрим чуть позже.
Ну и наверняка все видели, как выглядит дебаггер в Visual Studio.
Нажимаете F5, у вас появляются дополнительные окна.
Это, например, локальные переменные, watch window, показ coldstack и так далее.
Ну и красным кружочком здесь выделен breakpoint, как раз на нем программа сейчас установлена.
Ну и в связи с этим, раз есть уже какие-то дебаггеры, возникает вопрос, зачем, в принципе, понадобился еще один дебаггер для .NET.
Netcore DBG появился в 2017 году, ну и, в принципе, все существовавшие на тот момент решения имели те или иные ограничения.
Например, GDB JIT – это, по сути, просто отладочная информация для JIT-функции, соответственно, ее можно использовать в каких-то других дебаггерах, но там, естественно, нет никакого evaluation expression.
Затем достаточно мощное решение – это LDB с SOS-плагином, то, что, по сути, предоставляет Microsoft.
Но там тоже нет полноценного эвалюэйшена.
Я об этом еще чуть дальше скажу, когда буду сравнивать с Netcore DBG.
Есть еще Mono Soft Debugger.
У него другой дебаг API.
Как видно из названия, Mono Soft Debugger ориентирован на рантайм Mono, а Netcore DBG ориентирован на рантайм Core CLR.
У них разные дебаг API, нельзя использовать один дебаггер для другого рантайма.
Ну и с Visual Studio Debugger и AWS Code Debugger ситуация примерно одинаковая.
Они оба проприетарные.
У Microsoft есть некоторые патентные ограничения, чтобы их открыть.
Ну и они не поддерживают ARM32, софтфод и BI-архитектуру, что также было критично для Netcore DBG.
В принципе, есть еще какие-то другие решения.
Они либо не поддерживают Linux, либо не поддерживают какие-то архитектуры.
Вкупе с этим появился еще iCore Debug API, начиная с .NET Core 2.1, который предоставляет широкие возможности для написания отладчиков и взаимодействия с отлаживаемым процессом.
Но это все привело к тому, что появился как раз .NET Core DBG.
Что это такое?
Это Open Source .NET Debugger под MIT лицензией.
Здесь указана ссылка на репозиторий на GitHub.
Там сейчас 591 звездочка.
Если кто-то пользовался или воспользуетесь будущим и вам понравится, не забудьте поставить свою звездочку.
Разрабатывается компанией Samsung, но и open-source сообщество принимает активное участие в разработке, тоже об этом еще расскажу.
Среди поддержанных операционных систем это Tizen, Linux, Mac и Windows, а среди архитектур это ARM, x86 и x64.
Как я уже сказал, он основан на iCore Debug API, и с этим он поддерживает все версии, начиная с .NET Core 2.1 и выше, и, соответственно, .NET 5 и выше.
По фичам.
Во-первых, Netcore DBG поддерживает разные протоколы.
Это, например, GDB MI и VS Code протоколы для интеграции, соответственно, с Visual Studio и VS Code.
Но также есть похожий на GDB интерфейс командной строки для работы в консоли.
Ну и это как раз те самые протоколы, которые я на одном из предыдущих слайдов упоминал.
Соответственно, можно, используя их, интегрировать в какую-то свою IDE, если вам это нужно, и можно выбрать протокол, который вам больше нравится.
Ну, GDB, EMI и VS Code, они более машинные.
Соответственно, CLI протокол ориентирован на взаимодействие с человеком.
Ну и в принципе, наверное, для разработчиков на Linux привычнее консольные дебаггеры по типу GDB, для разработчиков на Windows привычнее дебаггеры с интерфейсом графическим в IDE.
Соответственно, CoreDBG позволяет использовать дебаггер так, как вам больше нравится.
Ну и классические фичи дебаггеров — это раскрутка степпинг по программе, установка брейкпоинтов и evaluation expression.
Как я уже сказал, open-source сообщество принимает активное участие в разработке.
Например, сообществом Netcore DBG был добавлен как бинарный пакет различные дистрибутивы.
Например, Arch и NixOS.
Была сделана интеграция в Vim с помощью плагина Vim Spectre, интеграция в Eclipse и интеграция в Red Hat Code Ready.
Ну и буквально недавно сообществом был добавлен патч, который добавляет поддержку Apple M1.
Пока что это только комьюнити саппортит архитектуру, соответственно, если возникают какие-то проблемы, чинит их комьюнити.
Ну давайте еще раз посмотрим на архитектуру, но уже непосредственно на CoreDBG.
Значит, у нас снова есть отлаживаемый процесс, в нем работает какое-то менедж приложение, например, написано на C-Sharp, и работает оно поверх рантайма, в нашем случае это рантайм CoreCLR.
Ну и в рантайме есть некоторая часть, которая отвечает за взаимодействие с дебаггером.
Когда у нас появляется процесс самого отладчика, в него загружается веблиотека DBG.shim, которая использует дебаггер интерфейс и Data Access Component, ну и некоторый IPC-механизм взаимодействует с отлаживаемым процессом, с рантаймом.
Ну а с другой стороны, на сторону дебаггера предоставляет iCore Debug API.
Менедж дебаггер вызывает какие-то функции из этого API, ставит какие-то коллбеки, которые затем вызываются во время работы рантайма, ну и таким образом взаимодействует с отлаживаемым процессом.
То, что здесь показано синим, это то, что предоставляет Microsoft, ну, runtime, понятно, это часть SDK, библиотека dbg-shim раньше тоже была частью SDK, сейчас идет как отдельный nugget-пакет.
То, что показано зеленым, это то, что пишет разработчик, например, приложение на C-sharp, но то, что показано фиолетовым, это в нашем случае менедж-дебаггер в Netcore DBG.
И я хочу подробнее остановиться на одной из основных фич, это evaluation expression.
Почему так?
Потому что она повышает продуктивность разработчиков, помогает быстрее понять, что происходит у вас в программе и быстрее выявить какие-то ошибки.
Для начала посмотрим на некоторые примеры.
Простой пример.
У нас есть целочисленная переменная, и в консоли выводится it-аргумент массива аргументов этой программы.
В Visual Studio наверняка все видели, при наведении курсора мыши производится автоматический evaluation некоторых экспрессионов.
Здесь показан результат evaluation для выражения «и».
И показан его результат.
То есть в таком сценарии что нам нужно?
Во-первых, нам нужен парсер языка, чтобы понять, какое выражение эвалировать.
Во-вторых, доступ к переменным.
То есть по сути здесь просто чтение памяти, и прочитанную память надо показать.
Другой пример.
У нас есть некоторый объект, и мы выводим в консоль поле этого объекта.
Аналогично при наведении курсора мыши такое выражение эволюируется, в данном случае это core.x.
Ну и в целом этот пример похож на предыдущий, но здесь нам уже нужен доступ к объектам, нам надо знать их тип и по сути знать лейаут памяти, по какому оффсету от начала объекта обращаться.
Другой пример.
У нас снова есть какой-то объект, и для него теперь мы в консоли выводим результат вызова функции.
Такие выражения уже автоматически не эвалюируются, но можно воспользоваться watchWindow, который я уже упоминал, ввести туда свое выражение, ну и будет показан результат.
Этот пример уже существенно отличается от предыдущих.
У нас здесь не просто чтение памяти, у нас здесь полноценное выполнение кода, мы здесь вызываем функцию.
То есть этот пример существенно сложнее.
Еще один пример.
У нас есть массив вещественных чисел, и в консоль мы выводим последний элемент этого массива.
Опять же, автоматически такое выражение не эвалюируется.
Воспользуемся WatchWindow, ну и здесь показан результат.
Этот пример еще сложнее, потому что у нас здесь в рамках одного выражения достаточно много операций, и поэтому нам нужен уже полноценный интерпретатор, чтобы в правильном порядке выполнять эти операции, чтобы в итоге получить правильный результат для данного выражения.
Все это приводит к тому, что у нас, по сути, есть две основные компоненты нашего эвалюатора, это парсер и интерпретатор.
Парсер для того, чтобы распарсить, понять, какое выражение нам нужно интерпретировать, но интерпретатор для того, чтобы его выполнить.
Если с чтением памяти все достаточно просто, мы используем iCore Debug API, то с выполнением кода возникают вопросы.
И можно делать это двумя способами.
Делать это на стороне самого отладчика в процессе отладчика, либо делать это на стороне отлаживаемого процесса.
И у того и другого подхода есть свои плюсы и минусы.
В случае, когда мы делаем это на стороне отладчика, скажем, мы обращаемся к полю какого-то объекта, который является полем еще какого-то объекта, у нас может быть какое-то сложное дерево объектов, соответственно, нам все это дерево объектов придется копировать в процесс отладчика.
Это будет занимать какое-то время, и кроме того, нам придется загружать все библиотеки, которые содержат типы для этих объектов.
К чему это приведет?
Что это негативно скажется на перфомансе.
Скажем, от момента нажатия пользователем кнопки Enter после ввода выражения или нажатия кнопки в IDE до показа результата будет проходить больше времени.
Ну и, соответственно, память процесса отладчика будет расти.
Но, с другой стороны, такой подход быстрее для арифметических операций, когда нам не нужно копировать сложное дерево объектов, можно скопировать, например, пару чисел.
Второй подход, когда мы делаем все в процессе оплатчика, у него есть свои минусы.
Во-первых, могут возникать сайд-эффекты.
Скажем, во время эволюэйшна мы вызываем какую-то функцию, которая меняет глобальную переменную.
Соответственно, если дальнейшее развитие нашей программы зависит от этой переменной, то все может поменяться.
Ну и могут возникать дедлоки, если, скажем, мы остановились на брейкпоинте в какой-то функции, которая взяла mutex, и мы пытаемся эволюировать expression, который вызывает функцию, которая берет тот же самый mutex, у нас будет дедлок.
Но, с другой стороны, этот подход быстрее для каких-то сложных выражений, где какие-то сложные зависимости между объектами, потому что не надо ничего копировать на сторону оплачика.
Ну и опять же, подход с эвалюейшеном на стороне отладчика для простых арифметических операций быстрее еще за счет того, что мы не делаем вызовы iCore Debug API, потому что это можно делать проще.
Ну и сейчас я покажу как.
В итоге, что есть в NetCore DBG?
По сути, комбинация двух подходов.
Какие-то простые арифметические операции делаются на стороне дебаггера, все остальные сложные делаются на стороне отлаживаемого процесса.
Ну и тот и другой подход, по сути, так или иначе использует iCore Debug API, как минимум для доступа к памяти, но и для установки новых значений, для вызова функций, для получения типов и так далее.
Подробнее об архитектуре.
На входе у нас есть какой-то expression, написанный, например, на C-sharp.
Он попадает на вход парсеру, и парсер это выражение отдает инстанцию компилятора Roslin.
У Roslin есть API, который позволяет из expression получать абстрактное синтаксическое дерево, ну или AST.
И затем парсер этой st некоторым образом преобразует в программу для стек-машины.
Ну и отдает ее на вход интерпретатору.
Интерпретатор эту программу берет и команду за командой начинает выполнять.
И для каждой команды смотрит, можно ли обойтись арифметическим эвалюатором, либо нужно использовать сложный.
Ну и оба используют iCore Debug API для взаимодействия с отлаживаемым процессом.
Опять же, то, что показано синим – это то, что предоставляет Microsoft.
Ну, Roslin – это, понятно, Roslin.
А iCore Debug API – это часть библиотеки dbg.shim, которая загружена в процесс отладчика.
Ну а все, что показано зеленым – это, собственно, части Netcore DBG.
Рассмотрим на примере.
Есть у нас некоторое выражение, вызывается функция foo.
Первый аргумент, который это результат вызова функции bool.
Второй аргумент это обращение к полю некоторого объекта.
Для него получается вот такое абстрактно-синтаксическое дерево, в корне которого invocation expression.
Это то, что выдает нам рослин.
Сложное достаточно дерево.
Посмотрим на еще более простом примере, где у нас есть чисто арифметические операции.
Чтобы получить из такого дерева программу для стек-машины, нужно использовать обход этого дерева в глубину.
И получится программа следующего вида.
Давайте посмотрим, как она выполняется по шагам.
Когда мы встречаем команду identifierName, стек у нас изначально пустой.
Когда мы встречаем команду identifierName, у нас на стек будет положено значение этой переменной.
Соответственно, после выполнения identifierNameA и identifierNameB у нас на стеке будут лежать значения этих перемен.
Затем команда add возьмет два своих аргумента со стека и результат своей работы тоже положит на стек.
AnalogEach на identifierNameC и identifierNameD также будут положены на стек, и снова MultiplyExpression возьмет с вершины стека два своих аргумента, и результат своей работы тоже положен на стек.
Ну и в итоге это все приводит к тому, что после выполнения всех команд этой программы у нас на стеке будет лежать результат эваливейшена этого выражения.
В чем есть здесь нюансы?
Что, во-первых, Рослин во время парсинга не знает, чем является конкретный идентификатор.
Вот в этом примере у нас есть какие-то идентификаторы foo, boo и так далее.
Для Рослина это безразлично, для него это все identifier name, и конкретный тип будет известен только на этапе интерпретации программы stack machine.
Соответственно, может оказаться так, что у нас foo, это вообще не функция, вызывать ее нельзя.
Может пользователь допустил какую-то ошибку в выражении.
Это будет понятно только на этапе интерпретации.
Даже простые операции могут оказаться вызовами функции.
Скажем, у нас есть какое-то выражение a плюс b. Это могут быть целые числа, а могут быть какие-то объекты.
Соответственно, для них нужно искать оператор плюс.
iCoreDebug API отвечает за все взаимодействие с рантаймом.
Это получение значений, переменных, установка новых значений, вызов функций, получение типов и так далее.
И в данный момент не все фичи языка поддерживаются.
Например, лямбда выражения.
Но, кстати, лямбда выражения не поддерживается и в Visual Studio.
И арифметический вал, который я упомянул, который работает на стороне самого отладчика.
Он работает для операции над примитивными типами.
И в чем здесь нюанс, что не требуется ручной сложный кастинг типов.
Для этого используется ключевое слово dynamic.
Вот так примерно выглядит операция сложения.
Соответственно, по сути, вся работа над конверсией перекладывается на сам рантайм.
Что ж, давайте посмотрим, как Netcore DBG, собственно, использовать.
И показывать я буду интерфейс командной строки, похожий на GDB.
Соответственно, использовать его можно и из IDE, как вам больше нравится.
И пример здесь будет достаточно специфичный.
Можно в детали этого примерно не сильно углубляться.
Таким же образом можно отлаживать любую программу, любую менеджер программу.
В общем, то, что я здесь буду откладывать, это ChrisGen2 Head-of-Time компилятор, который появился, начиная с версии .NET 5.
Возможно, кто-то с ним знаком.
И с помощью него мы будем компилировать одну из основных библиотек рантайма систем по Riot Core Lib.
Возможно, кто-то с ними сталкивался.
Итак, как, собственно, запустить NetCore DBG из консоли?
Во-первых, здесь используется опция "-interpretator="cli".
На самом деле она избыточная.
NetCore DBG и так запустится в таком режиме по умолчанию.
Я ее здесь для наглядности оставил.
И для запуска самого приложения используется host.corun.
Ну, наверняка все знакомы с хостом .NET, который используется и для билда, и для запуска приложений.
Вот host.corun — это некоторый сильно упрощенный хост, обычно он используется для запуска тестов, но можно его использовать и для запуска некоторых приложений.
Ну, а затем идут аргументы, собственно, программы, которые мы будем отлаживать.
В принципе, это не так важно.
Когда мы запустим Netcore DBG в таком режиме, первое, что у нас появится, это prompt, и мы поставим breakpoint на функцию main.
Что у нас сначала будет?
Breakpoint сначала будет не зарезовлен, потому что у нас еще не загружены никакие библиотеки, собственно, его пока некуда поставить.
Затем, когда мы выполним команду run, то, что здесь показано тремя точками, я здесь опустил вывод с загрузкой библиотек, я его чуть позже покажу.
Соответственно, в этот момент начнут загружаться библиотеки, в какой-то момент будет загружена библиотека, которая содержит функцию main, breakpoint станет зарезовлен, и когда эта функция начнет исполняться, мы остановимся.
Здесь как раз показан уже этот момент, причина остановки, что мы остановились на breakpoint, и показан frame, где мы остановились.
Ну и затем можно воспользоваться командой backtrace, сокращение bt, и посмотреть полный backtrace нашей программы.
Ну ниже main у нас нет никаких managed функций, поэтому у нас всего один фрейм.
Затем, если мы продолжим выполнение команды continue и, например, прервем его прерыванием control-c, то у нас теперь причина остановки будет не breakpoint, а interrupt.
И выполнив теперь команду backtrace, у нас будет уже какой-то набор фреймов.
Не так важно, какие это фреймы, но это ahead-of-time компилятор, он что-то компилирует в данный момент.
Конкретно на фреймах останавливаться не будем.
Важно сейчас, что это только managed-фреймы, мы не видим нативный код.
Какие еще есть команды у NetCore DBG?
Во-первых, есть команда catch.
Это, по сути, то же самое, что установка breakpoint, но установка breakpoint не на строчку кода, а на managed exception, что тоже может быть полезно.
Скажем, у вас какие-то есть exception, которые кидаются, где-то они ловятся, вы этого вообще не видите, хотите отловить это поведение.
Затем есть команда list.
Я еще подробнее позже покажу, как ее использовать, но она позволяет в рамках сессии отладчика видеть самый исходный код.
Для этого надо произвести некоторые манипуляции с csproj файлом, и в итоге сам исходный код программы будет добавлен в отладочную информацию для DLL в pdb файлы.
Ну, step – это stop or, и next – это step into.
Аналогично, как в Visual Studio, еще есть команда print для проведения evaluation expressions, которую я как раз подробнее только что показал.
И есть команда attach.
То есть то, что я показывал сейчас на примере Crossgen A2, это был запуск приложения с нуля, можно оттачиться к уже запущенному процессу.
В принципе, есть и другие команды, на них не будем сейчас останавливаться.
Если кому интересно, я еще в конце оставлю ссылку на полную документацию этого режима.
Что ж, перейдем, собственно, к Interop отладке.
Самая интересная часть выступления.
Для начала давайте посмотрим, что предоставляет Microsoft.
Интероп режим еще называется смешанным режимом или гибридным, и он позволяет отлаживать как менеджер, так и нативный код в рамках одной сессии оплатчика.
То, что предоставляет Microsoft, доступно только на Windows, это x86, x64, и буквально в конце прошлого года добавили ARM64, ну и, соответственно, это только Visual Studio.
Зачем это все нужно?
Потому что это повышает продуктивность разработчиков.
Скажем, у вас какое-то сложное приложение, где постоянно переходы из нативного в менеджд код, достаточно много слоев, и использовать отдельные отладчики для нативного кода, отдельный отладчик для менеджд кода может быть просто неудобно, когда хочется видеть реальную картину, что происходит в приложении.
Interop отладка позволяет, соответственно, убрать эту проблему.
Еще один пример, что можно делать на андроиде.
Можно смотреть вот такие смешанные бэктрейсы.
Здесь разными цветами показаны разные типы фреймов.
Зеленым показаны нативные фреймы, красным показаны менеджд джава фреймы.
Да, здесь они, конечно, скомпилированы как и Head of Time, но в принципе это не важно.
Соответственно, у нас приложение, реальный его стэктрейс выглядит таким образом.
Это чередующиеся слои нативного и менеджд кода.
Ну и тогда возникает некоторый виш-лист, чего бы хотелось для interrupt-дебага в .NET.
Во-первых, хотелось бы поддержку Linux.
Во-вторых, чтобы можно было видеть реальные, то есть смешанные стек-трейсы, как я только что показал.
Чтобы можно было ставить брейкпоинты как в нативном менедж-коде, чтобы можно было проводить как нативный, так и менедж-деваливейшн, как нативный менедж-степпинг, ну и так далее.
И есть такая статья в блоге Microsoft, достаточно давняя, называется она «Вы не хотите писать интероп-дебаггер?».
Как можете заметить, я стою здесь, мы не стали слушать название статьи, но учли челленджи, которые в ней описываются, и челленджей там достаточно много.
Во-первых, нужно разделять пользовательские системные треды.
Системные треды – это треды самого runtime и CURSELAR.
Почему это нужно делать?
Потому что мы не можем останавливать системные треды, чтобы у нас полноценно работала evaluation с вызовом функций и так далее.
Иначе у нас будут дедлоки.
Соответственно, да, возможны дедлоки.
Также нужно поддерживать самим различные версии дебаг-информации для нативного кода.
Также нужно разбираться с тем, что у нас есть нативный брейкпоинт, который поставил пользователь, и нативный брейкпоинт, который ставит сам RuntimeCourselar.
Потому что внутри он использует для установки менедж-брейкпоинтов нативный.
Когда мы используем только менедж-отладку, мы об этом не задумываемся, мы делаем какой-то вызов iCoreDebug API, и Runtime за нас сам все это делает.
То в случае нативной отладки надо это обрабатывать самим.
Ну и также необходимо делать полную имплементацию степпинга для каждой поддерживаемой архитектуры, потому что, скажем, ядро Linux может быть сближено без поддержки степпинга, используя P3, соответственно, нужно это эмулировать на стороне самого оплатчика.
Ну и потенциально могут быть какие-то изменения в самом iCore Debug API, чтобы более удобно взаимодействовать с рантаймом, тоже они заранее могут быть не до конца ясны.
Ну, все эти челленджи так и иначе решаются, и давайте посмотрим, какие есть подходы для, собственно, решения этих проблем.
Первый подход – это использовать два оплатчика, нативный и менеджд оплатчик.
Соответственно, нативный оплатчик будет отвечать за дебаг нативного кода, менеджд оплатчик будет отвечать за дебаг менеджд кода, ну и как-то нужно будет их синхронизировать.
Второй подход – это использовать self-debugging в самом рантайме.
По сути, то же самое, что сейчас делается для менедж-дебага, когда у нас рантайм сам отвечает за всю сложную логику.
Ну и третий подход – использовать компоненту для нативного дебага в рамках менедж-отладчика, ну или наоборот, компоненту для менедж-отладки в рамках нативного отладчика.
Давайте рассмотрим их все по очереди.
Значит, первый подход – использовать два дебаггера, нативный и менеджд.
Например, у нас будет нативный дебаггер gdb.ldb, и он будет отвечать за взаимодействие с нативным кодом, будет использовать системный вызов ptrace для взаимодействия с отложимым процессом.
Ну и будет менедждебаггер, например, netcore.dbg, и он будет также использовать iCoreDebug API для взаимодействия с рантаймом.
И каким-то образом нужно будет передавать контроль от одного отладчика другому, контроль над отложимым процессом в зависимости от того, какой код сейчас выполняется.
Какие плюсы у этого подхода?
Что, во-первых, будут использоваться стандартные дебаггеры, что может сильно упростить работу с нативным кодом.
То есть не нужно будет заново писать всю логику для установки нативных брейкпоинтов, для раскрутки стека, ну и так далее.
В чем минусы?
Что может потреблять существенное количество памяти?
Скажем, возможно, на каких-то больших серверах, мощных десктопах это не проблема, что у вас приложение, много приложений влезает.
На каких-то девайсах, где памяти мало, может само приложение дебаггер влезать с трудом.
Соответственно, если у нас возникнет два дебаггера, может получиться такая ситуация, что само приложение, которое мы хотим отлаживать, не влезает на девайс.
Будет не очень удобно.
Вторая проблема, это что необходимо синхронизовывать каким-то образом эти два процесса отладчиков.
Скажем, если мы остановились сейчас на нативном breakpoint, у нас LDB остановил все трейды отлаживаемого процесса, то и, например, у нас где-то в backtrace есть managed frame, мы хотим на этот frame перейти, сделать managed evaluation, то у нас возникнут проблемы, у нас, собственно, возникнет deadlock из-за того, что остановлены системные трейды самого runtime.
Соответственно, необходимо не останавливать системные трейды.
Ну и каким-то образом придется мерзнуть нативные managed стеки.
То есть реальная картина – это чередующиеся слои нативного и managed кода, но затем мы от нативного отладчика будем получать только нативные фреймы, а от managed отладчика будем получать только managed фреймы.
Надо каким-то образом их обратно смерзнуть в правильном порядке.
то тоже может быть проблема.
Ну и потенциально придется патчить нативный дебаггер, например, GDB, LDB.
Если кто-то их видеокод, у них достаточно он сложный, тоже нужно там хорошо разбираться.
Соответственно, это тоже может занять какое-то время.
В итоге для NetCore DBG этот подход не был принят.
Наверное, одной из основных причин – это потому, что он потребляет слишком много памяти.
В принципе, не получится отлаживать что-то на девайсах, где ее мало.
Другой подход – использовать self-debugging в рамках рантайма.
То есть рантайм теперь будет сам отвечать за дебаг нативного кода в рамках отложенного процесса, примерно так, как работает для менеджерской оплатки.
То есть картинка, которая здесь у нас есть, если мы уберем Native Pipeline, то это по сути то, как сейчас работает менедж-дебагер NetCore DBG.
Он использует iCore Debug API, скажем, делает запрос runtime, поставь мне breakpoint на такую-то строчку кода, и runtime уже дальше сам разруливает, как это делать для конкретной архитектуры.
Соответственно, подход здесь будет похожий.
NetCore DBG будет использовать iCore Debug API и его расширение
Расширение, которое сейчас доступно только на Windows и как раз необходимо для нативной отладки.
Соответственно, нужно будет на стороне рантайма имплементировать реализацию этого расширения Core Debug API для Linux.
Плюс такого подхода, что схема будет очень похожа на то, что у нас сделано для Windows.
Будет некоторая унификация.
Не будет проблем с синхронизацией двух процессов-отладчиков.
У нас будет всего один процесс.
За счет этого схема упрощается.
Ну и вся требуемая информация от отлаживаемого процесса может быть легко получена дебаггером, используя iCore Debug API.
По сути, в такой схеме у нас сильно упрощается отладчик.
Мы всю сложную логику перекладываем на рентайм.
Это еще и означает, что нам придется эти изменения каким-то образом вмерзать в upstream .NET.
А дебаггер становится сильно проще.
В чем минус?
Что даже у Windows имплементации есть некоторые ограничения.
Это расширение Core Debug API не позволяет отлаживать весь код.
Оно ограниченное.
Затем нельзя отлаживать сам рантайм и низкое уровень библиотеки.
По той же причине, что если мы поставим breakpoint где-то внутри рантайма и захотим начать делать managed evaluation, у нас просто может быть дедлог.
Ну и не будет работать со старыми версиями .NET, скажем.
Если бы эти изменения попадали в .NET 8, который, кстати, выходит через пару месяцев, то использовать такой интероп-дебаггер с .NET 6, например, который все еще LTS-версия, ну или с другими версиями .NET, которые местами иногда используются, было бы нельзя, что тоже не очень удобно.
Ну и в итоге, опять же, это все привело к тому, что этот подход не был принят для Netcore DBG.
Как я уже сказал, еще один здесь минус в том, что нужно, чтобы все изменения попали в апстрим рантайма, что этих изменений может быть достаточно много, и Microsoft может не очень хотеть их принимать.
Третий подход.
Использовать компоненту для нативного дебага в рамках менедж-дебагера.
Ну или наоборот, компоненту для менедж-дебага в рамках нативного дебагера.
Здесь мы рассмотрим только первый из них.
Соответственно, у нас будет снова менедж-дебагер NetCoreDBG, использовать iCoreDebug API для взаимодействия со сложным процессом, и будет компонент для нативного дебага, который использует системный вызов Petraise.
Соответственно, для каких-то...
низкоуровневой работы с дебаг-информацией для нативного кода, для раскрутки стека можно использовать какие-то небольшие библиотеки.
Ну и потенциально можно расширить iCore Debug API примерно как в предыдущем подходе, если возникают необходимости.
В чем плюс такого подхода?
Что есть полный контроль над кодом, можно использовать какие-то небольшие библиотеки, место использования сложных фреймворков типа GDB и LDB, соответственно, меньше.
Можно полностью контролировать код, полностью его изменять, как вы хотите.
И также не требуется существенное изменение в самом рантайме.
По сути, этот подход немного противоположен предыдущему.
У нас вся сложная логика переезжает из рантайма на настроенную дебаггер.
Но, опять же, можно ее контролировать полностью.
В чем минус такого подхода?
Что, конечно, все еще придется синхронизировать нативную и менеджерную части дебаггера, но это уже не так сложно, как синхронизация двух процессов.
И все еще нельзя будет отлаживать сам рантайм и низкоровные библиотеки, опять же из-за того, что могут быть дедлоки, если мы, скажем, остановим все треды самого рантайма.
Дедлоки во время эволюции.
Ну и в итоге именно этот подход был принят для NetCore DBG.
Более того, сейчас вообще не требуется никакие изменения в рантайме.
Это значит, что интероп-режим NetCore DBG можно использовать со всеми версиями, начиная с .NET Core 2.1, где как раз появился iCore Debug API.
То есть, по сути, поддерживаются все те же самые версии, что и обычного Managed NetCore DBG.
Ну и давайте посмотрим еще раз на архитектуру.
Теперь у нас в отлаживаемом процессе добавляется некий пользовательский нативный код, и пока что здесь нет никакого нативного дебага.
Когда появляется компонент для нативной отладки, менедж-дебагер продолжает ее контролировать и получает обратно некоторые ивенты, которые встраивают в свою очередь ивентов.
Ну а нативный дебагер использует сигналы и системные вызовы wait, pit и patrace для взаимодействия с нижележащими слоями, ну и в итоге с отлаживаемым процессом.
Как я уже сказал, когда мы работаем с менедж, с отладкой, мы очень сильно полагаемся на сам рантайм.
Когда мы используем Accord Debug API, мы говорим, поставим breakpoint, например, на такую строчку кода, и рантайм сам разруливает, как это делать для каждой конкретной архитектуры.
В случае нативной отладки все это приходится реализовывать самим, и там возникает достаточно много нюансов.
Вот на каких-то таких нюансах я сейчас как раз хочу остановиться.
Во-первых, как в принципе выглядит нативный брейкпоинт на различных архитектурах?
Он выглядит по-разному.
Например, на x86, x64 это инструкция interrupt3, однобайтная.
Когда она выполняется, у нас вызывается обработчик прерываний по брейкпоинту.
Из вектора обработчиков прерываний ядром в итоге посылается сигнал SIGTRAP отлаживаемому процессу, который отладчик перехватывает, используя whitepeat.
Примерно так же работает схема для ARM.
На ARM64 это инструкция BRK0 4-байтная, на ARM32 это в ARM или сам режиме, это некоторая нелегальная с точки зрения ядра инструкция, либо 4-байтная, либо 2-байтная.
Уже здесь возникают нюансы, скажем, в зависимости от того, что происходит во время выполнения этой инструкции.
Когда мы выполняем инструкцию interrupt3 на x86 и x64, у нас меняется программ-каунтер на, собственно, один байт.
На ARM он не меняется.
И нужно это учитывать, когда мы потом хотим попасть в начало инструкции breakpoint.
Я еще расскажу, зачем это нужно.
Как выглядит breakpoint, понятно.
Как нам теперь его, собственно, поставить, когда мы находимся в другом процессе?
Во-первых, нужно остановить все треды.
Ну и опять же используется ptrace.
ptrace.pickdate позволяет читать данные отлаживаемого процесса.
То есть мы читаем некоторые байты, соответствующие инструкции, куда мы хотим поставить breakpoint.
И затем в эти байты нужно закодировать наш breakpoint.
После того, как мы его туда закодировали, нужно обратно его записать в память отлаживаемого процесса и сохранить оригинальные байты, которые мы перезаписали байтами breakpoint.
Они нам еще понадобятся.
Ну и затем запустить все треды.
Здесь тоже уже есть нюансы, потому что Петрыс читает машинное слово.
Соответственно, например, на R64 мы будем читать 8 байт, а закодировать нам нужно туда только 4.
Нужно это тоже учитывать.
Затем, как нам, собственно, перешагнуть Breakpoint?
Скажем, мы остановились на Breakpoint и хотим сделать в дебаггере команду continue.
Нужно вернуться в начало инструкции, то, о чем я говорил два слайда назад, то, что делается по-разному на каждой архитектуре.
Затем нам нужно восстановить оригинальные байты, которые мы специально сохранили.
После того, как мы восстановили инструкцию, нужно одну ее выполнить.
Это называется single step.
Ну и восстановить breakpoint после того, как мы его перешагнули.
Здесь возникают свои нюансы, потому что этот single step может поддерживаться в P3, если у нас ядро собрано с ним, а может не поддерживаться.
Тогда нужно эмулировать на стороне самого процесса оплачка выполнение этой инструкции.
Как отслеживать загрузку нативных библиотек?
Как я уже говорил, когда мы работаем с менеджд-отладкой, мы, условно говоря, вешаем callback на загрузку менеджд-библиотеки, и runtime потом его вызывает.
С работой с нативным кодом опять все не так, и здесь нам помогает такая вещь, которая называется rendezvous breakpoint или rendezvous структура.
Она еще называется rdebug, и у нее есть несколько полей, которые нас сейчас интересуют.
Это rbrk, rstate, rmap.
rbrk, по сути, содержит адрес, куда нам нужно поставить breakpoint, который будет срабатывать каждый раз, когда у нас загружается какая-то нативная библиотека.
По сути, это адрес где-то внутри загрузчика.
И этот breakpoint будет срабатывать два последовательных раза, например, на загрузку или выгрузку библиотек.
Первый раз у нас будет меняться значение переменной rstate, и значение будет меняться либо это добавление, либо удаление библиотек.
А второй раз, когда он будет срабатывать, у нас будет меняться значение переменной rmap.
Переменная rmap как раз содержит связанный список загруженных в данный момент библиотек.
Соответственно, когда мы попадаем на загрузку новых библиотек, мы можем проверить, не попали ли у нас какие-то еще незарезовленные брейкпойнты во вновь загруженной библиотеке, можно их потом сделать зарезовленными.
Ну и наоборот, если у нас что-то выгружается, мы можем тогда какие-то брейкпойнты сделать незарезовленными.
Я здесь остановился на таких четырех небольших нюансах, но нюансов там достаточно много.
По сути, в каком-то смысле здесь нужно повторять логику, которая давно реализована в нативных дебаггерах типа GDB и LDB.
Что в итоге сейчас работает в интероп-режиме NodeCore DBG?
Во-первых, работают все функции менедж с отладчиком, что это означает, что можно полноценно отлаживать в этом режиме менедж с приложением, даже несмотря на то, что можно отлаживать нативный код.
Соответственно, работает оттач к уже запущенному процессу либо запуск приложения с нуля.
Работает отслеживание загрузки и выгрузки нативных библиотек, соответственно, и отслеживание дебаг-информации для них же.
Работают нативные breakpoints для x86, x64 и ARM.
Ну и для них же работает раскрутка стэка.
Соответственно, это может быть managed stack, это может быть полностью нативный стэк.
Скажем, у нас thread создан где-то в нативном коде, runtime о нем вообще ничего не знает, он никогда managed код не выполняет.
Ну либо это смешанный стэк, то, что я показывал до этого, это чередующиеся слои нативного и managed кода.
Ну, давайте посмотрим на некотором демо, как, собственно, Netcore DBG использовать.
Демо будет достаточно простое.
У нас есть некоторый main.
В нем в цикле вызывается функция log.
Это pnvog из нативной библиотеки либо locator.so.
У него два аргумента.
Это некоторое число, по сути, сколько мы будем лоцировать.
И второй аргумент – это callback, который будет как reverse pnvog вызываться из нативного кода.
Ну и да, сам callback показан чуть выше, он по сути просто в консоль будет выводить значения с указателем выделенной памяти и размер аллокации.
Ну а сам аллокатор реализован где-то на темном коде, он будет достаточно простой.
То есть это, по сути, простая ибертка над молоком, которая еще и вызывает callback, reverse pay invoke, ну и затем возвращает аллоцированную память.
Да, конечно, по-хорошему надо бы память отсюда освобождать, но это у нас чисто искусственный пример, посмотреть, что Netcore DBG может показать.
Ну и что мы будем делать?
Будем запускать это на x64, но, опять же, можно использовать любую другую архитектуру, которую я упоминал.
Для демо это вообще не критично.
Ну и, в принципе, оно работает на всех архитектурах, которые я упоминал.
Еще будем использовать .NET 6, но опять же это тоже не критично.
Можно использовать любой .NET, начиная с .NET Core 2.1.
В демо это, по-моему, вообще нигде не проявляется.
И будем добавлять вот такую строчку в cs.proj файле.
Зачем она нужна?
Она нужна, чтобы в pdb файлы, в отладочную информацию для DLL у нас добавился самый исходный код программы.
Это нам как раз понадобится для того, чтобы затем использовать команду list.
Это мы еще посмотрим.
Собственно, билдить, менедж часть будем в дебаге.
Нативную часть мы сбилдим с отладочными символами, чтобы они не были стрипнуты.
Соответственно, либо локейтер ISO будет содержать отладочную информацию.
И сам дебаггер мы будем билдить с поддержкой интеропа.
Здесь показана опция CBake, которую нужно передать при билде над CoreDBG, чтобы он был сбилжен с интеропом.
Ну а затем есть еще опция командной строки, которая позволяет даже при добавленном при билде интеропе включить его или выключить.
Что тоже может быть удобно, если вам, например, нужно его включить или выключить.
В зависимости от вашего приложения.
Ну для начала запустим без дебаггера в принципе.
Ну и здесь мы увидим, что у нас, собственно, печатаются какие-то адреса, локации и размеры.
Никаких проблем нет.
Давайте теперь воспользуемся managed.netcore.dbg, который я уже показывал на примере CRUZGEN A2, то есть пока что без introp.
Опять здесь опция interpreter равно CLI, избыточная по дефолту, он и так запустится в таком режиме.
И использовать мы будем host.net.
То есть в чем отличие с предыдущим примером, что там я использовал host.corun, который сам же сбилдил, здесь я использую host.net, который поставляется как часть SDK.
Нюанс здесь в том, что в SDK дебаг-символы для нативного кода не поставляются.
Еще мы это увидим.
Ну и само приложение это просто одна daily.
Затем мы поставим breakpoint на функцию reportCallback, это как раз та функция, которая вызывается reversePinvoke из нативного кода.
Опять же, он будет сначала не зарезовлен, и вот здесь уже после вызова команда run я оставил полный вывод, который показывает netcore.dbg.
Значит, первое, что у нас здесь произойдет, это загрузится system.private.core.lib.
Обратите внимание, что там написано no symbols loaded.
Для нее не загружены символы, потому что они по дефолту в SDK не поставляются.
Ну и показан адрес, куда эта библиотека загружена, и размер.
Затем начинают создаваться managed threads.
Продолжается здесь вывод.
Загружается библиотека нашего приложения.
Обратите внимание, теперь у нас символы загружены, и у нас модифицируется breakpoint, который мы поставили на функцию reportCallback.
Продолжают загружаться стандартные библиотеки рантайма.
В какой-то момент будет выведен вывод из мейна, и мы остановимся на нашем брейкпойнте.
Ну и затем, если выполнить команду backtrace, то увидим мы следующие четыре фрейма.
Нулевой фрейм — это, собственно, наша функция, где мы поставили брейкпойнт прямо на входе.
Мы остановились и...
Это функция, которая вызывается reverse pinwalk из нативного кода.
Затем у нас какие-то два непонятные native frames, ничего про них здесь не известно, но потом мы еще узнаем, что это такое.
И третий фрейм – это наш main, откуда pinwalk у нас вызывается, функция log.
Где-то в цикле, но так как это у нас первый раз MacPyNet Breakpoint, это первая итерация этого цикла для переменной i равно 0.
Здесь, собственно, можно сделать ManagedEvaluation для аргументов этой функции.
То есть с этим никаких проблем не возникает.
Это у нас пока что только менедж с оплаткой, мы еще не включили интероп.
И можно перейти на третий фрейм, это фрейм мейна, и сделать evaluation там уже для каких-то выражений посложнее.
Тоже с этим никаких нет проблем.
Посмотреть, например, значение объекта callback.
Снова можем перейти на нулевой фрейм и пару раз сделать команду step.
Отличие здесь будет в том, что причина остановки теперь будет не breakpoint, а конец stepping range.
Соответственно, можно степаться по строчкам кода.
И пару раз делая step, мы попадем на второй вызов функции report callback.
Ну и опять же здесь можно сделать evaluation, посмотреть, что у нас значение переменной изменилось, то есть все работает как надо.
Ну и команда list, про которую я уже говорил, которая позволяет смотреть сам исходный код прямо в рамках самого отладчика.
Для этого нам нужно модифицировать csproj файл.
Исходный код будет добавлен прямо в pdb файлы.
И затем дебаггер будет их подгружать.
Ну и здесь также показывается строчка кода, где мы остановлены.
Это все касалось менедж отладки.
Давайте теперь попробуем интероп отладку и посмотрим, в чем здесь будет разница.
Во-первых, нужно добавить опцию "-interopdebugging", чтобы ее включить, даже при добавленной при билде поддержке интеропа.
И затем мы также поставим breakpoint на функцию reportCallback, которая вызывается reversePinwalk.
Здесь у нас уже появляется существенно больше вывода.
Можете заметить, что у нас начинают загружаться нативные треды.
Раньше NetcoreDBG эту информацию не писал.
Также начинают загружаться нативные библиотеки, например, libpthread.
И обратите внимание, что для нее также символы не загружены.
На системе они не стояли.
Продолжают загружаться библиотеки, их становится существенно больше, существенно больше вывода.
Все еще продолжают загружаться.
Вот, например, загружается одна из основных библиотек рантайма, libcursorso.
Для нее также нет символов.
Как я уже сказал, она шла в SDK без них.
Все еще продолжается загрузка библиотек.
Теперь загружается Ellipse LRJIT.
Это тоже одна из основных библиотек рантайма, которая содержит RUJIT.
Продолжают загружаться библиотеки.
Их очень много.
В какой-то момент будет загружена наша нативная библиотека, библиотека нашего приложения.
И обратите внимание, что для нее загружаются символы, потому что мы ее так сбилдили.
И это как раз нам потом позволит поставить breakpoint в код из нее.
Ну и мы остановились на breakpoint.
Что будет теперь, если мы вызовем команду backtrace?
Ситуация существенно поменялась.
Я здесь разным цветом выделил разные фреймы.
Голубым выделены менеджд фреймы, красным, оранжевым выделены нативные фреймы.
Давайте опять пойдем по порядку.
Значит, нулевой фрейм – это у нас менеджд функция reportCallback, которая вызывается реверс по инвокам, точно так же, как при менеджд отладке.
Затем идет первый фрейм, некий Corsair Native Frame.
Я еще скажу, что это такое.
Второй фрейм – это наша функция Log из нативной библиотеки.
Третий фрейм – это опять некий Corsair Native Frame.
Ну а четвертый фрейм – это наш мейн.
Ну а все, что ниже мейна – это как раз нативная логика самого Host.NET, которая используется для того, чтобы приложение запустить, собственно, дойти до мейна.
Ну и как раз из-за того, что у нас нет отладочных символов для самого SDK, у нас показываются не имена функции, а оффсеты от начала shared object.
Ну а для label.accreter.so можете заметить, что указана и строчка кода, ну и имя функции, где мы остановились.
Соответственно, если у нас в приложении какое-то сложное, таких переходов много, может быть очень полезно видеть реальную картину.
Ну давайте теперь поставим breakpoint в нативный код в нашей библиотеке, сделаем команду continue, остановимся на нем и снова сделаем команду backtrace, но теперь не backtrace для одного фрейма, а вообще все фреймы посмотрим.
Я здесь покажу только два из них.
Во-первых, фрейм, который у нас продолжает работать, это один из системных тредов самого рантайма.
Можете заметить, что у него в его фреймах есть какой-то код из одной из стандартных библиотек рантайма.
Соответственно, его мы не останавливаем, чтобы у нас полноценно работал managed evaluation.
Ну а main у нас остановлен, и теперь мы остановлены уже не в managed коде, а в нативном коде.
И опять у нас некий Corsilar Native Frame.
Ну и, в принципе, все то же самое.
Соответственно, никакой проблемы с раскруткой, начиная с Managed Code, либо начиная с нативного кода, нет.
Что же такое тогда Corsilar Native Frame?
Возможно, кто-то копался в рантайме, возможно, в принципе, кто-то знает, что в рантайме часто используются ассемблерные вставки для, например, вызова функции, для маршринга аргументов.
Вот это как раз она и есть.
Соответственно, то, что у нас было на предыдущем слайде –
Третий фрейм – это ассемблерная вставка, которая необходима для того, чтобы промаршалить аргументы при вызове p-invoke.
Ну а первый фрейм – это точно такая же вставка, но промаршалить обратно из нативного кода в менедж-код.
Соответственно, когда у нас таких переходов много, очень удобно видеть всю эту информацию в одном дебаггере, вместо того, чтобы брать, например, gdb, а он может, если вы остановлены где-то в менедж-коде, вообще ничего не размотать.
Не очень удобно.
Еще по поводу этого примера.
Сейчас мы стоим на нативном фрейме.
Если мы перейдем на второй фрейм, это наш managed main, то мы можем полноценно делать evaluation.
Как раз из-за того, что у нас не остановлены системные трейды runtime.
То есть с этим никаких проблем нет.
В принципе, касательно демо, наверное, все.
По поводу компетиторов.
Во-первых, есть Visual Studio, но это только Windows и нет ARM32.
Ну и это не open source, соответственно, использовать ее на Linux нельзя.
Есть Android Studio Debugger, который тоже поддерживает Interop, но это только Java и Android, и нет ARM32 и x86.
Тоже для нас не подходит.
Вот то, что предоставляет Microsoft, это LDB с SOS-плагином.
Соответственно, Target.net, Linux и все архитектуры, которые я упоминал.
Но есть существенные отличия с Netcore DBG.
В чём же они заключаются?
Если мы посмотрим по фичам, соответственно, это нативные managed или смешанные backtrace, нативные или managed breakpoints, нативные или managed EVAL и нативные managed stepping, то у Visual Studio, по сути, всё есть, за исключением того, что нативный EVAL ограничен.
В Android Studio всё примерно так же, за исключением того, что почему-то в рамках IDE нельзя посмотреть смешанные backtrace, можно его посмотреть сторонней тулой, это вот то, что я показывал на одном из предыдущих слайдов.
Какие есть проблемы с LDB-Source плагином?
Во-первых, там не поддерживаются managed breakpoints на managed exception, что может быть очень удобно, опять же, если у вас где-то exception эти кидаются, где-то ловится, вы это просто не видите, но хотите понять, что происходит.
В Netcore DBG это есть.
Затем, менеджд и вал, как я уже сказал, в LDB со сплагином достаточно ограничены.
В чем причина?
Потому что LDB просто останавливает все треды.
Это приводит к тому, что вы можете читать память, но не можете выполнять код.
Соответственно, посмотреть значение переменной можно, а вызвать какую-то функцию во время эволюции уже нельзя.
Это как раз то, что отличает Netcore DBG от LDB.
Можно полноценно вызывать функции.
Ну и в LDB со сплагином нет менеджд степпинга, который также есть в Netcore DBG.
Чего в Netcore DBG пока нет, это брейкпоинтов по имени функции, пока что добавлены только брейкпоинты на строчку кода, ну и пока что нет эволюции нативного степпинга, но в будущем, я думаю, все это появится.
Еще один важный момент.
Как я уже говорил, на некоторых системах может быть достаточно мало памяти, если это не какой-то мощный десктоп, а, скажем, какая-то борда на ARM.
Соответственно, здесь показан пример на некотором приложении, не так важно на каком, что LDB с OS-плагином потребляет в пять раз больше памяти, чем на CoreDBG.
Соответственно, если попытаться засунуть их, если бы мы использовали схему отдельно с нативным дебаггером, отдельно с менедж-дебаггером, и попытались их засунуть на девайс, где памяти мало, могло бы получиться так, что приложение не влезает.
Это еще одно существенное отличие.
Не разбирались, почему с LDB, с соцплагином такая проблема, но память пропорциональна объему загруженной дебаг-информации.
Возможно, если кому-то будет интересно, можете поизучать.
Что можно делать дальше?
Какие есть дальнейшие направления?
Во-первых, можно добавить нативные функциональные брейкпойнты по имени функции, можно добавить остановку на нативных сигналах и на C++ эксепшенах из пользовательского кода, собственно, добавить нативный эвалиейшн и нативный стейпинг.
Итак, что мы сегодня узнали?
Во-первых, мы узнали, как .NET дебаггеры имплементируются, какие там есть нюансы, и детально узнали, как делать evaluation.
Это, наверное, одна из самых сложных фич дебаггера, потому что она очень полагается на iCore Debug API, но iCore Debug API не позволяет делать, собственно, выполнение самой программы для стек-машины.
Все это делается на стороне самого отладчика.
Еще мы узнали, как, собственно, имплементировать интероп-дебаггеры, узнали, какие есть подходы, в чем у каждого подхода свои плюсы и минусы.
Возможно, кто-то захочет написать свой интероп-дебаггер для .NET, а выберет другой подход.
Ну и посмотрели, как интероп-дебаггер для .NET использовать.
Мне кажется, это очень полезная фича, потому что позволяет, если у вас очень сложное приложение с постоянным переходом между нативным и менеджерским кодом, позволяет более удобно его отлаживать, что повышает продуктивность разработчиков.
Оставил я здесь некоторые ссылки на iCore Debug API.
Он достаточно большой.
Если кому интересно, можете поизучать.
Ссылки на блог Microsoft про Interop Debugging.
Там как раз одна из статей, которую я упоминал.
И ссылка на документацию NetCore DBG с как раз CLI интерфейсом.
То, что я сегодня показывал.
Соответственно, можно там изучить более подробно, как его использовать.
Возможно, кому-то это пригодится.
Можно ли как-то поучаствовать в проекте?
Конечно же, можно.
Проект open source.
Приходите, открывайте issue, делайте pull-request, в принципе, читайте код, фидбэк всегда полезен.
Ну и, надеюсь, кто-то после моего доклада сегодня попробует дебаггер использовать, и ему понравится.
В принципе, у меня все.
Вопросы?
Спасибо.
Спасибо большое.
Я точно попробую.
Мне понравилось.
В целом у нас есть время на пару вопросов.
Кто-нибудь хочет задать?
Тема довольно сложная, но тут, наверное, много чего хотелось бы уточнить.
Так.
Да, вот там есть желающий.
Здравствуйте, Артем.
Меня зовут.
Скажите, пожалуйста, какая целевая аудитория этого продукта?
Потому что я там в Enterprise кровавом лет 15, наверное, и все через студию, дебажи, вопросов вообще никаких нет.
Ни разу не было кейса, когда надо было в текстовом файле читать вот эти адские выводы.
Когда, короче, его использовать вообще?
Ну, соответственно, Visual Studio только Windows.
Если вы хотите делать что-то с .NET на Linux, имеет смысл использовать дебаггер для Linux.
Да нет, ну на Mac M1 Visual Studio ставится и спокойно дебажится, никаких проблем не замечено.
Ну, я имею в виду Linux, не Mac.
А, понятно.
Еще желающий?
Здравствуйте, Глеб.
Спасибо.
Было любопытно.
Планируется ли плагин для VS Code?
Для VS Code и так уже можно интегрировать.
Насколько я знаю, там эта интеграция достаточно простая.
По сути, надо указать путь к бинарнику.
В принципе, я думаю, в issue, либо даже, наверное, в документации, в репозитории на GitHub это все уже есть.
Так что можете уже пробовать.
Спасибо.
Так, там еще вопрос будет.
Спасибо за доклад, интересно.
Поближе чуть-чуть.
Спасибо за доклад, очень интересно.
Вопрос про удаленную отладку, подключиться к девайсу, например, ну, Raspberry возьмем, и Visual Studio Code на десктопе.
Возможен ли такой вариант с этим отладчиком?
Возможен, это как раз, то есть дебаггер будет работать на девайсе, нужен плагин, который будет каким-то образом взаимодействовать с девайсом.
Он есть, плагин?
Это зависит от вашего девайса, можете сами написать.
То есть для каждого девайса все по-разному.
Зависит от применения.
Ну, давайте конкретизируем о Raspberry.
Я думаю, конкретно под Linux для Raspberry плагина нету.
Его можно писать.
Либо можно пользоваться CLI-режимом, который я показывал.
А какие-то более человечные десктопные варианты Debian, под них есть подобные плагины, чтобы отлаживать именно в удаленном режиме?
Именно удаленный режим?
Да.
Ну, нужно поискать, возможно, сообщество уже что-то делало, о чем я не в курсе.
Возможно, они есть.
Понятно.
Спасибо.
Так, спасибо.
Так, ну,
Вопросы у кого-нибудь еще есть в зале?
Так, ну раз нет, в принципе, было хардкорно, действительно.
Довольно сложно.
Тема для всех будет интересная.
Кто-то, может, и захочет написать свой дебаггер или какой-нибудь плагин.
Сейчас предлагаю всем пройти в дискуссионную зону, чтобы еще помочь Глебу.
И, наверное, кто-то в онлайне захочет задать какой-нибудь тоже сложный вопрос.
Всем спасибо.
Спасибо.
Похожие видео: Глеб Балыков

Администрирование Линукс (Linux) - Урок 95 - tc netem инструмент эмуляции проблем сети

Денис Пешехонов, Александр Химушкин — Укрощаем DDD на практике

Linux by Rebrain: ФСТЭК для Linux. Часть 2

سیستم عامل چطوری کار میکنه؟ دنبال کردن فراخوانیهای سیستمی لینوکس مثل یک هکر و متخصص امنیت

Linux by Rebrain: пакеты RPM и DEB

