Laborator 07: Lucrul cu stiva

În acest laborator vom învăța cum este reprezentată stiva in limbajul de asamblare, care este utilitatea ei si cum se programeaza cu ajutorul acesteia.

Terminologie

In lumea algoritmicii, stiva este o structura de date abstracta prin intermediul careia se poate reprezenta informatie ce respecta regula “primul venit, ultimul servit”.

In lumea programarii assembly, stiva este o zona rezervata de memorie folosita pentru a tine evidenta operatiilor interne ale unui program: functii, adrese de retur, parametrii pasati, etc. Astfel, stiva poate fi vazuta ca o implementare la nivel hardware a stivei abstracte.

Utilitatea stivei

  • Apeluri de functii. Fiecare apel de functie necesita, printre altele, memorarea adresei de retur. Imaginati-va ca aveti un numar de functii N ce se apeleaza succesiv : prima functie o apeleaza pe a 2-a, a 2-a pe a 3-a si asa mai departe; cand functia N a fost apelata si si-a terminat executia, programul trebuie sa continue in contextul in care a avut loc apelul. Daca retinem adresele de retur intr-o stiva, ordinea apelurilor se pastreaza in mod natural.
  • Variabile temporare. In general, programele folosesc un numar mare de variabile si rezultate partiale; avand in vedere ca exista un numar limitat de registre, este posibil ca la un moment dat, sa nu avem niciun registru disponibil pentru a retine valoarea unei operatii. In cazul acesta, putem memora rezultatul la o adresa de memorie pe care o definim in zona de date. Aceasta metoda are dezavantajul ca vom “polua” tabela de offset-uri cu variabile pe care le folosim o singura data. In loc sa facem aceasta pentru fiecare rezultat partial, este mai usor sa punem pe stiva valoarea unui registru pentru a il putea folosi si a o recupera in momentul in care nu mai folosim variabila temporara.

Operatii asupra stivei

Stiva poate fi modificata in 2 moduri:

1. Prin utilizarea instructiunilor special implementate pentru lucrul cu stiva, dintre care cele mai uzuale sunt push si pop:

%include "io.inc"
 
section .text
global CMAIN
CMAIN:                        
 
    mov eax, 7
    mov ebx, 8
    add eax, ebx
    push eax            ; pune pe stiva continutul registrului eax
    mov eax, 10         ; acum putem folosi registrul, intrucat valoarea lui este salvata pe stiva
    PRINT_UDEC 4, eax   ; 10
    NEWLINE
 
    pop eax             ; recupereaza valoarea registrului eax
    PRINT_UDEC 4, eax   ; 15

2. Adresand memoria, cu ajutorului registrului in care este tinut pointerul catre capul stivei (“stack pointer”) esp:

%include "io.inc"
 
section .text
global CMAIN
CMAIN:                        
    mov eax, 7
    mov ebx, 8
    add eax, ebx
    sub esp, 4           ; rezerva 4 octeti pe stiva
    mov [esp], eax       ; muta la noua adresa catre care pointeaza esp continutul registrului eax
    mov eax, 10
    PRINT_UDEC 4, eax
    NEWLINE
 
    mov eax, [esp]       ; recupereaza valoarea de pe stiva
    add esp, 4           ; restabileste valoarea registrului esp
    PRINT_UDEC 4, eax

Comentati instructiunile sub esp, 4 si add esp, 4. Ce se intampla? De ce?

Stiva este folosita pentru a memora adresa de retur in momentul in care o functie este apelata

Remarcati faptul ca stiva creste de la adrese mari la adrese mici. Acesta este motivul pentru care alocarea memoriei pe stiva se face folosind instructiunea sub, iar eliberarea se face folosind instructiunea add.

Unele procesoare nu au suport pentru lucrul cu stiva: spre exemplu, procesoarele MIPS nu au instructiuni push si pop si nici un registru special pentru stack pointer. Astfel, daca am dori sa implementam operatiile pe stiva in procesorul MIPS aceasta s-ar realiza exact ca in exemplul de mai sus, doar ca am putea sa ne alegem noi orice registru pentru a tine minte stack pointerul.

Asadar, instructiunea push eax, pe un procesor x86, este echivalenta cu:

sub esp, 4
mov [esp], eax

Iar instructiunea pop eax:

mov eax, [esp]
add esp, 4

Tricks and tips

1. Regula de aur a utilizarii stivei este : numarul de push-uri == numarul de pop-uri intr-o functie. Avand in vedere ca stiva este folosita pentru apelarea functiilor, este foarte important ca in momentul in care o functie isi termina executia, sa actualizeze esp-ul astfel incat acesta sa indice catre aceeasi zona de memorie (a stivei) catre care indica in momentul intrarii in functie.

2. Avand in vedere ca exista situatii in care facem un numar N de push-uri si ajungem la finalul functiei fara sa fi facut pop pentru niciuna dintre valori, putem restabili capul stivei folosind instructiunea add.

