В поиске вопросов, или как создать новый отладчик

    Мы уделяем много внимания инструментам разработки: участвуем в горячих спорах о редакторах (Vim или Emacs?), долго настраиваем IDE под свой вкус, и тщательно выбираем языки программирования и библиотеки, которые с каждым днем становятся все лучше и удобнее. Однако, здесь можно выделить одну категорию, которая по какой-то причине остается незаслуженно забытой: отладчики не сильно изменились за последний десяток лет, хотя по-прежнему являются одним из базовых инструментов для отлова ошибок и навигации в коде.



    Изображение: Timothy Dykes @timothycdykes, unsplash.com


    Гораздо чаще мы предпочитаем быстро добавить пару printов вместо того, чтобы поставить в нужном месте точку останова и пройтись к ней отладчиком — и вопрос "почему?" не перестает меня занимать — ведь логи и printы дают ограниченную информацию и не позволяют интерактивно взаимодействовать с запущенным процессом (а отладчики могут работать даже и с "умершими" процессами!).


    Одной из главных причин я вижу как раз отсутствие значительного прогресса в разработке отладчиков.


    GDB появился еще во времена Горбачева, и с тех пор не претерпел фундаментальных изменений. И хотя у нас уже есть скоростной интернет, большие экраны с разрешением в 4K, доступные восьми- и шестидесятиядерные процессоры, принцип работы с отладчиком не изменился существенным образом. В лучшем случае, мы получаем интеграцию с IDE и редакторами вроде VS Code (или с браузером, если это JavaScript), но отладчики по-прежнему не умеют по-настоящему понимать наш код и часто не могут дать ответов на сложные вопросы.


    Эта ситуация довольно резко контрастирует с прогрессом в дизайне компиляторов и языков программирования. Такие языки как Rust, предлагающие стандартный набор незаменимых в современном мире инструментов разработки — хороший пример инновации в устоявшейся сфере системного программирования, где значительная часть инструментов застала уже даже не только Горбачева, но и Брежнева (например, программа Make была создана в 1976 году).


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


    Но вернемся к проблеме отладчиков. Если мы заглянем под капот GDB и LLDB, то увидим довольно странную картину: поддержка для новых языков программирования (Go или Rust) реализуется не при помощи уже существующих компиляторов, а через написание отдельных парсеров выражений на C или C++. Возникает сложная проблема поддержки и порочный круг — отладчики не реализуют в полной мере все выразительные возможности языка, поэтому их мало кто использует — и потому что их мало кто использует, мало кто занимается их поддержкой и разработкой. В результате такой стагнации поддержка отдельных языков удаляется из основной кодовой базы — как это случилось с Go и Java в LLDB.


    Что можно сделать, чтобы выпутаться из такой ситуации? Конечно, переписать все на Rust! Как показывает практика, создание новых современных отладчиков не настолько невероятная задача. Успех проекта Delve показывает, что отладчики заточенные под идиоматику отдельного языка (в данном случае — Go) востребованы, даже если поддерживается только архитектура x86-64 и ОС Windows/Linux/macOS.


    Создавая новый инструмент отладки, мы должны делать его модульным, расширяемым, и с возможностью интеграции с компилятором. Это откроет широкие возможности: пользователи смогут создавать собственные доменно-ориентированные отладчики, которые будут иметь полный доступ к контексту конкретного приложения (по аналогии с доменно-ориентированными языками). Модульная структура позволит реализовать REPL или аналог Jupyter с визуализацией данных и возможностью писать хоть целые новые функции в рантайме (как в Пайтоне, Руби, и других языках с виртуальной машиной).


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


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


    Если вам интересно поучаствовать к разработке, мы будем рады вас поддержать, даже если системное программирование пока что для вас ново. Вы также поможете, если поделитесь своим опытом отладки: с какими проблемами вы сталкивались, какие инструменты используете, и что бы хотели видеть в новом отладчике?


    Спасибо, что прочитали этот текст.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 14

      0
      Да, с отладчиками грустно, даже для C/C++. Если под Windows еще как-то выручает отладчик Visual Studio, то под Linux совсем беда — как с отладчиками, так и с IDE. Да, Qt Creator что-то может, но если редактор там еще нормальный, то отладчик — тоже некая не очень удобная надстройка над древним gdb.
        +1

        Для C/C++ под Линукс чуть помогает rr, который умеет «отматывать» время назад. В WinDbg Preview тоже такое недавно добавили. Но это только часть необходимых возможностей, конечно.

          0

          lldb в QtCreator также работает

            0
            Clion предоставляет вполне комфортную работу с GDB.
            +6
            Статья понятна, но не понятно, чего именно не хватает в современном отладчике. А то ощущение, что претензия одна — возраст Горбачева
              +1

              Если говорить о конкретике, то текста получилось бы еще на пару экранов. :) Основная проблема — это отсутствие поддержки языковых фич, да хотя бы даже парсеров выражений. В lldb, например, для Раста вообще используется парсер выражений от C++, что вызывает серьезные трудности. Если пытаться отлаживать менее прямолинейный код (например, async/await в Расте или код использующий thread-local переменные в C/C++), все эти проблемы начинают копиться как снежный ком, и в итоге получается, что проще вернуться к знакомым println и логам.

                +1
                А без конкретики ничего не понятно. Я могу предположить, что у меня Си головного мозга, но, например, я не очень представляю что вы хотите получить в итоге.
                  +1

                  Если смотреть с точки зрения Си, то, пожалуй, наиболее близкий пример здесь — это BPF, виртуальная машина внутри ядра Линукса, на базе которой, например, делают инструменты трассировки bcc. Через них можете смотреть, какие системные вызовы происходят, какие файлы открываются программой, сколько памяти выделяется (и отслеживать утечки), и т.д. — все это работает похожим на gdb образом, с той разницей, что брейкпоинты ставятся и обрабатываются "автоматически" — т.е., скажем, поставили брейкпоинт на вызов malloc(), и каждый раз, когда он дергается — считаем, сколько памяти выделили и откуда. В конце эту информацию суммируем и показываем пользователю.


                  Только в случае BPF это все обычно происходит в контексте ядра, а не в user space — но есть и версия BPF VM для юзерспейса, которая позволяет делать похожие штуки. То, что описывается в статье — близко по смыслу и духу к такому подходу, только в более общем направлении и с возможностью не только ставить брейкпоинты, но и читать произвольные области памяти. Как пример, можете посмотреть расширение для VS Code с визуализацией данных.


                  Да, это все можно делать, взаимодействуя с GDB через консоль или serial API, но это только та часть, которая про работу с процессами и памятью. Вторая же часть — символизация, которая в целом уже больше актуальна для Раста и других языков, т.к. в целом lldb работает в паре с clang и такой острой проблемы с тем же парсером выражений там нет.

              +1
              print позволяет одновременно наблюдать состояния разных контестов в разные моменты времени. В отладчике сложно выяснить что нибудь типа «какие значения аргумента у этой функции вообще встречались».
                +2

                Это, а также то, что «точка останова» очень слабо помогает в мультипоточной высококонкурентной среде, особенно если треды зеленые, и их — легион.


                Нерешенные алгоритмические задачи, в которых необходимость исследовать стек и кучу побайтово на каждом шагу, — подходят к концу. Я уже и не вспомню, когда мне приходилось обращаться к отладчику; выразительность языков программирования тоже не стоит на месте, просто и если я даже где-то ошибаюсь, простой принт вот прямо тут — выявит проблему гораздо быстрее и проще.


                Тип «обычных» ошибок сильно изменился со времен Горбачева; остановка выполнения просто больше не актуальна. Что прикажете делать в отладчике, если где-то в середине громоздкой обработки данных / вычисления в тысяче потоков, — гонка, которая портит результат?

                  +1

                  В случае гонки даже простое добавление/удаление print'а повлияет на итоговый результат.

                    +1
                    В случае гонки даже простое добавление/удаление print'а повлияет на итоговый результат.

                    Безусловно. И что?

                    +1
                    «точка останова» очень слабо помогает в мультипоточной высококонкурентной среде, особенно если треды зеленые, и их — легион.

                    Именно этот сценарий с отладкой зеленых тредов меня и заставил задаться вопросами из статьи. :)


                    остановка выполнения просто больше не актуальна

                    По-моему, тут дело в том, что мы воспринимаем отладку как интерактивный процесс, хотя это не обязательно так и не всегда так. "Остановка выполнения" может занимать миллисекунды и происходить автоматически, незаметно для вас. В комментарии выше я приводил примеры с BPF, который по сути тоже является отладчиком и как раз помогает работать с такими сценариями, когда у нас миллионы событий, и среди них надо выцепить нужное и понять, что именно происходит. DTrace в FreeBSD/Solaris решает ту же проблему с помощью специального DSL, позволяющего эффективно фильтровать события, агрегировать их, и выводить нужный результат — и это гораздо удобнее тех же принтов, потому что а) программу не надо перекомпилировать, б) точки трассировки можно добавлять прямо в рантайме в любой процесс (в том числе в продакшене), со сравнительно небольшим оверхедом, в) точки трассировки можно добавлять даже в ядро.

                      +1
                      BPF, который по сути тоже является отладчиком

                      Или «который по сути тоже является принтом» :)


                      DSL, позволяющего эффективно фильтровать события, агрегировать их, и выводить нужный результат

                      Я совершенно не чувствую себя компетентным в вопросах именно низкоуровневой отладки со стороны отладчика (да и со стороны пользователя тоже), но мне кажется, что это решение уже ближе к тому, что называется «телеметрия». Отправить асинхронное сообщение в чужой поток, который слушает, получает, фитльтрует, агрегирует, показывает, даже как-то взаимодействует когда надо с основным процессом —  задача несложная, и много где реализованная.


                      Когда я начинал, мутабельной была куча. И иногда даже стек. Литералли :)
                      Тут было архиполезно остановить мир, и тщательно изучить байты и заменить условный jmp на безусловный, чтобы отучить глупый Doom проверять ключик.


                      Потом мутабельными стали только объекты на несколько уровней абстракции выше. Уже там не было большой разницы: красиво развернуть дерево свойств объекта в VS/Netbeans, или просто дампнуть с именами переменных. Ну, поправить на лету было удобнее.


                      Теперь мутабельность, вроде, почти везде подвергнута анафеме, и по сути «принт», «телеметрия» и «отладка» — превратились примерно в одно и то же: насколько структурированно вы можете выплюнуть объект, чтобы его потом можно было легко проанализировать, классифицировать, привязать.


                      Зачем бы мне захотелось добавлять трассировку в продакшене я себе представляю слабо, но если захочется — я средствами языка это сделаю на коленке за пять минут. Причем так, что версия без оверхеда вовсе будет собираться просто с другим ключиком. И оно будет проще и надежней, чем любое generic решение.


                      Но, как я сказал, я могу целых пластов просто не понимать, или даже не видеть.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое