Вступление
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/app
5587db248000-5587db27b000 rw-p 00000000 00:00 0 [heap]
7f215e000000-7f215e024000 r--p 00000000 08:03 14552434 /usr/lib/libc.so.6
7f215e024000-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_error
if (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 стал чуть менее страшным и чуть более понятным.












