Blog

Dinamik Bellek Yönetimi – Dynamic Memory Management

Bellek yönetimine girmeden bellek yapısından, heap ve stack kavramlarından bahsetmek gerekir.

Stack ile başlayalım.

Stack, türkçe karşılığı yığın olan RAM bellekte bulunan soyut bir yapıdır. Belirli bir düzene ve kurala göre verileri içerisinde saklar. Bu kural yapısı kısaca LIFO yani (Last in First Out) son giren ilk çıkar mantığında çalışmaktadır.

Kısaca LIFO nun mantığını günlük hayattan örneklendirmek gerekirse şöyle açıklayabilirim.

Mutfakta kirli tabaklarınız var önce deterjanla köpürtüp temizleyip üst üste diziyorsunuz. Bu şekilde 10-15 tabağı üst üste dizdiğinizi düşünün. Temizleme işleminin ardından durulama aşaması geldiğinde ilk olarak, en alttaki tabaktan değil en üstteki tabaktan başlarsınız.  İlk önce en üstte ki tabağı alır durular bir kenara koyarsınız sonra sırasıyla diğerlerini yıkar durularsınız. LIFO mantığıda bu şekildedir. Stack de veriler, son giren Ilk çıkar mantığı ile işleme tabi tutulurlar. Bir düzen vardır. Biz bu yapıyı farkında olmadan günlük hayatta bir çok kez kullanıyoruz. Bizim yerimize kullandığımız bilgisayar bunu otomatik olarak yapıyor.

İlk değer vermediğimiz bir pointer oluşturduğumuz da bu pointer bellekte stack alanında bulunur.  Örnek char * türünde bir pointer oluşturalım.

Char *ptr;

Heap ile devam edelim.

Heap yapısının en önemli özelliği, veriler stack de olduğu gibi bir düzen veya kurala(LIFO)  göre değil karışık bir şekilde tutulurlar.  Bir diğer fark ise Stack üzerinde ki veri hemen silinebilirken Heap de ki veriyi silmek için çöp toplayıcı (garbage collector) algoritması gerekir. 

Dinamik bellek yönetiminde bellek üzerinden belirli bir boyutta bellek alanı ayırmak istediğimizde, bu alan heap den alınır. Aynı şekilde bu alanı büyütmek veya küçültmek istediğimiz de, gene heap üzerinde işlem yapılır.

Yukarıda oluşturduğumuz pointer’a bir dizinin adresi ile ilk değer verelim.

char a[20] = { 0 };

            char *ptr = a;

 

Artık ptr, a dizisinin adresini tutuyor.

Neden adres operatörünü kullanmadığımızı merak ediyor olabilirsiniz. Dizi işlemlerinde derleyiciye;

char *ptr = &a;

demek ile 

 char *ptr = a;

demek arasında bir fark yoktur.

Derleyiciden derleyiciye fark olsa da, tanımladığınız pointer veya değişken türlerinin bellekte kaplayacağı alan az çok bellidir. Yukarıda tanımadığımız pointer bellek üzerinde 8 byte yer kaplamaktadır. Linux altında kullandığım GCC derleyicisinde, tanımlanan değişken veya pointerlara ait tür bilgileri ve hafızada kapladıkları alanları şöyledir.

Int türü 4 byte

Int * türü 8 byte

Double türü 8 byte

Double * türü 8 byte

Char türü 1 byte

Char * türü 8 byte

Bu bilgileri elde etmek için sizeof() işlevi kullanılabilir.

#include <stdio.h>

int main(){

            printf("sizeof(char *)  = %d\n", sizeof(char *));

            printf("sizeof(int *) = %d\n", sizeof(int *));

            printf("sizeof(int) = %d\n", sizeof(int));

            printf("sizeof(char) = %d\n", sizeof(char));

            printf("sizeof(double) = %d\n", sizeof(double));

            printf("sizeof(double *) = %d\n", sizeof(double *));

return 0;

}

 

Şimdi gelelim dinamik bellek yönetimi fonksiyonlarını tanımaya.

Standart olarak C de gelen dinamik bellek yönetimi ile ilgili fonksiyonların bildirimleri “stdlib.h” başlık dosyası içerisinde yer alır.  Bu fonksiyonlar şunlardır:

malloc();

calloc();

realloc();

free();

 

Tanımladığımız diziler genelde belli boyutlara sahip oldukları için bellek üzerinde kapladıkları alanı tahmin etmek kolaydır. Fakat öyle senaryolar var ki, tanımladığımız dizilerin boyutunu kestirmek zor oluyor. Bu gibi durumlar da dizilerin boyutu dinamik olarak küçülüp büyüyebiliyor. Örnek, yemekhaneye giriş çıkışların kontrol edildiği bir yazılımınız var. Yemekhaneye giriş çıkış yapan kişilerin isim, soyisim, yaş, departman ve giriş saati gibi bilgilerini diziler üzerinde tutuyorsunuz. Fakat o gün yemekhaneye kaç kişinin giriş yapacağını kestiremiyorsunuz. Giriş yapanların sayısı 10 da olabilir 100 de.. Siz de bu duruma uygun bir biçimde dizi tanımlamalısınız. Bu duruma uygun bir biçimde dizi tanımlamak biraz zor ama dinamik bellek yönetimi ile kolay..

Şimdi fonksiyonlarımızın bildirimlerini ve tek tek nasıl kullanıldıklarını görelim.

Malloc işlevi

 void *malloc(size_t size);

Gördüğünüz gibi malloc işlevi adres döndüren bir işlev.. Bizden size_t türünden bir boyut bilgisi istiyor. (size_t türü unsigned int’tir)

Bize ne kadar bellek alanı istediğimizi soruyor ve bize bu bellek alanını kullanabilmemiz için bir adres bilgisi dönüyor. Adres bilgisini saklamak için pointerları kullanıyoruz.

Şimdi bellekten 40 byte’lık bir alan tahsis edelim.

#include <stdio.h>

#include <stdlib.h>

int main()

{

            int *ptr = malloc(sizeof(int) * 10);

           if(ptr){

                        printf(“Alan tahsis edildi”);

            }

            else{

                        printf(“Yetersiz bellek.\n”);

            }

 

return 0;

}

Calloc işlevi

Malloc işlevi ile 40 bytelık bir alan tahsisi gerçekleştirdik. Fakat bilmemiz gereken bir konu daha var. Malloc işlevi ile tahsis edilen bellekte ki alanlar çöp değerler ile tahsis edilir.

Calloc işlevi ise tıpkı malloc işlevi gibi alan tahsis eder ve malloc işlevinden farklı olarak tahsis ettiği bellek alanını sıfırlar. Yani bellek adreslerinden veri okunduğunda değer olarak 0 bulunur.

Calloc işlevinin bildirimi (function declaration) şu şekilde:

void *calloc(size_t ntane, size_t nboyutunda);

Calloc işlevinin parametre olarak bizden istediği iki adet argüman var. Bunlardan ilkikaç byte alan istenildiğini belirten argüman, bir diğeri ise hangi tür boyutta istenildiğini belirten argüman.. Aşağıdaki kod bloğu daha açıklayıcı olur sanırım.

#include <stdio.h>

#include <stdlib.h>

int main()

{

            int *cptr = calloc(10, sizeof(int)); // sizeof (int) değer olarak 4 byte döner.. 4 * 10 = 40 byte

            for(int i=0; i<10; ++i){

                        printf(“cptr[%d] = %d\n”, i, cptr[i]);

            }

return 0;

}

Hatırlarsak, malloc işlevinden farklı olarak calloc işlevi bellekteki değerleri sıfırlıyordu. Derleyip çalıştırdığımız da bize ayrılan bellek alanında ki değerleri görebiliriz.

Realloc işlevi

Yazının ilk başında verdiğim yemekhane örneğine bir gönderme ile başlayalım. Hatırlarsak yemekhane yazılımımız da bir dizi ile yemekhaneye giriş çıkış yapan kişilerin bilgilerini tutuyorduk. Fakat kaç kişinin giriş yapacağı konusunda bir fikrimiz olmadığından, dizi ile ayırdığımız bellek bloğu bize yetersiz gelebilir. Bu durumda ayırdığımız bellek bloğunu büyütüp küçültme ihtiyacı duyarız. Bu işi yapan işlev realloc işlevidir.

Realloc işlevinin bildirimi şu şekildedir:

 void *realloc(void *vp; size_t new_size);

Realloc işlevinin bildiriminden anlayacağımız üzere, işlev bizden iki argüman istemektedir. Birinci argüman büyültme veya küçültme yapacağımız bellek bloğunun adresi, ikinci argüman ise adresi alınan bellek bloğunun yeni boyutu.. İşlevi şu şekilde kullanıyoruz:

#include <stdio.h>

#include <stdlib.h>

int main()

{

            size_t n;

            printf("kac tamsayi: ");

            scanf("%zu", &n);

            int *pd = (int *)malloc(n * sizeof(int));

            if (!pd) {

                        printf("bellek yetersiz\n");

            }

            size_t nplus;

            printf("kac tamsayi daha ilave edilsin: ");

            scanf("%zu", &nplus);

 

            pd = realloc(pd, (n + nplus) * sizeof(int));

            if (!pd) {

                        printf("bellek yetersiz\n");

                        return 1;

            }

}

 Kodu yorumlamaya başlarsak :

size_t türünde n adında bir değişken tanımlıyoruz.  Scanf ile tanımladığımız değişkene bir tamsayi değeri atıyoruz. Daha sonra pd adında, geri dönüş türü int * olan bir pointer tanımlıyoruz ve buna değer olarak int * türüne cast edilmiş(dönüştürülmüş) malloc işlevi ile n * sizeof(int) kadar bellek alanı tahsis ediyoruz. İf deyimi ile bellek alanının tahsisi başarılı mı değil mi kontrol ediyoruz.

Artık pd adında bir pointer da n adet, int türünden bellek alanımız var. Bu bellek alanını büyütmek için tekrar size_t türünden nplus adında bir değişken tanımlıyoruz. Printf işlevi ile var olan bellek alanına ne kadar daha ekleme yapılması istenildiğini soruyor ve bu değeri scanf işlevi ile nplus değişkenine değer olarak veriyoruz.

Daha sonra realloc işlevini çağırıp işlem yapılacak bellek adresinin tutulduğu pointer’ı veriyoruz. İkinci argüman olarak da var olan n sayısına nplus ı ekleyip sizeof(int)  geri dönüş değeri ile çarpıyoruz. Sizeof(int) geri dönüş değerini 4 byte döneceğinden n + nplus toplamı 4 ile çarpılacak ve çıkan değer kadar “byte” bellekten tahsis edilmiş olacak. Bu örnek bellek alanının büyütülmesine verilen örnektir.

Free işlevi

Free işlevi, malloc veya calloc ile tahsis ettiğimiz bellek alanlarını işimiz bittiğinde tekrar serbest bırakmamıza yarayan çok güzel bir işlevdir. Dinamik bellek yönetiminde malloc veya calloc ile ayrılan bellek, heap bellek alanından verilir demiştik. Heap alanından aldığımız bellek bloğunu free ile geri vermezsek bellek alanı dolduğunda ve program işini yapamayacak duruma geldiğinde “memory leak” dediğimiz bellek sızıntısına sebep olur.

***Aşağıda madde madde belirtilen davranışlarda kesinlikle bulunmayın!***

1-) Dinamik bellek işleviyle yeri elde edilmemiş bir bellek bloğunu asla free etme girişiminde bulunmayın.

2-) free işleviyle bir dinamik bellek bloğunu küçültme girişiminde bulunmayın. Free ile bloğun tamamını geri verebilirsiniz.

3-) free edilmiş bir bellek bloğunun adresini tekrar free işlevine döndürmek tanımsız davranıştır. (undefined behavior)

4-) Geri verilmiş bir bellek bloğunu kullanma girişiminde bulunmayın. Tanımsız davranıştır.(undefined behavior)

5-) Tahsis ettiğiniz dinamik bellek bloğunu free ile geri vermeyi unutmayın.

Free işlevinin bildirimi şöyledir.

Void free(void *ptr);

Gördüğünüz üzere free bizden yanlızca free edilecek yani geri verilecek bellek bloğunun adresini istiyor. Yukarıda malloc ve calloc işlevleri ile tahsis ettiğimiz bellek bloklarını şu şekilde geri verebiliriz.

#include <stdio.h>

#include <stdlib.h>

 

int main()

{

            int *cptr = calloc(10, sizeof(int)); // sizeof (int) değer olarak 4 byte döner.. 4 * 10 = 40 byte

            for(int i=0; i<10; ++i){

                        printf(“cptr[%d] = %d\n”, i, cptr[i]);

            }

 free(cptr); // geri verilecek bellek bloğunun adresi

return 0;

}

Yazımızı okuma zahmetinde bulunduğunuz için teşekkürler. 




Yazar Hakkında

Lise son sınıf öğrencisidir, 2010 yılından beri siber güvenlik ile ilgilenmektedir. CSS, PHP, C/C++ dilleri ve Web Penetration Testing ile yakından ilgilenmektedir. Bunlara ek olarak Sistem, Network ve Güvenlik Uzmanlığı eğitimleri almış olan Kelepçe, Bug Bounty Ödül Avcılığı kapsamında birçok tanınmış şirketten ödül almıştır, şuanda bilişim faaliyet topluluklarında Bilgi Güvenliği ve Sızma Testleri işlerini gönüllü olarak yapmaktadır.






Yorum Yapmak İçin Giriş Yapın.