Автор: У Аоксианг / Пост-редактор: Чжан Ханьдун
оригинал:ошибка сегментации отладки gdb/lldb
По сравнению с инструментами статического анализа, такими как clippy/ra, инструменты динамического анализа требуют запуска программы для анализа, например официального стенда, теста
Зачем нужен динамический анализ
Выше приведена программа на Rust.Динамический анализСгенерированный график пламени, через график пламени, вы можете ясно видеть, что узким местом производительности программы часто является выделение памяти.
Благодаря динамическому анализу вы можете не только обнаруживать узкие места в производительности программы, но и отлаживать ошибки памяти во время выполнения, а также создавать деревья вызовов функций, чтобы новые члены команды могли быстро читать код проекта.
Содержание — Инструменты динамического анализа
- Общие средства отладки и средства обнаружения памяти:
Во-первых, через случай segfault мы представим, как общие инструменты отладки обнаруживают ошибки памяти в этом случае:
- coredumpctl
- valgrind
- gdb
- lldb/vscode-lldb/Intellij-Rust
- Инструменты динамического анализа, такие как график пламени/дерево вызовов функций (профиль)
- dmesg
- cargo-miri
- pref
- cargo-flamegraph
- KCachegrind
- gprof
- uftrace
- ebpf
- Наконец, используйте приведенные выше инструменты для анализа нескольких случаев ошибок памяти:
- SIGABRT/double-free
- SIGABRT/free-dylib-mem
случаи segfault и общие инструменты отладки
Ниже приведена часть исходного кода моей команды rewrite ls (далее именуемойls 应用
), полный исходный код находится вэтот репозиторий кода
fn main() {
let dir = unsafe { libc::opendir(input_filename.as_ptr().cast()) };
loop {
let dir_entry = unsafe { libc::readdir(dir) };
if dir_entry.is_null() {
break;
}
// ...
}
}
Как и в исходной команде ls, когда входным параметром является папка, она может отображать все имена файлов в папке.
Но когда аргументом приложения ls не является папка, происходит ошибка сегментации:
> cargo r --bin ls -- Cargo.toml
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/ls Cargo.toml`
Segmentation fault (core dumped)
coredumpctl
конфигурация systemd-coredump
Сначала посмотрите на файл конфигурации системы/proc/config.gz
Проверьте, включена ли функция записи дампа памяти
> zcat /proc/config.gz | grep CONFIG_COREDUMP
CONFIG_COREDUMP=y
потому что/proc/config.gz
даgzipдвоичный формат, а не текстовый формат, поэтому используйтеzcat
вместоcat
печатать
Посмотрите на изменение файла конфигурации coredumpctl/etc/systemd/coredump.conf
Измените ограничение размера журнала coredump по умолчанию выше 20G:ExternalSizeMax=20G
Затем перезагрузитесь:sudo systemctl restart systemd-coredump
Просмотр последней записи дампа памяти
пройти черезcoredumpctl list
Найдите последнюю запись coredump, которая является записью ошибки segfault, которая только что произошла.
Tue 2021-07-06 11:20:43 CST 358976 1000 1001 SIGSEGV present /home/w/repos/my_repos/linux_commands_rewritten_in_rust/target/debug/ls 30.6K
Обратите внимание, что 358976 перед идентификатором пользователя 1000 представляет собой PID процесса, используемый какcoredumpctl info
Запрос
coredumpctl info 358976
PID: 358976 (segfault_opendi)
// ...
Command Line: ./target/debug/ls
// ...
Storage: /var/lib/systemd/coredump/core.segfault_opendi.1000.d464328302f146f99ed984edc6503ca0.358976.1625541643000000.zst (present)
// ...
При желании используйте gdb для разбора файла coredump segfault:
coredumpctl gdb 358976
илиcoredumpctl debug 358976
Ссылаться на:core dump - wiki
valgrind проверяет ошибки памяти
valgrind --leak-check=full ./target/debug/ls
// ...
==356638== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==356638== Access not within mapped region at address 0x4
==356638== at 0x497D904: readdir (in /usr/lib/libc-2.33.so)
==356638== by 0x11B64D: ls::main (ls.rs:15)
// ...
причина ошибки анализа отладки gdb
§ gdb открывает исполняемый файл приложения ls:
gdb ./target/debug/ls
§ gdb черезl
илиlist
Команда печатает код исполняемого файла:
(gdb) l
§ gdb запускает приложение ls и передаетCargo.toml
Имя файла как входной параметр:
(gdb) run Cargo.toml
Starting program: /home/w/repos/my_repos/linux_commands_rewritten_in_rust/target/debug/ls Cargo.toml
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e5a904 in readdir64 () from /usr/lib/libc.so.6
§ Просмотр кадра стека при возникновении ошибки сегментации
(gdb) backtrace
#0 0x00007ffff7e5a904 in readdir64 () from /usr/lib/libc.so.6
#1 0x0000555555568952 in ls::main () at src/bin/ls.rs:15
В этот момент функция системного вызова, обнаружившая проблему,readdir64
, предыдущий кадр стека былls.rs
15 строк
$ чтобы увидеть несколько строк рядом с кодом проблемы
(gdb) list 15
§ просмотреть локальные переменные фрейма стека задач
-
info variables
Может печатать глобальные или статические переменные -
info locals
распечатать локальные переменные текущего кадра стека -
info args
Распечатать входные параметры текущего фрейма стека
(gdb) frame 1 # select frame 1
(gdb) info locals
(gdb) frame 1
#1 0x0000555555569317 in ls::main () at src/bin/ls.rs:20
20 let dir_entry = unsafe { libc::readdir(dir) };
(gdb) info locals
dir = 0x0
// ...
В этот момент кадр основного стека найденdir = 0x0
является нулевым указателем, вызывающим segfault системного вызова readdir
Проанализируйте причину ошибки
let dir = unsafe { libc::opendir(input_filename.as_ptr().cast()) };
loop {
let dir_entry = unsafe { libc::readdir(dir) };
// ...
}
Проблема в том, что нет судаopendir
Является ли системный вызов успешным, системный вызов терпит неудачу или возвращает NULL или -1
еслиopendir
Если тип файла, переданный системным вызовом, не является каталогом, вызов завершится ошибкой.
Таким образом, обходной путь ошибкиПроверьте, является ли переменная dir, созданная вышестоящим opendir, NULL
разрешить сегментацию
Просто добавьте код того, является ли каталог NULL, если он NULL, напечатайте сообщение об ошибке системного вызова
if dir.is_null() {
unsafe { libc::perror(input_filename.as_ptr().cast()); }
return;
}
Снова протестируйте приложение ls, чтобы прочитать файлы, не относящиеся к папкам.
> cargo r --bin ls -- Cargo.toml
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/ls Cargo.toml`
Cargo.toml: Not a directory
В этот момент программа не дала сбой, и было напечатано сообщение об ошибке.Cargo.toml: Not a directory
Изменения в коде для исправления ls, применяющего segfaultв этом коммите
Обратитесь к официальному руководству gnu.org:woohoo.bowbow.org/software/ только что…
отладка lldb
Отладка lldb почти такая же, как и gdb, но отдельные команды отличаются
(lldb) thread backtrace # gdb is
backtrace
error: need to add support for DW_TAG_base_type '()' encoded with DW_ATE = 0x7, bit_size = 0
* thread #1, name = 'ls', stop reason = signal SIGSEGV: invalid address (fault address: 0x4)
* frame #0: 0x00007ffff7e5a904 libc.so.6`readdir + 52
frame #1: 0x0000555555568952 ls`ls::main::h5885f3e1b9feb06f at ls.rs:15:34
// ...
(lldb) frame select 1 # gdb is
frame 1
frame #1: 0x0000555555569317 ls`ls::main::h5885f3e1b9feb06f at ls.rs:15:34
12
13 let dir = unsafe { libc::opendir(input_filename.as_ptr().cast()) };
14 loop {
-> 15 let dir_entry = unsafe { libc::readdir(dir) };
16 if dir_entry.is_null() {
17 // directory_entries iterator end
18 break;
§ переменная lldb печатается наframe variable
равен gdbinfo args
плюсinfo locals
(gdb) info args
равный(lldb) frame variable --no-args
Кромеprimitive types, lldb также может вывести значение переменной типа String, но не может знатьVec<String>
значение переменной типа
Отладка vscode-lldb
При запуске программы без каких-либо точек останова она будет указывать на следующий код
7FFFF7E5A904: 0F B1 57 04 cmpxchgl %edx, 0x4(%rdi)
На этом этапе вам следует обратить внимание на боковую панель «Отладка» в левой части vscode.CALL STACK
меню (он же gdb backtrace)
Меню стека вызовов сообщит readdir, что предыдущий кадр текущего ассемблерного кода (то есть второй кадр стека обратной трассировки) — это строка 15 основной функции.
Нажмите на кадр основного стека, который эквивалентен(gdb) frame 1
, вы можете перейти к строке, где находится исходный код проблемы
В кадре основного стека через меню переменных обнаруживается, что значение переменной dir, переданной readdir, равно NULL, что вызывает ошибку сегментации.
Отладка Intellij-Rust
Операция отладки может перейти непосредственно к строке, где находится код проблемы, и запроситьlibc::readdir(dir)
Значение переменной dir равно NULL.
Инструмент динамического анализа
dmesg для просмотра записей segfault
sudo dmesg
Вы можете просмотреть последние десятки сообщений ядра, и вы можете увидеть это сообщение после возникновения segfault:
[73815.701427] ls[165042]: segfault at 4 ip 00007fafe9bb5904 sp 00007ffd78ff8510 error 6 in libc-2.33.so[7fafe9b14000+14b000]
Cargo-miri проверяет небезопасный код
Жаль, что мири пока не поддерживает проверку функций вызова FFI.
[w@ww linux_commands_rewritten_in_rust]$ cargo miri run --example sigabrt_free_dylib_data
Compiling linux_commands_rewritten_in_rust v0.1.0 (/home/w/repos/my_repos/linux_commands_rewritten_in_rust)
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `/home/w/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri target/miri/x86_64-unknown-linux-gnu/debug/examples/sigabrt_free_dylib_data`
error: unsupported operation: can't call foreign function: sqlite3_libversion
--> examples/sigabrt_free_dylib_data.rs:5:19
|
5 | let ptr = sqlite3_libversion() as *mut i8;
| ^^^^^^^^^^^^^^^^^^^^ can't call foreign function: sqlite3_libversion
перфорация дерева вызовов функций
Проверить конфигурацию производительности
сначала черезperf record
Проверьте, может ли конфигурация perf считывать системные события, и если она возвращает ошибку, измените следующие файлы конфигурации.
sudo vim /etc/sysctl.d/sysctl.conf
Добавьте строку в файл конфигурации sysctl.conf:
kernel.perf_event_paranoid = -1
Затем перезапустите процесс sysctl, чтобы перезагрузить конфигурацию:
sudo systemctl restart systemd-sysctl.service
perf call-graph
Используйте perf-record для записи информации о вызовах программы Rust:
perf record -a --call-graph dwarf ./target/debug/tree
После запуска программы Rust файл данных perf.data будет сгенерирован в текущем каталоге.
perf-report по умолчанию откроет файл perf.data в текущем каталоге, либо вы можете указать файл данных через параметр -i
Анализ дерева вызовов функций программы на Rust с помощью perf-report приводит к пользовательскому интерфейсу командной строки, похожему на htop, написанному на curses:
perf report --call-graph
Вы можете выбрать символ функции tree::main и нажать Enter, чтобы выбратьzoom into tree thread
показать дерево вызовов подфункций основной функции
Основной метод просмотра заключается в перемещении курсора с помощью клавиш со стрелками вверх, вниз, влево и вправо, а затем с помощью клавиш **+** развертывание или свертывание дерева вызовов функций строки, в которой находится курсор.
На компьютере автора профилировщик Clion по умолчанию (детектор производительности) использует perf
cargo-flamegraph
Для cargo-flamegraph в системе должен быть установлен perf, который может отображать данные о производительности в пламенных графиках.
KCachegrind
valgrind --tool=callgrind ./target/debug/tree
Сгенерируйте данные callgrind.out.887505 (887505 — это PID) через valgrind, а затем откройте их через KCachegrind для визуализации
Ссылаться на:users.rust-wolf.org/his/is-it-broken…
gprof
gcc/клэнг плюс-pg
параметр, файл данных мониторинга будет создан после запуска программыmon.out
Затем gprof анализирует файл mon.out, к сожалению, в Rust нет частичной поддержки
uftrace
Чтобы поддерживать визуализацию данных в формате Flame Graph, установите utftrace вместе с Flame Graph:
yay -S uftrace-git flamegraph-git
Подобно grpof/KCachegrind, также необходимо собирать данные, а визуализация данных разделена на два этапа.
Во-первых, добавьте параметр, аналогичный параметру gcc -pg, при компиляции программы:
rustc -g -Z instrument-mcount main.rs
Или скомпилируйте с помощью gccrs или gcc.
gccrs -g -pg main.rs
Затем uftrace начинает запись данных:
uftrace record ./main
Эта статья ограничена по объему и представляет только то, как uftrace визуализирует с помощью графиков пламени:
uftrace dump --flame-graph | flamegraph > ~/temp/uftrace_flamegraph.svg && google-chrome-stable ~/temp/uftrace_flamegraph.svg
параметры записи uftrace:
- --no-libcall: uftrace может добавить параметр --no-libcall, чтобы не записывать системные вызовы.
- --nest-libcall: например, функция new() записывает встроенную функцию malloc()
- --kernel(need sudo): trace kernel function
- --no-event: не логировать планирование потоков
ebpf
Должна быть возможность анализировать программы на Rust с помощью ebpf, автор еще не пробовал
После того, как вы ознакомитесь с вышеуказанными инструментами, вы можете рассмотреть несколько случаев ошибок памяти.
SIGABRT/двойной бесплатный обмен делами
Ниже приведен код древовидной команды поиска в глубину для обхода папки (некоторый несущественный код,Полная ссылка на источник здесь)
unsafe fn traverse_dir_dfs(dirp: *mut libc::DIR, indent: usize) {
loop {
let dir_entry = libc::readdir(dirp);
if dir_entry.is_null() {
let _sigabrt_line = std::env::current_dir().unwrap();
return;
}
// ...
if is_dir {
let dirp_inner_dir = libc::opendir(filename_cstr);
libc::chdir(filename_cstr);
traverse_dir_dfs(dirp_inner_dir, indent + 4);
libc::chdir("..\0".as_ptr().cast());
libc::closedir(dirp);
}
}
}
Когда этот код запускается, он выдает ошибку:
malloc(): unsorted double linked list corrupted
Process finished with exit code 134 (interrupted by signal 6: SIGABRT)
Через отладку gdb можно узнатьstd::env::current_dir()
Вызов сообщает об ошибке, но причина ошибки неизвестна
Опыт: SIGABRT возможные причины
Из приведенного выше анализа segfaults мы знаем, что возможными причинами SIGSEGV являются, например.readdir(NULL)
разыменование нулевого указателя
Согласно опыту разработки автора, возможными причинами SIGABRT являются:double free
valgrind проверяет на двойное бесплатное
Следуя идее двойного освобождения, с помощью проверки памяти valgrind обнаруживается, чтоlibc::closedir(dirp)
Проблема с памятью с InvalidFree/DoubleFree
Анализировать двойные бесплатные причины
Снова взглянув на исходный код, указатель на подпапку создается до рекурсивного вызова, но указатель текущей папки закрывается во время рекурсивного поиска с возвратом.
Это означает, что если каталог имеет более 2 подпапок, указатель текущей папки может быть освобожден дважды.
Это уменьшает размер проблемы до следующих трех строк кода:
let dirp = libc::opendir("/home\0".as_ptr().cast());
libc::closedir(dirp);
libc::closedir(dirp);
Общий обходной путь для двойной бесплатности
Привычки программирования на языке C: после освобождения указателя указатель должен быть установлен в NULL
let mut dirp = libc::opendir("/home\0".as_ptr().cast());
libc::closedir(dirp);
dirp = std::ptr::null_mut();
libc::closedir(dirp);
dirp = std::ptr::null_mut();
В «однопоточных приложениях» это решение возможно,
После первого free указатель dirp устанавливается в NULL, а когда в dirp передается второй free, ничего не происходит
Поскольку первая строка большинства функций C/Java определяет, является ли ввод нулевым указателем.if (ptr == null) return
Почему иногда double free не сообщает об ошибке
Меня смущает вопрос:
- Почему closeir обрабатывает SIGABRT раньше времени, записывая несколько строк подряд?
- Почему закрытый процесс может нормально завершиться в цикле?
- Почему closeir вызывается несколько раз в цикле
std::env::current_dir()
СИГАБРТ?
Причина в том, что двойное освобождение может быть не обнаружено вовремя, это может привести к немедленному прекращению процесса или может не сообщать об ошибке до следующего вызова malloc.
потому чтоcurrent_dir()
Используя Vec для запроса памяти кучи, распределитель памяти обнаружил, что память процесса была повреждена, поэтому процесс был завершен.
Я привел выдержки из некоторых книг по системному программированию, объясняющих это явление:
one reason malloc failed is the memory structures have been corrupted, When this happens, the program may not terminate immediately
Заинтересованные читатели могут прочитать эту книгу:Beginning Linux Programming 4th editionиз 260 страниц
Обмен случаями SIGABRT/free-dylib-mem
Предположим, я хочу распечатать версию sqlite, информация о версии sqlite хранится в виде статической строки в/usr/lib/libsqlite3.so
#[link(name = "sqlite3")]
extern "C" {
pub fn sqlite3_libversion() -> *const libc::c_char;
}
fn main() {
unsafe {
let ptr = sqlite3_libversion() as *mut i8;
let version = String::from_raw_parts(ptr.cast(), "3.23.0\0".len(), "3.23.0\0".len());
println!("found sqlite3 version={}", version);
}
}
В результате, как только приведенный выше код запускается, ошибка памяти SIGABRT аварийно завершается.С помощью отладки gdb обнаружено, что предыдущий кадр стека перед ошибкой сообщается в процессе деструктурирования небезопасного блока кода.
Поскольку в коде только версия имеет тип String и требует вызова автоматического деструктора удаления, проблема блокируется ошибкой деструктора String.
Согласно знаниям об управлении памятью процесса операционной системы, когда процесс Rust хочет освободить память, которая не принадлежит процессу Rust, но принадлежит библиотеке динамической компоновки libsqlite3.so, он будет SIGABRT.
Решение состоит в том, чтобы предотвратить вызовы деструктора String через std::mem::forget, что также является наиболее распространенным сценарием приложения для mem::forget API.
Другие случаи отладки ошибок памяти
Вы можете следить за папкой src/examples авторского проекта linux_commands_rewrite_in_rust.
В каталоге примеров есть почти все распространенные примеры различных ошибок памяти, а также различные ошибки памяти, на которые наступил автор.
Ссылка на проект: GitHub.com/друг монго/лин…>