80X86 İşlemcilerinde Context Switch Kavramı


Windows mimarisinde MS-DOS 4.0'dan öncesinde single-tasking bir yapı mevcuttu ve bu nedenle işletim sistemi aynı anda tek bir process'i çalıştırabildiğinden dolayı context switching gibi bir kavram mevcut değildi. MS-DOS 4.0'dan sonra multitasking bir yapı gelmiş ve aynı anda birden fazla process'in çalıştırılabilmesi olanağı sağlanmış fakat bir process'i bırakıp diğerine geçildiğinde bırakılan process'e geri dönüldüğünde kaldığı yerden devam etmesini sağlamak için context switching denilen kavram işletim sistemi mimarisine girmeye başlamıştır. Context switching, virtualization dünyasında işletim sisteminin snapshot'ını almaya benzer şekilde o anki tüm register, memory vb değerlerinin kaydedilmesi kavramına denilmektedir. Bu durum thread kavramı geldiğinde benzer nedenlerden dolayı thread'leri de ilgilendiren bir kavram olmuştur.


Bir thread/process’in çalışmasına ara verilip diğer bir thread/process’in kalınan yerden çalışmaya devam ettirilmesi sürecine context switch denilmektedir. Context switch “preemptive” işletim sistemlerinde donanım kesmeleriyle yapılmaktadır. PC mimarisinde tipik olarak “IRQ1 Timer” kesmesi bu amaçla kullanılmaktadır. Kesme oluştuğunda akış kernel moda geçerek işletim sisteminin kesme koduna aktarılır. Context switch burada yapılmaktadır.


Context switching sırasında geçiş oluştuğu noktadaki tüm CPU yazmaçlarının (matematik işlemci de dahil olmak üzere) bir veri yapısına aktarılması ve yeni geçilecek thread/process'in yazmaçlarının CPU yazmaçlarına geri yüklenmesi gerekir. Genellikle thread/process’li işletim sistemleri her thread ve process için PCB (Process Control Block) veya TCB (Thread Control Block) biçiminde organize edilne bir veri yapısı oluşturmaktadır.


Linux dünyasında bu iki ayrı veri yapısı tek bir çatı altında sunulmaktadır. Bu nedenle Linux sistemlerinde proses kontrol bloğu için task_struct şeklinde tanımlanan veri yapısı thread’ler için de  kullanılmaktadır. Aşağıdaki resimde görüleceği üzere veri yapısının içi ve kaydedilen değerler görülebilmektedir. 


Her biri process'e ait thread'ler için oluşturulan TCB'ler aşağıdaki resimde görülebileceği üzere bağlı liste biçiminde organize edilmektedir.


Preemptive olmayan (“cooperative” de denilmektedir) sistemlerde context switch kesme yoluyla değil açıkça bir fonksiyonun çağrılması yoluyla yani yazılımsal olarak yapılmaktadır.

Notlar:


  • Context switching işlemi eğer verimli bir şekilde kullanılamazsa işleminin maliyetinden dolayı işletimi sistemine fazladan yük getirme ihtimali vardır. Bu nedenle  işletim sisteminin context switching algoritması önem arzetmektedir.
  • cooperative multitasking (non-preemptive multitasking) : Bu tür multitasking sistemlerde işletim sistemi doğrudan context switching işlemini gerçekleştirmez. Her process/thread belirli bir süre çalıştıktan sonra çalışmasını bırakıp diğer process/thread'e geçilmesini sağlar. Windows 95 öncesi Windows 9x versiyonları 16'bitlik eski uygulamalar nedeniyle cooperative multitasking yapısı kullanılıyordu.
  • Linux kernelinde PCB için sched.h başlık dosyası içinde task_struct veri yapısında tanımlanmıştır.
  • preemptive multitasking (preemptive scheduling) : Bir process/thread çalışırken herhangi bir sebeple daha yuksek önceliğe (priority) sahip bir process/thread'in gelmesi durumunda interrupt edilip daha sonra kaldıgı yerden devam edebildiği işletim sistemi mimarileri denilmektedir.
  • Çok threadli preemptive sistemlerde bile bir çeşit “cooperative” thread oluşturma mekanizması bulunabilmektedir. Bu mekanizmaya “fiber” denilmektedir. Fiber işlemleri işletim sisteminin çekirdeği tarafından bilinmez. Tamamen “user” modda gerçekleştirilebilmektedir. Windows işletimi de belli bir versiyonundan sonra Fiber kavramını API fonksiyonlarıyla destekler hale gelmiştir.



Korumalı Mod Nedir?

Linux Boot Sequence

Korumalı mod proseslerin bir arada çalıştığı çok prosesli sistemlerde sistem güvenliğini artırmak için düşünülmüştür. Intel’in koruma mekanizmasında dört öncelik derecesi vardır. Ancak işletim sistemleri genellikle yalnızca iki dereceyi kullanmaktadır: 0 ve 3. Intel sisteminde düşük numara daha yüksek, yüksek numara ise daha düşük öncelik belirtmektedir.

  • 0 önceliğine "çekirdek modu (kernel mode)" önceliği, 
  • 3 önceliğine de "kullanıcı modu (user mode)" önceliği denilmektedir. 

