32 Bit Intel İşlemcilerde Koruma Mekanizması


Çok işlemli (multiprocessing) sistemlerde kullanılan modern ve güçlü mikroişlemcilerin çoğu bir koruma mekanizmasına (protection mechanisms) sahiptir. Intel 80286 ile birlikte segment tabanlı, 80386 ile birlikte de sayfa tabanlı koruma mekanizmasına sahip olmuştur. ARM işlemcilerinin pek çok modelinde, PowerPC, Itanium, SPARC gibi RISC tabanlı modern işlemcilerde de koruma mekanizması vardır.

Koruma mekanizmasının üç yönü vardır


  • Bellek Koruması (Memory Protection): Çok işlemli sistemlerde tüm prosesler aynı fiziksel bellek üzerinde çalışırlar. İşte böyle bir çalışma sırasında bir prosesin (yani çalışan programın) kendi alanı dışına çıkarak başka proseslerin kullandığı bellek bölgelerine erişememesi gerekir. Aksi takdirde bir proses başka bir prosesin bellek alanını bozabilir, değiştirebilir ya da oradaki verileri çalabilir. 



  • Komut Koruması (Instruction Protection): Her prosesin her makine komutunu kullanması sistem güvenliğini tehlikeye atabilmektedir. Çünkü bazı makine komutları eğer rastgele bir biçimde ya da özensiz olarak kullanılırsa sistemin çökmesine yol açabilir. Örneğin Intel işlemcilerindeki CLI (Clear Interrupt Flag) makine komutu işlemcinin kesme bayrağını reset'lemektedir. Bu durumda işlemci donanım kesmelerine yanıt vermez. Bu ise tüm sistemin hemen çökmesine yol açabilecek bir durum oluşturur. CLI komutunun dışında tehlikeli olabilecek başka makine komutları vardır. Bu komutların yetkisiz ve sıradan prosesler tarafından kullanılmaması gerekir. 


  • IO Koruması (IO Protection): Merkezi işlemci (CPU) pek çok yerel işlemciye bağlıdır ve onlara elektriksel olarak komutlar gönderebilmektedir. Yetkisiz ve sıradan proseslerin önemli IO portlarına komutlar göndermesi de sistemi çökertebilir. Bu nedenle sistem güvenliği açısından sıradan bir prosesin önemli olabilecek IO portlarına erişiminin engellenmesi gerekir.


Şüphesiz koruma mekanizması bazı proseslere uygulanıp bazılarına uygulanmayacak biçimde esnek olmalıdır. Örneğin işletim sisteminin kodları koruma mekanizmasının denetiminden muaf olmak zorundadır. Çünkü işletim sistemi bir kaynak yöneticisidir ve kaynakları yönetirken de her türlü işlemi yapabilecek durumda olmalıdır. Benzer biçimde aygıt sürücüleri ve çekirdek modülleri de yaptıkları işin gereği olarak koruma mekanizmasından muaf olmak durumundadır.

Intel işlemcileri koruma mekanizması için 4 dereceli bir yetkilendirme modeline sahiptir. Ancak 4 dereceli yetkilendirmenin pratikte pek kullanışlığı olduğu söylenemez.



Bu nedenle Intel işlemcilerini kullanan Windows - Linux gibi sistemler 4 yetki derecesi yerine yalnızca iki yetki derecesini kullanmaktadır. Benzer biçimde Intel dışındaki diğer işlemci aileleri de 2 dereceli bir yetki sistemine sahiptir. Bu yetki derecelerinin birine "çekirdek modu (kernel mode)" diğerine ise "kullanıcı modu (user mode)" denilmektedir. Pek çok ayrıntı söz konusu olsa da kabaca çekirdek modunda çalışan kodların hiçbir koruma engeline takılmadığını söyleyebiliriz. Ancak kullanıcı modunda çalışan kodlar için işlemciler katı bir koruma denetimi uygulamaktadır. İşletim sistemlerinin kodları, aygıt sürücüler, çekirdek modülleri "çekirdek modunda" çalışan kodlardır. Bunların dışındaki tüm programlar (örneğin Chrome, Word gibi programlar ya da bizim yazdığımız programlar) kullanıcı modunda çalışırlar.

Daha önceden de belirtildiği gibi Intel işlemcileri reset edildiğinde "gerçek mod (real mode)" denilen bir moddan çalışmaya başlar. Gerçek mod işlemcinin 1978 yılında tasarlanmış olan 8086 işlemcisi gibi çalıştığı moddur. (DOS işletim sisteminin ilk kez 8086 işlemcisi için yazılmıştır.) Gerçek modda koruma mekanizması kullanılamamaktadır. Koruma mekanizmasının kullanılabilmesi için işlemcinin korumalı moda (protected mode) geçirilmesi gerekir. Intel işlemcilerinin korumalı moda geçirilmesi CR0 isimli bir kontrol yazmacının en düşük anlamlı bitinin 1 yapılmasıyla sağlanır. Bu yazmaç tamamen koruma mekanizmasıyla ilgili işlemlere yönelik bitlere sahiptir. CR0 yazmacının bitleri şöyledir:



Intel’de CR0 ve diğer kontrol yazmaçları diğer yazmaçlar gibi aritmetiksel ve bitsel işlemlere sokulamazlar. Bu yazmaçlar ancak başka genel amaçlı yazmaçlar ile MOV işlemine sokulabilmektedir. O halde işlemciyi korumalı moda geçirme işlemini aşağıdaki gibi bir kodla yapabiliriz:

mov eax, cr0
or     eax, 1
mov cr0, eax

Intel işlemcileri korumalı moda geçirildiğinde çalışma biçimlerinde önemli farklılıklar oluşmaktadır. Bu nedenle bunları korumalı moda geçirmeden önce bizim bazı hazırlıkları yapmış olmamız gerekir. Ayrıca Intel işlemcilerinde koruma mekanizmasını yalnızca koruma amacıyla kullanılan bir mekanizma olarak düşünmek de doğru değildir. Tasarım gereği (geçmişe doğru uyumun korunması ile de ilgili olarak) bu işlemcilerin bazı özellikleri ancak koruma mekanizması aktive edildiğinde kullanılabilmektedir.

Fonksiyona stack yoluyla parametre aktarımı

05:07
Fonksiyonlara parametre aktarımı yapılırken yaygın olarak kullanılan yöntem yazmaçları kullanmak değil stack'i kullanmaktır. Yazmaç sayısı kısıtlı olması nedeniyle stack'i kullanmak biraz da zorunlu bir hal haline gelmektedir. Yazmaç kullanmak hızlı ama kısıtlı olmasından dolayı stack kaçınılmaz bir yöntemdir.


AddProc fonksiyonu işini yaparken parametrelerin stack'te olduğunu bildiği için o değerleri sırasıyla erişerek işlemini gerçekleştirmiş ve ret komutu ile geri dönülmüştür. Compile işlemi nasm ile link işlemi gcc ile gerçekleştirilmiş ve kod çalıştırıldığında sorunsuz bir şekilde çalıştığı görülecektir. Tabii ki bu şekilde kod yazma ne portable ne de başkası tarafından kullanılabilir bir kod örneğidir. ABI kavramıda zaten bu tür durumlar için ortak bir standart oluşturmak için belirlenmiştir. Stack kullanılırken stack'i iş bittikten sonra kimin düzenleyeceği ayrıca karar verilmesi gereken bir sorundur. Bu örneğimizde stack 'i çağıran taraf add esp,8 diyerek düzenlemiştir. Kendi alıştırmalarınızda stack veya bir önceki yazımızda anlattığımız gibi yazmaç yoluyla parametre aktarımı gerçekleştirebilirsiniz.

Fonksiyona yazmaç (register) yoluyla parametre aktarımı

05:07 ,
Eğer tüm işlemlerini assembly dili ile kendiniz yazacaksanız bu durumda genel amaçlı tüm register'ları fonksiyonlarınıza parametre aktarma için kullanabilirsiniz. Aşağıdaki örnekte eax, ebx, ecx register'ları bu amaçlarla kullanılmıştır.


AddProc fonksiyonu işini yaparken parametrelerin nerede olduğunu bildiği için o yazmaçlardan değerleri alarak işlemini gerçekleştirmiş ve ret komutu ile geri dönülmüştür. Compile işlemi nasm ile link işlemi gcc ile gerçekleştirilmiş ve kod çalıştırıldığında sorunsuz bir şekilde çalıştığı görülecektir. Tabii ki bu şekilde kod yazma ne portable ne de başkası tarafından kullanılabilir bir kod örneğidir. ABI kavramıda zaten bu tür durumlar için ortak bir standart oluşturmak için belirlenmiştir.


Bir Program CPU Tarafından Nasıl Çalıştırılma Durumuna Getirilir?

04:05 ,

Bir program yazılıp derlendikten sonra nasıl çalışma aşamasına getirilmektedir? Eğer çalıştırma ortamı bir işletim sisteminin yüklü olduğu bir ortam değilse bizim programı bir biçimde reset vektöründen itibaren o ortamın belleğine (Ram) yüklememiz gerekir. Tipik olarak mikrodenetleyici (microcontroller) ile çalışmalar bu biçimde yürütülmektedir. Bir mikrodenetleyicinin içerisinde bir CPU'nun yanı sıra bir RAM ve EEPROM bellekte bulunmaktadır. Bu EEPROM belleğe mikrodenetleyicinin program belleği denilmektedir. Bu ortamlarda programcı programını başka ortamlarda yazıp derler ve "programlayıcı" denilen bir devre yoluyla mikrodenetleyicinin içerisine yerleştirir. Mikrodenetleyici dünyasının dışında genellikle bir işletim sisteminin üzerinde çalıyor durumdayızdır. (Gelişmiş mikrodenetleyicilerde işletim sistemi yüklenerek kullanılabilmektedir.)

Biz bir işletim sisteminin üzerinde çalışıyorsak programı yüklemek ve onu CPU'ya işletecek hade vermek tamamen artık işletim sisteminin bir görevidir. İşletim sistemlerinin programı yükleyip çalışır hale getiren kısmına kavramsal olarak yükleyici (loader) denilmektedir. İşletim sistemi üzerinde çalışılan bir ortamda tipik olarak program bir editörle (ya da IDE ile) yazılır. Derleyici ile derlenerek amaç dosya (object file) oluşturulur. Bu amaç dosya da bağlama (link) işlemine sokularak çalıştırılabilen (executable) dosya elde edilir.

Amaç dosyaların (object file) ve çalıştırılabilen dosyaların (executable file) içerisinde ne vardır?

Çalıştırılabilen bir dosyanın içerisinde en azından derlenmiş ve ikilik sisteme dönüştürülmüş kodlar ve bu kodların kullandığı data'lar bulunmak zorundadır. Tabii bunların dışında çalıştırılabilen dosyaların içerisinde dosyanın işletim sistemi tarafından yüklenebilmesi için gereken başka bir takım metadata bilgiler de bulunmak zorundadır. Örneğin yükleyici programı RAM'e yükledikten sonra IP (ya da PC) yazmacının ilk değerini nasıl verecektir. Bilindiği gibi C'de programın akışı main gibi bir fonksiyondan başlar ya da biz dışarıdan link edilirken nerden başlayacağını ayarlayabiliriz. İşte program akışının başlatılacağı adrese "enrty point" denilmektedir. Bu adres çalıştırılabilen dosyanın içerisinde belli bir yerde bulunur. Yükleyici de programı yükledikten sonra IP (ya da PC) yazmacına bu değeri atar. Sonra da program kendi kendine çalışmaya devam eder. Tabii çalıştırılabilen dosyaların içerisinde "entry point" dışında daha gerekli olan pek çok yükleme bilgisi de bulunmaktadır. O halde  çalıştırılabilen bir dosya kabaca şu biçimde çalıştırılma durumuna getirilmektedir:

  1. Yükleyici çalıştırılabilen dosyanın başlık kısımlarına bakarak onun içindeki kod ve data bilgilerinin nerelerde olduğunu anlar. Çalıştırılabilen dosyanın kod ve data bölümlerini bellekte uygun bir yere yükler.
  2. Yükleyici çalıştırılabilen dosyanın başlık kısımlarından programın “entry point” adresini belirler ve CPU'nun IP (ya da PC) yazmacına kodun başlangıç adresini yükler. Sonra da CPU'yu serbest bırakır.
  3. CPU IP (ya da PC) adresinde gösterilen yerden itibaren talimatları işletmeye başlar.



