İçsel Fonksiyonlar

GNU C eklentilerine göre bir içsel fonksiyon aşağıdaki gibi tanımlanabilir.

int main() {
    int local = 0;
    void inner() {
        ++local;
    }
}

Not: İçsel fonksiyonlar GNU C tarafından desteklenmesine karşın GNU C++ tarafından desteklenmemektedir.

İçsel inner fonksiyonu tanımlandığı fonksiyon bloğunda çağrılabildiği gibi adresi başka bir fonksiyona geçirilerek dışarıdan da çağrılabilir. Sırasıyla bu iki çağırma biçimini inceleyeceğiz.

Örnek kodda görüldüğü gibi inner fonksiyonu kendi yerel bilinirlik alanında olmayan, dıştaki main fonksiyonunun yerel değişkeninin değerini değiştirmektedir. Bu doğal olmayan kullanım şeklinin nasıl gerçekleştirildiğini incelemek için derleyicinin ürettiği sembolik makina koduna bakacağız. İncelemelerimizde 32 bit mimari hedefli sembolik makina kodu kullanacağız.

Not: 64 bitlik bir sistem kullanıyorsanız derleyicinize m32 anahtarı geçirerek 32 bitlik kod üretmesini sağlayabilirsiniz. 64 bitlik sistemde 32 bitlik kod üretebilmek ve çalıştırabilmek için ekstradan paketlere ihtiyaç duyulacaktır. Ubuntu 14.04.1 LTS için libc6-i386 ve lib32stdc++-4.8-dev paketleri sisteme kurulmuştur.

İçsel Fonksiyonun Tanımlandığı Blok İçinde Çağrılması

#include <stdio.h>
int main() {
    int local = 0;
    void inner() {
        ++local;
    }
    inner();
    printf("%d\n", local);
}

Yukarıdaki kodu inner.c adıyla saklayıp aşağıdaki gibi derleyebilirsiniz.

gcc -oinner inner.c -m32 --save-temps

--save-temps anahtarı ile derleyicinin ürettiği ara kodlar uygun ad ve uzantılarla dosya sistemine kaydedilmektedir. inner.c için derleyici aşağıdaki dosyaları üretecektir.

Dosya Adı İçerik
inner.i Önişlemcinin ürettiği kod
inner.s Derleyicinin ürettiği sembolik makina kodları
inner.o Gerçek makina kodlarını içeren ELF formatlı amaç kod
inner Çalıştırılabilir ELF formatlı kod

Not: Komut satırından kullandığımız gcc uygulaması aslında derleyici değil, derleme sürecinde gerekli olan uygulamaları uygun sıra ve parametrelerle çağıran bir sürücü (driver) programdır. Bir C kodu çalıştırılabilir hale gelene kadar, temel olarak, aşağıdaki aşamalardan geçmektedir.

  • Önişlem aşaması
  • Derleyeci tarafından sembolik makina kodlarının üretilmesi
  • Assembler tarafından gerçek makina kodlarının üretilmesi
  • Linker tarafından çalıştırılan dosyanın üretilmesi

Fakat biz burada detaya girmeden bütün bu süreçten derleme süreci olarak bahsedeceğiz.

Örnek kod derlenip çalıştırıldıktan sonra terminal ekranına 1 değerini basacaktır.

Sembolik makina kodlarını incelemeye başlamadan önce, binutils paketinden çıkan nm ve readelf araçları ile amaç dosyadaki sembollere bakalım.

$ nm inner.o

00000000 t inner.1826
0000000e T main
         U printf
$ readelf -s inner.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS inner.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000    14 FUNC    LOCAL  DEFAULT    1 inner.1826
     6: 00000000     0 SECTION LOCAL  DEFAULT    5
     7: 00000000     0 SECTION LOCAL  DEFAULT    7
     8: 00000000     0 SECTION LOCAL  DEFAULT    8
     9: 00000000     0 SECTION LOCAL  DEFAULT    6
    10: 0000000e    51 FUNC    GLOBAL DEFAULT    1 main
    11: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

Yazdığımız inner fonksiyonunun başlangıç adresinin inner.1826 sembolüyle temsil edildiğini görmekteyiz. Derleyici, global isim alanındaki aynı isimli bir fonksiyondan ayırmak için, fonksiyon adının sonuna ürettiği bir sayı eklemiş ve bu sembolü dışsal bağlanıma(external linkage) kapatmış. Bu aşamadan sonra sembolik makina kodlarını inceleyebiliriz. .cfi ile başlayan assembler direktiflerini göz ardı ettiğimizde main fonksiyonu için derleyicinin aşağıdaki gibi bir kod ürettiğini görmekteyiz. Üretilen sembolik makina kodunun AT&T sözdiziminde olduğuna dikkat ediniz.

