Menu

C'de Segmentation Fault: Nedenleri ve Çözümleri

Segfault, programınızın sahibi olmadığı belleğe dokunduğu anlamına gelir. İşte neredeyse hepsini açıklayan beş sebep; her biri en küçük örneği ve çözümüyle, ayrıca gdb ve AddressSanitizer ile tam satırı nasıl bulacağınız.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

Segmentation fault (core dumped)

O tek satır, C'de en çok aranan hatadır ve göründüğünden daha az gizemlidir. Programınız işlemciden bir bellek adresi istedi, işletim sistemi sürecinizin o adrese dokunmasına izin verilip verilmediğini denetledi ve cevap hayır oldu. Çekirdek sonra süreci bir SIGSEGV sinyaliyle öldürdü.

Kilit fikir: çökme bir belirtidir ve konumu çoğu zaman hata değildir. Kötü gösterici genellikle başka bir yerde, daha önce oluşturulmuştur ve burası yalnızca ilk kullanıldığı yerdir. Bu sayfa, hemen her segfault'u açıklayan beş sebebi ve ardından gerçek satırı saniyeler içinde bulan iki aracı ele alıyor.

Aşağıdaki çöken örnekler bilerek çalıştırılabilir bloklar değildir - tasarımları gereği çökerler. Onları okuyun, sonra ardından gelen düzeltilmiş sürümü okuyun.

"Sahibi Olmadığınız Bellek" Ne Demek

Programınız başladığında işletim sistemi adres uzayına birkaç bölge eşler: kod, küreseller, yığın ve öbek ne kadar büyüdüyse o. Adres uzayındaki diğer her şey - 0 adresi dahil - eşlenmemiştir. Eşlenmemiş bir adrese dokunun ya da salt okunur birine yazın, donanım bunu yakalar.

Yani bir segfault, derleyicinin sizi yakalaması değildir. Bir çalışma zamanı korkuluğudur ve yalnızca geçersiz adres eşlenmiş sayfalarınızın dışına düştüğünde devreye girer. Aynı hatanın bir makinede çökmesi ve başka birinde çalışıyor görünmesinin sebebi budur: bellek yerleşimi farklıdır.

Sebep 1: Bir NULL Göstericiyi Dereferans Etmek

En yaygın sebep ve düzeltmesi en kolay olanı. 0 adresi asla eşlenmez, dolayısıyla boş bir gösterici üzerinden okumak ya da yazmak her zaman hata verir.

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

int main(void) {
    int *p = NULL;
    *p = 42;              /* ÇÖKME: 0 adresine yazma */
    printf("%d\n", *p);
    return 0;
}

Gerçekçi sürüm, denetlenmemiş bir tahsistir:

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

int main(void) {
    int *data = malloc(1000000000000UL * sizeof(int));  /* başarısız, NULL döner */
    data[0] = 1;                                        /* ÇÖKME */
    free(data);
    return 0;
}

malloc, isteği karşılayamadığında NULL döndürür; dosya yoksa fopen da, karakter yoksa strchr da öyle. Sonucunu kullanmadan önce NULL döndürebilen her fonksiyonu denetleyin.

Göstericileri ilk değersiz bırakmak yerine NULL ile başlatın. Boş bir gösterici hemen ve apaçık çöker; çöp bir gösterici bir şeyi bozabilir ve çok sonra çökebilir. Kalıp hakkında daha fazlası boş göstericiler sayfasında.

Sebep 2: Bir Dizinin Sonunun Ötesine Yazmak

C dizi sınırlarını denetlemez. 10 elemanlı bir dizinin 10. indeksi yalnızca dizinin ardındaki bellektir ve derleyici o adresi şikâyet etmeden sizin için hesaplar.

#include <stdio.h>

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

    for (int i = 0; i <= 10; i++) {   /* < yerine <= : bir fazla */
        arr[i] = i;
    }

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

Bunun çöküp çökmeyeceği şanstır. Yerel bir dizinin dört bayt ötesine yazmak genellikle başka yığın verisine düşer - kaydedilmiş bir yazmaç, başka bir değişken, dönüş adresi - dolayısıyla program kendini bozar ve daha sonra alakasız bir yerde çöker. Büyük bir taşma eşlenmiş sayfadan çıkar ve anında segfault verir.

Daha vahşi sürüm her zaman çöker:

#include <stdio.h>

int main(void) {
    int arr[10];
    arr[1000000] = 42;      /* eşlenmiş her şeyin çok dışında: ÇÖKME */
    return 0;
}

Çözüm, i < n alışkanlığı ve n'i iki kez yazmak yerine hesaplamaktır:

Metinlerin bunun kendi sürümü vardır: sonlandırıcı '\0' için yeri olmayan bir tampon.

#include <string.h>

int main(void) {
    char name[5];
    strcpy(name, "Alexander");   /* 5 bayta 9 karakter + sonlandırıcı */
    return 0;
}

