Memory Allocation

Türkçe karşılığında en çok zorlandığımız terminolojilerin biri de bu meşhur Memory Allocation.

Bellek tahsisatı diyenler var, bellek edinimi şeklinde kullanımlarını da görüyoruz. Tahsisat fena durmuyor aslında ama tam da içimize sinmiyor. Allokasyon desek olmayacaktı, o yüzden şimdilik olduğu gibi bırakıyorum.

Konumuza geçelim.

Allocation gerçekte nasıl yapılır?

Bu soru şimdiye kadar hiç aklınıza takıldı mı? Yazılım mühendislerinin ekseri çoğunluğu bu sürecin detaylarını pek bilmez. Ama sistem programcısı adayı olarak bu satırları okuyorsanız, bu süreç hakkında daha fazla bilgi sahibi olmanız gerekir.

Allocation sürecine Linux ve glibc kütüphanesi özelinde biraz daha detaylı bakmaya çalışalım.

Uygulamalarda bellek ihtiyacımız olduğunda, işletim sisteminden bu alanı talep etmemiz gerekir. Çekirdekten yapılacak bu talep doğal olarak sistem çağrısı gerektirecektir; kullanıcı kipinde kendi kendimize bellek tahsisatı yapamayız.

Bu noktada henüz okumadıysanız Sistem Çağrıları bölümüne hızlıca göz atmanız önerilir.

C dilinde allocation için temel olarak malloc() fonksiyon ailesi kullanılır. Peki bir glibc fonksiyonu olarak malloc() doğrudan bir sistem çağrısında mı bulunuyordur, yani open() fonksiyonu ve sistem çağrısı örneğindeki gibi birebir bir karşılığı mevcut mudur?

Linux çekirdeğinde malloc adında bir sistem çağrısı bulunmamaktadır.

Uygulamaların bellek talepleri için 2 adet sistem çağrısı mevcut olup bunlar sırasıyla brk ve mmap çağrılarıdır.

Bizler uygulamamızda bellek talebini glibc fonksiyonları aracılığıyla yaptığımızdan bu noktada aklınıza glibc'nin bu sistem çağrılarından hangisini kullanıyor olduğu sorusu gelebilir. Cevabımız, her ikisini de kullandığı yönünde olacaktır. Peki o zaman bu iki sistem çağrısı arasındaki fark nedir ve hangi durumlarda kullanılmaktadır?

Process Bellek Yerleşimi (sadeleştirilmiş hali)

brk

Her process, ardışıl bir (contiguous) data alanına sahiptir. brk sistem çağrısı ile bu alanın sınırını belirleyen program break değeri artırılarak allocation işlemi de gerçekleştirilmiş olur.

NOT: data alanını büyütmekten kastımız HEAP (Program Break) sınırını yukarı yönlü kaydırmaktır. Data Segment ve BSS'in sınırları uygulama için sabittir ve çoğu dokümanda HEAP alanını büyütmek yerine data alanını büyütmek tabiri kullanılmakta, bu da Data Segment ile karıştırılabilmektedir. Data alanını birbirine komşu olan (Data Segment + BSS + HEAP) şeklinde düşünecek olursanız anlaşılması daha kolay olacaktır.

Bu yöntemle yapılan bellek tahsisatları çok hızlı olmasına rağmen, önemli bir dezavantaja sahiptir. Ardışıl yapısı nedeniyle, artık kullanılmayan bir alanı sisteme geri vermek her zaman mümkün olmaz.

Örnek olarak her biri 16 KB büyüklüğünde 5 adet alanı sırayla malloc() fonksiyonu üzerinden brk sistem çağrısı ile tahsis ettiğimizi düşünelim. Bu alanlardan 2 nolu olanla işimiz bittiğinde, ilgili kaynağı sistemin kullanabilmesi için geri vermemiz (deallocation) mümkün değildir zira brk çağrısıyla adres değerini 2 nolu alanımızın başladığı yeri gösterecek şekilde azaltacak olursak, 3, 4 ve 5 nolu alanlar için de deallocation işlemi yapmış oluruz.

