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
http://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