X86 Assembly/Makine kodu
Bu bölümde makine kodunun ne demek olduğunu, makine kodunun Assembly dili ile ilişkisini, sayı sistemlerini ve birkaç komutu görüp makine koduyla ilgili bir "deney" yapacağız.
Makine kodu nedir?
değiştirNormalde bilgisayarların tek anlayabildiği sayı sistemi ikilik sayı sistemidir. Bilgisayar bütün işlemleri ikilik sayı sistemi ile yapar. Yani mov
komutunun veya A
karakterinin bilgisayar için hiçbir anlamı yoktur. Karakterlerin bilgisayarda nasıl temsil edildiğini daha önce görmüştük. ASCII kodlarla temsil ediliyorlar. Yani A sayısı bilgisayarda (41)16 yani (01000001)2 sayısı ile temsil edilir. Yani bir bellek gözeneğinde (01000001)2 sayısı varsa o bellek gözeneğinde A karakteri saklanıyor demektir. Peki ya komutlar? Komutların bellekte nasıl saklandıklarını anlamak için şöyle bir deney yapalım. Debug'da Assembly moduna geçip şöyle bir program yazalım:
1524:0100 mov ah,9 1524:0102 mov dx,10b 1524:0105 int 21 1524:0107 mov ah,4c 1524:0109 int 21 1524:010b db "deneme$"
Bu program bildiğiniz üzere ekrana deneme
yazıp kapanır. Bu programı yazıp çalıştığından emin olmak için bir kere çalıştıralım. Sonra debug modundayken u 100
komutunu verelim. u komutu aktif segmentteki belirtilen ofsetten itibaren bir miktar komutu assembly ve makine kodu olarak ekrana yazar. Rehberlik etmesi açısından şimdiye kadar yaptıklarımızın ekran görüntüsü şu şekilde:
Bu resimdeki ilk sütun bellek adresleri, ikinci sütun ilgili bellek adreslerindeki makine kodları, diğer iki sütun da ilgili bellek gözeneklerindeki makine kodlarının Assembly dilindeki karşılıklarıdır. Eğer sadece u komutunu verseydik aktif segmentteki aktif ofsetten itibaren ilgili kodlar yazılacaktı. Aktif ofsetin değişik olma ihtimalini düşünerek ofseti kendimiz belirttik. İstersek u 100 10b
gibi bir komut vererek bellekteki hangi aralıktaki kodların gösterileceğini de belirleyebilirdik. Bu durumda bizi ilgilendirmeyen kodları görmemiş olurduk. Hatta u 1200:100 10b
gibi bir komut vererek segmenti de kendimiz belirleyebiliriz. Ayrıca ekran görüntüsünde de gördüğünüz gibi db talimatı belleğe olduğu gibi değil, db komutlarına parçalanarak geçirilmiş. Ancak bizim burada asıl odaklanmamız gereken nokta makine kodları ve bu kodların Assembly karşılıkları. Öncelikle bu kodları bir daha görelim:
1524:0100 B409 MOV AH,09 1524:0102 BA0B01 MOV DX,010B 1524:0105 CD21 INT 21 1524:0107 B44C MOV AH,4C 1524:0109 CD21 INT 21 1524:010B 64 DB 64 1524:010C 65 DB 65 1524:010D 6E DB 6E 1524:010E 65 DB 65 1524:010F 6D DB 6D 1524:0110 65 DB 65
Bu sonuçlardan görebileceğimiz üzere mov ah
'ın karşılığı B4'tür, mov dx
'in karşılığı da BA'dır. Yani mov'un belirli bir karşılığı yoktur. Ayrıca ikinci satırdan da anlayacağınız üzere veriler bellek ve yazmaçlarda düşük değerlikli bayttan itibaren yerleştirilir. Yani ilk önce en sağdaki bayt, sonra onun bir solundaki bayt, ... şeklinde gider. Buna Little Endian Byte Ordering denir.
Edit modu
değiştirEdit modu da tıpkı Assembly gibi bellek gözeneklerine veri girmek için kullanılan bir moddur. Ama veriler bellek gözeneklerine Assembly dili kullanılarak değil, makine kodu ile girilir. Farklı bellek gözenekleri arasında ilerlemek için boşluk tuşu kullanılır. Edit modundan debug moduna çıkmak için herhangi bir bellek gözeneğindeyken enter tuşuna basarız. Edit moduna girebilmek için ofseti belirtmek zorundayız. İstersek segmenti de belirtebiliriz. Herhangi bir bellek gözeneğindeki veriyi değiştirmek istemiyorsak ilgili bellek gözeneğine hiçbir şey yazmadan boşluk tuşuna basarız. Örnek ekran görüntüsü:
Burada 100., 101., 102. ve 103. ofsetlere programı sonlandırmaya yarayan makine kodlarını yazdık ve programı çalıştırdık. Elbetteki makine kodlarının ne anlama geldiğini bilmek zorunda değilsiniz. Bu konuyu yalnızca arkaplanda işleyen düzeneği kavramanız için anlatıyorum. İşin özü tıpkı karakterler gibi komutlar da bellekte ikilik sayılar olarak saklanıyor. Bir bellek gözeneğine sığmayacak sayılar yerleştirilmek istendiğinde ise düşük değerlikli kısımdan yerleştirilmeye başlanıyor. Yani 78F4562 gibi bir sayı belleğe 100. ofsetten itibaren yerleştirildiğinde aslında ofsetlerin durumu şöyle oluyor:
100. ofset: 62
101. ofset: 45
102. ofset: 8F
103. ofset: 07
Sayı sistemleri
değiştirDaha önce de dediğim gibi bilgisayar yalnızca ikilik sayı sisteminden anlar. Ancak ikilik sayı sistemi bilgisayarla iletişim kurmak için uygun değildir. Eğer Debug, bellekteki verileri on altılık değil de ikilik düzende yazsaydı ekranın büyük bir kısmı gereksiz yere 1 ve 0'larla dolardı. Üstelik programcının elde ettiği bu ikilik sistemdeki verilerden bir şeyler çıkarması daha zor olurdu. Peki debug, bu verileri neden onluk değil de on altılık düzende gösteriyor. Bunun cevabı da şu: on altılık düzen, ikilik düzenin bir türevidir. İkilik düzen ile on altılık düzen arasında dönüşüm yapmak oldukça kolaydır. Ayrıca on altılık düzen daha düzenli bir sistem sağlar. Şöyle ki; on altılık düzenin rakamları 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E ve F'dir. On altılık düzende bir bayt iki satırla ifade edilir ve on altılık düzendeki iki haneli son sayı aynı zamanda bir baytta depolanabilecek en yüksek değerdir. Yani on altılık düzendeki iki haneli bir sayı bir baytın depolayabileceği her türlü sayıyı ifade edebilir. Onluk düzende ise 255'i bir bayt depolayabilmesine rağmen, 256'yı depolayamaz. İkisi de üç haneli olmasına rağmen birisini bir bayt depolayabilmesine rağmen ötekini depolayamıyor. İşte bu düzensizliği önlemek için on altılık düzen kullanılıyor.
İkilik düzen ile on altılık düzen arasında dönüşüm yapmak için şu basit yöntemleri kullanabilirsiniz.
İkilik düzenden on altılık düzene dönüşüm
değiştir- İkilik sayı sağdan itibaren dörtlük gruplara ayrılır.
- Her bir grup on altılık düzene çevrilir.
- Her bir on altılık rakam birleştirilir.
ÖRNEK:
00100101 → 0010.0101 → 2.5 → 25
On altılık düzenden ikilik düzene dönüşüm
değiştir- On altılık sayının her bir basamağı parçalanır.
- Her bir on altılık düzendeki rakamın ikilik düzendeki karşılığı bulunur.
- Oluşan ikilik düzendeki sayılar birleştirilir.
ÖRNEK:
25 → 2.5 → 0010.0101 → 00100101
D komutu
değiştirD komutu debug modunda çalışır. U komutuna benzer. Yani bellekteki verileri ekrana yazmaya yarar. Ancak u komutundan farklı olarak ekrana Assembly komutları yazılmaz. Yalnızca makine kodları ve bu kodların ASCII karşılıkları yazılır. Tek başına kullanılılırsa aktif segment:ofset'ten itibaren bir miktar veri gösterilir. İstenirse ofset belirtilebilir. Hatta istenirse bir aralık da belirtilebilir. Başka bir segmentteki veriler de şu şekilde gösterilebilir:
-d 1234:100 105
Burada 1234. segmentteki 100. ofset ile aynı segmentteki 105. ofset aralığındaki makine kodları ve bu kodların ASCII karşılıkları ekrana yazılır.
Programları Debug'a aktarma
değiştirİstersek yazdığımız com uzantılı program dosyalaraımızı debug programına aktarabiliriz. Bunun için DOS modundayken
debug programismi.com
komutunu veririz. Bu komuttan sonra debug açılır ve programismi.com dosyasını oluşturan makine kodları herhangi boş bir segmente 100. ofsetten itibaren yüklenir. Aktif segment söz konusu boş segment ve aktif ofset de 100 yapılır.
Dw talimatı
değiştirDw talimatı Assembly modunda çalışır. Belleğe iki baytlık veri girmek için kullanılır. Örnek:
1234:0100 dw 73d2
Burada belleğe 1234:100 adresinden itibaren 73d2 sayısı giriliyor. Bu komuttan sonra 100. ofsette d2, 101. ofsette 73 saklanır. Çünkü Intel ve AMD işlemcileri Little Endian Byte Ordering kuralına sahiptir. Yani ilk önce düşük dereceleri baytlar belleğe yerleştirilir.
Bir deney
değiştirŞimdi bilgisayarınızda exe uzantılı bir program dosyası bulun. Boyutunun oldukça küçük ve tek başına çalışan bir program olmasına özen gösterin. Eğer bilgisayarınızda bu tipte bir program bulmakta zorlanacağınızı düşünüyorsanız buradan 9 Kb'lık, tek başına çalışan ücretsiz bir program olan Auto Shutdown'ı indirebilirsiniz. Program bilgisayarın otomatik olarak kapanmasına yarıyor. Ancak bizim için programın ne işe yaradığı önemli değil. Şimdi sırasıyla şu adımları izleyin:
- İndirdiğiniz programı Not Defteri ile açın. Bunun için programın uzantısını txt olarak değiştirebilirsiniz.
- Karşınıza bir sürü karmaşık yazılar çıkacaktır. Neyse ki küçük boyutlu bir programı açtığımızdan dolayı o kadar da fazla karmaşık yazı ile karşılaşmayacaksınız.
- Bu karmaşık yazılar programı oluşturan makine kodlarının ASCII koda dönüştürülmüş hâlidir.
- Şimdi bu karmaşık yazıdaki herhangi bir karakteri silip tekrar yazın ve dosyayı farklı kaydedin.
- Farklı kaydettiğiniz dosyanın uzantısını exe yapın ve açmaya çalışın.
- Yeni oluşan exe uzantılı dosyayı açamayacaksınız. Çünkü program artık bozuldu. Peki neden? Alt tarafı makine kodunda bir karakteri silip sonra aynı karakteri aynı yere tekrar yazdık.
- Burada aslında Not Defteri'nin azizliğine uğradık. Bildiğiniz üzere 0-FF arasındaki her sayı karakterleri kodlamak için kullanılmaz. Bazıları (0-7F) karakterleri kodlamak için bazıları da (80-FF) komutları kodlamak için kullanılır. İşte Not Defteri, 80-FF aralığında yer alan kısmı gösteremez. Çünkü bunlar karakter kodları değil, komut kodlarıdır. Dosyayı farklı kaydettiğimizde ise son dosyaya Not Defteri'nin gösteremediği o komut kodları dâhil edilmez. İşte bu yüzden program bozulur. Bunu çözmenin yolu Notepad++ gibi bir program indirmektir. Bu program, karakter kodlarını olduğu gibi, komut kodlarını da NULL olarak gösterir. Yani son tahlilde programdan bir kayıp olmaz. Notepad++'yı buradan indirebilirsiniz.
NOT: Bu deneyi elbette ki programların makina kodlarında değişiklik yapmak için anlatmadım. Zaten de yapamazsınız. Çünkü Notepad++ bütün makine kodlarını NULL olarak gösterir. Orada hangi makine kodunun olduğunu bilemezsiniz. Bu örneği sadece exe dosyalarının arkasında yatan düzeneği anlayabilmeniz için anlattım. Yalnızca, bir exe dosyasını açtığınızda çıkan karmaşık yazıların ilgili programı oluşturan makine kodlarının ASCII karşılıkları olduğunu bilmeniz yeterli.