Not: Derleyiciye -masm=intel anahtarını geçirerek Intel sözdizimine uygun sembolik makina kodu üretmesini sağlayabilirsiniz.

main:
 1          pushl   %ebp
 2          movl    %esp, %ebp
 3          andl    $-16, %esp
 4          subl    $32, %esp 
 5          movl    $0, %eax
 6          movl    %eax, 28(%esp)
 7          leal    28(%esp), %eax
 8          movl    %eax, %ecx
 9          call    inner.1826
 10         movl    28(%esp), %eax
 11         movl    %eax, 4(%esp)
 12         movl    $.LC0, (%esp)
 13         call    printf 
 14         leave
 15         ret

Baştaki 4 ve sondaki 2 makina kodu derleyici tarafından yazılan başlangıç(prologue) ve bitiş(epilogue) kodlarıdır. Başlangıç kodları genel olarak yığının ve yazmaçların hazırlanmasından, bitiş kodları ise yazmaçların eski durumlarına yüklenmesinden sorumludur.

32 bit sistemlerde yığının tepe noktası esp yazmacında tutulmakta ve yığın genel olarak büyük adresten küçük adrese doğru genişlemektedir. Başlangıç kodlarına baktığımızda 4 numaralı komut ile main fonksiyonu için yığında 32 byte'lık bir alan ayrıldığını görmekteyiz.

Bu aşamada başlangıç ve bitiş kodları arasındaki kodlar asıl ilgilendiğimiz kısmı oluşturmaktadır. İlk önce main sonrasında ise inner fonksiyonuna ait kodları tek tek inceleyelim. Sembolik makina kodlarını yorumlamak bir miktar aşinalık gerektirmektedir, burada mümkün olduğunca detaya girmeden komutların yaptıklarıyla ilgileneceğiz.

Sembolik makina kodu İşlevi
movl $0, %eax eax yazmacına 0 değeri yerleştirilmiş
movl %eax, 28(%esp) eax yazmacındaki 0 değeri, yığının başlangıcından itibaren 28 byte uzaklıktaki güvenli bir bölgeye yerleştirilmiş. Bu alan C kodundaki yerel lokal değişkenine karşılık gelmektedir. Otomatik ömürlü yerel değişkenlerin sabit(hardcoded) adreslere sahip olmayıp, yazmaç göreli(register relative) adreslere sahip olduğunu hatırlayınız
leal 28(%esp), %eax yığında yerel değişken için ayrılmış alanın adresi eax yazmacına atanmış
movl %eax, %ecx eax yazmacının değeri yani yerel değişken adresi ecx yazmacına kopyalanmış
call inner.1826 inner fonksiyonu çağrılmış

Buraya kadar olan sembolik makina komutlarının C dilindeki karşılığının aşağıdaki gibi olduğunu söyleyebiliriz.

int local = 0;
inner();

Bundan sonraki bitiş kodlarına kadar olan komutlar yerel değişkenin değerinin printf ile bastırılmasına ilişkindir.

Son durumda ecx yazmacında yerel değişkenin adresi bulunmakta ve inner fonksiyonu çağrılmakta. inner fonksiyonuna ait sembolik makina kodları ise aşağıdaki gibidir.

 inner.1826:
 1         pushl   %ebp
 2         movl    %esp, %ebp
 3         movl    %ecx, %eax
 4         movl    (%eax), %edx
 5         addl    $1, %edx
 6         movl    %edx, (%eax)
 7         popl    %ebp
 8         ret

Başlangıç ve bitiş kodları arasındaki kodları adım adım inceleyelim.

Sembolik makina kodu İşlevi
movl %ecx, %eax ecx yazmacındaki değer eax yazmacına kopyalanmış. eax yazmacı artık main fonksiyonunun yerel değişkeninin adresini tutmaktadır
movl (%eax), %edx eax yazmacının gösterdiği bellek adresindeki değer, yani main fonksiyonunun yerel değişkeninin değeri, edx yazmacına yazılmış
addl $1, %edx edx yazmacının değeri 1 arttırılmış
movl %edx, (%eax) edx yazmacındaki değer eax yazmacının gösterdiği bellek adresine yani main fonksiyonunun yerel değişkenine yazılmış

Özetleyecek olursak, içsel fonksiyonun tanımlandığı blok içinde çağrıldığı durumda, derleyici dıştaki fonksiyona(outer function) ait yerel değişkenin adresini ecx yazmacında saklamakta ve içsel fonksiyonda ecx yazmacını kullanarak kendini çağıran fonksiyonun yerel değişkeninin adresine ulaşmaktadır.