Döndürme (Rotate) Komutları

02:18 ,

Döndürme işlemi için C/C++ dillerinde özel bir operatör bulundurulmamıştır. Döndürme işlemi ötelemeye benzemektedir. Ancak ötelemede kaybedilen bit döndürme işleminde diğer tarafı beslemede kullanılır. Örneğin döndürme işlemlerini özetle aşağıdaki resimde olduğu gibi gösterilebiliriz:


Döndürme işlemi bir sayının belli kısımlarının yer değiştirilmesi için kullanılabilmektedir. Ayrıca sayıyı ötelemek yerine döndürdüğümüzde biz onu yeniden ters döndürerek eski haline de getirebiliriz. Diğer özel bazı durumlarda da döndürme işleminden faydalanılmaktadır. Intel işlemcilerinde 4 tane döndürme (rotate) komutu vardır. Komutların ikisi carry’li döndürme için diğer ikisi de carry’siz döndürme için kullanılır:

Carry’li döndürmede sanki CF bayrağı sayının en yüksek anlamlı ekstra biti gibi davranmaktadır. Dolayısıyla döndürmeye o da dahil edilir. Örneğin sağa bir kez carry’li döndürmeyi yukarıdaki ilk resimde görüleceği üzere RCR ve ROR komutlarında gösterildiği gibi carry flag'de ötelemeye dahil edilmiştir. Rotate komutlarının biçimleri de tamamen öteleme komutları gibidir. Yani bir kez döndürme için ayrı bir makine komutu vardır. Birden fazla kez döndürme sabit bir değerle ya da CL yazmacıyla yapılabilmektedir. Komut uzunlukları da yine öteleme komutlarında olduğu gibidir. Örneğin bazı geçerli döndürme komutları şöyledir:

ror eax, 6
rcl eax, 9
rcr eax, cl

CF bayrağı her zaman (carry’li döndürme ve carry’siz döndürme durumlarında da geçerlidir.) son döndürmede kaybedilen biti tutar (ötelemede olduğu gibi). Ancak döndürme döndürülecek değerin bit uzunluğunun bir eksiğinden fazla olursa bu bayrak tanımsız durumda olur. OF bayrağı yine yalnızca bir kez öteleme söz konusu olduğunda etkilenir. SF, ZF, AF, PF bayrakları ise normal biçimde etkilenmektedir.

32 Bit Intel Ailesinde çağırma biçimi (calling convention) kavramı

06:53 ,
Fonksiyonların çağrılması ve geri dönüş değerlerinin alınması konusundaki belirlemelere çağırma biçimi (calling convention) denilmektedir. Derleyicilerin bir fonksiyonu derlerken stack'e parametreleri hangi sirayla push edeceginin veya parametrelerin kullanılacak register'a hangi sıraya göre atılacağı gibi kuralların belirlendiği modeldir. Dolayisiyla farkli diller arasinda veya farklı ortamlar arasinda kullanilacak olan fonksiyonlarin bildiriminde bunu belirtmek gerekir.


Her programlama dilinde veya sistemde farklılık gösterebilir. Her dillin kendine ait çağırma biçimi olabilir hatta assembly ile istedigimiz biçimde convention oluşturabilir ve kullanabiliriz. Fakat bunların taşınabilir ve farkllı dillerle kullanılabilir olması durumunda standartlara uyma yükümlülüğü vardır. Özetle şu sorulara cevap bulmaya çalışır:

  • Çağrılan fonksiyon hangi yazmaçları bozma hakkına sahiptir, hangi yazmaçları korumak zorundadır?
  • Çağıran fonksiyon ile çağrılan fonksiyon arasında parametre aktarımı nasıl yapılacaktır?
  • Çağrılan fonksiyonun geri dönüş değeri çağrılan fonksiyona nasıl aktarılacaktır?
  • Parametre aktarımı için Stack kullanıldı ise stack'i düzenleme işi kime aittir?


