Принципы и инженерные практики | Совместное использование инструментов отладки и динамического анализа памяти Rust

задняя часть Rust

Автор: У Аоксианг / Пост-редактор: Чжан Ханьдун

оригинал:ошибка сегментации отладки gdb/lldb

По сравнению с инструментами статического анализа, такими как clippy/ra, инструменты динамического анализа требуют запуска программы для анализа, например официального стенда, теста

Зачем нужен динамический анализ

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

Благодаря динамическому анализу вы можете не только обнаруживать узкие места в производительности программы, но и отлаживать ошибки памяти во время выполнения, а также создавать деревья вызовов функций, чтобы новые члены команды могли быстро читать код проекта.

Содержание — Инструменты динамического анализа

  1. Общие средства отладки и средства обнаружения памяти:

Во-первых, через случай segfault мы представим, как общие инструменты отладки обнаруживают ошибки памяти в этом случае:

  • coredumpctl
  • valgrind
  • gdb
  • lldb/vscode-lldb/Intellij-Rust
  1. Инструменты динамического анализа, такие как график пламени/дерево вызовов функций (профиль)
  • dmesg
  • cargo-miri
  • pref
  • cargo-flamegraph
  • KCachegrind
  • gprof
  • uftrace
  • ebpf
  1. Наконец, используйте приведенные выше инструменты для анализа нескольких случаев ошибок памяти:
  • 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.rs15 строк

$ чтобы увидеть несколько строк рядом с кодом проблемы

(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/друг монго/лин…>