Glibc içerisindeki malloc implementasyonu bu senaryodaki bellek kaybını önleyebilmek adına, process data alanında bu şekilde tahsis edilmiş ve sonrasında free() fonksiyonu ile artık sisteme geri verilebileceği belirtilmiş olan yerleri, sonraki bellek tahsisatlarında kullanmak üzere takip eder.

Yani 16 KB'lık 5 adet alan tahsis edildikten sonra 2 nolu alan free() fonksiyonuyla geri verilmek istenip bir süre sonra tekrar bir 16 KB'lık alan daha istenecek olursa, brk sistem çağrısı aracılığıyla data alanını büyütmek yerine, hazırdaki 2 nolu alanın adresi geri dönülür.

Ancak eğer yeni talep edilen alan bu örneğimiz için 16 KB'dan büyük ise, bu durumda 2 nolu alan kullanılamayacağından, brk sistem çağrısı ile yeni bir alan daha ayrılarak data alanı büyütülmüş olur, 2 nolu alan ise kullanımda olmamasına rağmen sisteme geri de verilemez ve yer harcamaya devam eder. İşte bunun gibi senaryolar nedeniyle internal fragmentation diye adlandırılan durum oluşur ve aslında belleğin her tarafını sonuna kadar hemen hiç bir zaman kullanamayız. Meşhur Türk özdeyişi, restart kan yapar'ın kökeni esasen buralara kadar dayanmaktadır.

Aşağıdaki örnek uygulamayı derleyip çalıştırmayı deneyiniz.

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

int main(int argc, char* argv[]) {
    char *ptr[7];
    int i;

    printf("Pid of %s: %d\n", argv[0], getpid());
    printf("Initial program break        : %p\n", sbrk(0));
    for (i = 0; i < 5; i++) ptr[i] = malloc(16 * 1024);
    printf("After 5 x 16KB malloc        : %p\n", sbrk(0));
    free(ptr[1]);
    printf("After free of second 16KB    : %p\n", sbrk(0));
    ptr[5] = malloc(16 * 1024);
    printf("After allocating 6th of 16KB : %p\n", sbrk(0));
    free(ptr[5]);
    printf("After freeing last block     : %p\n", sbrk(0)); 
    ptr[6] = malloc(18 * 1024);
    printf("After allocating a new 18KB  : %p\n", sbrk(0));
    getchar(); 
    return 0;
}

Uygulama çıktısı:

Pid of ./m1: 5877
Initial program break        : 0x9f6000
After 5 x 16KB malloc        : 0xa1b000
After free of second 16KB    : 0xa1b000
After allocating 6th of 16KB : 0xa1b000
After freeing last block     : 0xa1b000
After allocating a new 18KB  : 0xa1b000

strace ile brk için çıktısı:

$ strace -e trace=brk ./m1 > /dev/null
...
brk(0)                                  = 0x9f6000
brk(0xa1b000)                           = 0xa1b000

Yukarıdaki çıktıda brk(0) şeklindeki özel çağrı yöntemiyle (parametre olarak 0 geçirilmesi) data alanının mevcut bitiş adresinin öğrenildiğini görüyoruz (0x9f6000). Ardından üzerine 0x25000 eklenerek data alanının bitiş adresi 0xa1b000 değerine kaydırılmak istenmiş ve herhangi bir hata alınmamış. Dolayısıyla tam bu noktada 0x25000 yani yaklaşık 148 Kb boyutunda bellek tahsisatı yapıldığını görmekteyiz. Şu soruları kendimize soralım:

  • Peki neden tam olarak örnek kodumuzdaki miktar kadar değil de ondan biraz daha fazla allocation gerçekleşti?
  • Allocation'ı sağlayan brk çağrısı kaynak kodumuzda tam olarak hangi satırdan kaynaklandı? İlk malloc() çağrısında mı gerçekleşti yoksa derleyici bir optimizasyon yapıp toplamdaki talebimizden biraz fazlasını talep ederek fazladan sistem çağrısı yapmamızı mı önledi?

