İç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;
}