strcpy, name'in ne kadar büyük olduğu hakkında hiçbir fikre sahip değildir. Siz söylediğiniz için bilen snprintf'i kullanın:

Sebep 3: free Sonrası Bir Göstericiyi Kullanmak (Sarkan Göstericiler)

free(p)'den sonra bellek tahsis ediciye geri verilir. Gösterici hâlâ eski adresi tutar, ama o adres artık sizin değildir.

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

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

    printf("%d\n", *p);   /* serbest bırakma sonrası kullanım: çöp yazdırabilir, çökebilir */
    free(p);              /* çifte serbest bırakma: genellikle durdurur ya da öbeği bozar */
    return 0;
}

İlgili tuzak, yerel bir değişkenin adresini döndürmektir. Yığın çerçevesi fonksiyon döndüğü anda gitmiştir:

#include <stdio.h>

int *make_number(void) {
    int value = 42;
    return &value;      /* çerçeve burada ölür; gösterici sarkar */
}

int main(void) {
    int *p = make_number();
    printf("%d\n", *p);   /* tanımsız: çöp ya da bir çökme */
    return 0;
}

Ne kastettiğinize göre iki çözüm. Gösterici yerine değeri döndürün ya da öbekte tahsis edip çağıranın serbest bırakmasına izin verin:

free'den hemen sonra göstericiyi NULL yapmak ucuz savunmacı alışkanlıktır: sessiz bir serbest-bırakma-sonrası-kullanımı anlık, apaçık bir null dereferans çökmesine dönüştürür ve ikinci bir free(p)'yi zararsız kılar, çünkü free(NULL)'ın hiçbir şey yapmadığı tanımlıdır. Bu hikâyenin tahsis tarafı dinamik bellek ve bellek sızıntıları sayfalarındadır.

Sebep 4: Kaçak Özyinelemeden Kaynaklanan Yığın Taşması

Her fonksiyon çağrısı yığına bir çerçeve yerleştirir ve yığın sabit boyutlu bir bölgedir (yaygın olarak 8 MB). Temel durumu olmayan - ya da hiç ulaşılmayan - özyineleme onun sonundan taşar.

#include <stdio.h>

int countdown(int n) {
    printf("%d\n", n);
    return countdown(n - 1);    /* temel durum yok: hiç durmaz */
}

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

Aynısı, özyinelemenin üzerinden atladığı bir temel durumla da olur:

int f(int n) {
    if (n == 0) return 1;
    return n * f(n - 2);    /* tek bir n'den başlayınca asla 0 olmaz */
}

Her özyinelemeli fonksiyonun her girdiden erişilebilir bir temel duruma ihtiyacı vardır:

Devasa bir yerel dizi de aynısını yapar - bir fonksiyonun içindeki int buffer[10000000];, 40 MB yığın ister ve ilk yazmada hata verir. Büyük tamponları öbekte malloc ile tahsis edin. İlgili boyutlar için bkz. yığın ve öbek, temel durum tasarımı için özyineleme.

Sebep 5: Bir Metin Sabitine Yazmak

Bu insanları şaşırtır, çünkü kod zararsız görünür.

#include <stdio.h>

int main(void) {
    char *s = "hello";
    s[0] = 'H';          /* ÇÖKME: metin sabitleri salt okunurdur */
    printf("%s\n", s);
    return 0;
}

Bir metin sabiti çalıştırılabilir dosyanın salt okunur bir bölümünde yaşar. char *s = "hello" onun içini gösterir; o gösterici üzerinden yazmak bir koruma hatasıdır ve işletim sistemi bunu eşlenmemiş bir erişim gibi segfault olarak bildirir.

Çözüm, kendi değiştirilebilir kopyasını alan bir dizi yapmaktır:

Sabit göstericilerini const char * diye bildirmek bu çalışma zamanı çökmesini bir derleme zamanı hatasına dönüştürür ki bu kesinlikle daha iyidir. Bunu alışkanlık hâline getirin.

Gerçek Satırı Bulmak: gdb

Çalıştırılabilir dosya hata ayıklama sembolleri taşısın diye -g ile derleyin, sonra onu hata ayıklayıcı altında çalıştırın:

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

gdb içinde:

