Вступление
Segmentation fault
Наверняка, далеко не один раз в жизни вы наблюдали сообщение об ошибке со следующим содержанием: "segmentation fault (core dumped)" — эта ошибка настолько популярна, что о ней давно уже слагают легенды, и немало бойцов пало в противостоянии с этой ошибкой
Распространенные примеры:
int arr[10];int abc = arr[10000]; // out-of-bounds обращение к массиву// илиint* ptr = nullptr;*ptr = 20; // разыменование null pointer
Segmentation Fault — это понятие на уровне ОС, сигнал, который операционная система посылает процессу, говоря о том, что произошла ошибка обращения к памяти. Если копнуть глубже, то это понятие исходит из исключений (exceptions) на уровне ниже, на уровне архитектуры процессора, а именно — Page Fault (чаще всего) / General Protection Fault, которые происходят при обращении к памяти, при котором что-то да точно не так (об этом дальше).
Virtual Memory
На большинстве современных систем используется подход virtual memory к управлению памятью. Для понимания segmentation fault и конкретно этой статьи требуется хотя бы общее понимание принципа работы данного подхода.
Основная часть
Виды segmentation faults
На самом деле, существует несколько типов segmentation fault:
-
SEGV_MAPERR— обращение к несуществующему участку памяти. -
SEGV_ACCERR— ошибка прав доступа к участку памяти.
Для начала небольшой ликбез по тому, как хранится информация о памяти в ядре Linux.
Рассмотрим картинку:
-
mm_struct— структура, отвечающая за хранение всей информации об адресном пространстве процесса. Так называемыйmemory descriptor. -
vm_area_struct— структура, хранящая информацию об отдельном регионе памяти: его начало, конец, флаги доступа и прочее. Так называемаяmemory region.
SEGV_MAPERR
Этот тип segmentation fault означает, что адрес, по которому хотели совершить какое-либо действие (read, write, execute), вообще не замаплен в адресном пространстве процесса — то есть не принадлежит ни одному региону vm_area_struct. Разберём на примере.
Пример 1
auto arr = std::make_unique<const volatile int[]>(10);std::println("Trying to access addr. {:#018x}", (uintptr_t)(arr.get() + 1000000));int val = arr.get()[1000000];
Программа выводит адрес, по которому собирается читать int — «0x00005587db62ad30».
Запомним его и выведем регионы адресного пространства процесса с помощью cat /proc/{pid}/maps (показаны только важные):
...5587ca00f000-5587ca010000 rw-p 0001f000 08:03 22957888 /bin/app5587db248000-5587db27b000 rw-p 00000000 00:00 0 [heap]7f215e000000-7f215e024000 r--p 00000000 08:03 14552434 /usr/lib/libc.so.67f215e024000-7f215e195000 r-xp 00024000 08:03 14552434 /usr/lib/libc.so.6...
Поскольку arr аллоцирован в heap, смотрим его диапазон адресов: 0x00005587db248000-0x00005587db27b000. Наш адрес 0x00005587db62ad30 выходит за его пределы и не попадает ни в один другой регион — то есть он не замаплен в адресном пространстве процесса. Всё строго по определению SEGV_MAPERR.
Убедимся в этом с помощью обработчика сигналов:
void segv_handler(int, siginfo_t* info, void* data) { std::println("Signal caught: {:#018x} {}", (uintptr_t)(info->si_addr), info->si_code == SEGV_MAPERR); _exit(0);}// in main:struct sigaction act{};act.sa_sigaction = segv_handler;act.sa_flags = SA_SIGINFO;int ret = sigaction(SIGSEGV, &act, nullptr);
Обработчик выводит "Signal caught: 0x000055ff08ab2d30 true", что подтверждает тот факт, что это именно SEGV_MAPERR.
SEGV_ACCERR
Эта ошибка прав доступа к региону памяти.
Рассмотрим на примере:
Пример 2
const static int dron = 10;int main() { // ... *const_cast<volatile int*>(&dron) = 20; // Segmentation fault here // "volatile" so it is not optimized out. // ...}
В этой программе мы объявляем переменную так, чтоб она загрузилась в read-only регион памяти при исполнении программы. В ELF бинарнике она хранится в секции .rodata, в чем мы можем убедиться с помощью readelf утилиты:
...Section Headers:... [14] .rodata PROGBITS **0000000000018000** 00018000 0000000000005510 0000000000000000 A 0 0 32...Symbol table '.symtab' contains 186 entries: Num: Value Size Type Bind Vis Ndx Name... 12: **0000000000018558** 4 OBJECT LOCAL DEFAULT 14 _ZL4dron...
При выполнении программы и установленном заранее обработчике сигналов мы получаем: "Signal caught: 0x000056429e5a9558 true (== SEGV_ACCERR)". Рассмотрим замапленные регионы адресного пространства процесса с помощью cat /proc/pid/maps:
...56429e5a9000-56429e5b0000 r--p 00018000 08:03 22957888 /home/klewy/myFiles/Code/c++/small_projects/sigsegv/a.out...
Как мы можем заметить, переменная действительно находится в read-only участке памяти.
Следовательно, это действительно SEGV_ACCERR, ведь в коде была попытка сделать write в read-only регион, что не соответствует правам доступа.
Segmentation Fault в ядре Linux
Теперь мы рассмотрим как именно ядро понимает, что это segmentation fault и какой именно.
Рассмотрим в контексте нормального исполнения программы: без эдж кейсов и прочего.
Грубо говоря, все начинается после исключения, вызванного Memory Management Unit, в функции handle_page_fault, в которой вызывается do_user_addr_fault.
SEGV_MAPERR
Как мы помним, данный тип segmentation fault происходит в том случае, если участок памяти вообще не замаплен в адресном пространстве процесса, т.е нет соответствующей vm_area_struct.
// function do_user_addr_fault:vma = lock_vma_under_rcu(mm, address);if (!vma) goto lock_mmap;...lock_mmap:...vma = lock_mm_and_find_vma(mm, address, regs);if (unlikely(!vma)) { bad_area_nosemaphore(regs, error_code, address); return;}
Если не находится подходящий vm_area, то с помощью функции bad_area_nosemaphore отправляется сигнал SIGSEGV процессу.
SEGV_ACCERR
Вспомним, что данный тип segmentation fault означает, что произошла ошибка прав доступа.
// function do_user_addr_fault:if (unlikely(access_error(error_code, vma))) { bad_area_access_error(regs, error_code, address, mm, vma); return;}// function bad_area_access_errorif (bad_area_access_from_pkeys(error_code, vma)) { // ...} else { __bad_area(regs, error_code, address, mm, vma, 0, SEGV_ACCERR);}
После __bad_area, в конце концов, вызывается знакомая нам функция bad_area_nosemaphore, откуда отправляется сигнал процессу.
Вот так и происходит все в могучем и страшном ядре Линукса.
Конечная
Итак, segmentation fault — это не просто страшное сообщение в терминале, а вполне конкретная цепочка событий: процессор бросает исключение → ядро перехватывает его в handle_page_fault → определяет тип (SEGV_MAPERR или SEGV_ACCERR) → отправляет SIGSEGV процессу. Ничего мистического, все строго по делу.
Надеюсь, после этой статьи segmentation fault стал чуть менее страшным и чуть более понятным.
ссылка на оригинал статьи https://habr.com/ru/articles/1038370/