32 bit Windows ve Linux gibi sistemler düz model (flat model) kullanmaktadır. Bu modelde tüm segment yazmaçlarının gösterdiği betimleyicilerin taban adresleri 0 ve limit değerleri de 4 GB’dir. Düz modelde segment tabanlı bir koruma uygulanmamaktadır. Yani bu modelde bir prosesin CS yazmacı dışındaki segment yazmaçlarının değerlerini değiştirmesi için bir gerekçe yoktur. Zaten düz model uygulayan sistemlerde genellikle tüm kullanıcı mod programları için tek bir kod ve data/stack betimleyicisi kullanılmaktadır. Bu betimleyicilerle de terorik olarak belleğin her yerine erişilebilmektedir. Korumalı modda o anda çalışmakta olan kodun öncelik derecesi CS yazmacının düşük anlamlı iki bitiyle belirlenmektedir. Bu bitlere CPL (Current Privilege Level) denir. Intel’de özel makine komutlarını ancak CPL değeri 0 olan kodlar kullanabilirler. Böylece sıradan prosesler CPL = 3 değeriyle çalıştıkları için bu makine komutlarını kullanamamaktadır. Ayrıca CPL değeri sayfalama mekanizması aktifken bellekte sayfalara erişirken de kontrol işlemlerine sokulmaktadır. Şöyle ki: Her sayfanın iki öncelik derecesi vardır. Önceliklerden birine "kullanıcı (user)", diğerine ise  "yönetici (supervisor)" önceliği denir. CPL değeri 1, 2 ve 3 olan kodlar ancak kullanıcı önceliğindeki sayfalara erişebilirler. CPL değeri 0 olan kodlar ise tüm sayfalara erişebilmektedir. İşletim sisteminin çekirdek kodları "yönetici (supervisor)" önceliğindeki sayfalarda tutulur. Böylece bu alanlara yalnızca işletim sisteminin kodları erişebilmektedir. 



Korumalı modda programcı DS, ES, SS, FS ve GS segment yazmaçlarındaki değerleri MOV komutlarıyla değiştirmek isterse bazı kontroller uygulanmaktadır. Özet olarak programcı CPL değerinden daha yüksek önceliğe sahip bir betimleyiciyi gösteren selektörü bu segment yazmaçlarına yükleyememektedir. Zaten yukarıda da ifade ettiğimiz gibi Windows, Linux gibi sistemler "düz bellek modeli (flat model)"  kullanmaktadır. Düz bellek modelinde de DS, ES, SS, FS ve GS segment yazmaçlarını yüklemek istemenin pratikte bir amacı yoktur. Korumalı modda çalışmakta olan kodun önceliğinin yükseltilmesi uzak CALL ve JMP işlemleriyle yapılamamaktadır. Bunu yapmanın tek yolu "kapı (gate)" denilen özel bir mekanizmayı kullanmaktır. Düşük öncelikli kodlar kapı ile belirtilen adresteki kodları yüksek öncelikle çalıştırabilmektedir.

İşletim Sisteminin Sistem Fonksiyonları ve Kapılar

Korumalı modda çalışan Windows, Linux ve MacOS X gibi işletim sistemlerinde sıradan proseslerin kodları CPL = 3 önceliğinde çalışmaktadır. Bu kodlar işletim sisteminin yüksek öncelikle çalışması gereken sistem fonksiyonlarını kapılar yoluyla çağırırlar.



Böylece işletim sisteminin sistem fonksiyonları çalışırken kodun önceliği CPL = 0'a yükseltilmiş olur. Bu sürece "prosesin kullanıcı modundan çekirdek moduna geçmesi (user mode to kernel mode transition)" denilmektedir. Yani bu sistemlerde bizim programlarımız aslında sürekli olarak CPL = 3 ile kullanıcı modunda çalışmamaktadır. Sistem fonksiyonları ya da aygıt sürücülerdeki kodlar çağrıldığında programımızın öncelik seviyesi geçici olarak CPL = 0'a yükseltilmektedir. İşte kapılar Intel işlemcilerindeki bu geçişi sağlayan mekanizmalardır. Linux, BSD ve MacOS X sistemlerinde sistem fonksiyonları geleneksel olarak 80h kesmesi yoluyla çağrılmaktadır. (Yeni sistemler 64 bit Intel işlemcilerindeki SYSENTER ve SYSEXIT makine komutlarını da bu amaçla kullanabiliyorlar.) Bu 80h kesmesi bir tuzak kapısını tetikler. Bu kapı da kodun önceliğini CPL = 0’a çekerek kodun işletim sisteminin belirlediği bir noktaya aktarılmasını sağlar. İşte o noktada çağrılan sistem fonksiyonunun numarasına göre akış ilgili sistem fonksiyonun koduna aktarılmaktadır. Örneğin Linux sistemlerinde sistem fonksiyonu 80h kesmesi ile çağrılmadan önce onun numarası EAX yazmacına yerleştirilir. Böylece akış çekirdek moduna geçtiğinde buradaki kod EAX yazmacının değerine bakarak akışı uygun yere aktarır. Bu süreci aşağıdaki kodla temsil edebiliriz:

SYS_ENTER:     // kapıya girildiğinde akışın aktarıldığı yer. Artık kod için CPL = 0'dır
switch (eax) {
case 1:
sys_exit();
break;
case 2:
sys_fork();
break;
case 3:
sys_read();
break;
...
}

Tabii bu sözde kodu (pseudo code) yalnızca kafamızda bir fikir oluşsun diye verdik. Aslında Linux'ta uygun sistem fonksiyonuna dallanma işlemi EAX yazmacı switch içerisine sokularak değil bir diziye index yapılarak bir "look up" tablosu yoluyla gerçekleştirilmektedir. Yani bu sistemlerde sistem fonksiyonlarının adresleri bir dizide tutulmaktadır. Sistem fonksiyonlarının numarası da (EAX yazmacı içerisindeki değer) bu diziye indeks yapılarak dolaylı CALL işlemi ile çağrılmaktadır. Linux sistemleriyle BSD ve MacOS X arasında sistem fonksiyonlarının çağrılması arasında küçük bir farklılık vardır. Linux’ta sistem fonksiyonlarının parametreleri yazmaçlarla aktarılırken BSD ve MacOS X sistemlerinde (C’deki gibi) stack yoluyla aktarım yapılmaktadır. (Ayrıca Linux sistemlerindeki sistem fonksiyonlarının numaralarının ve parametrik yapılarının BSD ve MacOS X sistemleriyle bire bir aynı olduğunu da düşünmemelisiniz.) Windows sistemlerinde ise çekirdek moduna geçiş genel olarak 2EH kesmesiyle yapılmaktadır. Fakat genel mekanizma Linux, BSD ve Mac OS X sistemlerine oldukça benzemektedir.

32 Bit Intel İşlemcilerinde Komut Kalıpları

06:58
İşlemcilerdeki yazmaç ve bellek kullanım kalıplarına adresleme modları (addressing mode) denilmektedir. Intel’in 32 bit işlemcilerinde komut kalıplarını aşağıda maddeler halinde tek tek ele alacağız. Intel sisteminde komutlar çoğunlukla iki operandlıdır. Ayrıca örneklerimizde pek çok assembly derleyicisinin kabul ettiği gibi komutların hedef operandları sol taraftaki operandla belirtilmektedir. Aşağıda bu syntax'a (Intel Syntax) uygun bir kod parçası görülmektedir.

Intel Syntax
Aşağıda ise bunun tam tersi (AT&T Syntax) hedef operand sağ taraftaki operandlı bir kod parçası görülmektedir.

AT&T Syntax

Sembolik makine dillerinde genel olarak komutlarda belirtilen işlemlerin kaç bit düzeyinde yapılacağı eğer komutta yazmaç varsa yazmacın uzunluğuyla tespit edilir. Ancak komutta yazmaç yoksa işlemlerin kaç bit düzeyinde yapılacağı onların yanına getirilen qword, dword, word, byte gibi anahtar sözcüklerle belirlenmektedir. Tabii bu anahtar sözcükler assembly derleyicisinden derleyicisine farklılık gösterebilmektedir. 32 Bit Intel İşlemcilerinin komut kalıpları özet olarak şöyledir:
  1. Yazmaçlarla + sabitler 
  2. Yazmaçlarla + yazmaçlar 
  3. Yazmaçlarla + bellekteki değerler
  4. Bellekteki değerlerle + sabitler
Bellek operandı genellikle sembolik makine dillerinde köşeli parantezlerle gösterilmektedir. Bellek operandı herhangi bir biçimde oluşturulamaz. Köşeli parantezlerin içerisine nelerin yazılabileceği önceden belirlenmiştir. Şimdi komut kalıplarını daha ayrıntılı olarak tek tek ele alalım:

1) Yazmaçlarla sabitler işleme sokulabilirler. Örneğin:

ADD EAX, 100
ADD AH, 20
MOV EAX, 512

Sembolik makine dillerinde doğrudan yazılan sayılara kavram olarak "Immediate, Constant" ya da "literal" kelimeleri de kullanılmaktadır.

2) Yazmaçlarla yazmaçlar işleme sokulabilirler. Ancak yazmaç - uzunluk uyumunun sağlanmış olması gerekir. Örneğin:

ADD EAX, EBX
SUB AX, CX
XOR AH, BL

Örneğin aşağıdaki komutlar yazmaç uzunlukları aynı olmadığı için geçersizdir:
ADD EAX, BX
XOR BX, AL

3) Yazmaçlarla köşeli parantez içerisindeki sabit değerler işleme sokulabilirler (Burada sabit değerler hafıza alanında bir adres belirtmektedir). Genel olarak bütün işlemciler bellekteki değerlere onların adreslerini alarak erişirler. Sembolik makine dillerinin çoğunda bir bellek adresindeki değerler köşeli parantezlerle belirtilmektedir. Bu durumda o yazmaçtaki değer ile köşeli parantez içerisindeki bellek adresinden başlayan değer işleme sokulmuş olur. İşlemin kaç bit düzeyinde yapılacağı yazmacın uzunluğuna bağlıdır. Örneğin:

ADD EAX, [1FC14A]

Burada EAX yazmacındaki değer ile belleğin 1FC14A adresinden başlayan 32 bitlik değer toplanmıştır. Sonuç EAX yazmacındaki değer bozularak oraya aktarılmaktadır. Örneğin:

MOV BX, [1FC14A]

Burada BX’e 1FC14A adresinden başlayan 16 bit değer aktarılmaktadır. Örneğin:

ADD AH, [1FC14A]

Burada AH içerisindeki değerle 1FC14A adresinden başlayan 8 bitlik değer toplanmış, sonuç yine AH’ya atanmıştır. Örneğin:

ADD [1FC14A], EAX

Burada EAX yazmacı ile bellekte 1FC14A adresinden başlayan 32 bit değer işleme sokulmuştur. Fakat sonuç belleğin yine 1FC14A adresinden itibaren 32 biti etkileyecek biçimde aktarılmaktadır. Kalıptaki köşeli parantezin işlevine dikkat ediniz. Köşeli parantezler olmasaydı komut tamamen asssembly derleyicisi tarafından farklı yorumlanırdı. Örneğin:

ADD EAX, 1FC14A

Bu komutta EAX yazmacındaki değer doğrudan 1FC14A sabiti ile (immediate) ile toplanmıştır. Sonuç EAX’te bulunacaktır. Fakat örneğin:
ADD EAX, [1FC14A]
Burada EAX yazmacındaki değer ile 1FC14A adresinden başlayan 32 bit değer toplanmıştır.

Not : RISC işlemcilerinde yazmaç ile bellek bölgesinin işleme sokulamamaktadır. Yazmaç-bellek işlemleri tipik olarak CISC tarzı eski işlemcilerde karşılaşılan komut kalıplarıdır.

Normal olarak köşeli parantez içerisindeki sabit adresler 32 bit korumalı modda 32 bit uzunluğundadır. Ancak köşeli parantez içerisindeki bellek adresi 16 bitten de oluşabilir. Ancak komutlardaki 16 bit adresler 32 bit korumalı modda 0x67 öneki gerektirmektedir. Bu nedenle böyle komutlarla 32 bit programlamada pek karşılaşmayız.

4) Köşeli parantez içerisindeki adresler ile sabitler işleme sokulabilirler. Bu durumda sembolik makine dili derleyicileri komutta yazmaç olmadığı için işlemin kaç bit üzerinden yapılacağını ek anahtar sözcükler yardımıyla işleme sokarlar. Örneğin:
ADD dword [1FC14A], 100
Burada belleğin 1FC14A adresinden başlayan 32 bitlik değer 100 ile toplanmıştır, sonuç yine 1FC14A adresinden başlayarak oraya aktarılmıştır. Komuttaki dword işlemin 32 bit (double word = 4 byte ( 8 bit ) = 32 bit) olduğunu assembly derleyicisine anlatmak için gerekmektedir. Bellekteki değerlerle sabitler işleme sokulurken köşeli parantez içerisindeki bellek adresi sonraki maddelerde olduğu gibi yazmaçlarla da oluşturulabilmektedir.

Not : RISC işlemcilerinde bellekteki değerlerle sabitler işlemlere sokulamamaktadır. 

5) Bir yazmaçla köşeli parantez içerisindeki bir yazmaç işleme sokulabilir. Bu durumda bellekte o yazmacın içerisindeki değerle belirtilen adresteki bilgi işleme sokulur. Örneğin:

MOV EAX, 1FC10A
MOV EBX, [EAX]

Burada EAX yazmacının içerisinde 1FC10A değeri vardır. O halde bellekteki 1FC10A adresinden başlayan 32 bit değer EBX yazmacına aktarılmıştır. Örneğin:

MOV EAX, 1FC10A
MOV EBX, [EAX]
ADD EAX, 4
MOV EBX, [EAX]
ADD EAX, 4
MOV EBX, [EAX]
...

Bu adresleme modunun neden kullanıldığı yukarıdaki örnek kodla anlaşılabilir. Biz bu sayede örneğin bir yazmaca bir dizinin adresini atayıp sonra o yazmacı köşeli parantez içerisinde kullanarak dizinin tüm elemanlarına erişebiliriz. Benzer biçimde gösterici işlemleri de derleyici tarafından bu komut kalıbıyla yapılmaktadır. Örneğin:

int *pi = (int *) 0x1FC41A;
*pi = 20;
Bu işlem aşağıdaki gibi makine komutlarına dönüştürülebilir (örneğimizdeki pi, pi göstericisinin adresini belirtiyor olsun):

MOV dword [pi], 1FC41A
MOV EAX, [pi]
MOV dword [EAX], 20

Anahtar Notlar: Sembolik makine dillerinde (ve tabii doğal makine dillerinde) değişkenlerin isimleri yoktur. Dolayısıyla biz onlara isimleriyle erişmeyiz. Biz ancak onlara onların adresleriyle erişebiliriz. Yani yüksek seviyeli dillerdeki değişken isimleri aslında programcılar tarafından uydurulmuş isimlerdir. Kaynak program derlendikten sonra artık çalıştırılabilen program bir değişken ismi içermez. Yüksek seviyeli dillerin derleyicileri belli adreslerden başlayan bilgileri bize belli bir isimle sunmaktadır. Tabii yazmaçlar bir adres belirtmezler. Onlar CPU içerisinde yeri belli olan küçük bellek bölgeleridir. Makine komutları bile yazmaçları bilmektedir. İşin aslı yazmaçların da makine komutlarında isimleri yoktur. Yazmaçlar ikilik sisteme dönüştürülmüş makine komutlarında numaralarla temsil edilirler.

