Menu

Segmentation fault di C: penyebab dan cara memperbaikinya

Segfault berarti programmu menyentuh memori yang bukan miliknya. Ini lima penyebab yang mencakup hampir semua kasusnya, masing-masing dengan contoh minimal dan perbaikannya, ditambah cara menemukan baris yang tepat dengan gdb dan AddressSanitizer.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Segmentation fault (core dumped)

Satu baris itu adalah error C yang paling sering dicari, dan sebenarnya tidak semisterius kelihatannya. Programmu meminta sebuah alamat memori ke prosesor, sistem operasi memeriksa apakah prosesmu boleh menyentuh alamat itu, dan jawabannya tidak. Kernel lalu menghentikan proses itu dengan sinyal SIGSEGV.

Pemahaman kuncinya: crash adalah gejala, dan lokasinya sering kali bukan tempat bug-nya. Pointer yang salah biasanya dibuat di tempat lain, lebih awal, dan baris ini hanyalah tempat pertama pointer itu dipakai. Halaman ini membahas lima penyebab yang mencakup hampir semua segfault, lalu dua alat yang menemukan baris sebenarnya dalam hitungan detik.

Contoh-contoh yang crash di bawah ini sengaja tidak dibuat sebagai blok yang bisa dijalankan, karena memang dirancang untuk crash. Baca contohnya, lalu baca versi perbaikan yang menyusul.

Arti "memori yang bukan milikmu"

Saat programmu dimulai, sistem operasi memetakan beberapa wilayah ke ruang alamatnya: kode, variabel global, stack, dan heap sebesar apa pun yang sudah tumbuh. Semua yang lain di ruang alamat itu, termasuk alamat 0, tidak dipetakan. Sentuh alamat yang tidak dipetakan, atau tulis ke alamat yang hanya bisa dibaca, maka hardware akan menangkapnya.

Jadi segfault bukan compiler yang sedang memergokimu. Itu pagar pengaman saat runtime, dan hanya bereaksi saat alamat yang tidak valid itu kebetulan berada di luar halaman yang dipetakan. Karena itulah bug yang sama bisa crash di satu mesin dan kelihatan bekerja di mesin lain: tata letak memorinya berbeda.

Penyebab 1: dereference pointer NULL

Penyebab paling umum, dan paling mudah diperbaiki. Alamat 0 tidak pernah dipetakan, jadi membaca atau menulis lewat null pointer selalu menyebabkan fault.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = NULL;
    *p = 42;              /* CRASH: writing to address 0 */
    printf("%d\n", *p);
    return 0;
}

Versi realistisnya adalah alokasi yang tidak dicek:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *data = malloc(1000000000000UL * sizeof(int));  /* fails, returns NULL */
    data[0] = 1;                                        /* CRASH */
    free(data);
    return 0;
}

malloc mengembalikan NULL saat tidak bisa memenuhi permintaan; begitu juga fopen saat file-nya tidak ada, dan strchr saat karakternya tidak ditemukan. Cek setiap fungsi yang bisa mengembalikan NULL sebelum memakai hasilnya.

Inisialisasi pointer dengan NULL, jangan dibiarkan tanpa nilai awal. Null pointer langsung crash dengan jelas; pointer sampah bisa merusak sesuatu dan baru crash jauh kemudian. Penjelasan lebih lanjut soal pola ini ada di halaman pointer NULL.

Penyebab 2: menulis melewati ujung array

C tidak memeriksa batas array. Indeks 10 dari array berisi 10 elemen hanyalah memori setelah array itu, dan compiler akan menghitung alamatnya untukmu tanpa protes.

#include <stdio.h>

int main(void) {
    int arr[10];

    for (int i = 0; i <= 10; i++) {   /* <= instead of < : one too many */
        arr[i] = i;
    }

    printf("done\n");
    return 0;
}

Apakah kode itu crash atau tidak hanya soal keberuntungan. Menulis empat byte melewati array lokal biasanya jatuh di data stack lain, seperti register yang disimpan, variabel lain, atau alamat kembali, jadi program merusak dirinya sendiri dan baru crash nanti di tempat yang tidak berhubungan. Penulisan yang melewati jauh akan keluar dari halaman yang dipetakan dan langsung segfault.

Versi yang lebih liar selalu crash:

#include <stdio.h>

int main(void) {
    int arr[10];
    arr[1000000] = 42;      /* far outside anything mapped: CRASH */
    return 0;
}

Perbaikannya adalah kebiasaan i < n, dan menghitung n alih-alih mengetiknya dua kali:

String punya versinya sendiri: buffer yang tidak punya tempat untuk '\0' penutup.

#include <string.h>

