4- x86 Assembly 3. Bölüm: Köprüden Önceki Son Çıkış (Addressing Modes)

Önceki yazıda "Hello, world" diye konsola çıktı veren gayet basit bir programı Assembly ile yazmıştık. Memory'den, register'lardan, system call'lardan ve nihayetinde de opcode ve instructionlardan bahsetmiştim. NASM'ın Intel diyalektiğindeki basit sentaks yapısını da göstermeye çalışmıştım. Bugün bu öğrendiğimiz bilgilerin üzerine yeni bilgiler ekleyeceğiz. Şu parçayı açın ve arkanıza yaslanarak yazıyı okumaya başlayın.

Konumuz başlıkta da ifade ettiğim gibi "Adressing Modes" yani "Adresleme Yöntemleri" diye Türkçe'ye çevirebileceğimiz bir kavramla ilgili. Bu okunduğu kadar ürkütücü bir konu değil. Aslında C veya C++'taki pointerler veya semantik olarak referanslarla çok benzer bir amacı teşkil ediyor. RAM yani bizim birincil hafızamızda bulunan her verinin bir adresi bulunmaktadır. Bu adresleri referans alarak eğer o adreste yer alan verinin boyutunu biliyorsak istediğimiz gibi o veriye ve o adresten itibaren diğer verilere ulaşabiliriz (genelde verinin boyutunu biliyor oluruz elbette). Aşağıdaki görseli göz önüne alalım. Toplamda 5 tane elemanı olan bir dizi (array) oluşturduğumuzu varsayalım. Elbette bu 5 elemanın aynı tipte bir boyutu olması da lazım. Diyelim ki bayt (byte) tipinde yani 8-bitlik 5 elemandan oluşmaktadır. Bu durumda eğer ki adres 101 elimdeyse ve bu da bu array'in 1. elemanının adresiyse (ki aynı zamanda dizinin de başlangıç adresi (base pointer) olmuş olur) bu durumda 4. elemana ulaşabilmek için çok basit bir aritmetik işlem yapabilirim: 101 + 4 * (1 byte). Dolayısıyla yeni adres olarak 105 adresine ulaşmış olurum. Çok basit değil mi?

117a1e6021a5e2aff9b935049683a30e.png

İşleri biraz daha kızıştıralım ve bunu assembly ile yazalım:

section .data
arr db 0x1,0x2,0x3,0x4

section .text
global _start

_start: 
    mov cl, byte [arr] 

    mov ebx, 0
    mov eax, 1
    int 0x80
section .data
arr db 0x1,0x2,0x3,0x4

section .text
global _start

_start: 
    mov cl, byte [arr] 

    mov ebx, 0
    mov eax, 1
    int 0x80

Yukarıdaki arr db 0x1,0x2,0x3,0x4 kısmında, geçen yazıdan hatırlarsak "," kullanarak o tipten veriyi bir dizi şeklinde yani birbirinin memory'de ardınca gelecek şekilde yerleştirilmesini sağlıyorduk. Bu bir string literal'i ile bir byte yani "Hello, world", 0xA de olabilirdi veya farklı ve ayrık bayt ifadeleri de. Eğer önceki yazıyı okumadıysanız veya uzun zaman önce okuduysanız burayı ve sonrasını daha iyi anlamak için okumanızı/tekrar okumanızı tavsiye ederim. Özetle arr db 0x1,0x2,0x3,0x4 bu tanım aslında char[5] = {1,2,3,4}; den farksızdır.

Devam edersek _start giriş noktasından itibaren program çalıştırıldığında ilk karşımıza çıkan instruction mov cl, byte [arr] 'dır. Burada da önceki yazılarda değindiğimiz şeyler gözümüze çarpıyor. Ancak byte[arr] ifadesini ilk kez görüyoruz. mov opcode'u bir veriyi bir register'dan başka bir registera, sabit bir değeri register'a, bir adrestan register'a veya bir adresten başka bir adrese veriyi taşımak için kullanılıyordu. Ve "Hello, world" uygulamasında hatırlarsak "msg" isimli bir veriyi system call için ecx register'ından (write system call'u için ecx'den string datasının pointer'ını aldığı için) alıyordu. Şöyle bir instruction'ındı:

mov     ecx, msg
mov     ecx, msg

Burada "msg" aslında bir memory'deki bir adres değerini tutmaktadır. Bunu göstermek için debugger kullanarak register'ın bu instruction çalıştırıldığındaki değerine bakabiliriz. Ancak bundan önce "build.sh" isimli bash scriptimizi şu şekilde birazcık değiştirmemiz lazım:

nasm -f elf -g -F dwarf "$1.asm" && ld -m elf_i386 "$1.o" -o $1 && ./$1 && rm "$1.o"

Bunu yaptıktan sonra "hello_world.asm"i yi tekrardan derlersek bu sefer hello_world isminde bir executable dosyanın da olduğunu görebilirsiniz. Bu noktada ayrıca "gdb" ye yani GNU Debugger'a ihtiyacınız var. Eğer sisteminizde yoksa paketi yükleyebilirsiniz.

"gdb" yi kullanarak şu şekilde programı çalıştırabilirim:

gdb hello_world

ve şu şekilde bir terminal aracı çalışmaya başlar:

2022-06-29-215033_629x317_scrot.png

burada da b 11  yazıp breakpoint koyarsam, yani program akışı esnasında o satıra geldiğinde program bir süre çalışmayı askıya alsın istiyorum, ve ardından da "run" yazarsam; program 11. satıra kadar çalışıp duracaktır. Çalışmayı durdurduğunda "info registers" veya daha kısa şekilde "i r" yazarsam bütün register'ların değerlerini şu şekilde gözlemleyebilirim:

2022-06-29-215733_714x472_scrot.png

Dikkatli bakarsam ecx register'ında oldukça büyük bir sayının (genelde adresler decimal olarak ifade edildiğinde bu kadar büyük olur) yani aslında msg datasının memory'deki adresinin ya da daha anlaşılır bir ifadeyle char msg[13] = {"H","e", ... , "l", "d"} şeklinde bir dizinin başlangıç adresini tuttuğunu görebiliriz. Dolayısıyla bu ecx = &msg[0] demekten başka bir şey değildir. Bu tabiki C ve C++'ta ampersand ile referansını almaktır.High level programlama dillerinin aksine assembly'de her değişken varsayılan olarak memory adreslerini tutar. Tabi bu bizim işimize her zaman yarayan bir durum değil. İşte bu yüzden o memory adresinin içerisindeki değeri almak istediğimde "adressing modes" ile birkaç farklı şekilde bunu yapma olanağım bulunur.

Tüm bu adresleme yöntemlerinde sentaktik olarak Intel diyalektiğinde "[ ]" kullanılır. Ancak bu parantezlerin önünde adresinin değerine ulaşılacak olan verinin tipini (boyutunu) da belirtmek mecburiyeti vardır. Yani "byte [ ]" gibi bir kullanım söz konusudur mesela ki "db" tipinde bir değişken için. Ancak bu da yeterli değil. Ayrıca hedeflenen register'ın ya da memory adresinin kesinlikle bu veri boyutuyla uyumlu olması gerekir. Yani "byte [ ]" için "eax", "ebx" vb. gibi 32 bitlik genişliğe sahip registerlar kullanılamaz. Bunun yerine 1 bayt 8 bit olduğu için "eax" register'ının alt bölümlerinden 8 bitliğe tekabül eden örneğin eax register'ı için "ah" veya "al" kullanılmak zorundadır. Bunu örnek üzerinden hemen görelim. hello_world.asm'deki şu satırı:

mov     ecx, msg
mov     ecx, msg

şu şekilde değiştirelim:

mov     cl, byte [msg]
mov     cl, byte [msg]

ve bu şekilde derleyerek debugger'da register'da bu sefer nasıl bir verinin olduğuna bakalım:

2022-06-29-221245_789x467_scrot.png

"ecx" register'ında 0x48 yani 72 sayısının olduğunu görüyoruz. Peki bu sayı nedir? Bu sayı "H" harfinin ASCII karşılığından başka bir şey değildir. Dolayısıyla "msg" ile bütün bir "Hello, world" ifadesinin "H" yani başlangıç adresi bu karakterin adresini tuttuğu için bu adresin değerini aldığımızda, ki bu aslında C ve C++'ta dereferans etmekle aynı şeydir, 0x48 değerini yani 72'yi "ecx" register'ına transfer etmiş olduk.

Ayrıca, "i r" veya "info registers" ın yanına ayrıca register'ı da yazarsanız sadece o registerın değeri listelenelir: "i r ecx" gibi.

Özetle önceki yazıdaki "hello_world.asm" şu hale gelmiş oldu:

section .data
msg db 'Hello world',0xA

section .text
global _start

_start:
    mov edx, 13
    mov cl, byte [msg]
    mov ebx, 1
    mov eax, 4
    int 0x80

    mov ebx, 0
    mov eax, 1
    int 0x80
section .data
msg db 'Hello world',0xA

section .text
global _start

_start:
    mov edx, 13
    mov cl, byte [msg]
    mov ebx, 1
    mov eax, 4
    int 0x80

    mov ebx, 0
    mov eax, 1
    int 0x80

Bu arada, adresler de nihayetinde birer tam sayı oldukları için, "flat memory model" de bunların mimariye göre bazı zorunlulukları var; x86 bir mimaride 32 bitlik bir register ile ancak bu adresleri tutabilir ve yine x64 bir mimaride ise 64 bitlik bir register ile tutabilirsiniz.

Adresleme yöntemleri aslında iki farklı kategoriye ayrılabilir: code ve data adreslemeleri. Adreslemekten kastımız aslında bir register'a veya memory adresine bir bilginin ulaştırılmaktan başka bir şey değildi. Bu bilgi doğrudan sabit bir değer olabilir, bir başka register-dan olabilir veya bir memory adresinden olabilir. İlkini ve ikincisi şuana kadar zaten kullandığımızı fark etmiş olmalısınız: mov edx, 13 veya mov eax, ebx bu kullanımlara örnek olarak kolayca yazılabilir.

Mesela, bir register-dan bir memory adresine veriyi taşımak istediğimizi varsayalım:

section .data
value dd 0x0

section .text
global _start

_start:
    mov eax, 10
    mov dword [value], eax   
    mov ebx, dword [value]

    mov ebx, 0
    mov eax, 1
    int 0x80
section .data
value dd 0x0

section .text
global _start

_start:
    mov eax, 10
    mov dword [value], eax   
    mov ebx, dword [value]

    mov ebx, 0
    mov eax, 1
    int 0x80

Bu örnekte yaptığımız gayet basit ve anlaşılır; value ismindeki değişkenin değerini eax register-ına verdiğim 10 değeriyle değiştiriyorum. Ve daha sonra aynı adresteki değeri ebx register-ina taşıyorum ki değişiklikleri gözlemleyebileyim. Nihayetinde de debug edersem şu şekilde olduğunu görebilirim:

2022-07-01-090344_451x122_scrot.png

eax ve ebx registerları 10 değerini almış. Aynı zamanda x/d &value diyerek value değişkenimin mevcut değerini de gözlemleyebilirim.

Adresin içerisindeki değerleri bu şekilde değiştirebiliyorum. Peki value iki elemanı olan bir dizi olsaydı, yani şu şekilde:

value dd 0x3, 0x5
value dd 0x3, 0x5

value[0]  = 3 ve value[1] = 5 şeklinde olduğunu görebiliriz. Peki ya value[1] yani dizinin 2. elemanının değerini almak istesem bunu nasıl yapardım? Aslında bir önceki örneği hatırlarsak elimde base adres zaten ve dolayısıyla bu adresin üzerine birkaç bayt (ulaşmak istediğim verinin boyutuna bağlı) ekleyerek 2. elemana da ulaşabilirim. Şu örneğe bakalım:

section .data
value dd 0x3, 0x5


section .text
global _start

_start:
    mov eax, dword [value]
    mov ecx, 4
    mov ebx, dword [value + ecx]
    xor edx, edx
    add edx, eax
    add edx, ebx

    mov ebx, 0
    mov eax, 1
    int 0x80
section .data
value dd 0x3, 0x5


section .text
global _start

_start:
    mov eax, dword [value]
    mov ecx, 4
    mov ebx, dword [value + ecx]
    xor edx, edx
    add edx, eax
    add edx, ebx

    mov ebx, 0
    mov eax, 1
    int 0x80

Bu programın yaptığı şey, öncelikle value[0]'i mov eax, dword [value] bu şekilde dereferans ederek adreste bulunan değeri eax register'ına almak ve daha sonra  mov ebx, dword [value + ecx] ile de ebx register'ına value + 4 diyerek 2. elemanın adresine ulaşarak bunu almaktır (ecx register'ına da 4 verdiğimize dikkat edin); 4 bayt ekledik çünkü değişken için "dd" tipini kullandık dolayısıyla her elemanı 4 bayt olacak bir dizi değişkeni tanımladık.

