Отладка приложений на Rust с помощью GDB

• Александр Яшкин • обучение • поддержите на Patreon

Введение

По мотивам статьи Михаэля Петерсона, которую мы переработали и сделали актуальной на данный момент.

В этой статье мы рассмотрим, как можно использовать отладчик GDB с программами на Rust. Для этого я использую:

1
2
3
4
5
$ rustc -V
rustc 1.7.0 (a5d1e7a59 2016-02-29)

$ gdb --version
GNU gdb (GDB) 7.11

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

Об отладчике GDB

Данная статья не является руководством по работе с GDB. Множество таких статей можно найти в Интернете, например:

Исходный код

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

quux.rs:

1
2
3
4
5
6
pub fn quux00<F>(x: F) -> i32
    where F: Fn() -> i32 {

    println!("DEBUG 123");
    x()
}

и main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod quux;

fn main() {
    let mut y = 2;
    {
        let x = || {
            7 + y
        };
        let retval = quux::quux00(x);
        println!("retval: {:?}", retval);
    }
    y = 5;
    println!("y     : {:?}", y);
}

Напишем для нашего кода Cargo.toml, чтобы собирать его с помощью утилиты Cargo:

1
2
3
4
5
6
7
8
9
10
[package]
name = "bar"
version = "1.0.0"
license = "GPLv3"
description = "Простой пример для отладки"

# Профиль dev используется по умолчанию при вызове команды cargo build
[profile.dev]
debug = true  # Добавляет флаг `-g` для компилятора;
opt-level = 0 # Отключаем оптимизацию кода;

А теперь соберём исполняемый файл для отладки:

1
2
$ cargo build
   Compiling bar v1.0.0 (...)

Начинаем отладку программы с помощью GDB и установим точки останова:

1
2
3
4
5
6
7
8
9
10
$ gdb target/debug/bar
(gdb) break bar::main
Breakpoint 1 at 0x40154c: file src/main.rs, line 4.
(gdb) rbreak quux00
Breakpoint 2 at 0x4017d3: file src/quux.rs, line 2.
static i32 bar::quux::quux00<closure>(struct closure);
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000040154c in bar::main at src/main.rs:4
2       breakpoint     keep y   0x00000000004017d3 in bar::quux::quux00<closure> at src/quux.rs:2

Когда я ставил первую точку останова, то знал полный путь к функции: bar::main.

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

Вторую точку останова ставим на функции которые содержат «quux00» в своём имени. Но таких функций может не оказаться, т. к. Rust может сам переименовывать имена функций. Поговорим об этом позже, а пока продолжаем.

Немного о rbreak

Вначале я не знал как поставить точку останова на функцию, которая находится не в файле, где объявлена функция main.

Команда rbreak очень полезная. Если вы захотите поставить точки останова на все-все функции в вашей программе, то команда rbreak . это сможет сделать, но вряд ли это вам понадобится для приложений на Rust, т. к. в исполняемом файле могут быть сотни функций, которые создал компилятор.

Тогда можно ограничить область поиска функций с помощью регулярного выражения только одним файлом:

1
2
3
4
5
(gdb) rbreak bar.rs:.
Breakpoint 1 at 0x40154c: file src/main.rs, line 4.
static void bar::main(void);
Breakpoint 2 at 0x40170c: file src/main.rs, line 6.
static i32 fnfn(void);

Начинаем отладку

Сейчас у нас есть две точки останова:

1
2
3
4
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000040154c in bar::main at src/main.rs:4
2       breakpoint     keep y   0x00000000004017d3 in bar::quux::quux00<closure> at src/quux.rs:2

Начинаем отладку:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(gdb) run
Starting program: D:\Code\Rust\debug\target\debug\bar.exe
[New Thread 14628.0x36d4]
[New Thread 14628.0x1be4]
[New Thread 14628.0x51c]
[New Thread 14628.0x2db0]

Thread 1 hit Breakpoint 1, bar::main () at src/main.rs:4
4           let mut y = 2;
(gdb) n
9               let retval = quux::quux00(x);
(gdb) list
4           let mut y = 2;
5           {
6               let x = || {
7                   7 + y
8               };
9               let retval = quux::quux00(x);
10              println!("retval: {:?}", retval);
11          }
12          y = 5;
13          println!("y     : {:?}", y);
(gdb) p y
$1 = 2
(gdb) p x
$2 = {__0 = 0x24fc8c}

