Segmentation fault (core dumped)
Эта одна строка — самая часто разыскиваемая ошибка в C, и она куда менее загадочна, чем кажется. Программа запросила у процессора адрес в памяти, операционная система проверила, разрешено ли процессу трогать этот адрес, и ответ был отрицательным. После этого ядро убило процесс сигналом SIGSEGV.
Ключевая мысль: падение — это симптом, и место падения часто не совпадает с местом ошибки. Плохой указатель обычно был создан где-то раньше, а здесь он просто впервые использован. На этой странице разобраны пять причин, на которые приходятся почти все segfault, и два инструмента, находящие настоящую строку за секунды.
Падающие примеры ниже намеренно не сделаны запускаемыми блоками — они падают по замыслу. Прочитайте их, а затем исправленную версию следом.
Что значит «память, которая вам не принадлежит»
Когда программа стартует, операционная система отображает в её адресное пространство несколько областей: код, глобальные данные, стек и то, до чего дорос heap. Всё остальное в адресном пространстве — включая адрес 0 — не отображено. Обратитесь к неотображённому адресу или запишите в доступный только для чтения — и оборудование это перехватит.
Так что segfault — это не компилятор вас поймал. Это ограждение времени выполнения, и срабатывает оно только тогда, когда некорректный адрес случайно оказался за пределами отображённых страниц. Именно поэтому одна и та же ошибка может падать на одной машине и как будто работать на другой: раскладка памяти разная.
Причина 1: разыменование NULL-указателя
Самая частая причина и самая простая в исправлении. Адрес 0 никогда не отображён, поэтому чтение или запись через нулевой указатель всегда даёт ошибку.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = NULL;
*p = 42; /* ПАДЕНИЕ: запись по адресу 0 */
printf("%d\n", *p);
return 0;
}
Реалистичный вариант — непроверенное выделение памяти:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *data = malloc(1000000000000UL * sizeof(int)); /* не удаётся, возвращает NULL */
data[0] = 1; /* ПАДЕНИЕ */
free(data);
return 0;
}
malloc возвращает NULL, когда не может выполнить запрос; так же ведёт себя fopen, когда файла нет, и strchr, когда символа нет в строке. Проверяйте результат каждой функции, которая может вернуть NULL, прежде чем им пользоваться.
Инициализируйте указатели значением NULL, а не оставляйте их неинициализированными. Нулевой указатель падает сразу и очевидно; мусорный указатель может что-нибудь испортить и упасть гораздо позже. Подробнее об этом приёме — в нулевых указателях.
Причина 2: запись за конец массива
C не проверяет границы массивов. Индекс 10 у массива из 10 элементов — это просто память сразу за массивом, и компилятор без единого возражения вычислит для вас этот адрес.
#include <stdio.h>
int main(void) {
int arr[10];
for (int i = 0; i <= 10; i++) { /* <= вместо < : на одну итерацию больше */
arr[i] = i;
}
printf("done\n");
return 0;
}
Упадёт ли это — дело случая. Запись четырёх байт за локальным массивом обычно попадает в другие данные на стеке — сохранённый регистр, другую переменную, адрес возврата, — так что программа портит сама себя и падает позже в совершенно другом месте. Большой выход за границы покидает отображённую страницу и даёт segfault сразу же.
Более дикий вариант падает всегда:
#include <stdio.h>
int main(void) {
int arr[10];
arr[1000000] = 42; /* далеко за пределами всего отображённого: ПАДЕНИЕ */
return 0;
}
Лечится привычкой писать i < n и вычислять n, а не набирать его дважды:
У строк есть своя версия этой беды: буфер, в котором не осталось места для завершающего '\0'.
#include <string.h>
int main(void) {
char name[5];
strcpy(name, "Alexander"); /* 9 символов + терминатор в 5 байт */
return 0;
}
strcpy понятия не имеет, какого размера name. Используйте snprintf, который знает размер, потому что вы ему его сообщаете:
Причина 3: использование указателя после free (висячие указатели)
После free(p) память возвращается аллокатору. Указатель по-прежнему хранит старый адрес, но этот адрес больше не ваш.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof *p);
*p = 42;
free(p);
printf("%d\n", *p); /* использование после free: может напечатать мусор, может упасть */
free(p); /* двойное освобождение: обычно abort или порча кучи */
return 0;
}
Родственная ловушка — возврат адреса локальной переменной. Её кадр стека исчезает в тот момент, когда функция возвращает управление:
#include <stdio.h>
int *make_number(void) {
int value = 42;
return &value; /* кадр умирает здесь; указатель повисает */
}
int main(void) {
int *p = make_number();
printf("%d\n", *p); /* неопределённое поведение: мусор или падение */
return 0;
}
Есть два исправления — в зависимости от того, что вы имели в виду. Вернуть значение вместо указателя или выделить память в куче и позволить вызывающему её освободить:
Обнуление указателя сразу после free — дешёвая защитная привычка: она превращает тихое использование освобождённой памяти в немедленное и очевидное падение по нулевому указателю, а второй free(p) делает безвредным, поскольку free(NULL) по стандарту не делает ничего. Сторона выделения памяти в этой истории описана в динамической памяти и утечках памяти.
Причина 4: переполнение стека из-за неуправляемой рекурсии
Каждый вызов функции кладёт кадр на стек, а стек — это область фиксированного размера (обычно 8 МБ). Рекурсия без базового случая — или с недостижимым базовым случаем — выходит за его пределы.
#include <stdio.h>
int countdown(int n) {
printf("%d\n", n);
return countdown(n - 1); /* нет базового случая: никогда не остановится */
}
int main(void) {
return countdown(5);
}
То же самое происходит с базовым случаем, который рекурсия перешагивает:
int f(int n) {
if (n == 0) return 1;
return n * f(n - 2); /* при нечётном n никогда не равно 0 */
}
Каждой рекурсивной функции нужен базовый случай, достижимый из любого входного значения:
Огромный локальный массив делает то же самое: int buffer[10000000]; внутри функции просит 40 МБ стека и падает на первой же записи. Большие буферы размещайте в куче с помощью malloc. Размеры разобраны в стеке и куче, а проектирование базового случая — в рекурсии.
Причина 5: запись в строковый литерал
Эта причина удивляет людей, потому что код выглядит совершенно безобидно.
#include <stdio.h>
int main(void) {
char *s = "hello";
s[0] = 'H'; /* ПАДЕНИЕ: строковые литералы доступны только для чтения */
printf("%s\n", s);
return 0;
}
Строковый литерал лежит в секции исполняемого файла, доступной только для чтения. char *s = "hello" указывает в неё; запись через такой указатель — это нарушение защиты, о котором ОС сообщает как о segfault, точно так же, как о доступе к неотображённой памяти.
Лечится созданием массива, который получает собственную изменяемую копию:
Объявление указателей на литералы как const char * превращает это падение во время выполнения в ошибку компиляции, что заведомо лучше. Сделайте это привычкой.
Как найти настоящую строку: gdb
Соберите с -g, чтобы исполняемый файл нёс отладочные символы, а затем запустите под отладчиком:
gcc -g program.c -o program
gdb ./program
Внутри gdb:
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555151 in process_item (item=0x0) at program.c:14
14 return item->count * 2;
(gdb) backtrace
#0 process_item (item=0x0) at program.c:14
#1 0x000055555555518a in main () at program.c:23
(gdb) print item
$1 = (struct Item *) 0x0
Три команды делают почти всю работу. run запускает программу и останавливает её там, где происходит сбой. backtrace (или bt) показывает цепочку вызовов, приведшую сюда: кадр #1 обычно и есть место, где на самом деле был создан плохой указатель. print смотрит на переменную, и item = 0x0 прямо называет проблему.
На macOS эквивалент — lldb ./program, затем run и bt.
Как найти быстрее: AddressSanitizer
Ещё лучше — пусть компилятор сам инструментирует программу. AddressSanitizer ловит некорректный доступ в момент, когда тот происходит, включая те случаи, которые не привели бы к падению:
gcc -g -fsanitize=address program.c -o program
./program
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
WRITE of size 4 at 0x602000000010 thread T0
#0 0x4011f6 in main program.c:11
0x602000000010 is located 0 bytes inside of 4-byte region
freed by thread T0 here:
#1 0x4011c9 in main program.c:10
previously allocated by thread T0 here:
#2 0x4011a6 in main program.c:8
Этот отчёт называет класс ошибки, строку, где она произошла, строку, где память была освобождена, и строку, где она была выделена. Это самый эффективный инструмент отладки ошибок работы с памятью в C, он работает с GCC и clang на Linux и macOS и стоит примерно двукратного замедления — что во время разработки совершенно неважно.
Добавьте к нему -fsanitize=undefined, чтобы заодно ловить знаковое переполнение и другое неопределённое поведение:
gcc -g -Wall -Wextra -fsanitize=address,undefined program.c -o program
valgrind ./program — альтернатива, не требующая пересборки; она сообщает о тех же классах ошибок плюс об утечках.
Чек-лист, когда вы на это наткнулись
- Пересоберите с
-g -Wall -Wextra -fsanitize=address,undefinedи запустите снова. В большинстве случаев отчёт назовёт строку, и на этом всё. - Если санитайзер недоступен, запустите под gdb и снимите
backtrace. Смотрите на кадр 1, а не только на кадр 0. - Проверьте каждый указатель в падающей строке. Напечатайте их все;
0x0означает нулевой указатель, а дикое значение вроде0x7fff5fc01000обычно говорит о неинициализированном или освобождённом. - Спросите себя, откуда взялся этот указатель. Непроверенный
mallocилиfopen? Адрес локальной переменной из уже завершившейся функции? Указатель, использованный послеfree? - Проверьте все границы циклов рядом с падением на предмет
<=там, где имелось в виду<. - Если стек вызовов глубиной в тысячи кадров — это неуправляемая рекурсия, а вовсе не ошибка с указателем.
Как их предотвращать
Привычки, благодаря которым segfault становятся редкостью:
- Всегда компилируйте с
-Wall -Wextraи относитесь к предупреждениям как к ошибкам. - Инициализируйте каждый указатель — значением
NULL, если ничего лучше нет. - Проверяйте возвращаемое значение
malloc,calloc,reallocиfopen. - Обнуляйте указатели сразу после освобождения.
- Используйте
snprintfиfgetsвместоsprintfиgets. - Объявляйте указатели на строковые литералы как
const char *. - Предпочитайте
sizeof arr / sizeof arr[0]жёстко зашитой длине. - Гоняйте набор тестов под AddressSanitizer в CI.
Segfault — это дружелюбный вид отказа: он сообщает вам, что что-то не так. Тот же класс ошибок, который тихо портит соседнюю переменную и выдаёт неверные ответы тремя функциями позже, гораздо хуже, а инструменты выше ловят и то, и другое.
Часто задаваемые вопросы
Что такое ошибка сегментации в C?
Это падение, которое вызывает операционная система, когда программа обращается к памяти, трогать которую ей не разрешено: читает или пишет через некорректный указатель, выходит за конец массива в неотображённую страницу или переполняет стек. Ядро посылает процессу сигнал SIGSEGV, который завершает его и печатает "Segmentation fault (core dumped)".
Как найти место, где происходит ошибка сегментации?
Соберите программу с отладочной информацией и запустите под отладчиком: gcc -g program.c -o program, затем gdb ./program, run, а после падения — backtrace. Он покажет точный файл и строку. Ещё быстрее для ошибок работы с памятью: gcc -g -fsanitize=address program.c -o program — достаточно просто запустить программу, и она напечатает полный отчёт о том, что и где пошло не так.
Почему моя программа на C падает с segfault только иногда?
Потому что некорректный доступ — это неопределённое поведение, а не гарантированное падение. Запись на один элемент дальше массива часто попадает в память, которая процессу всё-таки принадлежит, поэтому ничто вас не останавливает — вместо падения портится соседняя переменная. Segfault случается только тогда, когда плохой адрес оказывается за пределами отображённой страницы, а это зависит от раскладки конкретной сборки и запуска.
Означает ли ошибка сегментации, что у меня утечка памяти?
Нет — это противоположные проблемы. Утечка — это память, которую вы выделили и не освободили: программа продолжает работать и медленно растёт. Segfault — это обращение к памяти, которая вам не принадлежит. Двойное освобождение или использование указателя после free вызывают segfault; забытый free вызывает утечку.