Segmentation fault (core dumped)
Esa única línea es el error más buscado de C, y es menos misterioso de lo que parece. Tu programa le pidió al procesador una dirección de memoria, el sistema operativo comprobó si tu proceso tiene permitido tocar esa dirección, y la respuesta fue no. El núcleo entonces mató el proceso con una señal SIGSEGV.
La idea clave: el fallo es un síntoma, y su ubicación a menudo no es el error. El puntero malo se creó normalmente en otro sitio, antes, y este es simplemente el primer lugar donde se usó. Esta página cubre las cinco causas que explican casi todos los segfaults, y luego las dos herramientas que encuentran la línea real en segundos.
Los ejemplos que revientan de abajo deliberadamente no son bloques ejecutables: revientan por diseño. Léelos y luego lee la versión arreglada que viene después.
Qué significa "memoria que no te pertenece"
Cuando tu programa arranca, el sistema operativo mapea varias regiones en su espacio de direcciones: el código, las globales, la pila y lo que el montón haya crecido. Todo lo demás del espacio de direcciones —incluida la dirección 0— está sin mapear. Toca una dirección sin mapear, o escribe en una de solo lectura, y el hardware lo atrapa.
Así que un segfault no es el compilador pillándote. Es una barandilla de tiempo de ejecución, y solo se dispara cuando la dirección inválida da la casualidad de estar fuera de tus páginas mapeadas. Es por eso que el mismo error puede reventar en una máquina y parecer que funciona en otra: la disposición de la memoria difiere.
Causa 1: desreferenciar un puntero NULL
La causa más común y la más fácil de arreglar. La dirección 0 nunca está mapeada, así que leer o escribir a través de un puntero nulo siempre falla.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = NULL;
*p = 42; /* FALLO: escribir en la direccion 0 */
printf("%d\n", *p);
return 0;
}
La versión realista es una reserva sin comprobar:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *data = malloc(1000000000000UL * sizeof(int)); /* falla, devuelve NULL */
data[0] = 1; /* FALLO */
free(data);
return 0;
}
malloc devuelve NULL cuando no puede satisfacer la petición; también lo hace fopen cuando el archivo no existe, y strchr cuando el carácter no está. Comprueba toda función que pueda devolver NULL antes de usar su resultado.
Inicializa los punteros a NULL en lugar de dejarlos sin inicializar. Un puntero nulo revienta de inmediato y de forma obvia; un puntero basura puede corromper algo y reventar mucho después. Más sobre el patrón en punteros nulos.
Causa 2: escribir más allá del final de un array
C no comprueba los límites de los arrays. El índice 10 de un array de 10 elementos es simplemente la memoria que sigue al array, y el compilador calculará esa dirección por ti sin quejarse.
#include <stdio.h>
int main(void) {
int arr[10];
for (int i = 0; i <= 10; i++) { /* <= en vez de < : uno de mas */
arr[i] = i;
}
printf("listo\n");
return 0;
}
Que eso reviente es cuestión de suerte. Escribir cuatro bytes más allá de un array local suele aterrizar en otros datos de la pila —un registro guardado, otra variable, la dirección de retorno— así que el programa se corrompe a sí mismo y revienta después en algún lugar sin relación. Un desbordamiento grande se sale de la página mapeada y da segfault de inmediato.
La versión más salvaje siempre revienta:
#include <stdio.h>
int main(void) {
int arr[10];
arr[1000000] = 42; /* muy lejos de cualquier cosa mapeada: FALLO */
return 0;
}
La solución es el hábito de i < n, y calcular n en lugar de teclearlo dos veces:
Las cadenas tienen su propia versión de esto: un búfer sin sitio para el '\0' terminador.
#include <string.h>
int main(void) {
char name[5];
strcpy(name, "Alexander"); /* 9 caracteres + terminador en 5 bytes */
return 0;
}
strcpy no tiene idea de cuán grande es name. Usa snprintf, que sí lo sabe porque tú se lo dices:
Causa 3: usar un puntero después de free (punteros colgantes)
Después de free(p), la memoria vuelve al asignador. El puntero todavía guarda la dirección vieja, pero esa dirección ya no es tuya.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof *p);
*p = 42;
free(p);
printf("%d\n", *p); /* uso tras liberar: puede imprimir basura o reventar */
free(p); /* doble liberacion: normalmente aborta o corrompe el monton */
return 0;
}
La trampa emparentada es devolver la dirección de una variable local. Su marco de pila desaparece en el momento en que la función retorna:
#include <stdio.h>
int *make_number(void) {
int value = 42;
return &value; /* el marco muere aqui; el puntero queda colgando */
}
int main(void) {
int *p = make_number();
printf("%d\n", *p); /* indefinido: basura, o un fallo */
return 0;
}
Dos soluciones, según lo que quisieras decir. Devuelve el valor en lugar de un puntero, o reserva en el montón y deja que quien llama lo libere:
Poner el puntero a NULL justo después de free es el hábito defensivo barato: convierte un uso tras liberación silencioso en un fallo por desreferencia nula inmediato y obvio, y vuelve inofensivo un segundo free(p), ya que free(NULL) está definido para no hacer nada. El lado de la reserva de esta historia está en memoria dinámica y fugas de memoria.
Causa 4: desbordamiento de pila por recursión descontrolada
Cada llamada a función coloca un marco en la pila, y la pila es una región de tamaño fijo (habitualmente 8 MB). La recursión sin caso base —o con uno que nunca se alcanza— se sale de ella.
#include <stdio.h>
int countdown(int n) {
printf("%d\n", n);
return countdown(n - 1); /* sin caso base: nunca se detiene */
}
int main(void) {
return countdown(5);
}
Lo mismo pasa con un caso base que la recursión se salta:
int f(int n) {
if (n == 0) return 1;
return n * f(n - 2); /* desde un n impar, nunca llega a 0 */
}
Toda función recursiva necesita un caso base alcanzable desde cualquier entrada:
Un array local enorme también lo provoca: int buffer[10000000]; dentro de una función pide 40 MB de pila y falla en la primera escritura. Reserva los búferes grandes en el montón con malloc. Mira pila frente a montón para los tamaños en juego, y recursión para el diseño del caso base.
Causa 5: escribir en un literal de cadena
Este sorprende a la gente porque el código se ve inofensivo.
#include <stdio.h>
int main(void) {
char *s = "hello";
s[0] = 'H'; /* FALLO: los literales de cadena son de solo lectura */
printf("%s\n", s);
return 0;
}
Un literal de cadena vive en una sección de solo lectura del ejecutable. char *s = "hello" apunta dentro de ella; escribir a través de ese puntero es un fallo de protección, que el sistema operativo reporta como un segfault igual que un acceso sin mapear.
La solución es hacer un array, que obtiene su propia copia modificable:
Declarar los punteros a literales como const char * convierte este fallo de tiempo de ejecución en un error de compilación, lo que es estrictamente mejor. Hazlo un hábito.
Encontrar la línea real: gdb
Compila con -g para que el ejecutable lleve símbolos de depuración, y luego ejecútalo bajo el depurador:
gcc -g program.c -o program
gdb ./program
Dentro de 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
Tres comandos hacen casi todo el trabajo. run arranca el programa y se detiene donde falla. backtrace (o bt) muestra la cadena de llamadas que llegó ahí; el marco #1 suele ser donde se produjo realmente el puntero malo. print inspecciona una variable, y item = 0x0 nombra el problema sin rodeos.
En macOS el equivalente es lldb ./program, y luego run y bt.
Encontrarla más rápido: AddressSanitizer
Mejor aún, deja que el compilador instrumente el programa. AddressSanitizer atrapa el acceso inválido en el momento en que ocurre, incluidos los que no habrían reventado:
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
Ese informe nombra la clase de error, la línea que lo hizo, la línea que liberó la memoria y la línea que la reservó. Es la herramienta de depuración más eficaz para errores de memoria en C, funciona con GCC y clang en Linux y macOS, y cuesta aproximadamente el doble de tiempo de ejecución, lo cual es irrelevante durante el desarrollo.
Combínala con -fsanitize=undefined para atrapar a la vez el desbordamiento con signo y otros comportamientos indefinidos:
gcc -g -Wall -Wextra -fsanitize=address,undefined program.c -o program
valgrind ./program es la alternativa que no necesita recompilar, y reporta las mismas clases de error más las fugas.
Una lista para cuando te topes con uno
- Recompila con
-g -Wall -Wextra -fsanitize=address,undefinedy ejecútalo otra vez. La mayoría de las veces el informe nombra la línea y ya está. - Si el sanitizador no está disponible, ejecútalo bajo gdb y haz un
backtrace. Mira el marco 1, no solo el marco 0. - Comprueba cada puntero de la línea que revienta. Imprime cada uno;
0x0identifica un nulo, y un valor salvaje como0x7fff5fc01000suele significar sin inicializar o ya liberado. - Pregúntate de dónde vino ese puntero. ¿Un
malloco unfopensin comprobar? ¿La dirección de una local cuya función ya retornó? ¿Un puntero usado trasfree? - Revisa los límites de todos los bucles cercanos al fallo por si hay un
<=donde querías<. - Si la traza de pila tiene miles de marcos de profundidad, es recursión descontrolada, no un problema de punteros.
Cómo prevenirlos
Los hábitos que hacen raros los segfaults:
- Compila siempre con
-Wall -Wextray trata los avisos como errores. - Inicializa todos los punteros, a
NULLsi no hay nada mejor. - Comprueba el retorno de
malloc,calloc,reallocyfopen. - Pon los punteros a
NULLinmediatamente después de liberarlos. - Usa
snprintfyfgetsen lugar desprintfygets. - Declara los punteros a literales de cadena como
const char *. - Prefiere
sizeof arr / sizeof arr[0]a una longitud escrita a mano. - Ejecuta la batería de pruebas bajo AddressSanitizer en tu integración continua.
Un segfault es el modo de fallo amigable: te dice que algo está mal. La misma clase de error que corrompe en silencio una variable vecina y produce respuestas equivocadas tres funciones después es mucho peor, y las herramientas de arriba atrapan ambas.
Preguntas frecuentes
¿Qué es un fallo de segmentación en C?
Un fallo que el sistema operativo dispara cuando tu programa accede a memoria que no tiene permitido tocar: leer o escribir a través de un puntero inválido, pasarse del final de un array hacia una página sin mapear, o desbordar la pila. El núcleo le envía al proceso una señal SIGSEGV, que lo termina e imprime "Segmentation fault (core dumped)".
¿Cómo encuentro dónde ocurre un fallo de segmentación?
Compila con símbolos de depuración y ejecuta bajo un depurador: gcc -g program.c -o program y luego gdb ./program, run, y cuando reviente, backtrace. Eso imprime el archivo y la línea exactos. Aún más rápido para errores de memoria es gcc -g -fsanitize=address program.c -o program: con solo ejecutar el programa se imprime un informe completo de qué salió mal y dónde.
¿Por qué mi programa en C solo revienta a veces?
Porque el acceso inválido es comportamiento indefinido, no un fallo garantizado. Escribir un elemento más allá de un array suele aterrizar en memoria que tu proceso sí posee, así que nada te detiene: en su lugar corrompe una variable vecina. Solo hay segfault cuando la dirección mala da la casualidad de caer fuera de una página mapeada, lo que depende de la disposición de esa compilación y esa ejecución concretas.
¿Un fallo de segmentación significa que tengo una fuga de memoria?
No: son problemas opuestos. Una fuga es memoria que reservaste y nunca liberaste: el programa sigue corriendo y crece lentamente. Un segfault es tocar memoria que no te pertenece. Liberar memoria dos veces, o usar un puntero después de liberarlo, causa segfaults; olvidar liberar causa fugas.