%include "io.inc"
 
section .text
global CMAIN
CMAIN:
    mov eax, 5
    mov ebx, 6
    mov ecx, 7
 
    push eax
    push ebx
    push ecx
 
    add esp, 12     ; echivalent cu utilizarea a 3 pop-uri consecutive
    ret

3. Metoda de mai sus are dezavantajul ca tot trebuie sa cautam prin program cate push-uri am facut (ceea ce poate sa necesite destul de mult timp in programele din viata reala). Daca nu vrem sa nu ne batem deloc capul cu stack pointerul, putem sa folosim urmatoarea constructie:

%include "io.inc"
 
section .text
global CMAIN
CMAIN:
 
    mov ebp, esp       ; salveaza stack pointerul curent
 
    mov eax, 5
    mov ebx, 6
    mov ecx, 7
 
    push eax
    push ebx
    push ecx
 
    mov esp, ebp       ; restaureaza stack pointerul
    ret

Care este intrebuintarea principala a registrului ebp?

Exerciții

În cadrul exercițiilor vom folosi arhiva de laborator.

Descărcați arhiva, decomprimați-o și accesați directorul aferent.

[1p] 0. Recapitulare: Media aritmetică a elementelor dintr-un vector

Pornind de la exercițiul 0-recap-mean.asm din arhiva de laborator, implementați codul lipsă, marcat de comentarii de tip TODO, pentru a realiza un program care calculează media aritmetică a elementelor dintr-un vector. Afișați doar partea întreagă a mediei (câtul împărțirii).

Dacă ați făcut calculul corect, suma elementelor vectorului va fi 3735 iar media aritmetică a elementelor din vector va fi 287 (fără zecimale). Dacă veți avea o afișare de forma 287.287287... este bine.

[2p] Optional: afisati partea fractionara a mediei. Media va fi afișată cu un număr variabil X de zecimale (în exemplu, X = DECIMAL_PLACES = 5).

Pentru calculul zecimalelor, se înmulțește succesiv restul împărțirii anterioare cu 10 și se împarte la același împărțitor (în cazul de față, împărțitorul este numărul de elemente din vector). Câtul împărțirii constituie zecimala curentă.

În timpul rezolvării, folosiți-vă de un calculator pentru a vă verifica.

[1p] 1. Max

Calculați maximul dintre numerele din 2 registre (eax și ebx) folosind o instrucțiune de comparație, o instrucțiune de salt și instrucțiuni push/pop.

Gandiți-vă cum puteți să interschimbați două registre folosind stiva.

[1.5p] 2. Construirea array-ului inversat

Pornind de la exercițiul reverse-array.asm, implementați TODO-urile fără a folosi instrucțiunea mov în lucrul cu array-urile astfel încât în array-ul output la finalul programului să se afle array-ul input inversat.

Dupa o rezolvare corecta programul ar trebui sa printeze:

Reversed array:
911
845
263
242
199
184
122

[3p] 3. Adresarea si printarea stivei

Programul stack-addressing.asm din arhiva laboratorului alocă și inițializează două variabile locale pe stivă:

  • un array format din numerele naturale de la 1 la NUM
  • un string “Ana are mere”.
  1. [1p] Înlocuiți fiecare instrucțiune push cu o secvență de instrucțiuni echivalentă.
  2. [1.5p] Printați adresele și valorile de pe stivă din zona [ESP, EBP].
  3. [0.5p] Printați string-ul alocat pe stivă și explicați cum arată acesta în memorie.

Citiți sectiunea Operatii asupra stivei.

După o implementare cu succes, programul ar trebui să afișeze ceva de genul (nu fix același lucru):

0xfff8e92c: 0xf7e08637
0xfff8e928: 0x5
0xfff8e924: 0x4
0xfff8e920: 0x3
0xfff8e91c: 0x2
0xfff8e918: 0x1
0xfff8e914: 0x0
0xfff8e910: 0x6572656d
0xfff8e90c: 0x20657261
0xfff8e908: 0x20616e41

[1.5p] 4. Variabile locale

Programul merge-arrays.asm din cadrul arhivei de laborator, îmbină două array-uri sortate crescător (array_1 și array_2) punând array-ul rezultat în array_output definit în secțiunea .data.

Modificați programul astfel încat array_output să fie alocat pe stivă.

[2p] 5. GCD - Greatest Common Divisor

Deschideți gcd.asm și rulați programul. Codul calculează cel mai mare divizor comun dintre două numere date ca și parametru prin registrele eax și edx, și pune valoarea aflată tot în registrul eax.

  1. [1p] Faceți modificările necesare astfel încat mesajul de eroare - The program crashed! - să nu mai apară.
  2. [1p] În cadrul label-ului print afișați rezultatul sub forma:
gdc(49,28)=7
iocla/laboratoare/laborator-07.txt · Last modified: 2018/11/12 13:00 by razvan.deaconescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0