İçsel Fonksiyonun Dışarıdan Çağrılması

İncelememize örnek bir kod üzerinden başlayalım.

#include <stdio.h>

typedef void (*PF) ();

void foo(PF f) {
    //diğer işlemler..
    f();
}

int main() {
    int local = 0;
    void inner() {
        ++local;
    }
    foo(inner);
    printf("%d\n", local);
    return 0;
}

Örnekte içsel inner fonksiyonunun adresi foo isimli başka bir fonksiyona geçirilmekte ve bu şekilde dışsal olarak çağırılmaktadır. Kod derlenip çalıştırıldığında yine bir önceki 1 sonucunu üretecektir.

İçsel fonksiyonun tanımlandığı fonksiyon içinde çağrıldığı durumda, dıştaki fonksiyona ait yerel değişken adresinin ecx yazmacında saklandığını ve içsel fonksiyonun yerel değişken adresine ecx üzerinden ulaştığını hatırlayınız. Buradaki örnekte ise içsel fonksiyon başka bir fonksiyon tarafından çağırılmakta. Bu durumda içsel fonksiyonun adresinin geçirildiği foo fonksiyonunun, içsel fonksiyon çağrısından önce, ecx yazmacındaki değeri bozmayacağının bir garantisi yoktur. foo fonksiyonu kendisine geçirilen adresin içsel bir fonksiyona ait olup olmadığı bilgisine sahip değildir, kaldı ki foo fonksiyonu bir kütüphane fonksiyonu olabilir. Bu durumda ecx yazmacına yerel değişkenin adresin yazmak yeterli olmayacaktır.

Örnek kod için derleyicinin ürettiği sembolik makina koduna bakarak oluşan durumu inceleyelim. Örnek uygulama bir öncekine benzer şekilde aşağıdaki gibi derlenebilir.

gcc -oinner inner.c -m32 --save-temps -fno-stack-protector

Son argümanı derleyicinin yığın taşmalarını(stack overflow) tespit edebilmek için fazladan yazdığı kodları yazmasını engellemek için ekledik. Derleyici içsel fonksiyon için bir öncekiyle aynı kodu yazmasına karşın, main fonksiyonunun kodunun bir hayli değiştiğini görmekteyiz.

main:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $32, %esp
        leal    16(%esp), %eax
        addl    $4, %eax
        leal    16(%esp), %edx
        movb    $-71, (%eax)
        movl    %edx, 1(%eax)
        movb    $-23, 5(%eax)
        movl    $inner.1830, %ecx
        leal    10(%eax), %edx
        subl    %edx, %ecx
        movl    %ecx, %edx
        movl    %edx, 6(%eax)
        movl    $0, %eax
        movl    %eax, 16(%esp)
        leal    16(%esp), %eax
        addl    $4, %eax
        movl    %eax, (%esp)
        call    foo         #foo çağrısı
        movl    16(%esp), %eax
        movl    %eax, 4(%esp)
        movl    $.LC0, (%esp)
        call    printf
        movl    $0, %eax
        leave
        ret

main fonksiyonuna bakıldığında -71 ve -23 olmak üzere iki adet negatif değerin eax yazmacı referans alınarak belleğe yazıldığı görülmektedir. Negatif sayıların bellekte ikiye tümlenmiş halleriyle tutulduğunu hatırlayınız.

Not: Bir sayının ikiye tümleyenini bulmak için, ikili sayı sisteminde temsil edilen sayının, 1 olan bitleri 0 ve 0 olan bitleri 1 yapılarak önce bire tümleyeni alınır. Sonrasında elde edilen sonuç 1 ile toplanarak ikiye tümleyenine ulaşılır.

-71 ve -23 sayıları, birer byte ile, bellekte sırasıyla 0xb9 ve 0xe9 şeklinde tutulurlar. main için yığında yer ayrılmasından, foo fonksiyonu çağrısına kadar olan kodlar işletildiğinde yığının son hali aşağıdaki gibi olacaktır.

Dikkat edilecek olursa, beklentinin tersine, foo fonksiyonuna argüman olarak .text alanında bulunan içsel fonksiyonun başlangıç adresi geçirilmek yerine, yığında içeriği 0xb9 ile başlayan bölgenin adresi geçirilmiştir.

Derleyicinin foo için ürettiği kod ise aşağıdaki gibidir.

foo:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    8(%ebp), %eax
        call    *%eax
        leave
        ret

foo fonksiyonu kendisine argüman olarak geçirilen adresi eax yazmacına yazmış ve sonrasında o adrese dolaylı çağrı(indirect call) yapmıştır. Bu durumda foo fonksiyonu direkt olarak içsel inner fonksiyonuna çağrı yapmak yerine yığında güvenli bir bölgeye çağrı yapmaktadır. Bu noktadan sonra işlemci yığındaki kodları işleyecektir.

Not: Yığındaki bir kodun çalıştırılabilmesi için yığının çalıştırılabilir(executable stack) olarak işaretlenmesi gerekmektedir. Bu işlem çoğunlukla bağlayıcı(linker) tarafından, sembolik makina kodundaki direktiflere bakılarak yapılır. Bağlayıcı program ELF dosya formatı içerisindeki GNU_STACK başlık alanına yığının çalıştırılabilir olup olmadığı bilgisini yazar. Yığının çalıştırılabilir olarak işaretlenip işaretlenmediğini sembolik makina komutlarına veya ELF dosyasına bakarak anlayabiliriz. Örneğimiz için aşağıdaki komut çıktılarını inceleyiniz.


$ cat inner.s | grep -i stack

.section .note.GNU-stack,"x",@progbits


$ readelf -lW inner | grep -i stack

GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10


Ayrıca bağlayıcıya yığının çalıştırılabilir olarak işaretlenip işaretlenmeyeceği açık bir şekilde aşağıdaki gibi de geçirilebilir. İçsel fonksiyon içeren örneğimizde, yığın çalıştırılamaz olarak işaretlendiğinde, bellek üzerinde erişim ihlali oluştuğuna dikkat ediniz.


$ gcc -oinner inner.c -m32 --save-temps -fno-stack-protector -z noexecstack

$./inner

Segmentation fault (core dumped)


Not: Yığın üzerinde bir kodun çalıştırılabilmesinin güvenlik açığı oluşturacağına dikkat ediniz.

Yığındaki çalıştırılabilir bu kod trambolin(trampoline) olarak adlandırılır. Trambolin kodu içsel fonksiyona dallanmak(jump) için kullanılmıştır. Şekilde, yerel değişkenin başlangıç adresi 0xAABBCCDD ile, içsel fonksiyonun başlangıcıyla trambolin kodunun sonu arasındaki uzaklık ise 0xXXYYZZTT ile temsil edilmektedir.

Yığında b9 ile başlayan 10 byte uzunluğundaki blok trambolin kodunu oluşturmaktadır. b9, x86 mimarisinde ecx yazmacına kopyalamaya ilişkin gerçek işlem kodu(opcode), e9 ise göreli dallanma(relative jump) işlem kodudur. Trambolin kodu main fonksiyonuna ait yerel değişkenin adresini ecx yazmacına yazmakta ve sonrasında içsel fonksiyona dallanmaktadır. Bu şekilde içsel fonksiyon, güvenli bir şekilde, ecx yazmacındaki adresi kullanarak main fonksiyonunun yerel değişkenine ulaşabilmektedir.

Not: x86 mimarisinde, e9 makina komutu göreli dallanma işleminden sorumludur. e9 makina kodu, operand olarak, hedef adres ile kendinden sonraki makina komutuna ait adresin farkını almaktadır.

Not: Burada neden ecx yazmacının kullanıldığı gibi bir soru aklınıza gelebilir. C dili için fiili standart(de facto standard) çağırma biçimi(calling convention) olan cdecl(C declaration) çağırma biçiminde eax, ecx ve edx yazmaçlarının değerleri çağıran kod tarafından saklanmaktadır(caller-saved). ecx yazmacının değerinin saklanması çağıran tarafın sorumluluğunda olduğundan, bir içsel fonksiyon çağrısından önce çalışan trambolin kodunun, ecx yazmacının değerini değiştirmesinde bir mahsur yoktur.

Trambolin kodu main için ayrılan yığın alanında bulunmaktadır. Bu durumda, içsel bir fonksiyon tanımı içeren dışsal fonksiyon sonlandığında, yığın alanındaki trambolin koduna ait referans geçerliliğini yitirecektir. Bir fonksiyon sonlandığında ona ait yığın alanı geri verilmektedir.

İçsel fonksiyonun adresinin geçirilerek dışarıdan çağrılma durumunda, çağırma işlemi içsel fonksiyonu sarmalayan fonksiyon sonlanmadan yapılmalıdır. Aksi halde, yığının güvenirliliği kalmadığından, belirsiz davranış(undefined behaviour) oluşacaktır.

