Sistem Çağrıları Nedir ve Nasıl Çalışır ?

Daha önce günlüğümde yer alan iki kısa makaleyi birleştirip biraz da genişleterek bu hale getirdim. Bu konudaki türkçe kaynak sıkıntısın da faydalı olacağını düşünüyorum. Umarım ki bu konunun acemisi olarak fazla hata yapmamışımdır. Her ne kadar sürç-i lisan ettimse affola. :-)

Önce birkaç terime açıklık getirelim.

Protected mode: x86 (286 ve üstü intel ailesi ve uyumlu (amd,via vb.) ) işlemcilerde bellek adreslerine erişim belirli kısıtlamar çerçevesinde olur. Yani her uygulama her istediği bellek adresini istediği gibi kullanamaz. Her bellek bölgesinin erişim hakları descriptor denen kayıtlarla saklanır. Bu kayıtların arka arkaya dizilimi ile hafızanın tamamen adreslendiği bellek haritası ( memory map ) oluşturulur.

CPL: Code Privilege Level (Kod Yetki Düzeyi). x86 işlemcilerde descriptor tarafından ayarlanan ve o kodun çalıştığı yetki seviyesini (privilage level) gösteren 2 bitlik değer. Yazılan program kodları önce bilgisayarın merkezi işlem biriminin (CPU) anlayabileceği makina dilindeki komut setlerine (instruction set) dönüştürülür ve bu komut setleri yazmaçlara (registers) atanır. Her komut setinin belirli bir yetki seviyesi vardır. İşletim sistemi bu yetki seviyesini kontrol ederek o komut setini çalıştırılıp çalıştırılmayacağına karar verir.


0 en yüksek yetki seviyesidir, ring 0 diye geçer. 3 ise en düşük değerdir, ring 3 olarak geçer. Arada ring 1 ve ring 2 diye tanımlanan başka seviyeler olmasına karşın işletim sistemlerinin çoğu yalnızca ring 0 ve ring 3 seviyelerini kullanırlar.

Kernel Mode

Linux işletim sistemi açılışta kernel mode’da ( ring 0 ) baslar. Sistem açılışı sırasında kernel yüklenir yüklenmez işlemci Protected Mode'a geçer ve bunun ardından çalıştırılan tüm programlar User Mode'da çalışır ( bazı donanım sürücüleri hariç ). Kernel mode'da çalışan uygulamalar bütün hafıza adreslerine ve Giriş-Çıkış (harddisk ve benzeri) aygıtlarına tam yetki ile erişirler. Ayrıca bu mode'da iken tüm sistem fonksiyonlarına erişilebilir, hafıza yeniden adreslenebilir.

User Mode

Linux sistemi açılışta kernel mode da baslar. Sistem açılışı sırasında kernel yüklenir yüklenmez işlemci Protected Mode'a geçer. Bu mode da uygulamalar sistem için kullanılan fonksiyonları güvenlik açısından direkt olarak kullanamaz. Kullanıcı sadece kendi başlattığı uygulamaların adres alanları içerisinde kalmak suretiyle işlerini yürütebilir. Sistemin güvenli bir şekilde çalışabilmesi için kullanıcıya kısıtlı izinler tanınmıştır. Bu mode'da çalışan uygulamalar sistem kaynaklarına erişmek istediklerinde ring 3 seviyesinde çalışmakta olan çekirdek servisleri (sistem çağrıları) aracılığı ile çekirdeğe istek gönderirler. Sistem çekirdeği de eğer uygunsa isteği yerine getirir.



Yukarıda da belirtildiği gibi uygulamalar ve çekirdek arasıdaki iletişim sistem çağrıları ile sağlanır. Çekirdek çağrı isteyenin bu çağrıdan yararlanıp yararlanamayacağına karar verir. Örneğin her isteyenin sabit diske veri yazmaması sağlanır.

Linuxta strace komutu ile, çalıştırılan programların kullandığı sistem çağrılarını görebilirsiniz. Örneğin aşağıdaki basit C programı ekrana bir metin yazdırıyor.