Köşeli parantez içerisindeki yazmaçlar 16 bitlik yazmaçlar olabilir. Ancak 8 bitlik yazmaçlar olamaz. Örneğin:

ADD EAX, [BX]

komutu geçerlidir. Ancak:

ADD EAX, [BL]

komutu geçersizdir. Köşeli parantez içerisine 16 bit yazmaç yerleştirmek 32 bit programlamada çok nadir görülebilecek bir durumdur. Çünkü 16 bit yazmaç ile ancak belleğin tepesindeki 64K’ya erişebiliriz.

Not: 32 bit korumalı modda köşeli parantez içerisine 16 bit yazmaç yerleştrmek için komutun başında 0x67 önekinin bulunması gerektiğini anımsayınız. Bu önek komutu 1 byte büyütmektedir.

6) Bir yazmaçla "köşeli parantez içerisindeki bir yazmaç + 8 bit sabit" ya da "köşeli parantez içerisindeki yazmaç + 32 bit sabit" işleme sokulabilir. Bu gösterimi Intel [ base + disp8 ] ve [ base + disp32 ] ile belirtmektedir. Buradaki disp "displacement" sözcüğünden gelmektedir. Örneğin:

ADD EAX, [EBX + 1F] ; yazmaç + 8 bit
ADD EAX, [EBX + 1001FC01] ; yazmaç + 32 bit

Burada köşeli parantezin içindeki ifadeden bir bellek adresi elde edilmektedir. Bu bellek adresi oradaki yazmacın içerisindeki değerle o sabit değerin (displacement) toplamıyla oluşturulmaktadır. Örneğin:

MOV EBX, 1FC010
MOV EAX, [EBX + 1C]

Burada EBX değerinin içerisindeki değerle 1C değeri toplanarak bellek adresi elde edilmiştir. Yani işlemci 1FC010 + 1C = 1FC02C adresinden başlayan 32 bit değeri EAX yazmacına yerleştirir.

Köşeli parantez içerisinde 16 bit yazmaç kullanılırsa sabit değer (displacement) 8 bit ya da 16 bit olabilmektedir. (Fakat bu biçimdeki komutların önüne 0x67 öneki getirildiğini anımsayınız. Bu da makine komutunu bir byte büyüteceğini anımsayınız!) Örneğin:

MOV EAX, [BX + 1FC0]

Burada BX yazmacının içerisindeki değerle 16 bitlik 1FC0 değeri toplanarak bellek adresini oluşturmaktadır.

7) Bir yazmaçla "köşeli parantez içerisindeki iki yazmaç toplamı" işleme sokulabilir. Örneğin:

ADD EAX, [EBX + ECX]

Burada EAX yazmacının içerisindeki değer bellekte EBX ve ECX yazmaçlarının içerisindeki değerlerin toplamıyla belirtilen adresteki 32 bit değer ile toplanmaktadır. Sonuç EAX yazmacına aktarılmaktadır. Örneğin bir dizinin başlangış adresi EBX’te olsun. ECX’te de 0 değerinin bulunduğunu varsayalım:

MOV EAX, [EBX + ECX]
ADD ECX, 4
...
MOV EAX, [EBX + ECX]
ADD ECX, 4
...
MOV EAX, [EBX + ECX]
ADD ECX, 4

Not: Yine 32 bit korumalı modda bellek operandı ve bit düzeyini belirten yazmaçlar 16 bit olabilir. Bu durumda 0x66 ve 0x67 önekleri gerekecektir.

8) Bir yazmaçla köşeli parantez içerisindeki yazmaç + yazmaç + 8 bit sabit ya da yazmaç + yazmaç + 32 bit sabit işleme sokulabilir. Örneğin:

MOV EAX, [EBX + ECX + 1C] ; yazmaç + yazmaç + 8 bit sabit

Burada EAX yazmacına EBX, ECX yazmacının içindeki değerlerin toplamıyla 1C değerinin toplanması sonucunda elde edilen bellek adresindeki 32 bit değer aktarılmaktadır. Örneğin:

MOV EAX, [EBX + ECX + 001A121C] ; yazmaç + yazmaç + 32 bit sabit

9) Yukarıdaki 7’inci ve 8’inci kalıplarda toplama işlemine sokulan yazmaçlardan yalnızca bir tanesi iki ile, 4 ile ya da 8 ile çarpılabilir. Örneğin:

MOV EAX, [EBX + ECX * 4]

ya da örneğin:

MOV EAX, [EBX + ECX * 4 + 1C]

ya da örneğin:

MOV EAX, [EBX + ECX * 4 + 001a121C]

