Дебаггинг .NET Core приложения из командной строки в Linux

Миллион лет тому назад, ещё в университете, я делал курсовую по Unix на C++ и в какой-то момент мне пришлось дебаггить всё это третьекурсное великолепие из командной строки. Это было офигенно. И ощущение тотальной гикнутости происходящего, и поразительная эффективность процесса. Оказывается, в отсутствие UI отвлекаться больше не на что, и дебаггинг становится удивительно сфокусированным действом.

С тех пор как у .NET Framework появился кросс-платформенный брат близнец .NET Core, я всё выжидал, как бы это повторить давнишний подвиг ещё раз, но уже для C#, и недавно это-таки случилось. Не совсем гладко, но тем не менее. Давайте смотреть.

Подготовка

Для эксперимента потребуется Убунта, .NET Core SDK, lldb дебаггер и какое-нибудь демо-приложение. В конце апреля 2018-го вышла целая куча обновлений и релизов, так что теперь можно и с Ubuntu 18.04 поиграться, и с .NET Core SDK 2.1 RC1 замутить. Последний, кстати, теперь собирается под более свежий lldb-3.9, а не 3.6, как раньше, так что и тут обновка. Что касается демо-приложения, то любой hello-world с локальными переменными и параметрами у методов подойдёт.

Устанавливаем инструменты

По давней традиции я установлю всё в вируальную машину, и с этим поможет вот такой Vagrantfile с provision.sh в придачу:

Демо-проект

Тут вообще всё просто. Пусть будет бесконечный цикл с паузой внутри, а так же функцией, которая рассчитывает, как долго эта пауза была. Программа абсолютно бесполезная, но с какими-никакими вычислениями, так что будет что подебагить.

Подключаем дебаггер

Итак, запускаем приложение и пробуем прикрутить к нему дебаггер:

console.dll запустился в фоновом процессе, так что теперь можно взять его айдишку (6124), запустить lldb, загрузить в него SOS плагин для дотнэта и подключиться-таки к процессу:

Всё просто. Теперь убедимся, что SOS действительно заработал (например, выполнив его команду вроде clrthreads) и займёмся чем-нибудь полезным:

Устанавливаем брейкпоинт

Для этого есть SOS команда bpmd, на вход которой достаточно либо полного имени метода, либо адрес его дескриптора. Я, например, хочу поставить брейкпоинт в GetTicksElapsed, который принадлежит классу Program, что внутри console.dll, так что вот как это будет выглядеть:

Между прочим, ставить брейкпоинты по адресам дескрипторов методов иногда проще, чем по их именам. Всё-таки полное имя метода ещё выяснить нужно. А адреса дескрипторов валяются везде — в стеках, указателях на инструкции, в таблицах методов. Везде, в общем. Вот так, например, можно поставить брейкпоинт практически в любой метод текущего стека вызова:

Выбираем в качестве жертвы метод  Main, запоминаем его IP (instruction pointer) и творим мини-магию:

Легкотня. Теперь программу можно продолжить и ждать, когда она остановится на нашем брейкпоинте:

Практически мгновенно. Теперь можно заглянуть внутрь.

Смотрим на стек вызова

Команда clrstack (или sos ClrStack) нужна как раз для этого:

Вывод вполне понятен. Там даже имена файлов и номера строк есть. Но clrstack может больше. Например, у него есть -p  параметр, который вдобавок к именам функций выведет значения их аргументов. А это уже что-то:

Давайте, например, посмотрим, что же там за значения были. В lastTicks лежит число 0x8d5bacfae88e0b3, что в переводе в десятичный будет 636620323491995827, и в переводе в дату действительно выдаёт текущее UTC время:

Текущая дата

Что касается args, то скорее всего это аргументы командной строки, переданные в нашу программу, и это очень легко подтвердить:

Так и есть — пустой массив строк.

Смотрим в локальные переменные

Вот тут засада. clrstack -i , которой полагается выводить локальные переменные, в lldb-3.9 и libsosplugin.so от .NET Core SDK RC1 безбожно валится с segmentation fault. Более старые версии дотнета я не проверял, но в этом у меня SEGFAULT в ста процентах случаев.

Переходим в/через/из инструкции

В отличие от WinDBG, в SOS и lldb совсем не видно CLR-совместимой команды для перехода между инструкциями. Но можно пользоваться нативными lldb’шными, которые хоть и работают с ассемблерными инструкциями, но дают хоть какое-то пространство для манёвра. А там и clrstack можно подтянуть, чтобы проверить, в какой части управляемого кода мы сейчас находимся:

callq, если я не ошибаюсь, это вызов процедуры по x64 адресу, так что step должен зайти внутрь её и потому увести нас куда-то очень далеко:

Так и есть, теперь мы в DateTime.Now геттере. Кстати, это можно было предсказать, просто проверив адрес, по которому callq  собирался нас вести:

Я не нашёл псевдоним для команды выхода из текущей функции, так что можно просто воспользоваться её полной формой: thread step-out. Но с ней есть небольшая проблема. В половине случаев step-out выводил меня больше, чем на один уровень вверх. Может, это из-за того, что реальный стек вызова состоит как из управляемых, CLR функций, так и нативных, от рантайма. clrstack показывает только управляемые (-f покажет все), step-out учитывает все, так что, возможно, случается какой-то конфликт. А может, просто пальцы кривые.

Мораль

Вот так вот и выглядит он, дебаггинг .NET Core приложения из командной строки. Где-то логично, где-то не очень. Найти документацию по процессу почему-то достаточно сложно, так что я, скорее всего, что-то упустил. Но и того, что есть, вполне достаточно, чтобы провести начальное вскрытие запущенного процесса и проверить гипотезу-другую. Я вообще удивлён, насколько тонкая на самом деле грань между управляемым кодом и его ассемблерным представлением. Если по нативному адресу для callq можно найти соответствующий ему  управляемый метод, то, может, и в регистрах процессора можно выковырять какой-нибудь .NET объект. Как знать.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *