Skip to content

Latest commit

 

History

History
185 lines (170 loc) · 8.77 KB

File metadata and controls

185 lines (170 loc) · 8.77 KB

X86_64 Assembly'e merhaba deyin [bölüm 4]

Bir süre önce x86_64 için assembly programlaması hakkında bir dizi blog yazısı yazmaya başladım. Bunu asm etiketi ile bulabilirsiniz.Maalesef son zamanlarda meşguldüm ve yeni yazı yoktu, bu yüzden bugün assembly hakkında yazılar yazmaya devam ediyorum ve her hafta bunu yapmaya çalışacağım.

Bugün string ve bazı string işlemlerine bakacağız. Hala nasm assembler ve linux x86_64 kullanıyoruz.

Ters String

Assembly programlama dili hakkında konuştuğumuzda tabiki string veri türünden bahsedemeyiz aslında bayt dizisi ile ilgileniyoruz.Basit bir örnek yazmayı deneyelim, string verilerini tanımlayacağız ve stdout'a sonuçları ters çevirip yazacağız.Yeni programlama dilini öğrenmeye başladığımızda bu görevler oldukça basit ve popüler gözüküyor. Uygulamaya bakalım.

Her şeyden önce, ilk değer verilmiş olan veriyi tanımlarım.Veri bölümüne(Data Section) yerleştirilecektir(bölümler hakkında bilgi edinmek için önceki yazıları okuyabilirsiniz):

section .data
    SYS_WRITE   equ 1
    STD_OUT     equ 1
    SYS_EXIT    equ 60
    EXIT_CODE   equ 0

    NEW_LINE    db 0xa
    INPUT   db "Merhaba dunya!"

Burada dört sabiti görebiliriz:

  • SYS_WRITE - ‘write’ syscall numarası
  • STD_OUT - stdout file descriptor(dosya tanıtıcısı)
  • SYS_EXIT - ‘exit’ syscall numarası
  • EXIT_CODE - exit kodu

Syscall(sistem çağrısı) listesini bulabilirsiniz - burada .Ayrıca orada tanımlanmış:

  • NEW_LINE - yeni satır (\ n) sembolü
  • INPUT - giriş stringimiz, tersine çevireceğimiz Sonra buffer için bss bölümünü tanımladık, ters stringi koyacağız:
section .bss
    OUTPUT  resb 12

Tamam, sonuç alacağımız bazı veri ve bufferlarımız var, şimdi kod için text bölümünü tanımlayabiliriz.Ana program _start'dan başlayalım:

_start:
    ; INPUT'un adresini al
    mov rsi, INPUT
    ; sayaç için rcx'i sıfırla
    xor rcx, rcx
    ; df = 0 si++
    cld
    ; Fonksiyon çağrısından sonraki yeri hatırla.
    mov rdi, $ + 15
    ; stringin uzunluğunu al
    call    stringUzunluguHesapla
    ; rax'a sıfır yaz
    xor rax, rax
    ; tersString için ek sayaç
    xor rdi, rdi
    ; ters string
    jmp tersString

İşte bazı yeni şeyler. Nasıl çalıştığını görelim:İlk olarak 2'inci satırda Stdout'a yazarken yaptığımız gibi INPUT adresini rsi registerına yazdık.Ve rcx registerına sıfır yazdık.Stringimizin uzunluğunu hesaplamak için sayaç olacak.4'üncü satırda cld operatörünü görebiliriz.Df bayrağını sıfırlar.İçine ihtiyacımız var çünkü string uzunluğunu hesaplayacağız, bu stringin sembollerinden geçeceğiz ve eğer df bayrağı 0 olacaksa stringin sembollerini soldan sağa ele alacağız.Ardından stringUzunluguHesapla fonksiyonunu çağırıyoruz. 5. satırdaki mov rdi, $ + 15 talimatını unuttum, biraz sonra anlatırım. Ve şimdi, stringUzunluguHesapla uygulamasına bakalım:

stringUzunluguHesapla:
    ; kontrol et, stringin sonu mu?
    cmp byte [rsi], 0
    ; Evet ise programdan çık
    je programdanCikis
    ; rsi'den al'e bayt yükle ve rsi'ı artır.
    lodsb
    ; sembolü stack'e pushla
    push rax
    ; sayacı artır
    inc rcx
    ; tekrar dön
    jmp stringUzunluguHesapla

Adından da anlaşılacağı gibi, sadece INPUT stringinin uzunluğunu hesaplar ve sonucunu rcx registerında saklar.Her şeyden önce, rsi registerının sıfıra işaret etmediğini kontrol etmeliyiz, eğer bu stringin sonu ise ve fonksiyondan çıkabiliyorsak.Sıradaki lodsb komutu.Basit, sadece 1 bayt (ax'ın 16 bit az kısmı) al registerine koy ve rsi işaretçisini değiştir.Cld komutunu çalıştırdığımız gibi lodsb her zaman rsi'yi bir bayt soldan taşır bu yüzden string sembolleri taşırız.Sonra rax'ın değerini stack'e pushlarız şimdi stringimizden sembol içeriyor(lodsb, si'dan al'e bayt koyar. al rax'ın 8 bit az halidir).Sembolü stack'e neden pushladık?Stack'in nasıl çalıştığını hatırlamalısınız, LIFO (last input, first output) prensibine göre çalışır.Bu bizim için çok iyi.İlk sembolü si'den alacağız stack'e pushlayacağız sonra ikinciden ve vb.Böylece stack'in en üstünde string sembolü olacak.Daha sonra sembolü stack'ten sembol ile pop ederiz ve OUTPUT buffer'ına yazarız.Ondan sonra sayacımızı (rcx) artırırız ve tekrar döngünün başlangıcına döneriz.

Tamam, tüm sembolleri stringden stack'e pushladık, şimdi programdanCikis'a atlayarak _start 'a geri dönebiliriz. Nasıl yapılır? Bunun için ret komutumuz var. Fakat eğer kod şöyle olacaksa:

programdanCikis:
    ;_start'a geri dön
    ret

Çalışmayacak. Niye? Bu karmaşık. Hatırlayın _start da stringUzunluguHesapla'yı çağırdık. Bir fonksiyon çağırdığımızda ne olur?İlk olarak, tüm fonksiyon parametreleri sağdan sola stack'e pushlanır.Adres döndükten sonra stack'e pushlanır.Böylece fonksiyon çalıştırma bitiminden sonra nereye döneceğini bilecektir. Ama stringUzunluguHesapla'ya bakın sembolleri stringimizden stack'e pushladık ve şimdi stack'in üstünde geri dönüş adresi yok ve fonksiyon nereye döneceğini bilmiyor. Bununla nasıl olur?Şimdi çağırmadan önce garip komutlara bir göz atmalıyız:

mov rdi, $ + 15

Her şeyden önce:

  • $ - $'ın tanımladığı string'in hafızadaki pozisyonunu döndürür.
  • $$ - o anki bölüm başlangıcının hafızadaki konumunu döndürür.

Demek ki mov rdi, $ + 15 için konumumuz var ama neden buraya 15 ekleyelim? Bakın, stringUzunluguHesapla'dan sonraki satırın konumunu bilmemiz gerekiyor.

Dosyamızı objdump util ile açalım:

objdump -d ters

Objdump çıktımız:

ters:     file format elf64-x86-64

Disassembly of section .text:

0000000000401000 <_start>:
  401000:   48 be 01 20 40 00 00    movabs $0x402001,%rsi
  401007:   00 00 00
  40100a:   48 31 c9                xor    %rcx,%rcx
  40100d:   fc                      cld
  40100e:   48 bf 1d 10 40 00 00    movabs $0x40101d,%rdi
  401015:   00 00 00
  401018:   e8 08 00 00 00          callq  401025 <stringUzunluguHesapla>
  40101d:   48 31 c0                xor    %rax,%rax
  401020:   48 31 ff                xor    %rdi,%rdi
  401023:   eb 0e                   jmp    401033 <tersString>

Burada, satır 12'nin (mov rdi, $ + 15) 10 bayt ve satır 16'daki fonksiyon çağrısının 5 bayt aldığını görebiliriz,böylece 15 bayt alır.Bu nedenle dönüş adresimiz mov rdi, $ + 15 olacaktır. Şimdi rdi'den stack'e dönüş adresini pushlayabilir ve fonksiyondan geri dönebiliriz:

programdanCikis:
    ; geri dönüş adresini tekrar stack'e pushla
    push    rdi
    ; _start'a geri dön
    ret

Şimdi başlangıca dönebiliriz.stringUzunluguHesapla çağrısından sonra rax ve rdi'ye sıfır yazıp tersString etiketine atlıyoruz.Uygulaması aşağıdaki gibidir:

tersString:
    ; stringin sonu olup olmadığını kontrol et
    cmp rcx, 0
    ; Eğer evet ise stringi yazdır
    je  sonucuYazdir
    ; sembolü stackten al
    pop rax
    ; output buffer'a yaz.
    mov [OUTPUT + rdi], rax
    ; sayacın uzunluğunu azalt
    dec rcx
    ; ek olarak sayacı arttır (write syscall için)
    inc rdi
    ; tekrar döngü
    jmp tersString

Burada stringin uzunluğu olan sayacımızı kontrol ediyoruz ve eğer sıfır ise tüm sembolleri buffer'a yazdık ve yazdırabiliriz.Sayacı kontrol ettikten sonra, ilk sembolü stackten rax'a pop ederiz ve OUTPUT buffera yazarız.Rdi'yi ekleriz çünkü aksi halde buffer'ın ilk baytına sembol yazarız.Bundan sonra OUTPUT buffer ile sonraki hamle için rdi'yi arttırır, uzunluk sayacını düşürür ve etiketin başlangıcına atlarız.

tersString komutunun çalıştırılmasından sonra OUTPUT buffer içinde ters string'e sahibiz ve sonucu stdout'a yeni bir satırla yazabiliriz:

sonucuYazdir:
    mov rdx, rdi
    mov rax, 1
    mov rdi, 1
    mov rsi, OUTPUT
    syscall
    jmp yeniSatirYazdir

yeniSatirYazdir:
    mov rax, SYS_WRITE
    mov rdi, STD_OUT
    mov rsi, NEW_LINE
    mov rdx, 1
    syscall
    jmp cikis

ve programımızdan çıkalım:

cikis:
    ; syscall sayısı
    mov rax, SYS_EXIT
    ; exit kodu
    mov rdi, EXIT_CODE
    ; sys_exit'i çağır
    syscall	

Hepsi bu kadar, şimdi programımızı şununla derleyebiliriz:

all:
	nasm -g -f elf64 -o ters.o ters.asm
	ld -o ters ters.o
clean:
	rm ters ters.o

ve çalıştırın:

screenshot

String işlemleri

Elbette string/bayt manipülasyonları için başka birçok komut vardır:

  • REP - rcx sıfır değilken tekrar et
  • MOVSB - bayt stringi kopyalama (MOVSW, MOVSD ve benzeri.)
  • CMPSB - bayt stringi karşılaştırması
  • SCASB - byte stringi taraması
  • STOSB - baytı stringe yazdır