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.
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:
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
