x86 etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
x86 etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

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.



İş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.

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.)


Koşullu Jump Komutları

07:20 , ,
Jump komutları sembolik makine dillerinin mutlaka bilinmesi gereken komutlarındandır. Bazı işlemci ailelerinde bu komutlara branch (dallanma) komutları denilmektedir. Jump komutları olmadan yüksek seviyeli dillerdeki if, switch, while for gibi deyimler gerçekleştirilemez. Jump komutları 
  • koşulsuz (unconditional
  • koşullu (conditional) olmak üzere ikiye ayrılmaktadır. 
Koşulsuz jump komutları C’deki goto deyimi gibidir. Koşulsuz olarak EIP yazmacını belli bir değere çeker. Koşulsuz jump komutlarının doğrudan (direct) ve dolaylı (indirect) biçimleri de vardır. Koşulsuz jump komutları pek çok işlemcide olduğu gibi göreli uzaklık değerini operand olarak alır. Doğrudan koşulsuz jump komutlarının son byte’larından sonraki ilk byte göreli uzaklık için sıfır orijini belirtir. Negatif uzaklık "yukarıya", pozitif uzaklık "aşağıya" jump yapılacağı anlamına gelmektedir.

Koşullu Jump Komutları

Koşullu jump komutları bayraklara bakarak jump işlemi yapmaktadır. Intel’deki bütün koşullu jump komutları doğrudandır ve "göreli uzunluk" değerini operand olarak alırlar. Bayrakların uygun karşılaştırma sonuçlarını içermesi SUB ya da CMP komutlarıyla sağlanmaktadır. Bu nedenle önce bayrakların karşılaştırma için set ya da reset edilmesi gerekir. Yani koşullu jump komutları tipik olarak SUB ya da CMP komutlarından sonra uygulanmaktadır. Zaten onların isimlendirmeleri de SUB ya da CMP komutlarından sonra kullanılacağı fikriyle yapılmıştır. Intel’de çok fazla koşullu jump komutu varmış gibi görülse de aslında bazı komutlar diğerleriyle aynı işlemi yaparlar. Yani bu komutlar aynı makine kodunun farklı isimleridirler. Örneğin JA ile JNBE aslında aynı komutlardır. Bunlar tek bir komutun iki farklı isimleridir desek yanlış olmaz. Koşullu jump komutlarının isimlendirilmeleri SUB ya da CMP komutlarının birinci operandına göre yapılmıştır. Örneğin:

cmp eax, ebx
jb REPEAT

Burada jb (jump below) eax’teki değer ebx’teki değerden küçükse anlamına gelmektedir. İsimlendirmede A (Above) ve B (Below) işaretsiz tamsayılar için G (Greater) ve L (Less) de işaretli sayılar için kullanılmaktadır. Eşitlik E (Equal) ya da Z (Zero flag set) ile ifade edilebilmektedir. Koşullu jump komutlarında eğer koşul sağlanmışsa jump işlemi yapılır. Eğer koşul sağlanmamışsa sonraki komuttan devam edilir. Aşağıda tüm jump komutlarının listesi verilmiştir:





Sembolik makine dilinde döngüler ve if deyimleri koşullu ve koşulsuz jump komutlarının kullanılmasıyla gerçekleştirilir. Örneğin aşağıdaki gibi bir C kodunun sembolik makine dili karşılığını yazmak isteyelim:

int g_x = 0;
/* ... */
while (g_x < 10) {
printf(“AhmetUlucay\n”);
++g_x;
}

Böyle bir while döngüsü aşağıdaki gibi oluşturulabilir:


Döngüdeki en önemli nokta şurasıdır:

cmp dword [g_x], 10
jge EXIT

Burada g_x ile 10 değeri karşılaştırılmıştır. Eğer g_x 10'dan büyükse ya da 10'a eşitse while koşulu sağlanmadığı için döngüden çıkılmıştır. Eğer bu koşul sağlanmıyorsa akış aşağıdan devam eder. Orada da mesaj ekrana yazdırılmıştır. Dönünün devamının sağlanması için yukarıya jump yapıldığına dikkat ediniz:

; ...
inc dword [g_x]
jmp REPEAT

Şimdi de aşağıdaki while döngüsünü sembolik makine dilinde yazmaya çalışalım:

unsigned g_x = 10;
while (g_x != 0) {
/* ... */
--g_x;
}

Kodun eşdeğer assembly karşılığı şöyle olabilir:

REPEAT:
cmp dword [g_x], 0
je EXIT
;  ...
dec dword [g_x]
jmp REPEAT
EXIT:
; ...

Tabii bu döngüyü şöyle de oluşturabilirdik:

cmp dword [g_x], 0
je EXIT
REPEAT:
; ...
dec dword [g_x]
jnz REPEAT
EXIT:
; ...

Burada dec komutuyla g_x değeri sıfıra düştüğünde ZF bayrağı set edileceğine dikkat ediniz. do-while döngüleri de benzer biçimde yapılabilir. Örneğin:

unsigned g_x = 10;
do {
/* ... */
--g_x;
} while (g_x != 0);

İşlemi sembolik makine dilinde şöyle yapılabilir:

REPEAT:
; ...
dec dword [g_x]
jnz REPEAT

for döngüleri de benzer biçimde sembolik makine dilinde oluşturulabilir. (Örneklerimizde stack görmediğimiz için hep global değişkenleri kullanıyoruz). Örneğin:

int g_i;
/* ... */
for (g_i = 0; g_i < 10; ++g_i) {
/* ... */
}

Bu işlemin sembolik makine dili karşılığı şöyle oluşturulabilir:

MOV dword [g_i], 0
@2:
cmp dword [g_i], 10
jge @1
; ...
inc dword[g_i]
jmp @2
@1:
; ...

Notlar: jmp işlemlerinde etiket kullanırken isim uydurmak zordur. Sembolik makine dilinde @ karakteri geçerli bir isimlendirme karakteridir. Pek çok C derleyicisi programın sembolik makine dili karşılığını oluştururken bu biçimdeki fabrikasyon etiketleri kullanmaktadır.

if deyimleri de yine koşullu ve koşulsuz jump komutlarıyla gerçekleştirilir. Örneğin yalnızca doğruysa kısmı olan bir if deyimi düşünelim:

if (g_i > 100) {
/* ... */
}
/* ... */

Bu işlem aşağıdaki gibi sembolik sembolik makine dilinde ifade edilebilir:

cmp dword [g_i], 100
jle @1
; doğruysa kısmı
@1:
; if deyimin dışı

Şimdi de else kısmı olan bir if deyimini sembolik makine dili ile ifade etmeye çalışalım:

if (g_i > 10) {
/* ... */
}
else {
/* ... */
}

Bu işlemin eşdeğer sembolik makine dili karşılığı şöyle olabilir:

cmp dword [g_i], 10
jle @1
; if’in doğruysa kısmı
jmp @2
@1:
; if’in yanlışsa kısmı
@2:

Şimdi de else-if örneği üzerinde duralım:

int g_a;
if (g_a > 0) {
/* .... */
}
else if (g_a < 0) {
/* ... */
}
else {
/* ... */
}

Bu işlemin sembolik makine dili karşılığı şöyle oluşturulabilir:

cmp dword [g_a], 0
jle @1
; g_a > büyükse sıfır
jmp @3
@1:
cmp dword [g_a], 0
jge @2
; g_a < 0
jmp @3
@2:
; g_a == 0
@3:

x86 işlemci mimarisinde bayrak (flag) yapısı

13:34 ,


     Dünya genelinde hala en yaygın işlemci mimarisi olan x86 işlemci ailesinin bayrak (flag) yapısını öğrenmemiz assembly dilinde kod yazmak isteyenler için birazda zorunlu bir konu başlığıdır. En basitinden bir for döngüsünü yazabilmek için flag değerine başvurmamız gerekmektedir. Bayrak (flag) yazmaçları CPU'nun çalışmasını (real/protected mod gibi) belirlediği gibi çalışma sırasındaki durumlarını da öğrenmemizi sağlayan yazmaç değerleridir. İşlem sonucu 0 ise şunu yap şuraya dallan gibi işlemleri gerçekleştirebilmemiz için öğrenmemiz gerekmektedir. Şimdi sırası ile yazmaçları ve ne işe yaradıklarına kısaca bakalım;

Direction flag (DF)

Bir tane kontrol bayrağı (Control Flag) bulunmaktadır. Bu bayrak Direction flag (DF) bayrağıdır. String komutları denilen bir grup makine komutu bu bayrağa bakarak işlemin yönüne karar vermektedir (sağdan sola mı yoksa soldan sağa doğru mu). STD ( set direction flag ) ile set (yani 1) ve CLD (clear direction flag) ile reset (yani 0) işlemi yapılır. 

Durum bayrakları (Status Flag)

Aşağıda görüleceği üzere altı tane durum bayrağı (Status Flag) bulunmaktadır. Bunlar;

  • Carry flag (CF)
  • Parity flag (PF) 
  • Auxiliary carry flag (AF) 
  • Zero flag (ZF)
  • Sign flag (SF)
  • Trap flag (TF)
  • Interrupt flag (IF)
  • Overflow flag (OF)



Carry flag (CF)

İşaretsiz tamsayılar üzerinde işlemler yapılırken işlem sonucunda taşma ya da borç oluşursa bu bayrak set edilmektedir.

MOV         AH, 3F
MOV         AL, F4
ADD         AH, AL

Burada 3F ile F4 toplandığında sonuç 8 bite sığmamaktadır. Bu nedenle taşmadan dolayı carry flag set edilmiştir. İşaretsiz sayıların çıkartılması durumunda borç oluşursa da bu bayrak set edilmektedir. CF bayrağı işaretsiz düzeydeki çıkartma işleminde borç oluşuyorsa set edilir, borç oluşmuyorsa reset edilir.

MOV         AH, 3F
MOV         AL, F4
SUB         AH, AL

Yukarıda 8 bitlik çıkarma işlemi yapılmıştır. İşaretsiz düzeyde AH’taki değer AL’deki değerden (işaretsiz olarak) daha küçük olduğu için borç oluşur. Dolayısıyla işlem sonucunda CF bayrağı set edilecektir.

Parity flag (PF) 

Bir işlemin sonucundaki değerde 1 olan bitlerin sayısı çift ise PF bayrağı set (1) edilmektedir, Tek ise reset (0) edilmektedir. PF bayrağı “parity” denilen hata kontrol (error check) mekanizması için düşünülmüştür.

Auxiliary carry flag (AF) 

Bu bayrak 3’üncü bit'ten 4’üncü bit'e taşma oluşmuşsa set edilir, taşma yoksa reset edilir. Yani bu bayrak düşük anlamlı 4 bitteki taşmaya bakmaktadır.

Zero flag (ZF)

Son yapılan işlemin sonucu sıfır ise bu bayrak set edilir, sıfır değilse reset edilir. Örneğin:

MOV         EAX, 1
DEC         EAX

Buradaki DEC makine komutu EAX yazmacının içerisindeki değeri 1 eksiltir. EAX yazmacındaki değer 1 olduğuna göre DEC komutundan sonra işlem sonucu sıfır olduğu için ZF bayrağı set edilmektedir.

Sign flag (SF)

Bu bayrak işlem sonucunda elde edilen değerin en soldaki bitini (işaret bitini) tutar. Başka bir deyişle işlem sonucunda elde edilen değerin işaret bit'i (en soldaki biti) 0 ise bu bayrak reset edilir, 1 ise set edilir. Örneğin bu bayrak sayesinde biz son yapılan çıkarma veya işaretli toplama işlemden elde edilen değerin negatif olup olmadığını anlayabiliriz. Ayrıca CMP işleminden sonra kullanıldığında sayının büyük ya da küçük olup olmadığı kontrol edilebilir.

Trap flag (TF)

Trap flag debuggerlar için en önemli bayraktır. Set edilmişse (yani 1 değerinde ise) işlemci her bir makine komutundan sonra Single Step diye bilinen 1 numaralı kesmeyi çağırır. Set edildiğinde EI (Enable Interrupt); reset edildiğinde DI (Disable Interrupt) değerindedir. 

Interrupt flag (IF)

INTR girişini kontrol eder. Böylece IRQ kesmelerini kapatır ya da açar. 1 ise aktif 0 ise pasif durumdadır. Bu durumda iken gelen kesmelere cevap verilmez. Pasif durumdayken NMI (Non Maskable Interrupt) haricindeki bütün kesmeler iptal edilir. Reset etmek için CLI, set etmek için ise STI komutları kullanılır. Reset işleminden sonra DI (Disabled Interrupts), set edince EI (Enabled Interrupts) değerini alır. 

Overflow flag (OF)

Bu bayrak sıklıkla CF bayrağı ile karıştırılmaktadır. OF işaretli tamsayılarda bir taşma ya da borç oluştuğunda SET edilmektedir. Eğer bir işleme sokulan sayıların en soldaki bitleri (yani işaret biti) aynı ise fakat işlem sonucunda elde edilen değerin en soldaki biti bunlardan farklı ise OF set edilir aynıysa reset edilir. (İşleme sokulan sayıların işaret bitleri farklı ise OF bayrağının her durumda reset edildiğine dikkat ediniz.) Örneğin:

mov eax, 0xFFFFFFF0 ;       1111 1111  1111  1111 1111  1111 1111 0000
mov ebx, 0x000000FF ;       0000 0000 0000 0000 0000 0000 1111 1111
add  eax, ebx                 ; (1) 0000 0000 0000 0000 0000 0000 1110 1111

Yukarıdaki add işleminde OF bayrağı reset edilecektir. Çünkü sayıların işaret bitleri farklıdır. Ancak CF bayrağının set edileceğine dikkat ediniz. Örneğimizdeki 0xF0000000 sayısı işaretli olarak 10’luk sistemde -16’dır. 0x000000FF ise işaretli olarak 10’luk sistemde +255’tir. Toplama işleminin sonucunda 10’luk sistemde 239 sayısı elde edilecektir. Toplama işleminde işaretli düzeyde bir taşma olmadığına dikkat ediniz. 

Fakat örneğin:

mov eax, 0xFFFFFFF0 ;     1111 1111 1111 1111 1111 1111 1111 0000
mov ebx, 0x80000000 ;     1000 0000 0000 0000 0000 0000 0000 0000
add eax, ebx                 ;     (1) 0111 1111 1111 1111 1111 1111 1111 0000

Buradaki add komutunda sayıların işaret bitleri (yani en soldaki bitleri) 1’dir (yani aynıdır). Fakat işlem sonucunda elde edilen değerin işaret biti 0’dır. Bu durumda OF set edilecektir. (Ayrıca CF bayrağının da set edileceğine dikkat ediniz.) Pekiyi OF bayrağının programcı için anlamı nedir? OF bayrağı sayıların işaretli (signed) olduğu fikriyle işleme sokulması durumunda işlem sonucunda işaretli taşma ya da borç oluştuğunda set edilmektedir. OF bayrağı ile CF bayrağının birbirlerine benzediğine dikkat ediniz. CF bayrağı sayıların işaretsiz kabul edildiği durumda taşma ya da borç oluştuğunda set edilirken OF bayrağı sayıların işaretli kabul edildiği durumda taşma ya da borç olduğunda set edilmektedir.



Matematik İşlemci Nedir?


İlk üretilen mikroişlemciler (örneğin 8 bitlik işlemciler ve 16 bitlik işlemciler) yalnızca tamsayı işlemleri yapabiliyordu. Çünkü gerçek sayılarla işlemler için büyük mantık devreleri gerekiyordu. O zamanın teknolojileri buna fazlaca müsait değildi. O yıllarda matematiksel işlemler aslında arka planda tamsayı işlemleriyle, yani bir kod çalıştırılarak (başka bir deyişle fonksiyon çağrılarak) yapılıyordu. Örneğin 80’li yıllarında başında 8086 için zamanlarında aşağıdaki gibi bir kod yazmış olalım:

double a = 3.4, b = 67.8, c;
c = a + b;

İşte C derleyicileri bu tür işlemleri kendi kütüphane fonksiyonlarınu kullanarak adeta aşağıdaki gibi yapıyorlardı:

double a = 3.4, b = 67.8, c;
c = fadd(a, b);

Tabii gerçek sayı işlemlerinin tamsayı aritmetiğiyle emülasyon yoluyla yapılması yavaşlığa yol açıyordu. İşte Intel işlemleri hızlandırmak için 8087 isminde, 8086 işlemcisi ile bağlanarak koordineli çalışacak bir matematik işlemci (math coprocessor) de tasarladı. 8087 matematik işlemcisi gerçek sayı işlemlerini donanım yoluyla, yani elektrik devreleriyle yapıyordu. Böylece bu eski günlerde noktalı sayılarla bir işlem yaptığımızda eğer sistemimizde matematik işlemci yoksa bu işlemler emülasyon yoluyla, eğer sistemimizde matematik işlemci varsa matematik işlemcinin devreleriyle yapılmaktaydı. Intel 286’yı çıkartınca matematik işlemcisini de 80287 ismiyle güncelledi. Sonra 80386 çıktığında matematik işlemci 8037 oldu. İşte nihayet 80486 DX modeliyle birlikte artık Intel matematik işlemciyi de ana işlemciyla aynı entegre devre içerisine yerleştirmeye başladı. Bugünkü kullandığımız Intel işlemcilerinde yine matematik işlemci ayrı bir birim olarak vardır fakat bunlar aynı entegre devrenin içerisindedir. Intel’in matematik işlemci sistemi şöyle çalışmaktadır: Ana işlemci (yani tamsayı işlemcisi) komutu çektiğinde (fetch) onun başındaki byte’a bakar. O byte özel bir değerdeyse (bu tür byte’lara Intel terminolojisinde "prefix" denilmektedir). O komutun gerçek sayı işlemi yapan makine komutu olduğunu anlar. Komutu matematik işlemciye verir. Onu artık yardımcı işlemci çalıştırır. Intel bu konuda zaman içerisinde bazı optimizasyonlar yaptıysa da temel çalışma biçimi hala böyledir. Artık günümüzde tasarlanan yeni işlemciler kendi içlerinde gerçek sayı işlem birimini de içeriyorlar. Yani yeni tasarımlarda artık ayrı bir matematik işlemci diye kavram yoktur. Tasarımcı zaten işlemciyi gerçek sayı işlemlerini de yapacak biçimde tek parça olarak tasarlamaktadır. 

Notlar1: Bugün için hala küçük mikrodenetleyicilerin ve mikroişlemcilerin matematik işlemci modülleri yoktur. (Örneğin Microchip’in PIC16  mikrodenetleyicilerinin matematik işlemci birimleri yoktur. Bu mikrodenetleyicilerde noktalı sayılarla işlemler yine emülasyon yoluyla yapılmakatadır.)

Notlar2: Noktalı sayılarla işlemler özellikle IEEE 754 gibi kayan noktalı (floating point) formatlar zahmetlidir. Bu nedenle küçük mikrodenetleyicilerde programcılar sabit noktalı (fixed point) formatları tercih etmektedir. Çünkü sabit noktalı formatlarla işlemler tamsayılarla çok kolay emüle ederek halledebilmektedir.

Notlar3: Bugün modern kapasiteli mikroişlemcilerin hemen hepsi IEEE 754 kayan noktalı formatı kullanmaktadır. Kayan noktalı formatlar daha dinamik olduğu için daha verimlidir. Bu formatlarda sayı nokta yokmuş gibi ikilik sistemde saklanır. Sonra noktanın yeri sayı içerisinde bazı bitlerde tutulmaktadır.

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.

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.

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.

Fonksiyon Çağrılarında Yazmaç Değerlerinin Korunması

07:09 , ,

Sembolik makine dilinde bir fonksiyonu CALL ettikten sonra akış geri döndüğünde yazmaçların durumu ne olacaktır? CALL edilen fonksiyon yazmaçların değerlerini değiştirmiş olabilir. Bu durumda CALL etmeden önce çağıran fonksiyon yazmaçlarda belli değerleri saklamışsa CALL işleminden sonra artık o değerlerin yazmaçlarda olmayabileceğini göz önüne almalıdır. İşte bu konuda da çağıran fonksiyonla (caller) çağrılan fonksiyon (callee) bir anlaşma yapabilirler. Bu anlaşmaya göre çağrılan fonksiyon bazı yazmaçları koruyabilir, bazılarını korumayabilir. Örneğin C derleyicilerinde pek çok çağırma biçiminde çağrılan fonksiyonun EAX, ECX ve EDX yazmaçlarını bozmasına izin verilmiştir. Ancak diğer yazmaçları çağrılan fonksiyon korumalıdır. Yani onların değerleri akış fonksiyona girdiğinde neyse çıktığında da aynı olmalıdır. Tabii bazı yazmaçların korunmasında bir anlaşma yapılmışsa bu durum çağrılan fonksiyonun o yazmaç değerlerini hiç değiştirmeyeceği anlamına gelmez. Çağrılan fonksiyon eğer bu yazmaçların değerlerini değiştirecekse önce onların değerlerini saklaması gerekir. Bunun için stack kullanılmaktadır. Korunması istenen register değerleri fonksiyona girmeden push eder ardından fonksiyondan çıkmadan önce de pop eder geri yükleyebilir. Ancak onları bu biçimde koruma sorumluluğu çağrılan fonksiyona aittir.

Çağıran ve çağrılan fonksiyonların her ikisini de biz yazacaksak hangi yazmaçların çağrılma sırasında çağrılan fonksiyonlar tarafından korunucağını yine biz kendimiz belirleyebiliriz. Eğer biz yalnızca çağrılan fonksiyonu yazacaksak bu durumda çağıran fonksiyonun hangi yazmaçların korunacağı konusundaki beklentisini karşılamamız gerekebilir. Eğer biz yalnızca çağıran fonksiyonu yazacaksak çağrılan fonksiyonun hangi yazmaçları koruduğunu bilmek yine bize fayda sağlayabilir. Örneğin biz programın büyük kısmını C’de yazmış olalım ve oradan sembolik makine dilinde yazmış olduğumuz fonksiyonu çağırmak isteyelim. Bu durumda bizim C derleyicisinin yazmaç koruması konusundaki beklentilerini karşılamamız gerekir. Çünkü derleyici bazı yazmaçların fonksiyon tarafından bozulmayacağı beklentisiyle fonksiyon çağrısından sonra o yazmaçlardaki değerleri kullanıyor olabilir. 

Sonuç olarak ileride karşımıza çokça çıkacak yazmaç değerlerinin korunması ile ilgili kullanılan yöntemler bu şekilde daha anlaşılır hale gelecektir umarım.

32 Bit Windows Sistemler için "Hello World" Assembly Programı


Tamamen sembolik makine diliyle yazılmış ekrana "Hello World" yazısını çıkartan temel assembly  programı Windows'ta şöyle yazılabilir:


  • Aşağıdaki resimde görüleceği gibi ilk adım olan yazılan kodu 1 numaralı komutta olduğu gibi nasm ile derlenebilir:


Buradan ürün olarak HelloWorld.obj dosyası elde edilecektir. Komuttaki –f win32 seçeneği amaç kodun 32 bit Windows sistemleri için COFF formatında olmasını sağlar.

  • Yukarıdaki resimde görüleceği gibi 2 numaralı komutta olduğu gibi Microsoft’un link.exe programı ile link edilmelidir.

Komuttaki /entry:start seçeneği programın başlangıç noktasını belirlemek için kullanılır. HelloWorld.asm programının başlangıç noktası _start etiketinin bulunduğu yerdedir. Windows uygulamaları “GUI” ve “Console” olmak üzere ikiye ayrılmaktadır. Console uygulamalarında işletim sistemi programı yüklendiğinde bir console ekranını kendisi oluşturmaktadır. Console uygulaması için /subsystem:console seçeneği kullanılmalıdır. Link edilecek dosya HelloWorld.obj dosyasıdır. "Hello World" programında kernel32.dll içerisindeki çeşitli API fonksiyonları (sistem fonksiyonları) kullanılmıştır. Bu nedenle link aşamasına "kernel32.lib" isimli import kütüphanesinin dahil edilmesi gerekmektedir. Bu programda üç API fonksiyonu çağrılmıştır. Önce GetStdHandle API fonksiyonuyla (kodun içerisinde 1 numaralı kısım) console ekranının handle değeri elde edilmiş, sonra WriteFile API fonksiyonu (kodun içerisinde 2 numaralı kısım) ile oraya yazma yapılmıştır. Prosesin sonlanması için ExitProcess API fonksiyonuyla (kodun içerisinde 3 numaralı kısım) gerçekleştirilmiştir. C’nin standart exit fonksiyonu da zaten ExitProcess API fonksiyonu çağırır. Ve link işleminden sonra üretilen exe dosya çalıştırıldığında aşağıdaki gibi başarıyla çalıştığı görülebilir.





32 Bit Linux Sistemler için "Hello World" Assembly Programı


Linux sistemlerinde assembly diliyle "Hello World" programının kodu aşağıdaki gibidir.


1 numaralı kod parçası ekrana yazı yazan,
2 numaralı kod parçası ise programdan çıkış işleminin yapıldığı kısımdır.

Linux’taki merhaba dünya programı Windows’takine göre daha sadedir. Bunun nedeni Linux’ta sistem fonksiyonlarının dinamik kütüphaneden değil kesme (interrupt) yoluyla çağrılıyor olmasıdır. Bu nedenle ekrana yazı yazdırmak için "sys_write", programı sonlandırmak için de "sys_exit" isimli sistem fonksiyonu çağrılmıştır. Sistem fonksiyonları çağrılmadan önce onların parametreleri yazmaçlara yerleştirilmektedir. Her sistem fonksiyonunun bir numarası vardır. Çağrılacak sistem fonksiyonunun numaraları 32 bit sistemde EAX yazmacına yerleştirilir. Sonra sırasıyla EBX, ECX, EDX yazmaçlarına da fonksiyonun parametreleri yerleştirilmektedir. Örneğin void sys_exit(int exitcode) fonksiyonu şöyle çağrılmıştır; Yukarıdaki iki numaralı kod parçasında görüleceği üzere eax yazmacına 1 atanması sys_exit fonksiyonunu temsil etmekte, ebx yazmacına 0 atanması ise exitcode parametresine 0 değerinin gönderildiğini ve int 80h kesmesi ile de fonksiyonun çağrıldığını (call) temsil eder. Şimdilik bu kadar bilinmesi yeterlidir. Linux’taki sistem fonksiyonlarının çağrılma biçimi daha sonra detaylı biçimde anlatılacaktır.


1. kısım derleme işlemidir. Derleme işlemi için nasm programı kullanılmıştır.
2. kısım link işlemidir. Linker ise GNU projesi kapsamında geliştirilmiş olan Linux’un temel linker programı "ld" isimli programdır. ld programı kullanılırken –o seçeneği ile çalıştırılabilen dosyaya isim verilmiştir. Eğer link sırasında çalıştırılabilen dosyaya isim verilmezse default olarak a.out ismi kullanılır. Ayrıca Linux sistemlerinde "ld" programı ile link işlemi yapılırken "entry point" verilmediğine dikkat ediniz. "ld" linker’ı default olarak _start adresini "entry point" olarak almaktadır. Link işlemi bittikten sonra yukarıda görüleceği üzere programımız başarıyla çalışmış ve ekrana Hello World yazısı yazdırılmıştır.