int main(void) {
    char name[5];
    strcpy(name, "Alexander");   /* 9 chars + terminator into 5 bytes */
    return 0;
}

strcpy tidak tahu seberapa besar name. Pakai snprintf, yang tahu karena kamu memberitahunya:

Penyebab 3: memakai pointer setelah free (pointer dangling)

Setelah free(p), memorinya dikembalikan ke alokator. Pointernya masih menyimpan alamat lama, tapi alamat itu sudah bukan milikmu.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof *p);
    *p = 42;
    free(p);

    printf("%d\n", *p);   /* use after free: may print garbage, may crash */
    free(p);              /* double free: usually aborts or corrupts the heap */
    return 0;
}

Jebakan yang mirip adalah mengembalikan alamat variabel lokal. Stack frame-nya hilang begitu fungsinya kembali:

#include <stdio.h>

int *make_number(void) {
    int value = 42;
    return &value;      /* the frame dies here; the pointer dangles */
}

int main(void) {
    int *p = make_number();
    printf("%d\n", *p);   /* undefined: garbage, or a crash */
    return 0;
}

Ada dua perbaikan, tergantung apa yang kamu maksud. Kembalikan nilainya, bukan pointer, atau alokasikan di heap dan biarkan pemanggil membebaskannya:

Mengatur pointer ke NULL tepat setelah free adalah kebiasaan bertahan yang murah: kebiasaan ini mengubah use-after-free yang diam-diam menjadi crash dereference null yang langsung dan jelas, dan membuat free(p) kedua tidak berbahaya, karena free(NULL) didefinisikan tidak melakukan apa-apa. Sisi alokasi dari cerita ini ada di memori dinamis dan memory leak.

Penyebab 4: stack overflow karena rekursi yang tak terkendali

Setiap pemanggilan fungsi menaruh satu frame di stack, dan stack adalah wilayah berukuran tetap (umumnya 8 MB). Rekursi tanpa base case, atau dengan base case yang tidak pernah tercapai, akan melewati ujungnya.

#include <stdio.h>

int countdown(int n) {
    printf("%d\n", n);
    return countdown(n - 1);    /* no base case: never stops */
}

int main(void) {
    return countdown(5);
}

Hal yang sama terjadi dengan base case yang terlompati oleh rekursinya:

int f(int n) {
    if (n == 0) return 1;
    return n * f(n - 2);    /* from an odd n, never equals 0 */
}

Setiap fungsi rekursif butuh base case yang bisa dicapai dari setiap input:

Array lokal yang sangat besar juga bisa menyebabkannya: int buffer[10000000]; di dalam fungsi meminta 40 MB stack dan menyebabkan fault di penulisan pertama. Alokasikan buffer besar di heap dengan malloc. Lihat stack vs heap untuk ukuran-ukurannya, dan rekursi untuk merancang base case.

Penyebab 5: menulis ke string literal

Penyebab yang satu ini mengejutkan banyak orang karena kodenya kelihatan tidak berbahaya.

#include <stdio.h>

int main(void) {
    char *s = "hello";
    s[0] = 'H';          /* CRASH: string literals are read-only */
    printf("%s\n", s);
    return 0;
}

String literal tinggal di bagian file eksekusi yang hanya bisa dibaca. char *s = "hello" menunjuk ke sana; menulis lewat pointer itu adalah protection fault, yang dilaporkan sistem operasi sebagai segfault, sama seperti akses ke alamat yang tidak dipetakan.

Perbaikannya adalah membuat array, yang mendapat salinannya sendiri yang bisa diubah:

Mendeklarasikan pointer ke literal sebagai const char * mengubah crash saat runtime ini menjadi error saat kompilasi, dan itu jelas lebih baik. Jadikan kebiasaan.

Menemukan baris sebenarnya: gdb

Kompilasi dengan -g supaya file eksekusinya membawa simbol debug, lalu jalankan di debugger:

gcc -g program.c -o program
gdb ./program

Di dalam 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

Tiga perintah melakukan sebagian besar pekerjaannya. run memulai program dan berhenti di tempat fault terjadi. backtrace (atau bt) menampilkan rantai pemanggilan yang sampai ke sana: frame #1 biasanya tempat pointer yang salah itu benar-benar dibuat. print memeriksa sebuah variabel, dan item = 0x0 langsung menyebutkan masalahnya.

Di macOS padanannya adalah lldb ./program, lalu run dan bt.

Menemukannya lebih cepat: AddressSanitizer

Lebih baik lagi, biarkan compiler menginstrumentasi programnya. AddressSanitizer menangkap akses yang tidak valid tepat saat terjadi, termasuk akses yang tidak akan menyebabkan crash:

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

