This is an old revision of the document!
În acest laborator vom prezenta modul în care se realizează apeluri de funcții. Vom vedea cum putem folosi instrucțiunile call
și ret
pentru a realiza apeluri de funcții și cum folosim stiva pentru a transmite parametrii unei funcții.
Laboratorul este de forma learn by doing, partea practică alternând între secțiuni de tip tutorial, cu parcurgere pas cu pas și prezentarea soluției, și exerciții care trebuie să fie rezolvate.
call
și ret
pentru a realiza un apel de funcțieCand vine vorba de a chema o functie cu parametri exista doua mari optiuni de plasare a acestora:
1. Plasarea in registre - aceasta metoda, in mod intuitiv, presupune transmiterea parametrilor cu ajutorul registrelor.
Avantaje
Dezavantaje
2.Plasarea pe stiva - aceasta metoda presupune push-uirea pe stiva a tuturor parametrilor.
Avantaje
Dezavantaje
Atunci când apelăm o funcție, pașii sunt următorii:
După cum știm, operațiile pe stivă sunt de două tipuri:
push val
în care valoarea val
este plasată pe stivăpop reg/mem
în care ce se găsește în vârful stivei se plasează în registru sau într-o zonă de memorie
În momentul în care se face push
spunem că stiva crește (se adaugă elemente). În mod oarecum paradoxal însă, pointerul de stivă (indicat de registrul esp
pe 32 de biți) scade. Acest lucru se întâmplă întrucât stiva crește în jos, de la adrese mari către adrese mici.
La fel, în momentul în care facem pop
spunem că stiva scade (se scot elemente). Acum pointer-ul de stivă (indicat de registrul esp
pe 32 de biți) crește.
Un sumar al acestui lucru este explicat foarte bine la acest link: https://en.wikibooks.org/wiki/X86_Disassembly/The_Stack
Spre exemplu, daca avem functia foo cu urmatoarea semnatura (in limbaj C):
int foo(int a, int b, int c);
Apelul acestei functii va arata astfel:
mov ecx, [c] ; luam valoarea parametrului c dintr-o zona de memorie mov ebx, [b] mov eax, [a] push ecx ; punem parametrii in ordine inversa, incepand cu c push ebx ; apoi b push eax ; apoi a call foo ; apelam functia add esp, 12
În acest laborator vom introduce noțiunea de structură din limbajul assembly și vom lucra cu operații specializate pe șiruri.
Structurile sunt folosite pentru a grupa date care au tipuri diferite, dar care pot fi folosite împreună pentru a crea un tip compus.
În continuare vom trece prin pașii necesari pentru a folosi o structură: declararea, instanțierea și accesarea câmpurilor unei structuri.
În NASM, o structură se declară folosind construcția struc <nume structura>
, urmată de o listă de câmpuri și încheiată cu endstruc
.
Fiecare câmp al structurii este definit prin următoarele: o etichetă (folosită pentru a putea accesa membrii), specificatorul de tip și numărul de elemente.
Exemplu:
struc mystruct a: resw 1 ; a va referi un singur element de dimensiune un cuvânt b: resd 1 ; b va referi un singur element de dimensiune un dublu cuvânt c: resb 1 ; c va referi un singur element de dimensiune un octet d: resd 1 ; d va referi un singur element de dimensiune un dublu cuvânt e: resb 6 ; e va referi 6 elemente de dimensiune un octet endstruc
res
pentru a defini tipul de date și numărul de elemente pentru fiecare dintre câmpurile structurii. Pentru mai multe detalii despre sintaxa res
urmați acest link: https://www.nasm.us/doc/nasmdoc3.html#section-3.2.2
Fiecare etichetă ce definește un câmp reprezintă offset-ul câmpului în cadrul structurii. De exemplu, b
va avea valoarea 2, deoarece sunt 2 octeți de la începutul structurii până la câmpul b
(primii 2 octeți sunt ocupați de cuvântul a
).
.
(dot) astfel:
struc mystruct1 .a: resw 1 .b: resd 1 endstruc struc mystruct2 .a: resd 16 .b: resw 1 endstruc
Folosiți contrucția mystruct2.b
pentru aflarea valorii offset-ului lui 'b' din cadrul structurii mystruct2.
O primă variantă pentru a avea o structură în memorie este de a declara-o static în secțiunea .data
. Sintaxa folosește macro-urile NASM istruc
și iend
și keyword-ul at
.
În exemplul următor este prezentată instanțierea statică a structurii declarate mai sus, unde struct_var
este adresa din memorie de unde încep datele.
struct_var: istruc mystruct at a, dw -1 at b, dd 0x12345678 at c, db ' ' at d, dd 23 at e, db 'Gary', 0 iend
În cazul în care definiți câmpurile structurii folosind . (dot), instanțierea structurii se face în felul următor:
struct_var: istruc mystruct at mystruct.a, dw -1 at mystruct.b, dd 0x12345678 at mystruct.c, db ' ' at mystruct.d, dd 23 at mystruct.e, db 'Gary', 0 iend
Pentru a accesa și/sau modifica un anumit membru al structurii instanțiate trebuie să îi cunoaștem adresa. Această adresă se poate obține calculând suma dintre adresa de început a structurii și offset-ul din cadrul structurii al membrului dorit.
Următoarea secvență de cod prezintă punerea unei valori în câmpul b al structurii și, ulterior, afișarea valorii acestui câmp.
mov eax, 12345 mov dword [struct + b], eax ; adresa câmpului b este adresa de bază a structurii instanțiate static + offset-ul câmpului (dat de eticheta 'b') mov ebx, dword [struct + b] ; punerea valorii din câmpul b în registrul ebx pentru afișare PRINTF32 `%d\n\x0`, ebx
Putem considera un vector ca o înșiruire de elemente de același tip, plasate contiguu în memorie. Ați observat ceva similar în laboratoarele trecute când declaram static șiruri de caractere în secțiunea .data.
În general, datele statice declarate pot fi inițializate sau neinițializate. Diferențierea se face atât prin faptul că la datele inițializate oferim o valoare inițială, dar și prin sintaxa NASM folosită. De exemplu, pentru a declara un vector de 100 de cuvinte inițializate cu valoarea 42, vom folosi construcția:
section .data myVect: times 100 dw 42
Pe de altă parte, dacă dorim declararea unui vector de 20 de elemente dublu cuvinte neinițializate, folosim instrucțiuni din familia res
astfel:
section .bss myVect: resd 20
Adesea vom avea nevoie de vectori care să conțină elemente de dimensiuni mai mari decât cea a unui cuvânt dublu. Pentru a obține acest lucru vom combina cele două concepte prezentate anterior și vom folosi vectori de structuri. Bineînțeles, instrucțiunile de operare pe șiruri nu vor funcționa, deci vom fi nevoiți să ne întoarcem la metoda clasică de accesare a elementelor: cea prin adresarea explicită a memoriei.
Pentru exemplul din această secțiune, creăm o structură ce reprezintă un punct într-un spațiu 2D.
struc point .x: resd 1 .y: resd 1 endstruc
Deoarece NASM nu suportă niciun mecanism pentru a declara explicit un vector de structuri, va trebui să declarăm efectiv o zonă de date în care să încapă vectorul nostru.
Considerând că ne dorim un vector zeroizat de 100 de elemente de tipul structurii point (care este de dimensiune 8 octeți), trebuie să alocăm 100 * 8 (= 800) octeți.
Obținem:
section .data pointArray: times 800 db 0
În plus, NASM oferă o alternativă la calculul “de mână” al dimensiunii unei structuri, generând automat macro-ul <nume structura>_size
. Astfel, exemplul anterior poate deveni:
section .data pointArray: times point_size * 100 db 0
Dacă ne dorim să declarăm un vector de structuri neinițializat putem folosi:
section .bss pointArray: resb point_size * 100
Cum am mai spus, pentru accesarea câmpului unui element dintr-un vector trebuie să folosim adresarea normală (în particular adresarea “based-indexed with scale”). Formula pentru aflarea adresei elementului este baza_vector + i * dimensiune_struct
.
Presupunând că avem în registrul ebx
adresa de început a vectorului și în eax
indicele elementului pe care dorim să îl accesăm, exemplul următor prezintă afișarea valorii câmpului y a acestui element.
mov ebx, pointArray ; mutăm în ebx adresa de început a șirului mov eax, 13 ; să zicem că vrem al 13-lea element mov edx, [ebx + point_size * eax + point.y] ; se calculează adresa câmpului dorit între [] ; și apoi se transferă valoarea de la acea adresă ; în registrul edx PRINTF32 `%u\n\x0`, edx
Parcurgem vectorul, având la fiecare iterație indicele curent în registrul eax. Putem să afișăm valorile din ambele câmpuri ale fiecărui element din vector cu următorul program:
struc point .x: resd 1 .y: resd 1 endstruc section .data pointArray: times point_size * 100 db 0 section .text global CMAIN CMAIN: push ebp mov ebp, esp xor edx, edx xor eax, eax label: mov edx, [pointArray + point_size * eax + point.x] ; accesăm membrul x PRINTF32 `%u\n\x0`, edx mov edx, [pointArray + point_size * eax + point.y] ; accesăm membrul y PRINTF32 `%u\n\x0`, edx inc eax ; incrementarea indicelui de iterare cmp eax, 100 jl label leave ret
git pull origin master
din interiorul directorului în care se află repository-ul (~/Desktop/iocla
). Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă. Dacă doriți să descărcați repository-ul în altă locație, folosiți comanda git clone https://github.com/systems-cs-pub-ro/iocla ${target}
.
Pentru mai multe informații despre folosirea utilitarului git
, urmați ghidul de la Git Immersion.
Pornind de la fișierul fibo_sum.asm
, implementați un program care calculează suma primelor N numere din șirul fibonacci utilizând instrucțiunea loop
. Suma primelor 9 este 54.
În programul print_structure.asm
sunt afișate câmpurile unei structuri.
Urmăriți codul, observați construcțiile și modurile de adresare a memoriei. Rulați codul.
Scrieți cod în cadrul funcției main
astfel încât să modificați câmpurile structurii sample_student pentru ca:
1993
22
323CA
TODO
codul pentru modificarea structurii.
.data
, este vorba să folosiți cod pentru a modifca structura.
group
(adică octetul/caracterul cu indexul 2).
În fișierul getter_setter_printf.asm
implementați funcțiile get_int
, get_char
, respectiv get_string
, ce vor returna valorile câmpurilor int_x
, char_y
, respectiv string_s
din structura my_struc
. Valorile vor fi returnate prin registrul eax
.
esp + 8
și pentru a fi folosit ca pointer, trebuie citită valoarea sa într-un registru (ex. registrul ebx
).
Output-ul programului după o rezolvare corectă este:
1000 a My string is better than yours
Urmăriți comentariile marcate cu TODO.
Mai departe, implementați funcțiile set_int
, set_char
, respectiv set_string
, ce vor suprascrie valorile câmpurilor int_x
, char_y
, respectiv string_s
din structura my_struc
cu noile valori date.
esp + 8
(primul parametru) și esp + 12
(al doilea parametru).
Output-ul programului după o rezolvare corectă este:
2000 b Are you sure?
Urmăriți comentariile marcate cu TODO
.
În funcția main
, afișați câmpurile structurii utilizând apeluri ale funcției printf
. Verificați că programul afișază valorile corespunzătoare cu, respectiv fără, folosirea funcțiilor set_*
. Puteți folosi formaturile de la liniile 10-12 pentru a printa câmpurile.
Găsiți toate aparițiile subșirului substring
în șirul source_text
din fișierul find_substring.asm
.
Afișați rezultatele sub forma:
Substring found at index: <N>
strstr
(sau similar) pentru acest subpunct.
PRINTF32
, cât și funcția printf
, ca la exercițiile anterioare. Pașii pentru afișare folosind printf
sunt următorii:
print_format
printf
esp
(fiecare dintre parametri are 4 octeți).
Soluțiile pentru exerciții sunt disponibile: aici
git pull origin master
din interiorul directorului în care se află repository-ul (~/Desktop/iocla
). Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă.Dacă doriți să descărcați repository-ul în altă locație, folosiți comanda git clone https://github.com/systems-cs-pub-ro/iocla ${target}
.Pentru mai multe informații despre folosirea utilitarului git
, urmați ghidul de la Git Immersion.
Completați fișierul fibo.asm
din arhivă pentru a realiza un program care afișează primele N numere din șirul lui Fibonacci.
Aveți voie să folosiți doar memorie alocată pe stivă.
Deschideți fișierul hello-world.asm
, asamblați-l și rulați-l. Observați afișarea mesajului Hello, world!
Remarcați că:
hello-world.asm
folosește apelul funcției puts
(funcție externă modulului curent) pentru a efectua afișarea. Pentru aceasta pune argumentul pe stivă și apelează funcția.msg
din programul hello-world.asm
conține octetul 10
. Acesta simbolizează caracterul line-feed, mai cunoscut și sub forma \n
, folosit pentru a adăuga o linie nouă pe Linux.
Încheierea cu \n
este, în general, utilă pentru afișarea șirurilor. Funcția puts
pune automat o linie nouă după șirul afișat, însă aceasta trebuie adăugată explicit în cazul folosirii funcției printf
.
După cum spuneam, în final, totul ajunge în limbaj de asamblare (ca să fim 100% corecți, totul ajunge cod mașină care are o corespondență destul de bună cu codul asamblare). Adesea ajungem să avem acces doar la codul obiect al unor programe și vrem să inspectăm modul în care arată.
Pentru a observa acest lucru, haideți să compilăm până la codul obiect un program scris în C și apoi să-l dezasamblăm. Este vorba de programul test.c
din arhiva de laborator.
Ctrl+Alt+T
)gcc -m32 -o <executabil> <nume-fisier>
unde <nume-fisier>
este numele fișierului iar <executabil>
este executabilul rezultat.
gcc -m32 -c -o <fisier-obiect> <nume-fisier>
unde <nume-fisier>
este numele fișierului iar <fisier-obiect>
este fișierul obiect rezultat.
În cazul nostru, întrucât dorim doar să compilăm fișierul test.c
la modulul obiect, vom accesa din terminal directorul în care se găsește fișierul și apoi vom rula comanda
gcc -m32 -c -o test.o test.c
În urma rulării comenzii de mai sus în directorul curent vom avea fișierul obiect test.o
.
Putem obține și forma în limbaj de asamblare a acestuia folosind comanda
gcc -m32 -masm=intel -S -o test.asm test.c
În urma rulării comenzii de mai sus obținem fișierul test.asm
pe care îl putem vizualiza folosind comanda
cat test.asm
Pentru a dezasambla codul unui modul obiect vom folosi un utilitar frecvent întâlnit în lumea Unix: objdump
. Pentru dezasamblare, vom rula comanda
objdump -M intel -d <path-to-obj-file>
unde <path-to-obj-file>
este calea către fișierul obiect test.o
.
Veți obține un output similar celui de mai jos
2-test] $ objdump -M intel -d test.o test.o: file format elf32-i386 Disassembly of section .text: 0000054d <second_func>: 54d: 55 push ebp 54e: 89 e5 mov ebp,esp 550: e8 a6 00 00 00 call 5fb <__x86.get_pc_thunk.ax> 555: 05 ab 1a 00 00 add eax,0x1aab 55a: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 55d: 8b 10 mov edx,DWORD PTR [eax] 55f: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 562: 01 c2 add edx,eax 564: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 567: 89 10 mov DWORD PTR [eax],edx 569: 90 nop 56a: 5d pop ebp 56b: c3 ret 0000056c <first_func>: 56c: 55 push ebp 56d: 89 e5 mov ebp,esp 56f: 53 push ebx 570: 83 ec 14 sub esp,0x14 573: e8 83 00 00 00 call 5fb <__x86.get_pc_thunk.ax> 578: 05 88 1a 00 00 add eax,0x1a88 57d: c7 45 f4 03 00 00 00 mov DWORD PTR [ebp-0xc],0x3 584: 83 ec 0c sub esp,0xc 587: 8d 90 80 e6 ff ff lea edx,[eax-0x1980] 58d: 52 push edx 58e: 89 c3 mov ebx,eax 590: e8 4b fe ff ff call 3e0 <puts@plt> 595: 83 c4 10 add esp,0x10 598: 83 ec 08 sub esp,0x8 59b: ff 75 f4 push DWORD PTR [ebp-0xc] 59e: 8d 45 08 lea eax,[ebp+0x8] 5a1: 50 push eax 5a2: e8 a6 ff ff ff call 54d <second_func> 5a7: 83 c4 10 add esp,0x10 5aa: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 5ad: 8b 5d fc mov ebx,DWORD PTR [ebp-0x4] 5b0: c9 leave 5b1: c3 ret 000005b2 <main>: 5b2: 8d 4c 24 04 lea ecx,[esp+0x4] 5b6: 83 e4 f0 and esp,0xfffffff0 5b9: ff 71 fc push DWORD PTR [ecx-0x4] 5bc: 55 push ebp 5bd: 89 e5 mov ebp,esp 5bf: 53 push ebx 5c0: 51 push ecx 5c1: e8 8a fe ff ff call 450 <__x86.get_pc_thunk.bx> 5c6: 81 c3 3a 1a 00 00 add ebx,0x1a3a 5cc: 83 ec 0c sub esp,0xc 5cf: 6a 0f push 0xf 5d1: e8 96 ff ff ff call 56c <first_func> 5d6: 83 c4 10 add esp,0x10 5d9: 83 ec 08 sub esp,0x8 5dc: 50 push eax 5dd: 8d 83 8e e6 ff ff lea eax,[ebx-0x1972] 5e3: 50 push eax 5e4: e8 e7 fd ff ff call 3d0 <printf@plt> 5e9: 83 c4 10 add esp,0x10 5ec: b8 00 00 00 00 mov eax,0x0 5f1: 8d 65 f8 lea esp,[ebp-0x8] 5f4: 59 pop ecx 5f5: 5b pop ebx 5f6: 5d pop ebp 5f7: 8d 61 fc lea esp,[ecx-0x4] 5fa: c3 ret
Există multe alte utilitare care permit dezasamblare de module obiect, majoritatea cu interfața grafică și oferind și suport pentru debugging. objdump
este un utilitar simplu care poate fi rapid folosit în linia de comandă.
Este interesant de urmărit, atât în fișierul test.asm
cât și în dezasamblarea sa, modul în care se face un apel de funcție, lucru despre care vom discuta în continuare.
Pentru afișarea unui string putem folosi macro-ul intern PRINTF32
. Sau putem folosi o funcție precum puts
. În fișierul print-string.asm
este implementată afișarea unui string folosind macro-ul PRINTF32
.
Urmărind fișierul hello-world.asm
ca exemplu, implementați afișarea șirului folosind și puts
.
Programul print-string-len.asm
afișează lungimea unui șir folosind macro-ul PRINTF32
. Calculul lungimii șirului mystring
are loc în cadrul programului (este deja implementat).
Implementați programul pentru a face afișarea lungimii șirului folosind funcția printf
.
La sfârșit veți avea afișată de două ori lungimea șirului: inițial cu apelul macro-ului PRINTF32
și apoi cu apelul funcției externe printf
.
printf
este de forma printf("String length is %u\n", len);
. Trebuie să construiți stiva pentru acest apel.
Pașii de urmat sunt:
printf
ca simbol extern."String length is %u", 10, 0
.printf
, adică:printf
folosind call
.
Lungimea șirului se găsește în registrul ecx
.
În soluția de mai sus adăugați funcția reverse_string
astfel încât să aveți un listing similar celui de mai jos:
[...] section .text global main reverse_string: push ebp mov ebp, esp mov eax, dword [ebp+8] mov ecx, dword [ebp+12] add eax, ecx dec eax mov edx, dword [ebp+16] copy_one_byte: mov bl, byte [eax] mov byte [edx], bl dec eax inc edx loopnz copy_one_byte inc edx mov byte [edx], 0 leave ret main: push ebp mov ebp, esp [...]
reverse_string
în programul vostru, rețineți că fucția începe la eticheta reverse_string
și se oprește la eticheta main
. Eticheta copy_one_byte
este parte a funcției reverse_string
.
Funcția reverse_string
inversează un șir și are următoarea signatură: void reverse_string(const char *src, size_t len, char *dst);
. Astfel ca primele len
caractere și șirul src
sunt inversate în șirul dst
.
Realizați inversarea șirului mystring
într-un nou șir și afișați acel nou șir.
store_string times 64 db 0
Construcția creează un șir de 64 de octeți de zero, suficient pentru a stoca inversul șirului.
Apelul echivalent în C al funcției este reverse_string(mystring, ecx, store_string);
. În registrul ecx
am presupus că este calculată lungimea șirului.
Nu puteți folosi direct valoarea ecx
în forma ei curentă. După apelul funcției printf
pentru afișare numărului valoarea ecx
se pierde. Ca să o păstrați, aveți două opțiuni:
ecx
în prealabil pe stivă (folosind push ecx
înaintea apelului printf
) și apoi să o restaurați după apelul printf
(folosind pop ecx
).ecx
într-o variabilă globală, pe care o definiți în secțiunea .data
.
Nu puteți folosi un alt registru pentru că sunt șanse foarte mari ca și acel registru să fie modificat de apelul printf
pentru afișarea lungimii șirului.
Ne propunem implementarea funcției toupper
care traduce literele mici în litere mari. Pentru aceasta, porniți de la fișierul toupper.asm
din arhiva de exerciții a laboratorului și completați corpul funcției toupper
.
Șirul folosit este mystring
și presupunem că este un șir valid. Acest șir este transmis ca argument funcției toupper
în momentul apelului.
Faceți înlocuirea in place, nu este nevoie de un alt șir.
0x20
din valoare. Aceasta este diferența între litere mici și mari; de exemplu a
este 0x61
iar A
este 0x41
. Puteți vedea în pagina de manual ascii.
Ca să citiți sau să scrieți octet cu octet folosiți construcția byte [reg]
așa cum apare și în implementarea determinării lungimii unui șir în fișierul print-string-len.asm
, unde [reg]
este registrul de tip pointer în care este stocată adresa șirului în acel punct.
Vă opriți atunci când ați ajuns la valoarea 0
(NULL
byte). Pentru verificare puteți folosi test
așa cum se întâmplă și în implementarea determinării lungimii unui șir în fișierul print-string-len.asm
.
Implementați funcția toupper
astfel încât translatarea să aibă loc doar pentru caractare reprezentând litere mici, nu litere mari sau alte tipuri de caractere.
Realizați și folosiți o funcție care face translatarea rot13 a unui șir.
Implementați rot13
pe un array de șiruri: șirurile sunt continue în memorie separate prin terminatorul de șir (NULL
-byte, 0
). De exemplu: ana\0are\0mere\0
este un array de trei șiruri.
Aplicați rot13
pe caracterele alfabetice și înlocuiți terminatorul de șir cu spațiu (' '
, blank, caracterul 32
sau 0x20
). Astfel, șirul inițial ana\0are\0mere\0
se va traduce în nan ner zrer
.
mystring db "ana", 0, "are", 0, "mere", 0
.data
, de forma
len dd 10
în care să rețineți fie lungimea totală a șirului (de la începutul până la ultimul NULL
-byte), fie numărul de șiruri din array.
Soluțiile pentru exerciții sunt disponibile aici.