Bu soruların yanıtlarını kendiniz bulmaya çalışın, öğretici bir süreç olacaktır.

Address Space Layout Randomization: ASLR

Yukarıdaki örnek uygulamamızı arka arkaya çalıştırdığınızda, her defasında farklı adres değerleri görülecektir. Bu konuyu Address Space Layout Randomization (ASLR) başlığı altında ayrıca detaylandırmamız gerekiyor. Şimdilik şu kadarını söylemekle yetinelim, adres alanını bu şekilde rastgele değişecek hale getirmek, yazılımlara yönelik güvenlik ataklarının işini önemli ölçüde zorlaştırmakta ve yazılım güvenliğini artırmaktadır. Bununla birlikte 32 bitlik mimarilerde adres alanı rastgele hale getirmek için genelde 8 bit kullanılır. Bit sayısını artırmak, geriye kalan bit'ler üzerinden adreslenebilecek alan çok düşeceğinden uygun olmamakta, bununla birlikte sadece 8 bitlik kombinasyonların kullanımı da saldırgan açısından işleri yeterince zorlaştırmamaktadır. 64 bitlik mimarilerde ise ASLR işlemi için ayrılabilecek fazla fazla bit olduğundan çok daha geniş bir rastgelelik sağlanmakta ve güvenlik derecesi artmaktadır. Android tabanlı sistemlerde de Linux çekirdeği kullanılmaktadır ve ASLR özelliği Android 4.0.3 ve sonrasında tam olarak aktifleştirilmiştir. Sadece bu sebeple bile olsa, 64 bitlik bir akıllı telefonun 32 bitlik versiyonlarına göre önemli bir güvenlik avantajı sağladığını söylememiz yanlış olmayacaktır.

ASLR özelliğini aşağıdaki komutla geçici olarak devre dışı bıraktığınızda, bir önceki test uygulamasının her çalıştırıldığında aynı adres değerlerini verdiği görülecektir:

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Önceki haline geri döndürmek için aynı dosyaya 0 yerine 2 yazmanız yeterli olacaktır.

mmap

Bellek tahsisatı için kullanılan ikinci sistem çağrısıdır. brk sistem çağrılarıyla data alanını sadece tek yönlü kaydırabildiğimiz ve doğası itibariyle internal fragmentation ürettiği için, farklı bir yönteme de ihtiyaç duyulmuştur.

mmap çağrısı ile belleğin herhangi bir alanıdaki boş yer, çağrıyı yapan process'in adres uzayına haritalanır (mapping).

Bu şekilde yapılan bir bellek tahsisatında, bir önceki brk örneğimizdeki 5 adet 16 Kb'lık bölümden ikincisini free() fonksiyonu ile geri vermek istediğimizde, bu işlemi engelleyecek bir mekanizma bulunmaz ve ilgili bellek bölümü process'in adres uzayından çıkartılıp artık kullanmıyor şeklinde işaretlenerek sisteme iade edilir.

Peki madem mmap bu kadar iyi ve internal fragmentation oluşturmayacak bir model ile çalışıyor, neden sadece bunu kullanmıyoruz?

Çünkü mmap ile yapılan bellek tahsisatları, brk ile yapılanlara oranla inanılmaz yavaştır.

mmap ile belleğin herhangi boş alanındaki bir bölüm process'in adres uzayına haritalandığından, bu işlem tamamlanmadan önce tahsis edilen alanın içeriği sıfırlanır. Eğer bu şekilde sıfırlama yapılmıyor olsaydı, ilgili bellek alanını daha önce kullanan process'e ait verilere konuyla hiç alakası olmayan sonraki tahsisatı yapan process de ulaşabilir olurdu ki bu da sistemlerde güvenlik diye bir konudan bahsetmeyi olanaksız hale getirirdi.

results matching ""

    No results matching ""