Çağırma biçimi konusu C standartlarını ilgilendiren bir konu değildir. Çünkü C standartları böylesi aşağı seviyeli belirlemeleri derleyicilere bırakmıştır. Dolayısıyla çağırma biçimlerini oluşturmak için gereken anahtar sözcükler derleyicilerde bir eklenti (extension) biçiminde bulunurlar. Çağırma biçimlerine ilişkin anahtar sözcükler genel olarak tür belirten sözcük ile fonksiyon isimlerinin arasına yerleştirilmektedir. Örneğin:

  • Microsoft derleyicilerinde çağırma biçimleri yukarıdaki örnekte olduğu gibi iki alt tire ( __ ) ile başlayan anahtar sözcüklerle temsil edilmektedir. 

void __cdecl foo(int a, int b) {
   
}


  • gcc derleyicilerinde ise fonksiyon özellikleri (function attributes) biçimindeki bir sentaks ile temsil edilir. 

void __attribute__((cdecl)) foo(int a, int b) {
   
}

API ve ABI Kavramları Üzerine

Yazılımların tüm mimarilerde çalışması ve geriye doğru uyumlu (backward compatibility) olması en çok istenen özelliklerdir. Geliştirilen yazılımın belirli bir dağıtım veya mimariye bağımlı olmayıp, taşınabilir olması büyük kolaylık sağlayacaktır. Sistem seviyesinden bakıldığında taşınabilirlikle ilgili 2 farklı özellik bulunması gerekir. Bunlar;
  • Application Programming Interface (API)
  • Application Binary Interface (ABI)
Şimdi sırasıyla bu kavramları inceleyelim;

ABI ( Application Binary Interface )

ABI, yazılım bileşenleri arasında belirli bir mimari için amaç dosyalar (object file) arasında arayüz  (interface) tanımlamaktadır. ABI demek ayrı ayrı derlenmiş modüllerin bir arada çalışabileceklerine dair alt seviyeli ve detaylı teknik kurallar bütünüdür. ABI uygulama bileşenleri arasında makine kodu seviyesinde uyumluluğu sağlar. Bu uyum korunduğu müddetçe aralarında etkileşim bulunan yazılım bileşenlerinin arka planı değişse de yeniden derlenmeye ihtiyaç duymaksızın eskisi gibi kulanmaya devam ederler.

  • Fonksiyonların nasıl çağrılacağı (calling convention), 
  • Parametrelerin nasıl geçirileceği, 
  • Yazmaçların kullanım şekilleri,
  • Sistem çağrılarının gerçekleştirilme biçimi, 
  • Amaç dosyaların bağlanması (linklenmesi), 
  • Amaç (object dosya) formatı gibi konular ABI kavramı içerisinde değerlendirilebilir. 
Yazılım geliştirme sürecinde ABI kavramı çok karşımıza çıkmaz. Kullanılan geliştirme araçları hedef platform için belirlenen ABI kurallarına uygun kod üretirler. Alt seviye işler yapanlar için bu genelleme doğru değildir. Sistem düzeyinde kod geliştirmek isteyenlerin üzerinde çalıştığı ve diğer çalışmasını istediği sistemlerin ABI standartlarını bilmesi gerekir.


API ( Application Program Interface )

API, uygulamaların kaynak kod seviyesinde birbirleriyle iletişim kurabilmelerine imkan sağlayan, önceden kararlaştırılmış arayüzler (interface) olarak tanımlanabilir. Genellikle her bir API, daha karmaşık ve alt seviye detaylar içeren bir sürecin, çeşitli arayüzlerle (fonksiyon çağrıları gibi) soyutlanmasını sağlar. Bu şekildeki bir soyutlama üzerinden kullanılan API'yi hizmet olarak veren yazılım bileşenleri güncellense ve alt tarafta yapılan işlemlerle ilgili yöntemler değiştirilmiş bile olsa, API seviyesinde aynı arayüz sağlandığı müddetçe bu API'yi kullanan uygulamalar için bir değişiklik yapılmasına gerek olmayacaktır.




Kısaca bir yazılımın, kullandığı kütüphanelerin,servislerin sonraki versiyonlarında herhangi bir değişikliğe gitmek zorunda kalmadan problemsiz çalışabilmesini sağlayan bir arayüz tanımlanması API kavramının ürünüdür.