Burada ECX’in içerisindeki değer 4 ile çarpılıp toplama işlemine sokulmuştur. Yukarıdaki kalıplarda köşeli parantezler içerisinde hangi yazmaçların bulunabileceği konusunda da bazı kısıtlar vardır. Genel olarak EIP ve EFLAGS yazmaçlarının dışındaki 32 bit yazmaçların hepsi köşeli parantez içerisinde kullanılabilir. Fakat köşeli parantez içerisinde hem 32 bit yazmaçlar hem de 16 bit yazmaçlar toplama işlemine sokulamazlar. Ayrıca köşeli parantez içerisinde 16 bit yazmaçların kullanımı konusunda bazı ayrıntılar vardır.

Bit Düzeyinde İşlemler Yapan Makine Komutları

07:10 ,


Sembolik makine dillerinde bit düzeyinde işlemlere sıkça ihtiyaç duyulmaktadır. Bu nedenle bit düzeyinde işlem yapan komutları bilmek gerekmektedir. Şimdi temel bit işlemlerini yapan makine komutları sırasıyla görelim.

AND ve OR Komutları


AND ve OR komutları iki tamsayı değerin karşılıklı bitlerini AND ve OR işlemlerine sokmaktadır. Örneğin:

AND eax, ebx
OR eax, [ebx + ecx]
AND eax, 1

Komut sonucunda her zaman OF ve CF bayrakları reset’lenir.
AF bayrağının durumu tanımsızdır (undefined).
İşlemden SF, ZF, PF bayrakları etkilenir.

AND işleminin operand'ı etkilemeyen yalnızca bayrakları etkileyen TEST isimli bir komutta bulunmaktadır. AND ile TEST arasındaki ilişki SUB ile CMP arasındaki ilişkiye benzetilebilir. TEST işlemi AND işlemi yapar fakat bu işlemden yalnızca bayraklar etkilenir. Pekiyi TEST işleminin ne faydası vardır? Bir değeri kendisiyle AND işlemine soktuğumuzda onunla aynı değeri elde ederiz. Fakat bu işlemden bayraklar etkileneceği için artık koşullu jump işlemi yapılabilir. Örneğin EAX’teki değer negatifse jump etmek isteyelim. Henüz bir işlem yapmadığımıza göre bayraklara bakamayız. Bayrakları etkileyecek bir işleme ihtiyacımız vardır. Bu AND işlemi ya da TEST işlemi olabilir:

TEST EAX, EAX
JS EXIT

Bu işlemle EAX’teki değer negatif ise jump yapılmaktadır. Benzer biçimde:

TEST EAX, EAX
JNZ EXIT

Burada da EAX’teki değer sıfır değilse jump yapılmıştır.


XOR Komutu


XOR komutu iki değerin karşılıklı bitlerini exclusive or işlemine sokar. Exclusive or işlemi iki bit aynıyse 0, farklıysa 1 değerini veren bir işlemdir. Exclusive or işlemi geri dönüşümlüdür. Bu nedenle şifreleme gibi işlemlerde tercih edilir. Aynı değeri kendisiyle exclusive or işlemine sokarsak sıfır elde ederiz. Bu nedenle assembly programcıları bir yazmacı sıfırlamak için bu komutu sık kullanmaktadır:

XOR EAX, EAX

Aynı işlem MOV ile yapılırsa komut uzamaktadır:

MOV EAX, 0

Eskiden XOR işlemi SUB işleminden daha hızlıydı. Ancak artık bunlar aynı hızdadır:

SUB EAX, EAX

Komut sonucunda her zaman OF ve CF bayrakları reset’lenir.
AF bayrağının durumu tanımsızdır (undefined).
İşlemden SF, ZF, PF bayrakları etkilenir.

NOT Komutu

Tek operandlı bir komuttur. Girilen sayının değilini alır. Aşağıdaki örnekte olduğu gibi;

param equ                         0101 0011
NOT param -> Operand1:    1010 1100
çalışır.

PE ve ELF Çalıştırılabilen Dosyalarındaki Bölüm (Section) Kavramı

06:59 , ,

PE (Portable Executable) | ELF (Executable and Linkable Format) 



