Отладка приложений на Rust с помощью GDB
Введение
По мотивам статьи Михаэля Петерсона, которую мы переработали и сделали актуальной на данный момент.
В этой статье мы рассмотрим, как можно использовать отладчик 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. Множество таких статей можно найти в Интернете, например:
- http://betterexplained.com/articles/debugging-with-gdb/
- http://www.unknownroad.com/rtfm/gdbtut/gdbtoc.html
- http://beej.us/guide/bggdb
Исходный код
Для изучения мы возьмём пример кода из Интернета, чтобы он был простой и в тоже время был насыщен синтаксическими конструкциями, в т. ч. использовал замыкания и анонимные функции. Наш пример будет состоять из двух файлов находящихся в одном каталоге:
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() } |