Stack hafızanın özel bir bölümüdür,lifo(Last input, first output) ilkesine göre çalışır.
Geçici veri depolamak için 16 adet genel amaçlı registerımız var.Bunlar RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP ve R8-R15 dir.Ciddi uygulamalar için çok az. Böylece veriyi stack'te saklayabiliriz. Stack'in başka bir kullanımı şu şekildedir:Bir fonksiyon çağırdığımız zaman, stack'e kopyanlanmış adresi döndürürülür.Fonksiyonun çalışması bittikten sonra komut sayacına (RIP) adresi kopyalanır ve program fonksiyondan sonraki yerden çalışmaya devam eder.
Örneğin
global _start
section .text
_start:
mov rax, 1
call incRax
cmp rax, 2
jne exit
;;
;; Bir şey yap
;;
incRax:
inc rax
ret
Burada program çalıştıktan sonra rax'in 1'e eşit olduğunu görebiliriz. Ardından rax değerini 1'e yükselten bir incRax fonksiyonunu çağırırız ve şimdi rax'ın değeri 2 olmalıdır.Bu işlem 8. satırdan devam eder, burada rax değerini 2 ile karşılaştırırız. Ayrıca System System V AMD64 ABI'da okuyabildiğimiz gibi fonksiyonun ilk altı argümanı registerlara geçmiştir.Bunlar:
rdi - ilk argüman
rsi - ikinci argüman
rdx - üçüncü argüman
rcx - dördüncü argüman
r8 - beşinci argüman
r9 - altıncı argüman
Sonraki argümanlar stack'e geçirilecektir. Yani böyle bir fonksiyonumuz varsa:
int foo(int a1, int a2, int a3, int a4, int a5, int a6, int a7){
return (a1 + a2 - a3 - a4 + a5 - a6) * a7;
}
Daha sonra ilk altı argüman registerlara geçirilecek, ancak 7 argüman stack'e geçirilecektir.
Yazdığım gibi 16 tane genel amaçlı registerımız ve iki ilginç registerımız var, RSP ve RBP. RBP base(taban) pointer (işaretçi)registerıdır.Geçerli stack çerçevesinin tabanını işaret eder. RSP stack işaretçisidir, geçerli stack çerçevesinin en üstünü işaret eder.
Komutlar
Stack ile çalışmak için iki komutumuz var:
- push argument - Stack işaretçisini (RSP) artırır ve argümanı stack işaretçisiyle işaretlenen konuma kaydeder.
- pop argument - Stack işaretçisiyle gösterilen konumdan argümana veriyi kopyalar.
Basit bir örneğe bakalım:
global _start
section .text
_start:
mov rax, 1
mov rdx, 2
push rax
push rdx
mov rax, [rsp + 8]
;;
;; Bir şey yap
;;
Burada 1'i rax registera ve 2'yi de rdx registerına koyduk. Sonra registerların değerlerini stack'e pushluyoruz.Stack, LIFO (Last In First Out) olarak çalışır. Bu yüzden bu stack ya da uygulamamız aşağıdaki yapıya sahip olacak:
Daha sonra stack'den adresi rsp + 8 olan değeri kopyalarız.Bu stack'in üst kısmından adresini aldığımız, ona 8 ekleyeceğimiz ve bu adres ile verileri rax'e kopyalayacağımız anlamına gelir. Bundan sonra rax'ın değeri 1 olacaktır.
Bir örnek görelim. İki komut satırı argümanını alacak basit bir program yazacağız. Argümanların toplamanı alacağız ve sonucu yazdıracağız.
section .data
SYS_WRITE equ 1
STD_IN equ 1
SYS_EXIT equ 60
EXIT_CODE equ 0
NEW_LINE db 0xa
WRONG_ARGC db "İki komut satırı argümanı olmalıdır", 0xa
Öncelikle bazı değerler ile .data bölümünü tanımlıyoruz.Burada linux syscalls için dört sabit var;sys_write, sys_exit std_in ve exit_code.Ve ayrıca iki tane stringimiz var: İlki sadece yeni satır sembolü ve ikincisi hata mesajı.
Program kodundan oluşan .text bölümüne bakalım:
section .text
global _start
_start:
pop rcx
cmp rcx, 3
jne argcError
add rsp, 8
pop rsi
call str_to_int
mov r10, rax
pop rsi
call str_to_int
mov r11, rax
add r10, r11
Şimdi burada neler olup bittiğini anlamaya çalışalım: _start etiketinden sonra ilk komut stackten ilk değeri alır ve rcx registerına geçirir.Komut satırı argümanlarıyla programı çalıştırırsak,hepsi aşağıdaki sırayla çalıştıktan sonra stack içinde olacaktır.
[rsp] - stack'in üst kısmı, argüman sayısını içerecek.
[rsp + 8] - argv[0]'ı içerecek
[rsp + 16] - argv[1]'i içerecek
ve bunun gibi...
Böylece komut satırı argümanlarını alırız ve rcx'e koyarız. Sonra rcx'i 3 ile karşılaştırırız.Ve eğer eşit değilse hata mesajını yazdıran argcError etiketine atlarız:
argcError:
;; sys_write syscall
mov rax, 1
;; file descriptor(dosya tanıtıcı), standart çıktı
mov rdi, 1
;; mesaj adresi
mov rsi, WRONG_ARGC
;; mesajın uzunluğu
mov rdx, 34
;; write syscall'u çağır
syscall
;; programdan çık
jmp cikis
Neden iki argümanımız olduğu halde 3 ile karşılaştırıyoruz. Basit.İlk argüman bir program adıdır ve bununla birlikte hepsinden sonra programa aktardığımız komut satırı argümanlarıdır.Tamam, iki komut satırı argümanını geçersek 10'uncu satırın yanına gideriz.Burada rsp'yi 8'e kaydırıyoruz ve böylece ilk argümanı kaybediyoruz - programın adı. Şimdi rsp, geçtiğimiz ilk komut satırı argümanını işaret ediyor. Pop komutu ile aldık ve onu rsi'ye yazıp, tamsayıya çevirme fonksiyonunu çağırdık. Sonra str_to_int implementasyonu hakkında bir şeyler okuyoruz. Fonksiyonumuz çalışmasını bitirdikten sonra rax registarında tamsayı değerine sahibiz ve r10 registerina kaydettik. Bundan sonra aynı işlemi yapıyoruz ama r11 ile.Sonunda r10 ve r11 registerlarında iki tamsayı değerine sahibiz, şimdi add komutu ile toplayabiliriz.Şimdi sonucu stringe dönüştürmeli ve yazdırmalıyız. Nasıl yapılacağını görelim:
mov rax, r10
;; sayı sayacı
xor r12, r12
;; string'e dönüştürme
jmp int_to_str
Burada, komut satırı argümanlarının toplamını rax registerına koyduk, r12'i sıfıra ayarladık ve int_to_str'a atladık. Tamam şimdi programımızın tabanına sahibiz.String'in nasıl yazdırılacağını zaten biliyoruz ve ne yazdıracağınıda. Str_to_int ve int_to_str implemantasyonunda görelim.
str_to_int:
xor rax, rax
mov rcx, 10
next:
cmp [rsi], byte 0
je return_str
mov bl, [rsi]
sub bl, 48
mul rcx
add rax, rbx
inc rsi
jmp next
return_str:
ret
Str_to_int'in başlangıcında rax'i 0'a ve rcx'e 10'a ayarladık. Sonra, bir sonraki etikete gidiyoruz.Yukarıdaki örnekte görebildiğiniz gibi (str_to_int'in ilk çağrısından önce ilk satır) argv [1] 'i rsi'ye stackten alarak koyduk.Şimdi rsi'nin ilk baytını 0 ile karşılaştırıyoruz, çünkü her dize NULL sembolü ile bitiyor ve eğer geri dönüyorsa. Eğer 0 değilse, bu değeri bir bayt bl registerına kopyalayıp ondan 48 çıkartırız. Neden 48? 0 ila 9 arasındaki tüm sayılar asci tablosunda 48 ila 57 arasında kodlamaya sahiptir.Yani 48 numaralı sembolden çıkarsak (örneğin 57'den) sayı alırız. Ardından rax'i rcx ile çarpıyoruz (10 değerine sahip).Bundan sonra, bir sonraki baytı almak ve tekrar döngü yapmak için rsi'yi arttırıyoruz. Algoritma basittir. Örneğin, eğer rsi ‘5’ ‘7’ ‘6’ ‘\ 000’ dizisini gösterirse, aşağıdaki adımları takip edecektir:
rax = 0
ilk baytı al - 5 ve rbx'e koy
rax * 10 -> rax = 0 * 10
rax = rax + rbx = 0 + 5
İkinci baytı al - 7 ve rbx'e koy
rax * 10 -> rax = 5 * 10 = 50
rax = rax + rbx = 50 + 7 = 57
rsi \000'a eşit değilse tekrar döngü
Str_to_int'ten sonra rax'da bir sayıya sahip olacağız. Şimdi int_to_str'a bakalım:
int_to_str:
mov rdx, 0
mov rbx, 10
div rbx
add rdx, 48
add rdx, 0x0
push rdx
inc r12
cmp rax, 0x0
jne int_to_str
jmp cikis
Burada rdx'e 10 ve rbx'e 0 yazıyoruz.Daha sonra div rbx komutunu çalıştırıyoruz. Eğer str_to_int çağrısından önce kodlara bakarsak.Rax'ın içeriğine bakarsak, tam sayı olan iki komut satırı argümanının toplamı göreceğiz.Bu komutla rax değerini rbx değerine bölüyoruz ve rdx ile rax içinde bütün kalanı alıyoruz.Sonra rdx'i 48 ve 0x0'a ekliyoruz. 48 ekledikten sonra bu sayının asci sembolünü alacağız ve tüm stringler 0x0 ile bitecek.Bundan sonra, sembolü stack'e kaydederiz, r12 değerini artırır (ilk yinelemede 0 olur, _start'da 0 yaparız) ve rax'i 0 ile karşılaştırız, 0 ise tamsayıyı stringe dönüştürmeye son verdiğimiz anlamına gelir.Algoritmayı adım adım takip ediyoruz: Örneğin 23 sayımız var.
123 / 10. rax = 12; rdx = 3
rdx + 48 = "3"
stack'e 3'ü pushla
compare rax with 0 if no go again
12 / 10. rax = 1; rdx = 2
rdx + 48 = "2"
stack'e 2'yi pushla
rax ile 0'ı karşılaştırırız, eğer sıfıra eşitse fonksiyonun çalışmasını bitirebiliriz ve stackte "2" "3"... olacaktır.
Tamsayıyı string'e dönüştürmek ve tam tersine dönüştürmek için iki kullanışlı fonksiyon olan int_to_str ve str_to_int fonksiyonunu uyguladık.Şimdi, stringe çevirdiğimiz ve stack için sakladığımız iki tam sayınının toplamına sahibiz. Sonucu yazdırabiliriz:
yaz:
; sayının uzunluğunu hesapla
mov rax, 1
mul r12
mov r12, 8
mul r12
mov rdx, rax
; toplamı yazdır
mov rax, SYS_WRITE
mov rdi, STD_IN
mov rsi, rsp
; sys_write'ı çağır
syscall
; yeni satır
jmp yeniSatirYazdir
Stringi sys_write syscall ile nasıl yazdıracağımızı biliyoruz,ama burada ilginç bir kısım var.String'in uzunluğunu hesaplamak zorundayız.Eğer int_to_str'e bakarsanız,her iterasyonda r12 registerını arttırdığımızı görürsünüz,bu sayımız içindeki rakam miktarını içerir.8'ile çarpmalıyız (çünkü her sembolü stacke pushladık) ve bu yazmamız gereken stringin uzunluğu olacaktır.Bundan sonra her zaman 1'i rax'a koyacağız(sys_write sayısı),1'i rdi'ye (stdin),stringin uzunluğunu rdx'e ve stack'in üstündeki işaretçiye rsi'ye(string başlangıcı için) koyarız. Ve programımızın sonu:
cikis:
mov rax, SYS_EXIT
exit code
mov rdi, EXIT_CODE
syscall
Bu kadar.