Laporan itu menyebutkan jenis bug-nya, baris yang melakukannya, baris yang membebaskan memorinya, dan baris yang mengalokasikannya. Inilah alat debugging paling efektif untuk bug memori di C, bekerja dengan GCC dan clang di Linux dan macOS, dan memperlambat program sekitar dua kali lipat, yang tidak ada artinya selama pengembangan.

Pasangkan dengan -fsanitize=undefined untuk menangkap signed overflow dan undefined behavior lainnya sekaligus:

gcc -g -Wall -Wextra -fsanitize=address,undefined program.c -o program

valgrind ./program adalah alternatif yang tidak butuh kompilasi ulang, dan melaporkan jenis error yang sama ditambah kebocoran memori.

Daftar cek saat kamu mengalaminya

  1. Build ulang dengan -g -Wall -Wextra -fsanitize=address,undefined lalu jalankan lagi. Sering kali laporannya langsung menyebut barisnya dan selesai.
  2. Kalau sanitizer tidak tersedia, jalankan di gdb dan ambil backtrace. Lihat frame 1, bukan hanya frame 0.
  3. Periksa setiap pointer di baris yang crash. Cetak satu per satu; 0x0 menandakan null, dan nilai liar seperti 0x7fff5fc01000 biasanya berarti belum diinisialisasi atau sudah dibebaskan.
  4. Tanyakan dari mana pointer itu berasal. malloc atau fopen yang tidak dicek? Alamat variabel lokal dari fungsi yang sudah kembali? Pointer yang dipakai setelah free?
  5. Periksa setiap batas loop di dekat crash, apakah ada <= padahal yang dimaksud <.
  6. Kalau stack trace-nya ribuan frame dalamnya, itu rekursi yang tak terkendali, sama sekali bukan bug pointer.

Mencegahnya

Kebiasaan yang membuat segfault jarang terjadi:

  • Selalu kompilasi dengan -Wall -Wextra, dan perlakukan peringatan sebagai bug.
  • Inisialisasi setiap pointer, dengan NULL kalau tidak ada yang lebih baik.
  • Cek nilai kembalian malloc, calloc, realloc, dan fopen.
  • Set pointer ke NULL segera setelah membebaskannya.
  • Pakai snprintf dan fgets, bukan sprintf dan gets.
  • Deklarasikan pointer ke string literal sebagai const char *.
  • Utamakan sizeof arr / sizeof arr[0] daripada panjang yang ditulis langsung.
  • Jalankan rangkaian pengujian dengan AddressSanitizer di CI.

Segfault adalah bentuk kegagalan yang bersahabat: ia memberitahumu bahwa ada yang salah. Jenis bug yang sama yang diam-diam merusak variabel di sebelahnya dan menghasilkan jawaban salah tiga fungsi kemudian jauh lebih buruk, dan alat-alat di atas menangkap keduanya.

Pertanyaan yang sering diajukan

Apa itu segmentation fault di C?

Segmentation fault adalah crash yang dipicu sistem operasi saat programmu mengakses memori yang tidak boleh disentuhnya: membaca atau menulis lewat pointer yang tidak valid, melewati ujung array sampai ke halaman memori yang tidak dipetakan, atau stack overflow. Kernel mengirim sinyal SIGSEGV ke proses itu, yang menghentikannya dan mencetak "Segmentation fault (core dumped)".

Bagaimana cara menemukan di mana segmentation fault terjadi?

Kompilasi dengan simbol debug lalu jalankan di debugger: gcc -g program.c -o program lalu gdb ./program, run, dan saat crash, backtrace. Itu mencetak file dan baris yang tepat. Yang lebih cepat lagi untuk bug memori adalah gcc -g -fsanitize=address program.c -o program: cukup jalankan programnya, lalu laporan lengkap tentang apa yang salah dan di mana akan tercetak.

Kenapa program C hanya kadang-kadang mengalami segfault?

Karena akses yang tidak valid itu adalah undefined behavior, bukan crash yang pasti terjadi. Menulis satu elemen melewati ujung array sering kali jatuh di memori yang memang milik prosesmu, jadi tidak ada yang menghentikanmu: yang terjadi malah variabel di sebelahnya jadi rusak. Segfault hanya terjadi saat alamat yang salah itu kebetulan jatuh di luar halaman yang dipetakan, dan itu tergantung tata letak build dan eksekusi tertentu.

Apakah segmentation fault berarti ada memory leak?

Bukan, keduanya justru masalah yang berlawanan. Leak adalah memori yang kamu alokasikan dan tidak pernah dibebaskan: program tetap berjalan dan perlahan membesar. Segfault adalah menyentuh memori yang bukan milikmu. Membebaskan memori dua kali, atau memakai pointer setelah membebaskannya, menyebabkan segfault; lupa membebaskan memori menyebabkan leak.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai