Hakkında Künye

Valgrind ile C/C++ Bellek Hatalarını Bulmak

Valgrind, C/C++ programlarında çok sık karşılaşılan bellek hatalarını bulup yok etmenize yardımcı olacak özgür yazılımlardan biri. En son sürümünü indirmek ya da daha fazla bilgi edinmek için www.valgrind.org adresine uğrayabilirsiniz. Bu yazıda, valgrind’in ne olduğuna, nasıl çalıştığına ve neler yapabildiğine kısa bir bakış yapacağız.

Bu yazıyı biraz tersten yazalım. Valgrind’in ne olduğunu ve nasıl çalıştığını anlatmadan önce, neler yapabildiğini anlatalım. Bunu da en iyi örneklerle yapabiliriz, o yüzden en sevdiğimiz programımız merhaba dünyayla başlayalım:

/* merhaba.c */
#include <stdio.h>
int main(void)
{
  printf("Merhaba!\n");
  return 0;
}

Programımızı derleyelim:

$ gcc merhaba.c -g -o merhaba

Valgrindi iki parçaya bölebiliriz: Valgrind çekirdeği ve Valgrind araçları. Programınız valgrind içinde çalışırken Valgrind çekirdeği her bir x86 komutunu kendi özel makina koduna çevirir (UCode). Valgrind araçları ise bu Ucode gösterimini kullanarak program üzerinde istedikleri kontrolleri yaparlar. Daha sonra çekirdek Ucode’u tekrar x86 komutuna çevirir. Valgrind araçları içinde en çok bilineni (ve bu yazıyı en çok ilgilendireni) Memcheck’tir. Bir programı Valgrind/Memcheck ile çalıştırmak için yapmamız gereken:

$ valgrind –tool=memcheck

Valgrind şuna benzer bir çıktı verecektir:

==7222== Memcheck, a memory error detector.
==7222== Copyright (C) 2002-2005, and GNU GPL’d, by Julian Seward et al.
==7222== Using LibVEX rev 1471, a library for dynamic binary translation.
==7222== Copyright (C) 2004-2005, and GNU GPL’d, by OpenWorks LLP.
==7222== Using valgrind-3.1.0-Debian, a dynamic binary instrumentation framework.
==7222== Copyright (C) 2000-2005, and GNU GPL’d, by Julian Seward et al.
==7222== For more details, rerun with: -v
==7222==
Merhaba!
==7222==
==7222== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 11 from 1)
==7222== malloc/free: in use at exit: 0 bytes in 0 blocks.
==7222== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
==7222== For counts of detected errors, rerun with: -v
==7222== No malloc’d blocks — no leaks are possible.

Kendi programımızın çıktısı olan "Merhaba"nın bulunduğu satır hariç her satır ==7222== ile başlıyor. ==’ler arasında kalan sayı programımızın süreç numarası. Valgrind konsola yazdığı bütün çıktıların başına ==7222== ifadesini ekleyecektir. Bu şekilde çıktıların hangilerinin bizim programımız hangilerinin Valgrind tarafından yazıldığı anlaşılabilir.

Valgrind/Memcheck hangi tür hataları yakalayabilir?

Yazının başında, Valgrind’in yakalanması zor pek çok çalışma zamanı hatasını yakalayabildiğinden bahsetmiştim. Bakalım neler yakalayabiliyor:

1) İlk değer verilmemiş değişkenler:

 /* ilkdeger.c */
1 #include <stdio.h>
2 int main(void)
3 {
4       int x, y, z;
5       x = ( x == 0 ? y : z );
6       printf("%d\n", x);
7       return 0;
8 }

Bu programı Valgrind/Memcheck içinde çalıştırdığımızda (yerden kazanmak için sadece ilgili kısımı gösteriyorum):

==8456== Conditional jump or move depends on uninitialised value(s)
==8456==    at 0×8048380: main (ilkdeger.c:5)
==8456==

Burada Valgrind bize 5. satırdaki üçlü ifadede ilk değer atılmamış değişkenler olduğunu söylüyor. (Not: Eğer Valgrind’in hata olan satırı da bildirmesini istiyorsak, kodumuzu gcc’de -g anahtarıyla derlemeliyiz.)

==8456== Use of uninitialised value of size 4
==8456==    at 0×4069FE7: (within /lib/tls/i686/cmov/libc-2.3.6.so)
==8456==    by 0×406D459: vfprintf (in /lib/tls/i686/cmov/libc-2.3.6.so)
==8456==    by 0×40736EF: printf (in /lib/tls/i686/cmov/libc-2.3.6.so)
==8456==    by 0×80483A8: main (ilkdeger.c:6)

Burada Valgrind’in diğer bir özelliğini görüyoruz. Valgrind yeni bir hata keşfettiğinde bize sadece hata olan işlevin adını değil, main fonksiyonuna kadar bütün işlev çağırım sırasını gösteriyor.

2) Bellek erişimleri:

/* erisim.c */
1  #include <stdio.h>
2  #include <stdlib.h>
3  int main(void)
4  {
5          int dizi[50], *isaretci, x, y;
6          isaretci = (int*) malloc(sizeof(int));
7          x = dizi[55];
8          y = isaretci[3];
9             printf("%d\n", isaretci[0]);
10        return 0;
11 }

Gördüğünüz gibi, bu kodda 50 tam sayılık yer ayırdığım dizi yapısının 56. elemanına erişmeye çalşıyorum. Ayrıca, malloc ile dinamik olarak 1 tam sayılık yer aldığım isaretci dizisinin 4. elemanına erişmeye çalışıyorum. Bu tip hataların en can sıkıcı yanı bulunmalarının çok zor olması. Örneğin, yukarıdaki kodu çalıştırdığınızda büyük ihtimalle kod hiç bir hata vermeden çalışacak ve başarıyla(!) sonlanacaktır. Daha büyük bir programda yanlış bellek erişimleri programın başka yerlerindeki doğru bilgileri de değiştirebilir. Bakalım Valgrind burada bize nasıl yardımcı oluyor:

Valgrind çıktısı:

==8740== Invalid read of size 4
==8740==    at 0×804839A: main (erisim.c:8)
==8740==  Address 0×4160034 is 8 bytes after a block of size 4 alloc’d
==8740==    at 0×401B422: malloc (vg_replace_malloc.c:149)
==8740==    by 0×804838A: main (erisim.c:6)

Bu hata çıktısını incelediğimizde, Valgrind bize 8.satırda 4 baytlık bir okuma hatası olduğunu, okunan bölgenin 6. satırda alınan 4 baytlık bölgenin 8 bayt dışında olduğunu söylüyor.

Burada Valgrind’in en güzel yanlarından birini görüyoruz. Valgrind sadece 8.satırda hata olduğunu söylemekle kalmadı, aynı zamanda bana bu bellek noktasına erişmenin niye yanlış olduğunu da söyledi.

Aynı zamanda bu örnek Valgrind’in yetersiz kaldığı bir noktayı da gösteriyor. Bildiğiniz gibi;

int dizi[50];

gibi bir tanım yaptığımızda, 50 tam sayılık yer statik olarak (yığıt üzerinde) alınır. Valgrind, bellek erişim kontrolünü sadece dinamik olarak (yığın üzerinde) alınan bellek üzerine yapabilir. Bu yüzden, erisim.c’nin 7.satırındaki yanlış erişim Valgrind tarafından yakalanmaz.

C/C++ programlarında çok sık yapılan diğer bir bellek erişim hatası free ile geri verilen bir belleğe tekrar erişmektir. Örneğin,

/* serbest.c */
1 #include <stdlib.h>
2 int main(void)
3 {
4    int *isaretci, x;
5    isaretci = (int*) malloc(sizeof(int) * 8 );
6    free(isaretci);
7    x = isaretci[0];
8    return 0;
9 }

Burada çok basit bir kod olduğu için anlaşılıyor ama daha büyük ve karmaşık bir programda bu tip bir hatayı tespit etmek çok zor olacaktır. Birazdan göreceğimiz gibi Valgrind bu tip hataları bulmayı da çok kolaylaştırıyor.