xor edx, edx intruction'ı ile edx register'ına kendisini xor ederek (değeri 0 lamış oluyoruz) bunun sonucu veriyoruz (aşağıda matematiksel ve mantıksal işlemler için gerekli birkaç opcode'u paylaşacağım). Ardından da eax registerında şuanda 3 ve ebx registerında da 5 değerleri olduğuna göre bunları toplayarak 8 değerini gözlemlemeye çalışıyoruz.

add edx, eax
add edx, ebx
add edx, eax
add edx, ebx

add opcode'u toplama işlemi yapmak için kullanılıyor. Burada edx registerı ile eax registerının değerlerini topladık ve edx registerına bu sonucu yazdık. Matematiksel operasyonlar bu şekilde çalışır genellikle; yani sonuc ilk operand'a yazılır her zaman. Devam edersek, yine aynı şekilde edx registerı ile bu sefer ebx registerının değerlerini toplanım yine edx registerına yazıldı. Nihayetinde de:

0 + 5 = 5

5 + 3 = 8

işlemleri yapılmış oldu. Kodumuzu debug edip, registerlarını incelersek de böyle olduğunu görebiliriz:

2022-07-02-153534_583x277_scrot.png

Ayrıca addressing mode kullanarak aslında birçok tanımladığım bir array-in elemanlarını saydırabilirim (iterate) edebilirim. Şöyle bir örnek üzerinde bunu görelim:

section .data
value db 0x48,0x45,0x4C,0x4C,0x4F
dest  db 0x0,0x0,0x0,0x0,0x0,0x0

section .text
global _start

_start:
    mov eax, value 
    mov ecx, dest
    mov esi, 0
    mov edi, 4

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop
    
_done:
    mov edx, 5
    mov ecx, dest
    mov ebx, 1
    mov eax, 4
    int 0x80

    mov ebx, 0
    mov eax, 1
    int 0x80
section .data
value db 0x48,0x45,0x4C,0x4C,0x4F
dest  db 0x0,0x0,0x0,0x0,0x0,0x0

section .text
global _start

_start:
    mov eax, value 
    mov ecx, dest
    mov esi, 0
    mov edi, 4

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop
    
_done:
    mov edx, 5
    mov ecx, dest
    mov ebx, 1
    mov eax, 4
    int 0x80

    mov ebx, 0
    mov eax, 1
    int 0x80

Evet, biraz ürkütücü göründüğünün farkındayım. Yeni birçok opcode kullandık. Ancak adım adım anlattıktan sonra hepsinin aslında ne kadar basit şeyler olduğunu anlayacaksınız. Öncelikle üst kısımda iki farklı array tanımladığıma dikkat etmişsinizdir. Bunlar byte tipinde olmakla beraber bir tanesi 0x48,0x45,0x4C,0x4C,0x4F şu şekilde baytlar içeriyor; diğeri ise bir fazla eleman ile tamamen sıfırlardan oluşuyor. 0x48,0x45,0x4C,0x4C,0x4F bu baytlar aslında "HELLO" string datasından başka bir şey değildir. Her harfin ascii karşılığı bir bayt olarak tutuluyor. Bunu doğrudan value db "HELLO" şeklinde de yazabilirdik.

Yazdığımız programın yapacağı şey value array-indeki değerleri dest array-ine taşımak olacak. Dolayısıyla böylelikle finalde de SYS_WRITE sistem çağrısı ile dest değişkenini konsolda yazdırdığımızda "HELLO" yazdığını göreceğiz.

_start entry pointinden çalışmaya başladığında, programın, çalıştıracağı ilk instructionlar şunlar:

mov eax, value
mov ecx, dest
mov esi, 0
mov edi, 4
mov eax, value
mov ecx, dest
mov esi, 0
mov edi, 4

eax register-ına value array-inin base pointer'ını yani "H" harfinin olduğu adresi taşıyor. Yani dizinin ilk elemanın adresini eax register-ında tutuyoruz. ecx register-ında da hedef olarak kullanacağım ve sistem çağrısından çağıracağım diğer array-in base pointer'ını tuyoruz. Burada neden ecx seçtiğime gelirsek yapacağımız "write" sistem çağrısında buffer'ın ecx register'ından argüman olarak verildiğini önceki yazıdan hatırlayabilirsiniz. Daha sonra ise esi ve edi registerlarında oluşturacağımız döngüyü kontrol edebilmek ve adres üzerinde indexleme (pointer aritmetiği) yapabilmek için değerler tutuyoruz. Bunlardan birazdan bahseceğim.

Devam edersek.Daha sonra _loop ve _done labelları geliyor:

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop
    
_done:
    mov edx, 5
    mov ecx, dest
    mov ebx, 1
    mov eax, 4
    int 0x80
_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop
    
_done:
    mov edx, 5
    mov ecx, dest
    mov ebx, 1
    mov eax, 4
    int 0x80

Bu label-lar aslında birer sıçrama noktası olarak düşünülebilir. _loop içerisinde:

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop
_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop

İlk instruction-da yaptığımız şey aslında yukarıda bahsettiğimiz addressing mode'tan başka bir şey değil ancak biraz daha farklı olduğuna dikkat etmişsinizdir:

mov ebx, dword [eax + esi * 4]
mov ebx, dword [eax + esi * 4]

burada yapılan şey [eax + esi * 4] ile esi içerisindeki değerin 4 ile çarpılarak eax'te tutulan base pointer-ına eklenmesidir. Özetle şunun gibi bir şeyden bahsediyorum:

char value[5] = {'H','E','L','L','O'};

for(int i = 0; i < 5; i++) {
    char c = value[i];
}
char value[5] = {'H','E','L','L','O'};

for(int i = 0; i < 5; i++) {
    char c = value[i];
}

ancak ve tabi, C veya high-level herhangi bir programlama dilinde compiler, subscript operatörünün "[ ]"  içerisindeki indeksi, indekslenen array-in tipini bildiği için ona göre de hangi boyutlarda offset olması gerektiğini anlayarak otomatik olarak boyutun bayt değeriyle çarpar. Yani [esi * 4] ile bizim manuel olarak 32 bitlik değerler için 1 byte * 4, yani 4 byte, yani 32 bit şeklinde işlem yaptığımız gibi o bunu otomatik olarak yapabilir.  Dolayısıyla ebx registerı esi registerının içerisindeki değere göre base pointer + (esi * 4 bayt) şeklinde basit bir aritmetik işlem ile tüm elemanlara ulaşabilir olacaktır.

cmp esi, edi
cmp esi, edi

buradaki cmp opcode-unun yaptığı şey sahip olduğu iki operandın (esi ve edi) değerlerini karşılaştırması ve ardından aralarındaki ilişkiye göre "conditional jump" için ,"koşula bağlı sıçrama" diye çevirebiliriz, bazı flagları (2. bölümde bahsetmiştik) set etmesidir. Bu şekilde koşullu ifadeler yazabiliriz (yani if-else veya doğrudan döngü oluşturulabilir). Tam bu noktada iki önemli başlık karşımıza çıkar.

Koşulsuz Dallanmalar (Uncoditional Branches)

Program çalışırken memory üzerindeki kaynaklara ulaşabildiği gibi, hatırlarsak bir önceki yazıda bahsettiğimiz ve program counter olarak ifade ettiğimiz EIP register-ı ile de bir sonraki çalıştırılacak instruction'ın adresini tutabiliyordu. **jmp** isminde bir instruction yardımı ile koşulsuz şartsız; EIP register-ındaki değeri değiştirerek program akışında sıçramalar yapabiliriz. Ancak tabi istediğimiz öyle herhangi bir adrese sıçrayamayız (teoride sıçrayabiliriz ancak konumuzun dışında). Ancak bir label-ın adresine gidebiliriz. Bu sıçramaları da performansa ve memory üzerinde sıçrama mesafesine bağlı olarak üç farklı başlıkta değerlendirebiliriz:

Bunların her birisinin hususi özellikleri bulunur ve genellikle performansı ve ne kadar baytlık sıçrama olacağına göre birbirinden ayrılırlar. Fazla detaylarına girmeden sadece bahsetmiş olalım. Hususi araştırmanızda fayda var elbette.

Ayrıca fonksiyon call-ları (bir sonraki yazı hususi bununla ilgili olacak) ve interrupt-lar koşulsuz sıçrama yapar.

Koşula Bağlı Dallanmalar (Conditional Branches)

Koşula bağlı sıçramalarda (conditional jumps) **cmp **ile yapılan karşılaştırma operandların aralarında ilişkiye göre eFLAGS (x64'te aynı registerlardaki e-r değişikliği gibi bu da rFLAGS olarak adlandırılı) register-larını set eder. **cmp **için operandların aynı boyutta register veya değer (dereference edilirken) olmaları şarttır. Bu set edilen flanglere göre sıçramayı durumu sağlayan özet opcode-ları kullanarak yapabiliriz:

je label
je label

je opcode'u cmp karşılaştırılan operand-lar birbirine eşit (jump if equals) ise belirtilen label'a (adrese) sıçrama yapacaktır. Bunun dışında da birçok farklı koşul için de yine "j**" şeklinde opcode-lar bulunur:

photo_2022-07-10_08-36-14.jpg

yukarıdaki tablo hangi flagların set edilmesiyle sıçrama yapabiliriz tüm opcode-ları ayrıntılı olarak göstermektedir. Ancak genelde sık kullanılanlar şu şekildedir:

photo_2022-07-10_08-36-17.jpg

burada dikkat edilirse signed (işaretli) ve unsigned (işaretsiz) sayılara göre deopcode-ların kullanımlarının özelleştiği görülebilir. signed ve unsigned sayılar aslında en soldaki (most significant bit) bir bitin eksi işareti (-) olarak değerlendirilmesi veya değerlendirilmemesinden başka bir şey değildir. Bu konumuz değil ancak bilinmesi önemli olan bir konu olduğu için şuraya bakmanızı tavsiye ederim.

Konumuza geri dönersek, şu instuction-ları inceliyorduk:

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop
_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop

ve burada da en son cmp esi, edi instuction-ına bakmıştık. Artık ne olduğunu bildiğimiz için sonraki instruction-ın ne yaptığını anlamaya çalışabiliriz:

jz _done
jz _done

burada **cmp **karşılaştırmasının ardından birinci ve ikinci operandların durumlarına göre eFLAGS-lerden ZF registerını (yukarıda bir kitaptan aldığım tabloya tekrardan bakabilirsiniz) kontrol ederek, onun 0 veya 1 olma koşullarına göre _done isimli label-a (bir adrese) program sıçrama yapar. Dolayısıyla önceki instuctionlar-la beraber yorumlarsak eax'in içerisindeki adrese value 4'er baytlık değerler ekleyerek dizinin sırasıyla 0,1,2,3 ve 4. indekslerindeki değerleri ecx registerın'daki adresin yine 4'er baytlık değerlerle artmasıyla başka bir dizinin 0, 1, 2, 3 ve 4. indekslerine değerleri aslında kopyalıyor. Her seferinde de **inc **ismindeki bir opcode ile, ki bu verilen register veya değeri bir artırırır (+1),  esi registerın-ın içerisindeki değeri artıyor. Yani esi artarak 0,1,2,3 ve nihayet 4 oluyor ardında da cmp opcode ile karşılaştırıldığında bu sefer eşit oldukları için ZF (zero flag) set oluyor. Bu defa da jz _done sağlandığı için program bu döngüden çıkmış oluyor ve _done label-ından çalışmaya devam ediyor. Tabi **esi **değeri bir artırıldıktan sonra her seferınde _loop 'a koşulsuz sıçrama yapıp bunun bir döngü olmasını da sağlamış oluyor, bunu da unutmayalım.

mov eax, value
mov ecx, dest
mov esi, 0
mov edi, 4

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop

_done:
    mov edx, 5
    mov ecx, dest
    mov ebx, 1
    mov eax, 4
    int 0x80
    
    mov ebx, 0
    mov eax, 1
    int 0x80
mov eax, value
mov ecx, dest
mov esi, 0
mov edi, 4

_loop:
    mov ebx, dword [eax + esi * 4]
    cmp esi, edi
    jz _done
    mov dword [ecx + esi * 4], ebx
    inc esi
    jmp _loop

_done:
    mov edx, 5
    mov ecx, dest
    mov ebx, 1
    mov eax, 4
    int 0x80
    
    mov ebx, 0
    mov eax, 1
    int 0x80

_done label-ında ise önceki yazılarda da bahsettiğimiz system call-lar yapılarak; value dizinden "HELLO" ifadesini dest register-ındaki bir başka 0-larla tanımlanmış diziye kopyalıdığımızi görmek için SYS_WRITE ile konsola yazdırıyoruz. Ve son olarak da SYS_EXIT ile "return 0" ile programdan 0 çıkış kodunu döndürecek sistem çağrısını yapıyoruz.

Nihayet programı çalıştırırsam konsola "HELLO" yazdığını görebilirim:

2022-07-10-092156_320x84_scrot.png

Bu yazıda burada tamamlanmış oldu. Bir sonraki yazıda da "Stack ve avanesinden" bahsedeceğiz :).

Anasayfaya Geri Dön