İçeriğe atla
0 XP 0
14 dkderinleşmeyle 15 dk50 XP7 etkinlik

x86-64 kayıtçılar ve temel komutlar

Bu derste
  • İşlemcinin kısa belleği olan kayıtçıları ve mov, lea, cmp, test ile atlama komutlarını disassembly satırında okumayı öğrenirsin.
  • x86-64 mimarisinde on altı genel amaçlı kayıtçı vardır ve her birinin alışılmış bir rolü olur.
  • Bu derste komut kümesinin günlük işini gören küçük çekirdeğini tanır ve satırları elle izlemeyi öğrenirsin.

Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +50 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.

Assembly okumak, işlemcinin nasıl düşündüğünü tanımaktan geçer. İşlemci hesap yaparken belleği değil, kayıtçıları kullanır. Bu derste bir yordamı satır satır izler ve her komuttan sonra hangi kayıtçının değiştiğini takip edersin. Amaç bütün komut kümesini ezberlemek değil.

Komut kümesinin küçük bir çekirdeği günlük işin büyük kısmını görür ve onu tanımak yeter. Bu çekirdekte mov, lea, cmp, test, atlamalar ve yığın komutları vardır. Bu çekirdeği kavradığında bir hata ayıklayıcı çıktısını ya da geri derleyici altındaki assembly satırını okuyabilir hale gelirsin.

Kayıtçılar: işlemcinin kısa belleği

Kayıtçı, işlemcinin içindeki çok hızlı küçük bir kutudur. Bellek işlemciye göre uzaktadır ve erişimi yavaştır; kayıtçı ise işlemcinin hemen yanı başındadır. Bu yüzden işlemci hesabı önce kayıtçıda yapar, sonucu gerektiğinde belleğe yazar. Bir yordamı okurken verinin çoğu zaman bellek ile kayıtçı arasında gidip geldiğini görürsün; hesap kayıtçıda olur, saklama bellekte.

x86-64 mimarisinde on altı genel amaçlı kayıtçı vardır. Adları şöyledir: rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp ve r8 ile r15 arası sekiz tane. Her biri 64 bit taşır.

Bazı kayıtçıların alışılmış görevleri vardır. rax dönüş değerini taşır. rsp yığının tepesini gösterir. rbp yığın çerçevesini işaretler.

Gerisi argüman ve ara değer taşır. Bu görevler donanım kuralı değil, bir sözleşmedir; ama derleyici çıktısında hep uyulur. Sözleşmeyi bilirsen, bir satırın ne yaptığını komuta bakmadan tahmin edersin. Örneğin bir çağrıdan hemen sonra rax okunuyorsa, orada dönüş değeri işleniyordur.

Bir kayıtçı daha vardır ama genel amaçlı sayılmaz: rip, işlemcinin yürüteceği bir sonraki komutun adresini tutar. Onu mov ile doğrudan değiştiremezsin; atlamalar, call ve ret değiştirir. Disassembly satırında [rip+0x2e5a] gibi bir ifade görürsen, komut kendi konumuna göre bir genel değişkene erişiyordur.

Aynı kayıtçının farklı genişlikte parçaları vardır ve her parçanın ayrı bir adı olur. rax 64 bittir. Alt 32 biti eax, alt 16 biti ax, en alttaki 8 biti al olarak anılır.

Disassembly satırında eax görürsen rax'ın alt yarısına bakıyorsundur; ayrı bir kayıtçı değildir, aynı kutudur. Bu isim değişimini tanırsan, hangi bitin değiştiğini doğru anlarsın. Bir çökme raporunda eax, ax ya da al geçmesi seni yanıltmaz.

Genişlik meselesinin bir de ters yönü vardır: küçük bir değeri büyük bir kayıtçıya taşımak. Bu durumda üst bitlerin ne olacağı seçilmelidir. movzx üst bitleri sıfırla doldurur. movsx ise işaret bitini kopyalar ve negatif sayıyı negatif bırakır.

Fark negatif sayılarda ortaya çıkar. Bir baytlık 0xff değeri işaretli okunursa -1 eder. movsx ile 64 bitlik -1 olur, movzx ile 255 olur. Boyut hataları çoğu zaman tam burada saklanır ve bir karşılaştırmanın neden beklenmedik dala girdiğini açıklar.

Etkinlik 1 / 7

eax kayıtçısına bir değer yazınca rax'ın üst 32 bitine ne olur?

Veri taşıma: mov ve lea

mov  rax, 5        ; rax = 5
add  rax, 3        ; rax = 8
mov  rbx, rax      ; rbx = 8
lea  rcx, [rax+4]  ; rcx = rax + 4, bellek okunmaz
mov  rdx, [rax]    ; rax adresindeki değeri oku
Köşeli parantez bellek demektir; lea yalnız adresi hesaplar.

Bu satırları elle izlemek en sağlam alışkanlıktır. Her komuttan sonra hangi kayıtçının değiştiğini kâğıda yaz ve yeni değeri yanına not et. mov hedefe yeni bir değer koyar, add ise hedefteki değerin üstüne ekler; ikisi ardışık geldiğinde son değeri bulmak için önceki adımı bilmen gerekir.

mov en sık gördüğün komuttur ve veriyi bir yerden bir yere taşır. İlk işlenen hedeftir, ikinci işlenen kaynaktır; yön hep soldan sağa değil, hedeften kaynağa doğru okunur.

Köşeli parantez varsa bellek adresi söz konusudur. mov rdx, [rax] satırı rax'ın gösterdiği adresteki değeri okur ve rdx'e koyar. Parantezsiz mov rdx, rax ise doğrudan rax'ın kendi değerini kopyalar. Bu tek karakterlik fark, satırın belleğe dokunup dokunmadığını belirler.

Parantezin içi bir hesap da taşıyabilir: bir taban kayıtçı, bir indeks kayıtçısı, 1, 2, 4 ya da 8 olabilen bir ölçek ve sabit bir uzaklık. [rbx+rcx*8+0x10] biçimi, 0x10 uzaklığında başlayan ve sekiz baytlık elemanlardan oluşan bir dizide rcx'inci elemana erişmenin tipik görüntüsüdür. Analizde bu ayrımı kaçırmak, veri akışını baştan yanlış kurmana yol açar.

lea komutu kafa karıştırır. Adı yükleme (load effective address) der ama bellekten okuma yapmaz. Yalnız köşeli parantez içindeki adres hesabını yapar ve sonucu kayıtçıya koyar.

Derleyici bunu iki amaçla kullanır: gerçek bir adresi hesaplamak ve bazen de toplama ile çarpmayı tek satırda yapmak. lea rcx, [rax+rax*2] satırı rcx'e rax'ın üç katını yazar, belleğe hiç dokunmadan. Bu yüzden lea gördüğün her yerde bellek erişimi olduğunu varsaymazsın.

Etkinlik 2 / 7

İki komut koştuktan sonra rax kayıtçısında ne durur?

Önce kodu oku, sonra çıktıyı seç.

mov rax, 5
add rax, 3

İlk tahminde toplama ile taşımayı izledin. Şimdi daha ince bir ayrıma bak: köşeli parantezin varlığı. Aynı kayıtçı, parantezle ve parantezsiz, bambaşka iki anlam taşır.

Biri belleğe dokunur, öteki dokunmaz. Bu fark çökme analizinde de işine yarar.

rbx geçersiz bir adres taşıyorsa mov rax, [rbx] satırı programı çökertir. Aynı adresle lea rax, [rbx] çökmez, çünkü belleğe hiç gitmez. Bu tek karakterlik farkı görmek, veri akışını baştan doğru kurmanın anahtarıdır.

Etkinlik 3 / 7

Bu iki satırın farkı nedir?

Önce kodu oku, sonra çıktıyı seç.

mov rax, [rbx]
lea rax, [rbx]

xor ile sıfırlama: derleyicinin kısayolu

Bir değeri kendisiyle XOR işlemek her zaman sıfır üretir, çünkü her bit kendisiyle karşılaştırıldığında eşittir. Derleyici bir kayıtçıyı sıfırlamak istediğinde çoğu zaman mov eax, 0 yazmaz; xor eax, eax yazar.

İki satır aynı sonucu verir ama xor daha az bayt kaplar ve işlemci onu özel olarak hızlı çalıştırır. Bu yüzden derlenmiş kodun her yerinde xor eax, eax satırını görürsün. Onu bir hesap değil, bir sıfırlama kalıbı olarak tanırsın.

Kalıpları tanımak okuma hızını katlar. xor eax, eax sıfırlamadır. test rax, rax bir kayıtçının sıfır olup olmadığını sınar.

sub rsp, 0x20 yığında yer açar. Bu satırları tek tek çözmek yerine, gördüğün an ne anlama geldiğini bilirsin.

Fonksiyonların arasına dizilmiş nop ya da int3 baytları da bir kalıptır. Bunlar derleyicinin hizalama dolgusudur, çalışan kod değildir. Deneyimli analistle acemi analist arasındaki farkın büyük kısmı, bu kalıpları ezbere tanımaktan gelir. Kalıbı bilmeyen her satırı yeniden hesaplar ve yavaşlar.

Etkinlik 4 / 7

Bir kayıtçıyı sıfırlayan klasik satırı bul.

Zafiyetli satıra tıkla.

Hata ayıklayıcıda kayıtçıları okumak

Statik okuma kadar önemli olan bir beceri de kayıtçıları çalışırken izlemektir. Bir hata ayıklayıcı (debugger) programı adım adım yürütür ve her adımda kayıtçı tablosunu gösterir. gdb içinde info registers komutu bütün genel amaçlı kayıtçıları ve o anki değerlerini basar. Tek bir makine komutu ilerlemek için stepi, kısaca si yazarsın.

Bir call satırının içine girmeden üstünden geçmek için nexti, kısaca ni yazarsın. Adım adım yürütürken her adımdan sonra kayıtçı tablosuna bakmak alışkanlık olur. Kâğıt üstünde yaptığın izlemeyi böylece gerçek değerlerle doğrularsın.

Bu doğrulama neden değerli? Çünkü elle izlemede insan hata yapar; bir add satırını atlar ya da bir değeri yanlış okur. Hata ayıklayıcı gerçeği söyler. Statik okumada bir yordamın rax'a ne koyduğunu tahmin edersin, dinamik izlemede o değeri gözünle görürsün.

Örneğin kâğıtta add satırından sonra rax için 8 bulduysan, tabloda 0x8 görmelisin. İki yöntem birbirini denetler. Kritik bir satırda ikisi çelişiyorsa, hatanı statik okumada ararsın.

Etkinlik 5 / 7

gdb içinde tüm genel amaçlı kayıtçıları listele.

ogrenci@lab

Karar komutları: cmp, test ve atlamalar

Karar veren komutlar cmp ve test olur. İkisi de sonucu hiçbir yere yazmaz; yalnız bayrakları kurar. cmp iki değeri çıkarır ve sonuca göre sıfır, işaret, elde ve taşma bayraklarını ayarlar. test ise iki değeri VE işler ve bayrakları ona göre kurar.

Hemen ardından gelen atlama komutu o bayraklara bakar. Yani cmp ve test tek başına bir şey yapmaz; onları izleyen atlamayla birlikte anlam kazanır.

Atlama komutları koşullu ve koşulsuz olarak ikiye ayrılır. jmp koşulsuz atlar. je eşitse, jne eşit değilse atlar.

jl, jle, jg ve jge işaretli sayıları karşılaştırır; jb, jbe, ja ve jae ise işaretsiz sayıları. Aynı cmp satırından sonra jg yerine ja görmek, kaynak kodda değişkenin işaretsiz tanımlandığını ele verir. Bayrak kayıtçısı da hata ayıklayıcı tablosunda görünür.

İçinde sıfır (ZF), elde (CF), işaret (SF) ve taşma (OF) bitleri durur. Bir atlamanın alınıp alınmayacağını merak ediyorsan, ondan önceki cmp ya da test satırına ve o anki bayraklara bakarsın. cmp eax, 0 ardından gelen je satırı, eax sıfırsa atlar demektir. Karşılaştırmayı atlamadan ayrı okursan koşulu ters çevirirsin.

Şimdiye kadar öğrendiklerini tek bir örnekte birleştir. Aşağıdaki blok, bir yerel değişkenin sıfır olup olmadığına göre iki yoldan birine giden tipik bir karar bloğudur. Kaynak kodda bu, tek satırlık bir koşuldur; ikilide ise bir okuma, bir sınama, bir atlama ve iki dal olarak açılır.

Satırları elle izle ve her adımda ne olduğunu kendine söyle. Assembly okumanın büyük kısmı, işte bu tür küçük blokları tek bir koşula geri çevirmektir.

mov  eax, [rbp-0x4]   ; yerel değişkeni eax'a al
test eax, eax         ; eax sıfır mı? yalnız bayrakları kur
je   .sifir           ; sıfırsa .sifir etiketine atla
mov  edi, 1           ; değilse edi = 1
jmp  .son
.sifir:
mov  edi, 0           ; sıfırsa edi = 0
.son:
test ile je birlikte 'değer sıfırsa' kararını kurar.

Bloğu satır satır çöz. İlk satır rbp eksi 4'teki yerel değişkeni eax'a okur. İkinci satır test eax, eax ile eax'ı kendisiyle VE işler; sonucu hiçbir yere yazmaz, yalnız bayrakları kurar. Değer sıfırsa sıfır bayrağı 1 olur.

Üçüncü satır je, sıfır bayrağı 1 ise yani değer sıfırsa .sifir etiketine atlar. Değer sıfır değilse atlama alınmaz ve akış dördüncü satıra düşer; edi 1 olur, sonra jmp ile son'a gidilir. Değer sıfırsa .sifir bloğu çalışır ve edi 0 olur. Karşılaştırmayı atlamadan ayrı okursan bu koşulu ters çevirirsin; o yüzden test ile je satırını hep birlikte okursun.

Etkinlik 6 / 7

Komutu işiyle eşleştir.

Soldan bir tane seç, sağdan eşini seç. Kullanılmış bir seçeneği yeniden seçersen yeni öğeye taşınır.

Komut kümesi yüzlerce komut içerir ama günlük işin büyük kısmını bir avuç komut görür: mov, lea, add, sub, cmp, test, atlamalar, push, pop, call ve ret. Bu çekirdeği tanıdığında çoğu yordamı okursun. Kayıtçıları da ezberlemek yerine rollerini öğrenirsin: çağrıdan hemen önce dolan kayıtçı argümandır, çağrıdan sonra okunan kayıtçı sonuçtur. Bir çökme raporunda eax, ax ya da al görürsen, hepsinin aynı fiziksel kayıtçının farklı genişlikteki parçaları olduğunu bilirsin.

Derinleş:8086'dan kalan adlar ve REX öneki

Kayıtçı adları rastgele seçilmedi. 1978'in 16 bitlik 8086 işlemcisinde dört ana kayıtçı vardı ve her birinin bir görevi anlatan adı vardı: AX toplayıcı (accumulator), BX taban (base), CX sayaç (count), DX veri (data). O gün bir çarpma sonucu AX'te toplanır, bir döngü CX ile sayılırdı. 1985'te 32 bite geçilince adların önüne extended anlamında E harfi eklendi: EAX, EBX.

2003'te ilk AMD64 işlemcileri çıktı, mimari 64 bite geçti ve adların önüne R harfi geldi: RAX. Aynı geçişte sekiz yeni kayıtçı geldi: r8 ile r15. Bunlara ve çoğu 64 bitlik işleme ulaşmak için komutun önüne REX adında tek baytlık bir önek konur.

Bir disassembly satırında r8 ile r15 gördüğünde ya da 64 bitlik bir toplama gördüğünde, arkada çoğu zaman bu öneki okuyorsundur. Yani bugünkü rax, kırk yıllık bir toplayıcının torunudur.

Etkinlik 7 / 7

64 bit kayıtçı rax'ın 32 bitlik alt parçasının adı: ____

Bu derste bir hata mı gördün? Bildir