Valgrind çıktısı:

==11235== Invalid read of size 4
==11235==    at 0×80483CD: main (serbest.c:7)
==11235==  Address 0×4160028 is 0 bytes inside a block of size 32 free’d
==11235==    at 0×401BFCF: free (vg_replace_malloc.c:235)
==11235==    by 0×80483C9: main (serbest.c:6)

Diğer örnekleri bilgisayarınızda denediyseniz bu çıktı kolayca anlaşılır olmalı.

3) Bellek sızdırma:

/* sizdir.c */
1  #include <stdlib.h>
2  void sizdir(int b)
3  {
4         int *gereksiz;
5         gereksiz = (int*) malloc( b );
6  }
7  int main(void)
8  {
9         sizdir(8);
10        return 0;
11 }

sizdir işlevinin içinde 8 baytlık bir yer alıp daha sonra bunu sisteme geri vermiyoruz. Bu yüzden,

sizdir işlevi döndükten sonra bu belleği tamamen kaybetmiş oluyoruz.

Valgrind çıktısı:

==11286== LEAK SUMMARY:
==11286==    definitely lost: 8 bytes in 1 blocks.
==11286==      possibly lost: 0 bytes in 0 blocks.
==11286==    still reachable: 0 bytes in 0 blocks.
==11286==         suppressed: 0 bytes in 0 blocks.

Bellek sızdırma teknik olarak bir hata olmadığı için Valgrind programın çalışmasında bir hata bildirmeyecektir. Fakat, programın sonunda sızdırılmış bellek "definitely lost" adı altında gösterilecektir. Tabii, bu çıktı bize pek yararlı olmadığı için, Valgrind’e bellek sızdırma hakkında daha çok bilgi istediğimizi söyleyeceğiz.

Bu sefer, programı şöyle çalıştırıyoruz:

$ valgrind –tool=memcheck –leak-check=full ./sizdir
==11455== 8 bytes in 1 blocks are definitely lost in loss record 1 of 1
==11455==    at 0×401B422: malloc (vg_replace_malloc.c:149)
==11455==    by 0×8048370: sizdir (sizdir.c:5)
==11455==    by 0×804839D: main (sizdir.c:10)

Valgrind/Memcheck nasıl çalışıyor?

Valgrind/Memcheck her bir bayt bellek için, 1 bayt geçerlilik(validity), 1 bit adreslenebilirlik(addressability) bilgisi tutuyor. Geçerlilik baytındaki n’inci bit 1 ise, gösterdiği bir baytlık bellekteki n’inci bite bir ilk değer atanmış oluyor. Yani, yazıda bunu kullanmamış olsak da, bir baytlık bir belleğin ilk 4 bitine ilk değer atayıp, son 4 bitini kullansaydık valgrind bizi uyaracaktı. İkinci tuttuğu bilgi, adreslenebilirlik ise, bellekteki o baytın o program tarafından erişilir olup olmadığını gösteriyor. Örneğin, eğer bir malloc çağırısı bize 128-144 arasındaki baytları verseydi, Valgrind’in kendi içinde tuttuğu adreslenebilirlik dizisinde 128 ile 144 arasındaki bitler 1 olacaktı.

Valgrind/Memcheck her bir bellek erişimini teker teker kontrol eder. Bu da, doğal olarak ciddi bir performans kaybına sebep oluyor. Genel olarak, Valgrind içinde çalışan programınız 15-25 kat arası daha yavaş çalışır. Bu çok ciddi bir yavaşlama olsa da, Valgrind ile dikkatli yapılan birkaç test ile kodunuzdaki pek çok hatayı kolayca yakalayabileceğiniz için, bence bu kabul edilebilir bir dezavantaj.

Bu yazıda sizlere kısaca Valgrind’i ve en önemli eklentilerinden Memcheck’i anlatmaya çalıştım. Umarım bu yazı sizlere faydalı olmuştur ve sizler de C/C++ ile kod yazarken Valgrind’ten yararlanırsınız.



Doğacan Güney
- 6 -