Segmentation Fault: как оно устроено?

от автора

Вступление

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:

  1. SEGV_MAPERR — обращение к несуществующему участку памяти.

  2. SEGV_ACCERR — ошибка прав доступа к участку памяти.

Для начала небольшой ликбез по тому, как хранится информация о памяти в ядре Linux.
Рассмотрим картинку:

Устройство памяти в Linux

Устройство памяти в 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/