(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

İşin çoğunu üç komut yapar. run programı başlatır ve hata verdiği yerde durdurur. backtrace (ya da bt) oraya götüren çağrı zincirini gösterir - #1 çerçevesi genellikle kötü göstericinin gerçekte üretildiği yerdir. print bir değişkeni inceler ve item = 0x0 sorunu doğrudan adlandırır.

macOS'ta karşılığı lldb ./program, sonra run ve bt'dir.

Daha Hızlı Bulmak: AddressSanitizer

Daha da iyisi, derleyicinin programı enstrümante etmesine izin verin. AddressSanitizer, geçersiz erişimi olduğu anda yakalar - çökme vermeyecek olanlar dahil:

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

O rapor hata sınıfını, ona yol açan satırı, belleği serbest bırakan satırı ve onu tahsis eden satırı adlandırır. C bellek hataları için tek en etkili hata ayıklama aracıdır, Linux ve macOS'ta GCC ve clang ile çalışır ve kabaca 2 kat çalışma zamanına mal olur ki bu geliştirme sırasında önemsizdir.

İşaretli taşmayı ve diğer tanımsız davranışları aynı anda yakalamak için onu -fsanitize=undefined ile eşleyin:

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

valgrind ./program, yeniden derleme gerektirmeyen ve aynı hata sınıflarını artı sızıntıları bildiren alternatiftir.

Biriyle Karşılaştığınızda Bir Kontrol Listesi

  1. -g -Wall -Wextra -fsanitize=address,undefined ile yeniden derleyin ve tekrar çalıştırın. Çoğu zaman rapor satırı adlandırır ve işiniz biter.
  2. Denetleyici yoksa programı gdb altında çalıştırın ve bir backtrace alın. Yalnızca 0. çerçeveye değil 1. çerçeveye bakın.
  3. Çöken satırdaki her göstericiyi kontrol edin. Her birini yazdırın; 0x0 bir null'u tanımlar ve 0x7fff5fc01000 gibi vahşi bir değer genellikle ilk değersiz ya da serbest bırakılmış demektir.
  4. O göstericinin nereden geldiğini sorun. Denetlenmemiş bir malloc ya da fopen mu? Bu arada dönmüş bir yerelin adresi mi? free'den sonra kullanılan bir gösterici mi?
  5. Çökmenin yakınındaki her döngü sınırını < kastedilmişken <= var mı diye kontrol edin.
  6. Yığın izi binlerce çerçeve derinse, bu bir gösterici hatası değil kaçak özyinelemedir.

Onları Önlemek

Segfault'ları nadir kılan alışkanlıklar:

  • Her zaman -Wall -Wextra ile derleyin ve uyarılara hata gibi davranın.
  • Her göstericiye ilk değer verin; daha iyi bir şey yoksa NULL.
  • malloc, calloc, realloc ve fopen'ın dönüşünü kontrol edin.
  • Serbest bıraktıktan hemen sonra göstericileri NULL yapın.
  • sprintf ve gets yerine snprintf ve fgets kullanın.
  • Metin sabiti göstericilerini const char * bildirin.
  • Sabit yazılmış bir uzunluk yerine sizeof arr / sizeof arr[0]'ı tercih edin.
  • Test paketini CI'da AddressSanitizer altında çalıştırın.

Segfault dostça başarısızlık biçimidir - size bir şeyin ters gittiğini söyler. Komşu bir değişkeni sessizce bozup üç fonksiyon sonra yanlış cevaplar üreten aynı hata sınıfı çok daha kötüdür ve yukarıdaki araçlar ikisini de yakalar.

Sıkça Sorulan Sorular

C'de segmentation fault nedir?

Programınız dokunmasına izin verilmeyen belleğe eriştiğinde işletim sisteminin tetiklediği bir çökmedir - geçersiz bir gösterici üzerinden okumak ya da yazmak, bir dizinin sonunu geçip eşlenmemiş bir sayfaya girmek ya da yığını taşırmak. Çekirdek, sürece süreci sonlandıran ve "Segmentation fault (core dumped)" yazdıran bir SIGSEGV sinyali gönderir.

Bir segmentation fault'un nerede olduğunu nasıl bulurum?

Hata ayıklama sembolleriyle derleyin ve bir hata ayıklayıcı altında çalıştırın: gcc -g program.c -o program, sonra gdb ./program, run ve çöktüğünde backtrace. Bu tam dosya ve satırı yazdırır. Bellek hataları için daha da hızlısı gcc -g -fsanitize=address program.c -o program'dır - yalnızca programı çalıştırmak, neyin nerede ters gittiğine dair tam bir rapor yazdırır.

C programım neden yalnızca bazen segfault veriyor?

Çünkü geçersiz erişim garanti bir çökme değil tanımsız davranıştır. Bir dizinin bir eleman ötesine yazmak çoğu zaman sürecinizin sahibi olduğu belleğe düşer, dolayısıyla hiçbir şey sizi durdurmaz - bunun yerine komşu bir değişkeni bozar. Yalnızca kötü adres eşlenmiş bir sayfanın dışına düştüğünde segfault alırsınız ki bu, o belirli derlemenin ve çalıştırmanın yerleşimine bağlıdır.

Segmentation fault, bellek sızıntım olduğu anlamına mı gelir?

Hayır - bunlar zıt sorunlardır. Sızıntı, tahsis edip hiç serbest bırakmadığınız bellektir: program çalışmaya devam eder ve yavaşça büyür. Segfault ise sahibi olmadığınız belleğe dokunmaktır. Belleği iki kez serbest bırakmak ya da bir göstericiyi serbest bıraktıktan sonra kullanmak segfault'a yol açar; serbest bırakmayı unutmak sızıntıya yol açar.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA