3 puan yazan GN⁺ 2025-07-14 | 1 yorum | WhatsApp'ta paylaş
  • x86-64 assembly'ye giriş için hazırlanan serinin ilk yazısı tanıtılıyor
  • Modern 64 bit sistemler temel alınarak araç kurulumu ve temel yapı açıklanıyor
  • Başlıca geliştirme ve hata ayıklama araçları olarak Flat Assembler (FASM) ve WinDbg kullanımı anlatılıyor
  • PE biçimi, DLL import'ları, Windows çağrı kuralları gibi pratikte gerekli temel bilgiler özeti de yer alıyor
  • Basit bir çıkış programı yazımı ve hata ayıklama süreci uygulamalı deneyim odağında açıklanıyor

Giriş ve Önemi

  • x86 assembly ile ilk kez karşılaşıldığında üniversitelerde genelde eski ortamlar (16 bit, DOS, segment bellek) temel alınarak ders işlendiği deneyimleniyor
  • Günümüzde 64 bit işlemciler baskın olduğundan, bu seri yalnızca gerçekte kullanılan x86-64 ortamını ele alıyor ve eski unsurları tamamen dışarıda bırakıyor
  • Bu eğitim dizisi, Windows işletim sistemi üzerinde çalışan 64 bit program geliştirmeye odaklanıyor
  • Kütüphane kullanmadan, işletim sistemine doğrudan erişen en küçük kodlarla başlanıyor
  • Yazı, assembly öğrenmek isteyen geliştiricileri hedefliyor ve temel C/C++ bilgisi varsayıyor

Geliştirme Araçlarını Hazırlama

Assembler

  • CPU yalnızca insanların anlamasının zor olduğu makine kodunu yorumlayabilir; bunun insan tarafından okunabilir hâli assembly dilidir
  • Assembly dilini makine koduna dönüştüren programa assembler denir
  • x86-64 assembly dili için tek bir standart yoktur; assembler'lar arasında sözdizimi ve çalışma biçimi farklılık gösterir
  • Bu seride Flat Assembler (FASM) kullanılıyor; küçük, kullanımı kolay ve güçlü bir makro sistemiyle editör sunuyor

Debugger

  • Yazılan assembly kodunu analiz etmek ve yürütme akışını gözlemlemek için debugger vazgeçilmez bir araçtır
  • WinDbg öneriliyor; register'lar, bellek ve assembly kodu bağımsız olarak incelenip değiştirilebiliyor
  • Windows 10 SDK içinden yalnızca ilgili bileşenler seçilerek kurulabiliyor
  • Debugger sayesinde programın iç durumu, bellek yapısı ve register değişimleri doğrudan gözlemlenebiliyor

Assembly Programlamaya Bakış

CPU Yapısı ve Komut Kümesi

  • CPU, belirli bir komut kümesine göre yalnızca sınırlı işlemler yapabilir
  • Komut, CPU'nun gerçekleştirebildiği en temel iş birimidir
  • Her komut, parametrelerle birlikte çok basit şekilde çalışır (değer yazma, aritmetik işlemler vb.)
  • Düşük seviye programlama ve hata ayıklamada, bu yapının tüm yüksek seviye kavramların temeli olduğunu anlamak kritik önemdedir

Register'lar

  • Register'lar, CPU içine gömülü son derece hızlı özel bellek alanlarıdır
  • x86-64'te genel amaçlı register sayısı 16'dır ve hepsi 64 bittir
  • Her register'a byte, word ve double word düzeyinde kısmi erişim mümkündür
Register Alt byte Alt word Alt double word
rax al ax eax
rbx bl bx ebx
rcx cl cx ecx
rdx dl dx edx
rsp spl sp esp
rsi sil si esi
rdi dil di edi
rbp bpl bp ebp
r8~r15 r8b~r15b r8w~r15w r8d~r15d
  • rsp stack pointer, rsi/rdi ise string işleme indeksleri olarak çalışır; yani bazı register'lara özel amaçlar atanmıştır
  • rip instruction pointer, rflags ise işlem sonucundaki durum bayraklarını tutan özel bir register'dır

Bellek ve Adresler

  • Bellek, 0. indisten başlayan kesintisiz bir byte dizisi gibi çalışır
  • Eski x86 yapılarında segment-offset yöntemi zorunluydu, ancak x86-64'te tüm bellek flat adres alanı olarak ele alınır
  • Gerçekte ise işletim sistemi ve donanım, her süreç için sanal adres alanını fiziksel belleğe dinamik olarak eşler
  • Yani aynı sanal adres, farklı süreçlerde farklı fiziksel belleğe karşılık gelebilir
  • Komutlar ve veriler aynı bellekte bulunur (von Neumann mimarisi); bu yapı, Arduino'da kullanılan AVR gibi veriyi ayrı saklayan Harvard mimarisinden farklıdır

İlk Assembly Programını Yazmak

  • FASM kurulduktan sonra aşağıdaki basit programın yazılması ve derlenmesi gösteriliyor
format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
        int3
        ret

Kodun Açıklaması

  • format PE64 NX GUI 6.0 : FASM'ın üreteceği çalıştırılabilir dosya biçimini belirtir; burada PE (Portable Executable) 64 bit GUI seçilmiştir
  • entry start : Programın başlayacağı entry point'i tanımlar; yürütme ilgili etiketin (start) bulunduğu konumdan başlar
  • section '.text' code readable executable : Bunun PE'nin kod bölümü olduğunu belirtir; yürütülebilir alandır
  • start: : Az önce tanımlanan giriş noktasına isim verir
  • int3 : Programı durdurup durumu incelemek için kullanılan debugger breakpoint komutudur
  • ret : Stack'ten bir adres alıp denetimi oraya aktarır; bu programda doğrudan çıkışa karşılık gelir

Hata Ayıklama Uygulaması

  • WinDbg'de yukarıdaki programın çalıştırılabilir dosyası (.exe) açılır ve disassembly, register gibi çeşitli pencereler hazırlanır

  • F5 ile program breakpoint'e kadar çalıştırılır, F8 ile her seferinde bir komut yürütülerek adım adım ilerlenir

  • Register'ların (rip vb.) değişimi gerçek zamanlı olarak gözlemlenebilir

  • ret çalıştıktan sonra denetim işletim sistemine geçer; ardından RtlExitUserThread çağrısıyla thread ve süreç sonlandırma akışı devam eder

  • Not: Yalnızca ret ile çıkış yapıldığında, thread dışında arka planda ek çalışma olup olmamasına bağlı olarak süreç açık kalabilir; bu yüzden doğru sonlandırma için ExitProcess çağrılması önerilir

PE Biçimi ve DLL Import'ları

DLL Fonksiyonu Import Yapısına Genel Bakış

  • ExitProcess gibi WinAPI fonksiyonları KERNEL32.DLL içinde bulunur
  • Bu tür dış fonksiyonları kullanmak için çalıştırılabilir dosyanın import tablosu (.idata bölümü) oluşturulmalıdır
  • idata bölümündeki Import Directory Table (IDT), DLL adı, fonksiyon adı ve IAT/ILT gibi adreslerin (RVA) bilgilerini içerir
  • IAT (Import Address Table), çalışma zamanında OS loader tarafından gerçek fonksiyon adresleriyle üzerine yazılır
  • Hint/Name Table, her fonksiyonun adını ve ipucu bilgisini içerir

FASM'da .idata Bölümü Tanımlama Örneği

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 
  • db/dw/dd/dq : byte/word/double word/quad word (8 byte) birimlerinde değer ekler
  • rva : sembolün sanal adresini (Relative Virtual Address) hesaplar
  • IAT ve Name Table elle kurulup DLL fonksiyonlarına başvuru yapılabilir

64 Bit Windows Çağrı Kuralı (MS x64 Calling Convention)

  • Fonksiyon çağrılarında argümanların nasıl aktarılacağını ve stack'in nasıl kullanılacağını belirleyen standart kuraldır
  • 64 bit Windows'ta Microsoft x64 Calling Convention kullanılır
  • Temel özellikler:
    • Stack pointer her zaman 16 byte hizalı olmalıdır
    • İlk 4 tam sayı/pointer argümanı rcx, rdx, r8, r9 register'larında taşınır
    • İlk 4 kayan nokta argümanı xmm0~xmm3 içine konur
    • Ek argümanlar stack üzerinden aktarılır
    • Argüman sayısından bağımsız olarak stack üzerinde 32 byte shadow space ayrılmalıdır
    • Stack temizliği çağıran tarafın sorumluluğundadır

ExitProcess Çağrısı Örneği

format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
    int3
    sub rsp, 8 * 5
    xor rcx, rcx
    call [ExitProcess]

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 

Yeni Kodun Analizi

  • sub rsp, 8 * 5 : Stack pointer'ı ayarlar (40 byte ayırır); 16 byte hizalamayı ve shadow space ayrımını tek seferde sağlar

  • xor rcx, rcx : İlk argüman olan rcx register'ına 0 yazar (çıkış kodu olarak kullanılır)

  • call [ExitProcess] : Import tablosunda yazılı gerçek ExitProcess fonksiyon adresine sıçrar

  • WinDbg ile adım adım yürütmede stack pointer (rsp) ve rcx register değişimleri ile süreç sonlandırma akışı doğrudan görülebilir

Sonuç

  • Bu yazı, temel araç kurulumundan PE biçimine, DLL import'larına, x64 çağrı kuralına, ilk programı yazmaya ve hata ayıklamaya kadar x86-64 assembly'nin genel akışını uygulama odaklı biçimde anlatıyor
  • Sonraki bölümde daha çeşitli işlevler ve gerçek kod örnekleri ele alınacak

1 yorum

 
GN⁺ 2025-07-14
Hacker News görüşleri
  • Birkaç yıldır geliştirdiğim bir projeyi paylaşmak istiyorum
    https://asm-editor.specy.app
    M68K, MIPS, RISC-V, X86 gibi çeşitli assembly dillerini destekleyen çevrimiçi etkileşimli bir IDE
    Assembly programlamayı öğretmek için pek çok farklı özelliği var
    Başka web sitelerine de gömülebiliyor

  • İşaretçi indeksleme register'larının düşük adresli baytlarına doğrudan erişim özelliği olduğunu bilmiyordum (ör. 16/32 bit'te si/esi'ye sil olarak erişilebilmesi)
    Bu, ax/eax'ten al'ye erişmeye benzer bir kavram
    x86_64'te yeni eklenen opcode'ların gerçekten var olup olmadığını merak ediyorum
    Platform spesifikasyonuna tekrar bakmam gerektiğini düşündürdü
    Bunu tamamen meraktan soruyorum

  • Kendi yazdığım assembly giriş materyalini paylaşıyorum
    https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming

  • CPU emülatörü dispatch'imi C++'tan daha hızlı yapıp yapamayacağımı merak ettiğim için assembly ile optimizasyon denedim
    Fibonacci programını çalıştırdım ama sonuçlar hiç yaklaşmadı
    Sonunda bunu yalnızca varsayılan olarak devre dışı bir seçenek şeklinde birleştirdim
    Yine de bunun daha hızlı yapılmasının kesinlikle bir yolu olduğuna inanıyorum
    https://github.com/libriscv/libriscv/blob/master/lib/libriscv/amd64/inaccurate_dispatch.nasm
    Belleğe erişim yöntemlerini öğrenirken performansı biraz iyileştirdim
    Jump table'ı 64 bit'ten 32 bit'e küçülttüm ve .text bölümüne koyarak RIP-relative erişim kullandım
    Fibonacci programı çok fazla bytecode gerektirmiyordu
    Daha da iyileştirmek için ipuçlarını gerçekten duymak isterim

    • Yazdığınız kodu C++ derleyicisinin ürettiği kodla doğrudan karşılaştırdınız mı diye merak ediyorum
      Bağlamı tam bilmiyorum ama farkın dispatch mekanizmasından (komut fetch yöntemi) değil, gerçek komut uygulamalarındaki farktan kaynaklanıyor olması da mümkün gibi geliyor
      Bir optimizasyon yaklaşımı olarak, emüle edilen register'ları gerçek x86-64 register'larına eşleyip bunları hiç belleğe taşırmamak mümkün
      Bunu yaparsanız add gibi işlemlerde bellekten yüklemek yerine doğrudan işlem yapabilirsiniz
      Ancak bu yöntem emülatör yazmayı çok daha zahmetli hale getirir
  • Tarayıcıda pratik yapılabilen bir x86 assembly giriş materyali
    Herhangi bir yerel kurulum olmadan örnekleri hemen çalıştırabilirsiniz
    https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html
    Bu arada bunu ben yazdım

    • Girdi doğrulaması yapıp yapmadığınızı merak ediyorum
      Doğrudan NASM ile assemble edip ardından binary'yi çalıştırıyor gibi görünüyor, bu yüzden güvenliği merak ettim
  • Sadece profil fotoğrafına bakınca junferno sandım

  • Assembly'ye bir kez olsun dokunmak bile genel anlayışı derinleştirdiği için her zaman iyi bir deneyim oluyor
    Büyük bir proje yapmanıza gerek yok; cesaret edip az da olsa kendiniz denemenizi öneririm

  • O zamanki (2020) HN tartışma bağlantısını paylaşıyorum
    https://news.ycombinator.com/item?id=24195627

  • Intel tarzı assembly sözdizimi olduğu için sevindim

    • Başka hangi assembly sözdizimlerinin olduğunu merak ettim
  • Assembly ile bir şeyler yapmak istiyorum ama aklıma özel bir fikir gelmiyor

    • TIS-100 adlı oyunu öneririm
      Bu, bir tür pseudo-assembly ile bulmaca çözdüğünüz bir oyun
      Böyle oyunların assembly merakını giderebileceğini düşünüyorum