Как работает отладчик Rust под капотом: пошаговый разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Отладчик Rust: как заглянуть под капот step by step

Вы когда-нибудь задавались вопросом: «Step by step interactive debugger for Rust: как это работает под капотом?» Если вы писали на Rust и пытались отловить хитрый баг, то наверняка сталкивались с GDB, LLDB или даже с такими инструментами, как rust-gdb. Но как именно они умеют останавливать выполнение программы, показывать значения переменных и делать шаги по исходному коду?

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

Почему Rust — особенный случай для отладки?

Rust компилируется в нативный код через LLVM. Это значит, что для отладки мы используем те же инструменты, что и для C/C++: GDB и LLDB. Но Rust добавляет свои сложности:

  • Собственность и заимствование — переменные могут быть перемещены, и отладчик должен это отслеживать.
  • Мономорфизация дженериков — каждый T превращается в конкретный тип, и имена функций становятся «зашифрованными».
  • Оптимизации — компилятор может переставлять код, и отладчик должен «размотать» это обратно.

Именно поэтому step by step interactive debugger for Rust — это не просто запуск программы под GDB. Это целая система, которая переводит машинные адреса обратно в строки исходного кода.

Как работает точка останова (breakpoint) под капотом?

Представьте: вы ставите точку останова на строке println!("Hello");. Что происходит в процессоре?

Когда вы запускаете программу под отладчиком, он делает следующее:

  1. Читает отладочную информацию (формат DWARF) из исполняемого файла. Там хранится соответствие: адрес в памяти → строка в коде.
  2. Заменяет инструкцию по этому адресу на специальную команду INT3 (на x86 — 0xCC). Это «ловушка» для процессора.
  3. Когда выполнение доходит до этой точки, процессор генерирует сигнал SIGTRAP.
  4. Отладчик перехватывает сигнал, сохраняет состояние регистров и показывает вам текущую строку.

Вот пример простой программы на Rust:

fn main() {
    let x = 42;
    let y = x + 10; // <-- точка останова
    println!("y = {}", y);
}

Когда вы ставите брейкпоинт на let y = x + 10;, отладчик находит адрес этой инструкции в бинарнике и подменяет первый байт на 0xCC. После остановки он восстанавливает оригинальный байт, выполняет шаг, и снова ставит ловушку — если вы хотите продолжить.

Step over, step into, step out — как отладчик понимает, где «следующая строка»?

Это, пожалуй, самая магическая часть. Как отладчик знает, что после let y = x + 10; идёт println!?

Всё дело в отладочных таблицах. Компилятор Rust (через LLVM) генерирует секцию .debug_line в формате DWARF. Это сжатая таблица, которая сопоставляет адреса инструкций с номерами строк в исходном коде.

Например, для нашего кода таблица может выглядеть так:

Адрес 0x1000 → строка 2 (let x = 42;)
Адрес 0x1010 → строка 3 (let y = x + 10;)
Адрес 0x1020 → строка 4 (println!)

Когда вы нажимаете step over, отладчик вычисляет адрес следующей инструкции, которая не принадлежит текущей строке, и ставит временную точку останова туда. Выполнение продолжается до этого адреса — и вы переходите на следующую строку.

Для step into — отладчик проверяет, есть ли по текущему адресу вызов функции. Если да, он ищет первую строку этой функции и ставит брейкпоинт туда.

Просмотр переменных: как отладчик знает значение?

Rust — язык со строгим контролем памяти. Переменная может быть:

  • В регистре процессора (если она маленькая и часто используется).
  • На стеке (локальные переменные).
  • В куче (через Box, Vec и т.д.).

Отладчик использует информацию из .debug_info (тоже DWARF). Там описано: «переменная x типа i32 находится по смещению -8 от RBP» или «переменная s типа String находится в регистре RAX».

Пример: если вы отлаживаете такой код:

fn main() {
    let s = String::from("hello");
    let len = s.len(); // брейкпоинт тут
    println!("{}", len);
}

Отладчик знает, что s — это структура из трёх полей: указатель на кучу, длина и ёмкость. Он может показать вам содержимое строки, разыменовав указатель. Это работает, потому что отладочная информация содержит описание типа String.

Проблема: «зашифрованные» имена (mangled symbols)

Если вы когда-нибудь смотрели backtrace в Rust без отладчика, вы видели что-то вроде:

_ZN3std2io5stdio6_print6output17h3b8c5f3c9f5e7b9cE

Это mangled name (искажённое имя). Rust, как и C++, использует name mangling, чтобы кодировать информацию о модулях, дженериках и трейтах в одном имени функции.

Хороший отладчик (например, rust-gdb) автоматически «деманглит» эти имена обратно в читаемый вид: std::io::stdio::_print::output.

Без этого step by step interactive debugger for Rust был бы практически бесполезен — вы бы видели только адреса и кракозябры.

Собственный отладчик на Rust: минимальный пример

Чтобы понять, как всё работает на низком уровне, давайте напишем крошечный отладчик на самом Rust. Используем крейт nix для работы с системными вызовами.

Этот код ставит точку останова на конкретный адрес и ждёт сигнала:

use nix::sys::ptrace;
use nix::sys::signal::Signal;
use nix::sys::wait::waitpid;
use                

Блоги

Рекомендаций по книгам