Bugün için en çok kullanılan çalıştırılabilir (executable) dosya formatları PE ve ELF’tir. PE formatını Microsoft 32 bit Windows sistemleri ilk çıktığında tasarlamıştır.  Microsoft daha önce 16 bit Windows 3.X sistemlerinde NE (New Executable) denilen bir format kullanmıştır. Microsoft’un DOS’ta kullandığı çalıştırılabilen dosya formatı da MZ (by Mark Zbikowski = aynı zamanda exe'yi tasarlayan kişi) formatıydı. 


UNIX/Linux dünyasında da pek çok çalıştırılabilir dosya formatı denenmiştir. "a.out" isimli format uzun süre pek çok UNIX türevi sistemde kullanılmıştır. Linux'te başlangıçta bu formatı kullanıyordu. Artık UNIX türevi sistemlerin büyük kısmı ELF formatını birincil çalıştırılabilen format olarak desteklemektedir. Ayrıca bir işletim sistemi birden fazla çalıştırılabilen dosya formatını destekliyor olabilir. Örneğin Linux sistemleri ELF formatının yanı sıra hala klasik "a.out" formatını da desteklemektedir. Windows’un pek çok versiyonu NE ve MZ formatlarını da desteklemiştir. PE ve ELF formatlarının 32 bitlik ve 64 bitlik birbirine çok benzeyen biçimleri de vardır. Böylece bazen bu formatlar PE32, PE64, ELF32, ELF64 isimleriyle de belirtilmektedir. PE ve ELF formatları genel tasarım olarak aslında birbirlerine benzemektedir. Her iki formatta da önemli bilgilerin dosyanın neresinde bulunduğunu gösteren bir başlık (header) kısmı vardır. Her iki format da bölümlerden (sections) oluşmaktadır. Bu formatlara sahip bir program çalıştırılmak istendiğinde işletim sistemi çalıştırılabilen dosyayı açar formattaki bölümleri inceler ve bölümleri belleğe (RAM’e) yükler.



İşletim sisteminin çalıştırılabilen dosyayı okuyarak çalıştırmak üzere belleğe yükleyen kısmına kavramsal olarak yükleyici (loader) denilmektedir. Bölümler aynı özelliklere sahip ardışıl sayfalardan (page) oluşmaktadır. Bölümlerin birer isimleri vardır. ELF ve PE formatında geleneksel olarak bölümler başında "." olacak biçimde isimlendirilmektedir. Tabii aslında böyle bir zorunluluk yoktur. İşletim sistemi bölümler için bellekte yer ayırıp içini çalıştırılabilen dosyadan ilgili alanları okuyarak yüklemektedir.

PE ve ELF Formatlarındaki Çok Karşılaşılan Bölümler



PE ve ELF formatlarında en çok karşılaşılan bölümler şunlardır:

.text Bölümü: 

Bir programın bütün makine kodları (yani kaynak kodları) bu bölümde bulunur. Yani yukarıdaki C programında programdaki main, foo, bar ve tar fonksiyonlarının kodları .text bölümüne yerleştirilmektedir.

.data Bölümü:

Bu bölümde ilk değer verilmiş global değişkenler ve static yerel değişkenler tutulmaktadır. Yani örneğin yukarıdaki C programında g_a ve count değişkenleri derleyici tarafından tipik olarak ".data" bölümünde tutulacaktır. Derleyiciler ilk değer verilmiş global değişkenleri ilk değerleriyle birlikte çalıştırılabilen dosyanın ".data" bölümüne yerleştirirler. İşletim sisteminin yükleyicisi de onları bu bölümden alıp blok olarak fiziksel belleğe yüklemektedir. Bu nedenle ".data" bölümündeki değişkenlerin çalıştırılabilen dosyada yer kaplamaktadır. Ancak bazı çalıştırılabilen dosya formatları bölümler içerisinde hangi ilk değerden ne miktarda olduğunu tutma yeteneğine sahiptir (örneğin Windows’un PE formatı böyledir). Böylece aşağıdaki gibi global bir dizi bu sistemlerdeki çalıştırılabilen dosyalarda çok fazla yer kaplamayabilir:

int g_x[1000000] = {1, 2, 3};

Ancak ELF gibi bazı formatların bu yeteneği yoktur. Dolayısıyla bu formatlarda yukarıdaki dizinin hepsi ".data" bölümünde ilk değerleriyle bulunacak, dolayısıyla bu da çalıştırılabilen dosyanın uzunluğunu büyütecektir.

.rdata | .rodata Bölümleri: 

PE formatındaki ".rdata", ELF formatındaki ".rodata" bölümleri global ve static read-only verileri tutmak için düşünülmüştür. String ifadeleri genellikle bu sistemlerdeki derleyiciler tarafından bu bölümlerde saklanmaktadır. Örneğin yukarıdaki C programında g_b, g_name ve ival değişkenleri derleyici tarafından tipik olarak ".r(o)data" bölümünde tutulacaktır. Windows ve Linux’un yükleyicileri bu bölümlerdeki bilgileri "read-only" sayfalara yüklerler. Dolayısıyla programın çalışma zamanı sırasında buradaki değerler değiştirilmek istenirse exception (page fault) oluşur.

.bss Bölümü: 

Bu bölümde ilk değer verilmemiş global değişkenler (g_c, g_d) ve ilk değer verilmemiş static değişkenler tutulmaktadır. Bunlara ilk değer verilmediği için bunların çalıştırılabilen dosyalarda boşuna yer kaplamasına gerek de yoktur. PE ve ELF formatlarında bu bölümün yalnızca uzunluğu çalıştırılabilen dosya içerisinde tutulur. İşletim sisteminin yükleyicisi bu uzunluğa bakarak ".bss" bölümünü bellekte (RAM) tahsis eder ve orayı sıfırlar. (C ve C++’ta ilk değer verilmemiş global ve static yerel nesnelerini içerisinde 0 değeri bulunmaktadır. || bss alanının sıfırlanması henüz akış main fonksiyonuna girmeden derleyicilerin başlangıç kodları (startup codes) tarafından da yapılabilmektedir.)


Öteleme (Shift) Komutları

07:11 ,


C/C++ dillerinde "<<" ve ">>" operatörleri aslında işlemcinin sola ve sağa öteleme komutlarına karşılık gelmektedir. Öteleme işlemleri  Intel işlemcilerinde SAL, SAR, SHL ve SHR makine komutları ile yapılmaktadır.

  • SAL (Shift Arithmetic Left) ve SAR (Shift Arithmetic Right) komutlarına aritmetik öteleme komutları
  • SHL (Shift Logical Left), SHR (Shift Logical Right) komutlarına da mantıksal öteleme komutları denilmektedir.

SAL ve SHL komutları arasında farklılık yoktur. SAR ile SHR komutları arasında ise küçük bir farklılık vardır. Diğer yüksek seviyeli dillerden de bilindiği gibi sola bir kez ötelemede bütün bitler bir sola kaydırılır ve sağdan sıfır ile besleme yapılır. Sağa bir kez ötelemede ise bütün bitler bir sağa kaydırılır ancak en soldan 0 ile mi 1 ile mi besleme yapılacağı SAR ve SHR komutlarında değişmektedir. SAR komutunda besleme işaret bitiyle, SHR komutunda ise her zaman 0 ile yapılmaktadır. SAL ve SHL komutları arasında bunun dışında hiçbir fark yoktur. Mantıksal bütünlük oluşturmak için sanki iki farklı komut varmış gibi isimlendirme yapılmıştır. (Yani aslında SAL ve SHL iki ayrı makine komutu değil aynı komutun iki farklı ismidir.) 1' den fazla kez ötelemede aynı işlemler birden fazla yapılmaktadır. Öteleme komutlarında ötelenecek operand yazmaç ya da bellek olabilir. Bir kez öteleme için 2 byte’lık bir komut versiyonu (opcode) bulundurulmuştur. Birden fazla öteleme yapılmak isteniyorsa öteleme sayısı ya sabit olarak verilmek zorundadır ya da CL yazmacına yerleştirilmek zorundadır. Öteleme miktarının sabit olarak verilmesi durumunda ise komut uzunluğu 3 byte olur. Eğer komut uzunluğu CL yazmacına yerleştirilirse bu durumda komut uzunluğu yine 2 byte’tır. Örneğin bazı geçerli öteleme komutları şöyle olabilir:

sal  eax, 1      ; komut uzunluğu 2 byte
shl  dword [ebx], 5    ; komut uzunluğu 3 byte 
sar  byte [ebx + ecx], cl   ; komut uzunluğu 2 byte
Aşağıdaki komutlar ise geçersizdir:
sal eax, bl ; geçersiz!
sal ebx, ecx ; geçersiz!

Komutların bayrakları etkilemesi şöyle olmaktadır: Her zaman kaybedilen bit CF bayrağına yerleştirilir. Yani örneğin biz sola bir kez öteleme yaptığımızda en soldaki bit CF bayrağına yerleşecektir. Sağa bir kez öteleme yaptığımızda da en sağdaki bir CF bayrağına yerleşir. Birden fazla öteleme yapıldığında son ötelemede kaybedilen bit CF’de kalır. SHL ve SHR komutlarında öteleme sayısı ötelenmek istenen değerin bit uzunluğunun bir eksiğini aşıyorsa bu durumda CF bayrağı tanımsız durumdadır. OF bayrağı yalnızca 1 kez ötelemede etkili olur. Birden fazla kez ötelemede OF de tanımsız durumdadır. Bir kez ötelemede OF bayrağı bize sayıda işaretli taşma olup olmadığını bildirmektedir. Ayrıca SF, ZF ve PF bayrakları normal biçimde işlemden etkilenirler. Sıfır kere öteleme geçerlidir. Ancak bu durumda işlemden bayraklar etkilenmez.

Sola öteleme bilindiği gibi iki ile çarpma anlamına, sağa öteleme ise iki ile bölme anlamına gelmektedir. İşaretli sayıların sağa ötelenmesi için SAR komutu işaretsiz sayıların sağa ötelenmesi için SHR komutu kullanılmaktadır. İşaretli ya da işaretsiz sola öteleme için aslında yukarıda da belirtildiği gibi tek bir komut vardır. Bu komuta SAL ve SHL isimleri verilmiştir.

Mantıksal sağa ötelemede en soldan beslemenin 0 ile artimetik sağa ötelemede ise 1 ile yapıldığını anımsayınız. Örneğin AL yazmacında aşağıdaki değerin bulunduğunu düşünelim:

AL: 1011 0111

Şimdi AL yazmacındaki değeri SAR AL, 1 ile aritmetik olarak bir kez sağa öteleyelim. AL’deki değer şu hale gelecektir.

AL: 1101 1011

Oysa AL’ye SHR AL, 1 komutuyla mantıksal sağa öteleme uygulasaydık AL’deki değer şu hale gelecekti:

AL: 0101 1011

Bildiğiniz gibi sağa öteleme sayıyı tamsayısal olarak (yani nokta oluşmayacak biçimde) ikiye bölme anlamına gelir. Fakat aritmetik sağa ötelemede eğer ötelenecek değer negatifse sonuç azalacak biçimde (yani eksi sonsuza doğru) tamsayı olarak elde edileceğine dikkat ediniz. Yani örneğin biz -3 değerini bir kez sağa aritmetik ötelemek isteyelim:

1111 1101 -3

Sağa bir kez artimektik ötelendiğinde sayının -2 olduğuna dikkat ediniz:

1111 1110 -2

Örnek çalışmalar;