Daha önce içsel fonksiyonların genel olarak başka kodlara geçirildiğini, bu sayede onların davranışlarını değiştirdiğini veya olan bir olaydan haberdar olmayı sağladığını söylemiştik. Birçok dilde bu amaçlar için kullanılabilmelerine rağmen bir GNU C eklentisi olan içsel fonksiyonlar bir olayı dinlemek üzere asenkron çağrılmaya uygun değillerdir. Buna karşın senkron çağrılmaları durumunda diğer fonksiyonlara güvenle geçirilebilirler. Örnek olarak standart bir C fonksiyonu olan qsort verilebilir. qsort fonksiyonuna karşılaştırma amaçlı kullanması için, global isim alanında görünmeyen, içsel bir fonksiyon son argüman olarak geçirilebilir.

   void qsort(void *base, size_t nmemb, size_t size,
                  int (*compar)(const void *, const void *));

İçsel fonksiyonlar ayrıca iç içe çoklu döngülerde istenilen bir noktada tüm döngülerden çıkmak için kullanılabilir. Aşağıdaki örneği inceleyiniz.

#include <stdio.h>

int main() {
    int i, j, k;
    void inner() {
        for (i = 0; i < 100; ++i) {
            for (j = 0; j < 100; ++j) {
                for (k = 0; k < 100; ++k) {
                    /*tüm döngülerden tek hamlede çıkılıyor*/
                    if (k > 0) return;
                }
            }
        }
    }
    inner();
    printf("i: %d\tj: %d\tk: %d\n", i, j, k);
}

Tüm döngülerden çıkılmak istendiğinde, birden çok break deyimi kullanmaksızın tek bir return deyimi kullanılabilir.

Son olarak içsel fonksiyonların bir GNU C eklentisi olan statement expressions içinde kullanımından bahsedeceğiz. Bu eklenti ile bir bileşik deyim(compound statement), parentezler içine alınarak bir ifade(expression) gibi ele alınabilir. Birleşik deyimlerin küme parentezlerine alınarak oluşturulduğunu hatırlayınız. Bileşik deyimin en sonundaki noktalı virgül ile sonlandırılmış ifade, tüm yapının değeri olarak ele alınır. Genel formu aşağıdaki gibidir.

({deyim veya deyimler; dönüş değeri;})

Bir fonksiyondan dönen değerin mutlağını alan örnek bir kod aşağıdaki gibidir.

#include <stdio.h>

int get_int() {
    return -111;
}

int main() {
    int abs = 0;
    abs = ({    int a; int b;
                a = get_int();
                b = a < 0 ? -a : a;
                b; });
    printf("%d\n", abs);
    return 0;
}

İçsel fonksiyonlar da bu yapı içersinde tanımlanabilir. Aşağıdaki örneği inceleyiniz.

#include <stdio.h>

typedef void (*PF) (int);

void foo(PF f) {
    f(111);
}

int main() {
    int local = 0;
    PF pf = ({ void inner (int x) { local = x; } inner; });
    foo(pf);
    printf("local: %d\n", local);
    return 0;
}

inner isimli içsel fonksiyon, bileşik ifadenin bir parçası olarak tanımlanmakta ve adresi de bu yapının ürettiği sonuç olarak ele alınmaktadır. inner fonksiyonunun adresi önce pf göstericisine atanmış, ardından foo fonksiyonuna geçirilmiş. Aynı işlem tek hamlede aşağıdaki gibi de yapılabilir.

#include <stdio.h>

typedef void (*PF) (int);

void foo(PF f) {
    f(111);
}

int main() {
    int local = 0;
    foo(({ void inner (int x) { local = x; } inner; }));
    printf("local: %d\n", local);
    return 0;
}

Önişlemci kullanılarak içsel fonksiyonlar görünüşte anonim fonksiyonlarmış gibi kullanılabilirler. Aşağıdaki örnekte içsel fonksiyon açık bir şekilde, isim verilerek, tanımlanmak yerine bu iş için bir makro kullanılmaktadır. İçsel fonksiyona ait geri dönüş türü ve parametre listesiyle fonksiyon gövdesi lambda makrosuna argüman olarak geçirilmiş.

Önişlemcinin ürettiği çıktıyı derleyiciye -E anahtarı geçirerek görebilirsiniz.

#include <stdio.h>

#define lambda(return_type, function_body) \
({ \
      return_type _fn_ function_body \
      _fn_; \
})

typedef void (*PF) (int);

void foo(PF f) {
    f(111);
}

int main() {
    int local = 0;
    lambda(void, (int x) { local = x; }) (111);
    printf("local: %d\n", local);
    return 0;
}

results matching ""

    No results matching ""