void main(
{
printf("Denemen");
}

Şimdi bu programı derleyip, strace ile kullandığı sistem çağrılarını inceleyelim.
# strace ./

execve("./1", ["./1"], [/* 32 vars */]) = 0
...
...
write(1, "Denemen", 7)= 7
....

 "execve" ile yeni bir process başlatılıyor. "write" ile deneme  ile gösterilen dosyaya 7 bayt uzunluğunda "denemen" yazılıyor.

Yukarıda anlatılanları burada somut olarak görmekteyiz. Uygulamamız çalıştığında write çağrısını kullanıyor. Çekirdek ise burada programın isteğini değerlendiriyor ve eğer uygunsa izin veriyor.


Burada bir noktayı hatırlatmak istiyorum. Biz direk makine koduyla (assambler) çalışıyor dahi olsak. Uygulamamız ve işlemcinin arasında çekirdek bulunduğundan sistem çağrılarını kullanmamız gerekir. Biraz daha açmak gerekirse çekirdeğin yüklenmiş olması işlemcinin artık meşhur protected mode safhasına geçmiş olması demektir. Bu noktadan sonra bizim uygulamamızın çekirdeğin izini olmadan aygıtlara direk erişimi söz konusu değildir.

 
Konunun daha açıklayıcı olması açısından sistemin başlama anına geri dönelim ve neler olduğuna bir bakalım:

Linux çekirdeği sadece standart bir C programıdır aslında. Yalnızca iki önemli farklılık vardır. C dilinde
yazılan programların başlangıç noktası main(int argc,char **argv) yordamıdır. Linux çekirdeği start_kernel(void) kullanır. Sistem baslarken ve çekirdek yüklenecekken daha program çevresi mevcut değildir. Yani ilk C yordamı çağrılmadan önce bir kaç şey daha yapılmalıdır. Bu isi gerçekleştiren donanım kodu arch/i386/asm/ dizini altında yer alır.

Assembler dili yordamı çekirdeği tam olarak 0x100000 (1 Mbyte) bellek adresine yükler, ardından da başlatma süreci esnasında dışlamalı olarak kullanılan kesme hizmet yordamlarını (interrupt servicing routines), genel dosya tanımlayıcı çizelgeleri (global file descriptor tables) ve kesme tanımlayıcı çizelgeleri (interrupt descriptor tables) yükler. Bu noktada işlemci korunumlu moda (protected mode) girer. Çekirdeği başlatmak için gereksiniminiz olan her şey init& dizini altındadır. Burası çekirdeği doğru düzgün başlatmakla görevlendirilmiş, geçirilen tüm açılış (boot) parametrelerini dikkate alan start_kernel() yordamıdır. İlk süreç sistem çağrıları kullanılmadan oluşturulur (daha henüz sistemin kendisi yüklenmemiştir).



Ayrıca Aşağıdaki tablo'da işlerin genel olarak nasıl yürüdüğünü anlamamıza yardımcı olacaktır. Buradaki gösterim basit ve anlaşılabilir olması için yalnızca temel öğeleri içermektedir. Ancak temel yapıyı kavramak için oldukça yeterlidir




Yukarıda printf fonksiyonun sebep olduğu write çağrısını basitçe gördük. Ancak işlem sistem çağrısı gerçekleştiğinde bitmiyor. Aşağıda read çağrısı ve ardından gerçekleşen olaylar gösterilmiştir.







“read” çağrısın işlevi basitçe herhangi bir veriyi kaynağından alıp, onu isteyen programın bu iş için ayırdığı belleğe kopyalamaktır. Bir sistem çağrısının çekirdeğe iletilmesi belirli bir "yazılım kesmesi" tarafından gerçekleştirilir. Bu kesme (aynı zaman da "kapı" da denir) x86 işlemcili Linux sistemler için 0x80 kesmesidir.

0x80 kesmesi hakkında not: Programcının işletim sistemi çekirdeğinden sistem servisi alması için kullanılır. Dosya açma, dosya kapama, aygıtlara erişim, terminal'e çıktı gönderme, yeni süreç yaratma gibi işlemleri yaparken çekirdek bize servis sağlar. Bu yapı, her programcının aygıtlar için kendi erişim kodunu tekrar tekrar yazması gibi bir zorluktan kurtarır bizi (ve tabi zaman kazandırır).Birçok programcı 0x80 kesmesini kullanmaz, hatta çoğu hayatında bir yada birkaç kez sadece ismini duymuş olabilir. Bunun nedeni ise bu tür servislerin üst seviye dillerde (HLL) programcının kolay kullanımı için önceden yazılmış fonksiyonlar tarafından yapılıyor olmasıdır. 0x80 kesmesinin direkt olarak kullanılmak zorunda olduğu tek dil Assembly Programlama dilidir.

İstediğiniz sistem çağrısını "EAX" yazmacına (yazmaç=register) atarsınız. Çağrının kabul ettiği parametreleri de geriye kalan yazmaçlara (EBX, ECX gibi) atayarak "0x80" kesmesini çağırırsınız. Şekilde direk olarak görünmese de aslında atanan __NR_read değil buna karşılık gelen çağrı kodudur. Söz konusu çağrı kodu ve __NR_read in alabileceği diğer parametreler /usr/include/asm/unistd.h dosyasında tanımlanmıştır. Ayrıca read ile ilgili man sayfasına "man 2 read" komutuyla ulaşabilirsiniz.

Buraya kadar yapılan işlemler "user space" de (user mode) da gerçekleşti. Basitçe read adlı sistem çağrısını 0x80 kapısını kullanarak "kernel space" e aktardık. Şimdi bundan sonra kernel space'de (kernel mode da) olanlara bir bakalım.

Çekirdek ilk olarak bütün yazmaçları kaydeder (SAVE_ALL). Bir sonraki adımda EAX yazmacı tarafından yapılan isteğin bellek sınırlarını aşıp aşamdığı kontrol edilir (Check Limit of EAX) [1]. Daha sonra system çağrıları tablosuna bakıp (EAX yazmacındaki isteği kullanarak) uygun fonksiyonu çaliştırır. ( syscall_tab[EAX]() ). Burada çalıştırılacak fonksiyon sys_read fonksiyonudur.

Bir sonraki aşamada dosyaya ilişkin parametreler ve izinler kontrol edilir, eğer herşey uygunsa dosyadaki veri alınır (kopyalanır). Sonuç uygulamanın istediği belleğe kopyalanır ve sonuca dair onay değeri uygulamaya gönderilir.

Bu aşamadan sonra işlem tekrar user mode da devam eder. Uygulama kontrol değerine bakar hatasız ise işlemine devam eder. Artık uygulama istediği verinin hatasız biçimde geldiğini ve hafızadaki yerini bilmektedir.

Bu yazıda sizlere bir uygulamanın çekirdekle olan iletişimini ve sistem çağrılarının nasıl işlediğini elimden geldiğince sade ve basit bir biçimde anlatmaya çalıştım.
Umarım faydalı olmuştur.



Serbülent ÜNSAL
serbulentunsal (et) meds.ktu.edu.tr



NOTLAR

[1]:İşlemcilerdeki hafıza koruma mekanizması ile ilgili daha ayrıntılı bilgi için (http://www.cs.ucsb.edu/~cs170/notes/memory/)

Her ne kadar belge içinde uygulamaya yönelik fazla birşey olmasa da bu belgeye dayanarak sisteminize bir zarar vermeyi başarırsanız, bu belgenin yazarı sorumluluk kabul etmeyeceğini beyan eder. ( Ayrıca bunu başarmanız durumunda sizi tebrik de eder. )

Bu belgedeki bilgiler GNU/Linux sistemler için olmasına karşın genel olarak diğer pek çok sistem içinde doğrudur.

Belgenin ilk kısmındaki hatalarımı düzelten ve ikinci bölümümü yazmam için esin kaynağı olan Sn.Tonguç Yumruk'a teşekkür ederim.

Kaynaklar:

http://www.acikkod.org/yayingoster.php?id=30
h
ttp://www.linuxfocus.org/Turkce/January1998/article19.html
http://www.core.gen.tr/Security/Documents/12.html
http://www.freebsd.org/doc/en_US.ISO8859-1/books/design-44bsd/overview-kernel-service.html
http://plg.uwaterloo.ca/~itbowman/CS746G/a1/ http://www.linux-mag.com/2000-11/gear_01.html http://searchsecurity.techtarget.com/searchSecurity/downloads/29667C05.pdf