Что интересно, мы одним шагом перешагнули с 5 по 8 строку, где присваиваем переменной x адрес анонимной функции, который мы можем видеть в последней строке вывода.

Сейчас мы остановились на строке 8 (в этом можно убедиться с помощью команды frame). Теперь продолжим исполнение кода до следующей точки останова — функции quux00:

1
2
3
4
5
6
7
8
(gdb) frame
#0  bar::main () at src/main.rs:9
9               let retval = quux::quux00(x);
(gdb) c
Continuing.

Thread 1 hit Breakpoint 2, bar::quux::quux00<closure> (x=...) at src/quux.rs:2
2               where F: Fn() -> i32 {

Отлично. Вторая точка останова сработала. Определим своё местоположение в коде:

1
2
3
4
5
(gdb) frame
#0  bar::quux::quux00<closure> (x=...) at src/quux.rs:2
2               where F: Fn() -> i32 {
(gdb) p x
$3 = {__0 = 0x24fc8c}

Теперь мы внутри метода quux00 и остановились перед первой инструкцией, посмотрев содержимое аргумента x, в которой хранится адрес нашей анонимной функции. Далее мы войдём в эту анонимную функцию и посмотрим её работу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) n
DEBUG 123
5           x()
(gdb) s
fnfn () at src/main.rs:7
7                   7 + y
(gdb) p y
$4 = 2
(gdb) n
8               };
(gdb) n
bar::quux::quux00<closure> (x=...) at src/quux.rs:6
6       }
(gdb) n
bar::main () at src/main.rs:10
10              println!("retval: {:?}", retval);

Превосходно! Мы пошагово посмотрели как работает анонимная функция и снова вернулись в функцию main. Кстати, обратите внимание, что компилятор дал анонимной функции имя fnfn. Запомним это имя, так как оно нам ещё пригодиться в дальнейшем.

А теперь дойдём до последней строки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) list
5           {
6               let x = || {
7                   7 + y
8               };
9               let retval = quux::quux00(x);
10              println!("retval: {:?}", retval);
11          }
12          y = 5;
13          println!("y     : {:?}", y);
14      }
(gdb) p retval
$5 = 9
(gdb) n
2       <std macros>: No such file or directory.
(gdb) p y
$6 = 2
(gdb) c
Continuing.
retval: 9
y     : 5
[Thread 8728.0x1a94 exited with code 0]
[Thread 8728.0x23b8 exited with code 0]
[Thread 8728.0x494 exited with code 0]
[Inferior 1 (process 8728) exited normally]

В сообщениях выше нам попалось сообщение <std macros>: No such file or directory, на которое не стоит обращать внимание. Разработчики уже в курсе проблемы: rust-lang/rust#17234.

Устанавливаем точки останова на все методы в main.rs

Давайте теперь поставим точки останова на все функции в файле main.rs:

1
2
3
4
5
6
$ gdb target/debug/bar
(gdb) rbreak main.rs:.
Breakpoint 1 at 0x40154c: file src/main.rs, line 4.
static void bar::main(void);
Breakpoint 2 at 0x40170c: file src/main.rs, line 7.
static i32 fnfn(void);

Хех, снова видим имя fnfn, которым названа наша анонимная функция. Таким методом можно поставить точки останова на анонимные функции. Если мы начнём процесс отладки, то остановимся лишь вначале функции main и вначале нашей анонимной функции, которая вызывается из метода quux00:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) r
Starting program: D:\Code\Rust\debug\target\debug\bar.exe
[New Thread 2400.0x17a8]
[New Thread 2400.0x2154]
[New Thread 2400.0x3480]
[New Thread 2400.0x3ac]

Thread 1 hit Breakpoint 1, bar::main () at src/main.rs:4
4           let mut y = 2;
(gdb) c
Continuing.
DEBUG 123

Thread 1 hit Breakpoint 2, fnfn () at src/main.rs:7
7                   7 + y
(gdb) p y
$1 = 2

Запрещаем компилятору изменять имена функций

Старые версии компилятора Rust изменяли в исполняемых файлах имена функций. В данный момент (версия 1.7) такое не наблюдается, но можно явно указать компилятору, чтобы он не изменял имена следующим образом:

1
2
3
4
5
6
7
#[no_mangle]
pub fn quux00<F>(x: F) -> i32
    where F: Fn() -> i32 {

    println!("DEBUG 123");
    x()
}