x86-64 Assembly Öğrenmek
(gpfault.net)- 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 |
rspstack pointer,rsi/rdiise string işleme indeksleri olarak çalışır; yani bazı register'lara özel amaçlar atanmıştırripinstruction pointer,rflagsise 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ştirentry start: Programın başlayacağı entry point'i tanımlar; yürütme ilgili etiketin (start) bulunduğu konumdan başlarsection '.text' code readable executable: Bunun PE'nin kod bölümü olduğunu belirtir; yürütülebilir alandırstart:: Az önce tanımlanan giriş noktasına isim verirint3: Programı durdurup durumu incelemek için kullanılan debugger breakpoint komutudurret: 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 (
ripvb.) değişimi gerçek zamanlı olarak gözlemlenebilir -
retçalıştıktan sonra denetim işletim sistemine geçer; ardındanRtlExitUserThreadçağrısıyla thread ve süreç sonlandırma akışı devam eder -
Not: Yalnızca
retile çı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 (
.idatabölümü) oluşturulmalıdır idatabö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 olanrcxregister'ına 0 yazar (çıkış kodu olarak kullanılır) -
call [ExitProcess]: Import tablosunda yazılı gerçekExitProcessfonksiyon adresine sıçrar -
WinDbg ile adım adım yürütmede stack pointer (
rsp) vercxregister 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
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'yesilolarak erişilebilmesi)Bu,
ax/eax'tenal'ye erişmeye benzer bir kavramx86_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
.textbölümüne koyarak RIP-relative erişim kullandımFibonacci programı çok fazla bytecode gerektirmiyordu
Daha da iyileştirmek için ipuçlarını gerçekten duymak isterim
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
addgibi işlemlerde bellekten yüklemek yerine doğrudan işlem yapabilirsinizAncak 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
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
Assembly ile bir şeyler yapmak istiyorum ama aklıma özel bir fikir gelmiyor
TIS-100adlı oyunu öneririmBu, bir tür pseudo-assembly ile bulmaca çözdüğünüz bir oyun
Böyle oyunların assembly merakını giderebileceğini düşünüyorum