Sunteți pe pagina 1din 380

Programarea procesoarelor IA-32

în limbaj de asamblare
Ștefan Gabriel Șoriga Iulian Bădescu

UNIVERSITATEA POLITEHNICA BUCUREȘTI

PROGRAMAREA PROCESOARELOR IA-32 ÎN


LIMBAJ DE ASAMBLARE
PREFAȚĂ

Limbajul de asamblare reprezintă o metodă de codificare simbolică, relativ


uşor de citit şi de interpretat de către om, a instrucţiunilor în format binar executate
de procesor. Spre deosebire de alte limbaje de programare, limbajul de asamblare
nu reprezintă un limbaj de sine stătător, ci, mai degrabă, o categorie de limbaje.
Fiecare familie de procesoare are propriul limbaj în asamblare. Din această relaţie
de cauzalitate rezultă că programatorul, pe lângă noţiunile specifice limbajului,
trebuie să deţină şi cunoştinţe minime legate de structura internă a procesorului.
Devine evident că programarea în limbaj de asamblare este mult mai dificilă decât
programarea într-un limbaj de nivel înalt. De fapt, limbajul de asamblare trebuie
utilizat numai atunci când este absolut necesar.
Deoarece asamblarea este o etapă intermediară a procesului de compilare,
instrumentele folosite la translatarea codului scris în limbaj de nivel înalt în limbaj
maşină sunt capabile să gestioneze şi programe hibride, în care codul limbajului de
nivel înalt conţine secvenţe scrise în limbaj de asamblare. Altfel spus, compilatorul
acceptă introducerea de linii scrise în limbaj de asamblare direct în textul sursă al
limbajului de nivel înalt, iar editorul de legături poate combina module obiect
generate din surse scrise în limbaj de nivel înalt cu module obiect obţinute din
surse scrise în limbaj de asamblare. Aceste construcţii hibride apar din câteva
considerente, dintre care amintim:
• optimizări – atunci când sunt necesare secvenţe critice sub aspectul
timpului de execuţie şi consumului de memorie;
• acces la instrucţiuni specifice procesorului;

În prezent, chiar şi în aceste situaţii, eficienţa compilatoarelor a crescut


până la nivelul în care acestea concurează cu orice programator în asamblare, cu
excepţia unuia foarte bun, iar evoluţiile tehnologice fac ca avantajul codului maşină
optimizat prin instrucţiuni în asamblare să fie minim. Pe de altă parte, odată cu
răspândirea unor platforme relativ sărace în resurse de procesare şi memorie, cum
ar fi telefoanele mobile inteligente sau microcontrolere, se preconizează o creştere
a cererii de specialişti capabili să folosească cât mai eficient resursele existente.
Totodată, sunt şi situaţii în care programarea în limbaj de asamblare nu poate fi
evitată. De exemplu, anumite componente ale sistemului de operare au restricţii
stricte în ce priveşte performanţa şi consumul de resurse. Acestea nu se pot realiza
decât prin utilizarea cât mai eficientă a instrucţiunilor şi caracteristicilor
procesorului. În general, programatorii de sistem au nevoie de cunoştinţe avansate
de programare în limbaj de asamblare.
În cazul programatorilor de aplicaţii, principalele motive pentru care se
recomandă experienţa programării în limbaj de asamblare constă în familiarizarea
cu modul de organizare a programelor în memorie, cu principiile de funcţionare a
diverselor componente hardware sau cu rolul sistemului de operare. Toate ajută
programatorul de aplicaţii să scrie programe mai eficiente. De asemenea, depanarea
unui program de nivel înalt poate depăşi nivelul textului sursă, ajungându-se la
depanarea codului obiect, proces care necesită cunoaşterea limbajului de
asamblare. Evident, dacă se doreşte ca o aplicaţie să profite cât mai mult de
beneficiile limbajului de asamblare (cod compact şi rapid, consum de resurse
minim), aceasta poate fi scrisă complet în limbaj de asamblare. Un astfel de caz
este aplicaţia antivirus NOD32.
Cartea de faţă abordează limbajul de asamblare corespunzător familiei de
procesoare Intel de 32 de biţi, de la 80386 până la Pentium 4. Indiferent de
particularităţile limbajului de asamblare specific arhitecturilor Intel, noţiunile de
bază şi principiile generale se aplică şi altor categorii de procesoare şi chiar
microcontrolerelor. Gradul de complexitate al informaţiilor creşte gradual, pe
parcursul desfăşurării capitolelor. Publicul ţintă este reprezentat de studenţii de
anul II de la specializarea Telecomenzi şi electronică în transporturi din cadrul
Facultăţii Transporturi, dar şi de studenţi de la alte facultăţi, de exemplu, Facultatea
de Automatică şi Calculatoare sau Electronică şi Telecomunicaţii, care au în
programă cursuri de Arhitectura microprocesoarelor sau Calculatoare şi sisteme de
operare. Lucrarea este dedicată celor care iau contact pentru prima dată cu limbajul
de asamblare şi, chiar dacă publicul ţintă a urmat cel puţin un curs introductiv de
programare, nu se fac presupuneri cu privire la cunoştinţele acumulate anterior.
Conceptele sunt prezentate simplu şi concis, natura intrinsecă a domeniului
necesită oricum un grad ridicat de concentrare şi exerciţiu. Programele sunt la fel
de simple, orientate către ilustrarea conceptelor. În final, pentru a forţa cititorul să
editeze programele manual, am ales ca volumul să nu fie acompaniat de un CD. Se
ştie că tentaţia de a copia codul cu ajutorul mouse-ului e mare. Cititorul trebuie să
conştientizeze faptul că obiectivul său este să îşi însuşească informaţiile şi
conceptele prezente în acele programe, nu să le ruleze pur şi simplu. Autorii
recomandă parcurgerea secvenţială a cărţii, întotdeauna cu calculatorul în faţă.
Această carte nu se citeşte, se execută. Conceptele noi sunt prezentate pe baza celor
dinainte, iar capitolele sunt scrise astfel încât să conducă cât mai repede la exemple
practice. Cu o atitudine adecvată, la finalul cărţii, în cel mai pesimist scenariu,
cititorul va cunoaşte conceptele generale de dezvoltare a programelor în asamblare,
funcţiile sistemului de operare şi organizarea calculatorului din perspectiva unui
programator.
Considerăm că platforma hardware şi software cea mai indicată pentru
însuşirea conceptelor teoretice este cea reală. De aceea, toate uneltele de
programare discutate în această carte sunt disponibile pe orice distribuţie Linux.
Linux oferă un excelent mediu de studiu şi dezvoltare. În plus, contactul cu Linux
reprezintă o oportunitate în sine. Executabilele sunt generate şi rulate chiar de
procesorul calculatorului (cu excepţia cazului în care alegeţi să folosiţi o maşină
virtuală). Cerinţa obligatorie este să facă parte din familia Intel de 32 sau 64 de biţi.
Deoarece procesoarele moderne de 64 de biţi sunt „compatibile înapoi”, toate
informaţiile prezentate pentru arhitecturile de 32 de biţi rămân valabile.
Mediul de dezvoltare

În procesul de dezvoltare a programelor, majoritatea programatorilor


folosesc un mediu integrat de dezvoltare (IDE - Integrated Development
Environment). Acesta pune la dispoziţie toate uneltele necesare generării
executabilului din codul sursă, dar, totodată, ascunde detaliile acestui proces. În
această carte folosim componentele (editor de text, compilator, asamblor, editor de
legături, depanator) individual, astfel încât rolul fiecăruia să poată fi observat
direct. Sistemul de operare este Ubuntu, orice variantă între 10.04 şi 13.04.

Organizarea capitolelor

Primul capitol prezintă sistemele de numeraţie şi modul de reprezentare a


caracterelor alfanumerice în sistemele de calcul. Sunt oferite numai informaţii strict
necesare. Scopul este să ne însușim rapid un set de cunoştinţe minim pe baza căruia
să putem asimila, prin exemple practice, conceptele ulterioare.
Al doilea capitol prezintă arhitectura sistemelor de calcul, cu cele două
fațete ale sale, hardware și software. Subiectul este abordat din perspectiva
programatorului. Ne concentrăm asupra structurii interne a procesoarelor Intel de
32 de biţi.
Al treilea capitol prezintă limbajul de asamblare în contextul interacțiunii
utilizator - calculator și al poziței sale în ierarhia limbajelor de programare.
Capitolul poate fi privit ca o încercare de definire a limbajului de asamblare prin
gen și diferență specifică, unde genul este reprezentat de clasa limbajelor de
programare, iar diferența specifică de proprietățile caracteristice. Din acest capitol
se desprinde faptul că limbajul de asamblare este o etapă parcursă de compilatoare
în procesul de translatare a codului sursă în cod mașină. Aşadar, fie că dorim să-l
folosim sau nu, el este oricum utilizat de compilatoare în procesul de generare a
codului obiect.
Al patrulea capitol prezintă procesul de dezvoltare a programelor în limbaj
de asamblare şi uneltele utilizate în etapele acestuia. Sunt prezentate numai
informaţii esenţiale legate de limbajul de asamblare. Toate capitolele următoare vor
face referire la acestea.
Al cincilea capitol este dedicat conceptului central al programării în orice
limbaj, cu atât mai mult al programării în limbaj de asamblare: organizarea şi
adresarea memoriei principale.
Al şaselea capitol prezintă formatul instrucţiunilor maşină. Pe lângă aceste
informaţii, rolul acestui capitol este să familiarizeze cititorul cu modul de
prezentare a informaţiilor în documentaţia oficială Intel1.
Al şaptelea şi al optulea capitol acoperă toate instrucţiunile dedicate

1
The Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volumes
2A & 2B: Instruction Set Reference
operaţiilor cu numere întregi, respectiv cu numere reale.
Ultimele trei capitole acoperă noţiuni mai avansate, precum definirea
funcţiilor şi interacţiunea limbajului de asamblare cu sistemul de operare şi
limbajul de nivel înalt C.
Cuprins

1.! REPREZENTAREA INFORMAŢIEI ÎN SISTEMELE DE CALCUL14!


1.1.! Sisteme de numeraţie ......................................................................14!
1.1.1.! Sistemul zecimal......................................................................15!
1.1.2.! Sistemul binar ..........................................................................15!
1.1.3.! Sistemul hexazecimal ..............................................................16!
1.2.! Operații de conversie ......................................................................16!
1.2.1.! Conversia numerelor din zecimal în binar ..............................16!
1.2.2.! Conversia numerelor din zecimal în hexazecimal ...................17!
1.2.3.! Conversia numerelor din hexazecimal în binar .......................17!
1.2.4.! Conversia numerelor din binar în hexazecimal .......................18!
1.2.5.! Conversia numerelor reale.......................................................19!
1.3.! Reprezentarea caracterelor alfanumerice .......................................20!
1.4.! Exerciții ..........................................................................................22!

2.! ARHITECTURA CALCULATOARELOR..........................................24!


2.1.! Structura unui sistem de calcul .......................................................24!
2.1.1.! Arhitectura von Neumann .......................................................24!
2.1.2.! Modelul bazat pe magistrală....................................................26!
2.1.1.! Magistrala de sistem ................................................................27!
2.1.2.! Unitatea de intrare/ieşire .........................................................28!
2.1.3.! Memoria ..................................................................................29!
2.2.! Arhitectura IA-32 ...........................................................................30!
2.2.1.! Arhitectură şi microarhitectură ................................................31!
2.2.2.! Structura de principiu a procesorului ......................................33!
2.2.3.! Funcţionarea procesorului .......................................................37!
2.2.4.! Registrele procesorului ............................................................38!
2.2.5.! Întreruperile .............................................................................42!
2.3.! Exerciţii ..........................................................................................43!

3.! LIMBAJUL DE ASAMBLARE ...........................................................44!


3.1.! Tipuri de limbaje de programare ....................................................46!
3.1.1.! Limbajul maşină ......................................................................46!
3.1.2.! Limbajul de asamblare ............................................................48!
3.1.3.! Limbaje de nivel înalt ..............................................................48!
3.2.! Procesul de compilare.....................................................................51!
3.2.1.! Preprocesarea...........................................................................53!
3.2.2.! Compilarea ..............................................................................53!
3.2.3.! Asamblarea ..............................................................................55!
3.2.4.! Editarea legăturilor ..................................................................57!
3.2.5.! Formatul fişierelor executabile ................................................57!
3.3.! Avantajele limbajului de asamblare ...............................................58!
3.4.! Exerciţii ..........................................................................................59!

4.! DEZVOLTAREA PROGRAMELOR ÎN LIMBAJ DE ASAMBLARE


60!
4.1.! Sintaxa limbajului de asamblare .....................................................60!
4.2.! Structura programului .....................................................................61!
4.3.! Structura procesului ........................................................................64!
4.3.1.! Memoria virtuală în Linux ......................................................65!
4.4.! Tipuri de date ..................................................................................67!
4.4.1.! Date iniţializate........................................................................67!
4.4.2.! Date neiniţializate ....................................................................74!
4.5.! Procesul de dezvoltare al programelor ...........................................75!
4.5.1.! Editarea textului ......................................................................75!
4.5.2.! Asamblarea ..............................................................................81!
4.5.3.! Editarea legăturilor ..................................................................85!
4.5.4.! Automatizarea sarcinilor cu GNU Make .................................86!
4.5.5.! Execuţia programului ..............................................................89!
4.5.6.! Depanarea fişierului executabil ...............................................90!
4.6.! Întrebări şi exerciţii.......................................................................100!

5.! MEMORIA ..........................................................................................101!


5.1.! Declararea datelor în bloc .............................................................101!
5.2.! Adresarea memoriei în modul protejat .........................................103!
5.2.1.! Adresare imediată şi adresare la registre ...............................104!
5.3.! Optimizarea accesului la memorie ...............................................117!

6.! ARHITECTURA SETULUI DE INSTRUCŢIUNI ............................122!


6.1.! Simboluri „cheie” .........................................................................122!
6.2.! Codificarea instrucţiunilor ............................................................124!
6.2.1.! Spaţiul codurilor operaţionale ...............................................126!
6.2.2.! Octetul ModR/M ...................................................................128!
6.2.3.! Spaţiul codurilor 386 (0F + ...) ..............................................129!
6.2.4.! Prefix de dimensiune operand ...............................................130!
6.3.! Codificarea adreselor de memorie ................................................131!
6.3.1.! Octetul SIB ............................................................................132!
6.4.! Formatul instrucţiunilor ................................................................133!
6.4.1.! Prefixele de instrucţiune ........................................................133!
6.4.2.! Modurile de adresare prin octetul ModR/M ..........................134!
6.4.3.! Modurile de adresare prin octetul SIB ..................................137!
6.5.! Studiu de caz.................................................................................139!

7.! OPERAŢII CU NUMERE ÎNTREGI .................................................145!


7.1.! Reprezentarea numerelor cu semn................................................145!
7.1.1.! Reprezentarea cu bit de semn şi magnitudine .......................146!
7.1.2.! Reprezentarea în complement faţă de unu.............................146!
7.1.3.! Reprezentarea în complement faţă de doi .............................147!
7.2.! Extinderea întregilor .....................................................................151!
7.3.! Indicatori de stare .........................................................................153!
7.3.1.! Indicatorul de zero .................................................................153!
7.3.2.! Indicatorul de transport .........................................................154!
7.3.3.! Indicatorul de depăşire ..........................................................155!
7.3.4.! Indicatorul de semn ...............................................................156!
7.3.5.! Indicatorul de transport la jumătate .......................................156!
7.3.6.! Indicatorul de paritate ............................................................157!
7.4.! Instrucţiuni de transfer condiţionat ...............................................158!
7.5.! Operaţii aritmetice ........................................................................160!
7.5.1.! Instrucţiuni de adunare ..........................................................160!
7.5.2.! Instrucțiuni de scădere ...........................................................165!
7.5.3.! Instrucțiuni de comparare ......................................................170!
7.5.4.! Incrementare şi decrementare ................................................170!
7.5.5.! Instrucţiuni de înmulţire ........................................................171!
7.5.6.! Instrucţiuni de împărţire ........................................................172!
7.6.! Instrucţiuni de interschimbare a datelor .......................................173!
7.7.! Instrucțiuni de prelucrare la nivel de bit .......................................178!
7.7.1.! Instrucţiuni logice ..................................................................178!
7.7.2.! Instrucţiuni de deplasare ........................................................181!
7.7.3.! Instrucţiuni de rotație.............................................................185!
7.7.4.! Instrucţiuni de testare şi modificare a unui bit ......................185!
7.7.5.! Instrucţiuni de scanare pe bit .................................................186!
7.7.6.! Instrucţiuni de setare condiţionată .........................................186!
7.8.! Instrucțiuni de transfer al controlului ...........................................187!
7.8.1.! Instrucţiuni de salt necondiţionat ..........................................188!
7.8.2.! Instrucţiuni de salt condiţionat ..............................................189!
7.8.3.! Instrucţiuni de ciclare ............................................................191!
7.9.! Procesarea şirurilor .......................................................................193!
7.9.1.! Instrucţiuni de transfer...........................................................193!
7.9.2.! Instrucţiuni de transfer la/de la acumulator ...........................197!
7.9.3.! Instrucţiuni de comparare ......................................................200!
7.9.4.! Instrucţiuni de parcurgere ......................................................201!
7.10.! Operaţii cu stiva ..........................................................................202!
7.10.1.! Introducerea şi extragerea datelor .......................................203!
7.10.2.! Introducerea şi extragerea registrelor ..................................205!
7.10.3.! Exemple de lucru cu stiva ...................................................206!
7.11.! Exerciţii ......................................................................................208!

8.! OPERAŢII CU NUMERE ÎN VIRGULĂ MOBILĂ .........................209!


8.1.! Reprezentarea numerelor reale .....................................................209!
8.1.1.! Formatul în virgulă mobilă ....................................................209!
8.1.2.! Standardul IEEE 754 .............................................................214!
8.1.3.! Valori în virgulă mobilă specifice IA-32 ..............................218!
8.2.! Arhitectura unităţii în virgulă mobilă ...........................................219!
8.2.1.! Registre de date .....................................................................219!
8.2.2.! Registre de control şi stare ....................................................220!
8.3.! Instrucţiuni în virgulă mobilă .......................................................224!
8.3.1.! Transferul datelor în virgulă mobilă ......................................224!
8.3.2.! Operaţii aritmetice în virgulă mobilă ....................................230!
8.3.3.! Instrucţiuni transcedentale .....................................................235!
8.3.4.! Instrucţiuni de comparare ......................................................236!
8.3.5.! Instrucţiuni FPU de transfer condiţionat ...............................240!
8.4.! Exerciţii ........................................................................................244!

9.! FUNCŢII .............................................................................................247!


9.1.! Modularizarea programelor ..........................................................247!
9.2.! Apelarea funcţiilor ........................................................................248!
9.3.! Definirea funcţiilor .......................................................................249!
9.4.! Transferul controlului ...................................................................250!
9.5.! Metode de transfer al parametrilor ...............................................251!
9.5.1.! Prin registre ...........................................................................251!
9.5.2.! Prin variabile globale.............................................................253!
9.5.3.! Prin stivă ................................................................................254!
9.5.4.! Structura funcţiei ...................................................................262!
9.6.! Mecanisme de transfer al parametrilor .........................................266!
9.6.1.! Transfer prin valoare .............................................................266!
9.6.2.! Transfer prin referinţă ...........................................................269!
9.7.! Conservarea stării registrelor ........................................................271!
9.8.! Scrierea funcţiilor în fişiere separate ............................................271!
9.9.! Exerciţii ........................................................................................275!

10.! INTERFAŢA CU SISTEMUL DE OPERARE ................................278!


10.1.! Întreruperi software ....................................................................278!
10.2.! Formatul apelurilor de sistem .....................................................281!
10.2.1.! Alocarea statică a bufferelor................................................288!
10.2.2.! Macroinstrucţiuni ................................................................289!
10.2.3.! Funcţii de lucru cu transferuri de intrare/ieşire ...................296!
10.2.4.! Operaţii cu fişiere ................................................................303!
10.2.5.! Argumente în linia de comandă...........................................307!
10.3.! Operaţii aritmetice în reprezentări ASCII şi BCD .....................311!
10.3.1.! Formatul BCD .....................................................................312!
10.3.2.! Erori şi corecţii în ASCII.....................................................312!
10.3.3.! Erori şi corecţii în BCD compactat .....................................320!
10.3.4.! Conversia ASCII binar ........................................................322!
10.3.5.! Conversia caracterelor ASCII în binar ................................324!
10.3.6.! Conversia numerelor din binar în ASCII ............................331!
10.4.! Biblioteci de funcţii ....................................................................335!
10.4.1.! Crearea bibliotecilor statice .................................................335!
10.4.2.! Crearea bibliotecilor partajate .............................................337!
10.4.3.! Procesul de generare a bibliotecilor partajate......................344!
10.4.4.! Instalarea şi utilizarea bibliotecilor partajate.......................346!
10.5.! Apelul funcţiilor de sistem prin biblioteca standard C ...............348!
10.5.1.! Editarea legăturilor cu funcţiile C .......................................352!

11.! INTERFAŢA CU LIMBAJELE DE NIVEL ÎNALT .......................355!


11.1.! Apelarea modulelor în asamblare din C ....................................356!
11.1.1.! Structura funcţiei în asamblare ............................................357!
11.1.2.! Cadrul de stivă .....................................................................359!
11.1.3.! Compilarea modulelor .........................................................361!
11.2.! Linii de asamblare în codul sursă C............................................364!
11.2.1.! Sintaxa AT&T .....................................................................364!
11.2.2.! Formatul de bază .................................................................368!
11.2.3.! Formatul extins ....................................................................373!

Bibliografie .................................................................................................379!
1. REPREZENTAREA INFORMAŢIEI ÎN
SISTEMELE DE CALCUL

Oamenii comunică prin intermediul unui număr apreciabil de simboluri:


cifre, litere, semne de punctuație, operatori aritmetici, alte semne speciale.
Calculatorul nu este capabil să utilizeze decât două simboluri (0 și 1). Pentru a
reuși să comunice cu un calculator, omul trebuie să convertească simbolurile și
comenzile sale într-o formă pe care calculatorul să o poată înțelege.
Capitolul începe cu studiul diferitelor sisteme de numerație utilizate în
interacțiunea cu calculatorul. Când se lucrează cu mai multe sisteme de numerație,
adeseori este nevoie să convertim un număr dintr-o reprezentare în alta.
Prezentăm detalii cu privire la aceste conversii. Apoi ne concentrăm asupra
reprezentării caracterelor alfanumerice. În general, prin caractere alfanumerice se
înțeleg toate caracterele care pot fi introduse de la tastatura unui calculator.
Încheiem cu un set de exerciții destinat să mijlocească reținerea
conceptelor și procedeelor utilizate de-a lungul capitolului.

1.1. Sisteme de numeraţie

În mare, sistemul de calcul poate fi considerat o sumă de circuite


electronice. Într-un circuit electronic, prezenţa unui anumit voltaj poate indica 1, în
timp ce lipsa acestuia, în acelaşi punct, poate indica 0. Raţionamentul poate fi
extins la prezenţa sau lipsa unui curent electric, a unei capacităţi, etc.. Oricare ar fi
suportul electronic, putem spune că sistemele de calcul stochează informaţia sub
forma unor şiruri de 0 şi 1. De exemplu:

0101001101101001011011010111000001101100011101010010000
0011000110110000100100000010000100111010101101110011000
01001000000111101001101001011101010110000100101110

Sistemul de numeraţie care utilizează numai aceste două simboluri (digiţi,


cifre) poartă numele de sistem binar. Numărul de simboluri constituie baza
sistemului de numerație. Calculatoarele stochează toată informaţia în baza 2 -
limbajul nativ al maşinilor de calcul. Pe de altă parte, pentru noi, fiinţe dotate cu
zece degete şi obişnuite cu sistemul zecimal, operarea în reprezentare binară este
incomodă.

1.1.1. Sistemul zecimal

Numerele în baza 10 sunt reprezentate folosind zece simboluri: 0, 1, 2, 3, 4,


5, 6, 7, 8, 9. Fiecare digit al numărului zecimal are asociată o putere a lui 10, în
funcţie de poziţia sa în şirul de cifre al reprezentării (astfel de sisteme de numeraţie
se mai numesc şi poziţionale). Poziţia cifrei în număr este evaluată de la dreapta la
stânga, începând cu zero.

369!" = 3 ⋅ 10! + 6 ⋅ 10! + 9 ⋅ 10!

1.1.2. Sistemul binar

Numerele în baza 2 sunt compuse din doi digiţi: 0 şi 1. Fiecare digit al unui
număr, în funcţie de poziţia sa, are asociată o putere a lui 2.

11010! = 1 ⋅ 2! + 1 ⋅ 2! + 0 ⋅ 2! + 1 ⋅ 2! + 0 ⋅ 2!
= 16 + 8 + 2
= 26!!

Exemplul ilustrează şi conversia unui număr din binar în zecimal.


Un singur digit binar se numeşte bit (binary digit) şi se prescurtează cu
litera b. Pentru a lucra mai uşor cu numerele binare, putem grupa mai mulţi biţi
împreună:
• un şir de 4 biţi formează un nibble,
• un şir de 8 biţi un byte sau octet,
• un şir de 16 biţi formează un word sau un cuvânt,
• 32 de biţi un double word sau un dublu cuvânt,
• 64 de biţi un quad word (cuvând cvadruplu),
• 80 de biţi un ten byte.

Şirul binar de la începutul capitolului are un număr de 20 de octeţi. Dacă îl


împărţim în octeţi şi convertim fiecare octet în zecimal obţinem şirul:

83 105 109 112 108 117 32 99 9 32


66 117 110 97 32 122 105 117 97 46

Acelaşi şir de biţi, structurat în grupuri de cuvinte (16 biţi) reprezentate


zecimal, devine:
21329 28016 27765 8291 24864
17013 28257 8314 26997 24878

În grupuri de dublu cuvinte:

1399418224 1819615331 1629504117


1851859066 1769300270

Cu cât şirurile de biţi devin mai lungi, cu atât ne este mai greu să le
convertim în zecimal. De aceea, folosim ca intermediar sistemul hexazecimal.

1.1.3. Sistemul hexazecimal

În sistemul de numerație hexazecimal se utilizează 16 simboluri: 0, 1, 2, 3,


4, 5, 6, 7, 8, 9, A, B, C, D, E, F. Semnificația zecimală a simbolurilor A, B, C, D,
E, F este, în ordine, 10, 11, 12, 13, 14, 15.
Similar celorlalte sisteme de numeraţie, fiecare digit al numărului
hexazecimal are asociată o putere a lui 16, în funcţie de poziţie:

3AE!" = 3 ⋅ 16! + A ⋅ 16! + E ⋅ 16!


!!!!!!!!!!!!!= 768 + 10 ⋅ 16 + 14 ⋅ 1
!!!!!!!!!= 768 + 160 + 14
= 942!"

Exemplul ilustrează şi conversia unui număr din hexazecimal în zecimal.


Este acelaşi algoritm de conversie folosit anterior la trecerea din binar în zecimal.
S-a schimbat numai baza.

1.2. Operații de conversie

Am văzut cum putem transforma un număr din baza 2 în baza 10 sau din
baza 16 în baza 10. În continuare vom demonstra alte conversii utile.

1.2.1. Conversia numerelor din zecimal în binar

Pentru a converti un număr zecimal în binar, nu trebuie decât să-l împărţim


succesiv la 2 până obținem câtul 0, iar grupul resturilor obţinute, scrise de la stânga
la dreapta, începând cu ultimul, formează reprezentarea acestuia în baza 2.
Exemplu:
135 : 2 = 67 rest 1 => d! = 1
67 : 2 = 33 rest 1 => d! = 1
33 : 2 = 16 rest 1 => d! = 1
16 : 2 = 8 rest 0 => d! = 0
8 : 2 = 4 rest 0 => d! = 0
4 : 2 = 2 rest 0 => d! = 0
2 : 2 = 1 rest 0 => d! = 0
1 : 2 = 0 rest 1 => d! = 1

Grupând resturile în ordinea inversă obţinerii lor, rezultă:

135!" = d! d! d! d! d! d! d! d! = 10000111! .

Verificăm:

10000111! = 1 ⋅ 2! + 1 ⋅ 2! + 1 ⋅ 2! + 1 = 128 + 4 + 2 + 1 = 135!"

1.2.2. Conversia numerelor din zecimal în hexazecimal

Transformăm numărul zecimal 1345 în hexazecimal. Algoritmul de trecere


din zecimal în hexazecimal este identic cu cel folosit la conversia din zecimal în
binar, dar împărţim la noua bază, 16.

1345 : 16 = 84 rest 1 => d! = 1


84 : 16 = 5 rest 4 => d! = 4
5 : 16 = 0 rest 5 => d! = 5

În concluzie, 1345!" = d! d! d! = 541!"

Verificăm:

541!" = 5 ⋅ 16! + 4 ⋅ 16 + 1 = 5 ⋅ 256 + 64 + 1 = 1280 + 65 = 1345!"

1.2.3. Conversia numerelor din hexazecimal în binar

Reprezentarea hexazecimală este folosită de obicei ca notaţie prescurtată


pentru anumite grupuri de biţi. Tabelul 1.1 ne ajută să înţelegem mecanismul prin
care se converteşte un număr hexazecimal în binar.
Tabelul 1.1 Reprezentarea numerelor în diferite sisteme de numeraţie
Zecimal Binar Hexazecimal
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 A
11 1011 B
12 1100 C
13 1101 D
14 1110 E
15 1111 F

Deoarece sistemul hexazecimal are 16 digiți, fiecare cifră hexazecimală


poate specifica un grup unic de patru biţi.
Din calculele precedente a reieșit că 135!" este 87 în hexazecimal și
10000111 în binar. Aşadar, putem deduce că reprezentarea binară a numărului
87!" este 10000111. Din tabel observăm că reprezentarea binară a lui 8!" este
1000, iar reprezentarea binară a lui 7!" este 0111. Cu alte cuvinte, 10000111
reprezintă alăturarea echivalentului binar al cifrelor hexazecimale 8 și 7.
În concluzie, pentru a converti orice număr hexazecimal în binar, nu
trebuie decât să identificăm reprezentarea binară a numerelor hexazecimale.
De exemplu, 1A3B!" :
1!" = 0001!
A!" = 1010!
3!" = 0011!
B!" = 1011!

rezultă 1A3B!" = 0001101000111011!

1.2.4. Conversia numerelor din binar în hexazecimal

Pentru a converti un număr din binar în hexazecimal trebuie numai să


inversăm procesul. Grupăm biţii numărului binar câte patru, de la dreapta la stânga,
dacă este nevoie completăm cu 0 biţii ultimului grup, şi identificăm echivalentul în
hexazecimal pentru fiecare nibble.
De exemplu, numărul binar 10010001011010 poate fi grupat de la dreapta
la stânga astfel:
10 0100 0101 1010.

Completăm la stânga cu doi de zero,

0010 0100 0101 1010,

şi identificăm pentru fiecare nibble echivalentul în hexazecimal. Rezultatul este


245A!" .
Aplicăm această regulă pe şirul de biţi dat la începutul capitolului. Împărţit
în octeţi, obţinem:

53 69 6d 70 6c 75 20 63 61 20
42 75 6e 61 20 7a 69 75 61 2e

Împărţit în cuvinte (unirea doi câte doi a octeţilor de mai sus):

5369 6d70 6c75 2063 6120 4275 6e61 207a 6975


612e

Împărţit în dublu cuvinte (unirea două câte două a cuvintelor de mai sus):

53696d70 6c752063 61204275 6e61207a 6975612e

Din ultimele conversii deducem avantajul conversiei din binar în


hexazecimal faţă de conversia din binar în zecimal. Reprezentarea în hexazecimal
este mult mai uşor de obţinut. În plus, reprezentarea hexazecimală a grupurilor de
numere binare este „transparentă”: putem forma cuvinte prin alăturarea octeţilor,
dublu cuvinte prin alăturarea cuvintelor, ş.a.m.d.. Aceste avantaje poziţionează
sistemul hexazecimal ca excelent intermediar între sistemul binar, utilizat de
calculator, şi sistemul zecimal, nativ utilizatorului uman. De aici reiese un concept
foarte important – sistemul hexazecimal este modalitatea cea mai convenabilă prin
care omul poate reprezenta şiruri de biţi. Un număr nu este intrinsec binar, zecimal
sau hexazecimal. O valoare poate fi exprimată la fel de precis în toate cele trei
sisteme de numeraţie. Din acest punct de vedere, ea poate fi exprimată la fel de
precis în orice sistem de numeraţie.

1.2.5. Conversia numerelor reale

Conversia unui număr cu parte întreagă şi parte fracționară se face prin


conversia separată a celor două părţi. Conversia numărului subunitar (partea
fracţionară a unui număr) din baza 10 într-o bază oarecare se face prin înmulţirea
succesivă a părţilor fracţionare cu baza respectivă, până când se ajunge la zero, la
perioadă, sau se depăşeşte capacitatea de reprezentare (se obţin cifre suficiente deşi
algoritmul nu s-a finalizat). Cifrele care depăşesc partea fracționară la fiecare
înmulţire formează numărul în baza către care se face conversia.
Exemplul care urmează converteşte numărul 0.57 în binar şi hexazecimal.
Cum partea întreagă este deja 0, convertim numai partea fracţionară.

0,57 * 2 = 1.14 => d! = 1


0,14 * 2 = 0.28 => d! = 0
0,28 * 2 = 0.56 => d! = 0
0,56 * 2 = 1.12 => d! = 1
0,12 * 2 = 0.24 => d! = 0
0,24 * 2 = 0.48 => d! = 0
0,48 * 2 = 0.96 => d! = 0
0,96 * 2 = 1.92 => d! = 1
0,92 * 2 = 1.84 => d! = 1
0,84 * 2 = 1.68 => d! = 1
0,68 * 2 = 1.36 => d!" = 1
0,36 * 2 = 0.72 => d!! = 0

Aşadar,
0,57!" = 0, d! d! d! d! d! d! d! d! d! d! d!" d!! = 0,100100011110! = 0,91E!"

1.3. Reprezentarea caracterelor alfanumerice

Deoarece calculatorul poate reprezenta informaţia numai sub formă de


şiruri de 0 şi 1, toate simbolurile alfanumerice (literele mari şi mici ale alfabetului,
cifrele de la 0 la 9, semnele de punctuaţie, operatori aritmetici şi de relaţie, alte
caractere speciale) trebuie traduse în format binar. Reprezentarea informaţiei
conform unui anumit format se numeşte codificare. Rezultatul codificării este un
cod - un set de simboluri elementare împreună cu o serie de reguli care specifică
modul de formare al acestor simboluri.
Un astfel de cod, bazat pe alfabetul englez, este ASCII (American Standard
Code for Information Interchange). ASCII este definit de Institutul American de
Standarde şi recomandat de ISO (International Standard Organization) ca standard
actual de codificare alfanumerică. Codul ISO pentru ASCII este ISO 8859. Iniţial,
caracterele ASCII au fost codificate pe 7 biţi, ceea ce însemna un număr maxim de
128 (2! ) de simboluri. Ulterior, a fost extins de IBM la 8 biţi, rezultând 256 de
simboluri (codul fiecărui simbol este reprezentarea în binar a numărului său de
ordine). Chiar extins la 8 biţi, multe limbi conţin simboluri care nu pot fi
condensate în 256 de caractere. De aceea, au fost create variante care conțin
caractere şi simboluri regionale. De exemplu, tabela ASCII cunoscută sub numele
de ISO 8859-1 cuprinde caractere pentru limbile indo-europene apusene (America
de Nord, Europa Occidentală, Australia şi Africa). Pentru limba română se
utilizează tabela ISO 8859-2, destinată limbilor indo-europene răsăritene, sau ISO
8859-16, zona est-europeană.
Tabelul 1.2 Codul ASCII
Dec Hex Char Dec Hex Char Dec Hex Char Dec Hex Char
0 0 NULL (null) 32 20 Space 64 40 @ 96 60 `
1 1 SOH (start of heading) 33 21 ! 65 41 A 97 61 a
2 2 STX (start of text) 34 22 “ 66 42 B 98 62 b
3 3 ETX (end of text) 35 23 # 67 43 C 99 63 c
4 4 EOT (end of transmission) 36 24 $ 68 44 D 100 64 d
5 5 ENQ (enquiry) 37 25 % 69 45 E 101 65 e
6 6 ACK (acknowledge) 38 26 & 70 46 F 102 66 f
7 7 BEL (bell) 39 27 ' 71 47 G 103 67 g
8 8 BS (backspace) 40 28 ( 72 48 H 104 68 h
9 9 TAB (horizontal tab) 41 29 ) 73 49 I 105 69 i
10 A LF (NL line feed, new line) 42 2A * 74 4A J 106 6A j
11 B VT (vertical tab) 43 2B + 75 4B K 107 6B k
12 C FF (NP form feed, new page) 44 2C ´ 76 4C L 108 6C l
13 D CR (carriage return) 45 2D - 77 4D M 109 6D m
14 E SO (shift out) 46 2E . 78 4E N 110 6E n
15 F SI (shift in) 47 2F / 79 4F O 111 6F o
16 10 DLE (data link escape) 48 30 0 80 50 P 112 70 p
17 11 DC1 (device control 1) 49 31 1 81 51 Q 113 71 q
18 12 DC2 (device control 2) 50 32 2 82 52 R 114 72 r
19 13 DC3 (device control 3) 51 33 3 83 53 S 115 73 s
20 14 DC4 (device control 4) 52 34 4 84 54 T 116 74 t
21 15 NAK (negative acknowledge) 53 35 5 85 55 U 117 75 u
22 16 SYN (synchronous idle) 54 36 6 86 56 V 118 76 v
23 17 ETB (end of trans. block) 55 37 7 87 57 W 119 77 w
24 18 CAN (cancel) 56 38 8 88 58 X 120 78 x
25 19 EM (end of medium) 57 39 9 89 59 Y 121 79 y
26 1A SUB (substitute) 58 3A : 90 5A Z 122 7A z
27 1B ESC (escape) 59 3B ; 91 5B [ 123 7B {
28 1C FS (file separator) 60 3C < 92 5C \ 124 7C |
29 1D GS (group separator) 61 3D = 93 5D ] 125 7D }
30 1E RS (record separator) 62 3E > 94 5E ^ 126 7E ~
31 1F US (unit separator) 63 3F ? 95 5F _ 127 7F DEL

Primele 32 de caractere ASCII sunt coduri de control. Printre ele se


regăsește un grup de caractere albe (whitespace). Caracterele albe dictează
structura fişierelor text prin împărţirea acestora în linii (caracterul de linie nouă -
newline) sau introduc spaţii albe între cuvinte (caracterul tabulator orizontal – TAB,
şi caracterul de spaţiu - Space). Fişierele text conţin litere mici, mari, digiţi
numerici, semne de punctuaţie. În total sunt 94 de caractere vizibile. Nu trebuie să
rețineți Tabelul 1.2. Totuși, este indicat să vă familiarizați cu unele din
caracteristicile sale generale. În special, observați cum caracterele numerice, ‘0’
…’9’, apar într-o secvență contiguă de coduri, 30!" …39!" . Același lucru este
valabil și pentru majuscule, ‘A’…’Z’, și litere mici, ‘a’ …’z’. De asemenea,
observați că literele mici au valoare numerică mai mare, iar diferența între codurile
celor două reprezentări pentru un anumit caracter alfabetic este 20!" .
Pe lângă caracterele prezente în codul ASCII trebuie să reprezentăm
simboluri matematice, caractere cu indicatori de accent, litere greceşti, etc.. Acest
lucru a dus la apariţia unui cod nou, care în prezent se bucură de răspândire foarte
mare şi este predominant în industria producătoare de programe de calculator:
UTF-8, bazat pe standardul UNICODE. În segmentul dezvoltatorilor de aplicaţii
web şi servicii Internet a devenit practic noul standard. UTF-8 codifică fiecare din
cele 1.112.069 simboluri din setul de caractere UNICODE folosind unul până la
patru octeţi. Organizaţia non-profit care coordonează dezvoltarea UNICODE îşi
propune să definească un cod standard pentru simbolurile alfanumerice utilizate în
toate ţările lumii: litere, semne de punctuaţie, semne monetare specifice tuturor
limbilor de pe planetă. Avantajul major constă în compatibilitatea cu ASCII.
Primele 128 de caractere ale setului UTF-8 sunt reprezentate pe un singur octet şi
echivalente unu la unu cu cele ASCII. Astfel, orice text scris anterior cu ASCII
devine compatibil cu UTF-8.
Înainte să treceţi la secțiunea următoare traduceţi cu ajutorul tabelului
ASCII de mai sus semnificaţia şirului dat la începutul capitolului. Pentru a vă uşura
misiunea, puteţi folosi reprezentarea hexazecimală sau zecimală dedusă în
paragrafele anterioare.

1.4. Exerciții

1.1. Convertiți în hexazecimal următoarele șiruri de biți:

a) 0100 0001 1110 1100 c) 1010 0101 0100 0010


b) 1011 1111 0011 1001 d) 0000 1101 0111 0110

1.2. Convertiți în binar următoarele numere hexazecimale:

a) 78CF c) AAAA e) 5591


b) 3A7B d) FFFF f) 66DD

1.3. Convertiți în zecimal următoarele valori hexazecimale:

a) A1 e) 0C i) A000
b) 12 f) 10 j) FFFF
c) FF g) 64 k) ABCD
d) 80 h) 4E l) 1234
1.4. Convertiți în reprezentări hexazecimale de 8 biți următoarele numere zecimale:
a) 100 e) 32 i) 1024
b) 128 f) 64 j) 513
c) 255 g) 16 k) 32767
d) 85 h) 8 l) 2011

1.5. Convertiți în binar următoarele numere reale:

a) 32.45 e) 15.32
b) 147.83 f) 7.8
c) 3.0125 g) 63.25
d) 255.255 h) 18.5

1.6. Convertiți în caractere ASCII următorul text:

„Telurile sunt prea marete,


Eu sunt mic, uitat, nebagat in seama”

1.7. Convertiți în text următorul șir de biți reprezentat în hexazecimal:

45 72 61 6D 20 73 74 75 64 65 6E 74 2C 20 65 72 61 20 73 74 75 64 65 6E 74 61
20 0D 0A 65 72 61 6D 20 65 6D 69 6E 65 6E 74 2C 20 65 72 61 20 69 6D 69 6E
65 6E 74 61 20 0D 0A 73 69 20 65 72 61 75 20 73 74 65 6C 75 74 65 20 EE 6E 20
67 65 6E 65 6C 65 20 65 69 2E

Secvenţa 0D 0A denotă trecerea la rând nou (valabil pentru textul scris în


Windows; în Linux, rândul nou este anunţat numai de 0A, caracterul standard \n).

1.8. Scrieți următoarele numere sub formă de caractere ASCII:

a) 123 d) 1094
b) 430 e) 1452
c) 15 f) 9966

1.9. Efectuați operațiile date mai jos folosind coduri ASCII hexazecimale.
Exprimați rezultatul tot ca reprezentare ASCII.

a) ‘F’ + 20!" e) ‘q’ – ‘Q’


b) ‘F’ - 20!" f) ‘g’ – ‘F’
c) ‘o’ - 20!" g) ‘h’ – ‘a’ + 30!"
d) ‘m’ - 20!" – 4D h) ‘4’ + ‘6’
2. ARHITECTURA CALCULATOARELOR

Capitolul de faţă prezintă arhitectura calculatorului, văzut ca platformă de


dezvoltare a programelor. Capitolul începe cu descrierea structurii hardware a
sistemelor de calcul. Se pune accent pe arhitectura Intel de 32 de biți.
Funcționalitatea și organizarea internă a componentelor hardware sunt prezentate
din perspectiva procesului de dezvoltare a programelor.

2.1. Structura unui sistem de calcul

În capitolul precedent am subliniat faptul că sistemele de calcul lucrează


numai cu şiruri de cifre binare 0 sau 1 şi am arătat cum pot acestea reprezenta
numere sau caractere. Acest tip de informaţie poartă numele de date. Pe de altă
parte, sistemele de calcul nu sunt simple dispozitive de stocare a datelor. De
exemplu, pot fi comandate să execute toate operaţiile realizate de noi: conversia
dintr-o bază în alta, interpretarea unui şir binar în ASCII, etc.. Fiecare operaţie de
prelucrare a datelor este codificată tot sub formă de şir binar. Dar pentru că nu
codifică o valoare, ci o comandă, aceste şiruri binare se numesc instrucţiuni.
Instrucţiunile specifică operaţiile care trebuie efectuate de procesor. Dar atenţie,
procesorul recunoaşte şi execută numai instrucţiunile pentru care a fost construit.
Ansamblul instrucţiunilor recunoscute formează setul de instrucţiuni caracteristic
acelui tip de procesor. Un şir de instrucţiuni şi/sau date organizate logic după un
algoritm formează un program. Prin intermediul programului, utilizatorul transmite
procesorului o anumită sarcină; de exemplu, să citească date de la tastatură şi să le
afișeze la monitor.

2.1.1. Arhitectura von Neumann

La acest nivel, programul constă dintr-o secvenţă de coduri instrucţiune


stocate în memorie. Dacă programele conţin şi date (majoritatea programelor
includ unele date constante), ele sunt de asemenea stocate în memorie. Când
sistemul de operare lansează în execuţie un program, codurile instrucţiune sunt
copiate (încărcate) de pe disc în memorie. Rularea unui program înseamnă că
procesorul extrage fiecare instrucţiune din memorie şi execută operaţia codificată
de aceasta. Dacă operaţia implică prelucrare de date, acestea sunt extrase de
asemenea din memorie. Arhitectura unui sistem de calcul care stochează datele şi
instrucţiunile împreună într-o unitate de memorie separată de unitatea de procesare
se numeşte arhitectură von Neumann. Arhitectura poartă numele celui care a
descris-o abstract într-un studiu din 1945, matematicianul John von Neumann (deşi
conceptul a fost menţionat şi de alţi pionieri din domeniul calculatoarelor). La
momentul respectiv, acest model de lucru în care datele de prelucrat şi rezultatele
calculelor intermediare sunt stocate alături de instrucţiuni într-o memorie comună,
era diferit de cel al calculatoarelor cu funcţionalitate predeterminată, unde
programele erau stocate pe medii externe, precum cartele sau benzi perforate. În
plus, von Neumann a arătat că sistemul de calcul trebuie să aibă o structură fizică
fixă, simplă, şi să fie capabil de a executa orice fel de calcule pe baza unui control
programat, fără a fi necesară modificarea unităţilor fizice. De atunci, aceste idei,
numite de obicei „tehnica programelor stocate”, au devenit esenţiale şi au fost
acceptate universal. Într-un calculator cu arhitectură von Neumann, programul
poate fi interpretat şi manipulat ca date. Acest lucru a dat naştere la compilatoare şi
sisteme de operare şi face posibilă versatilitatea calculatoarelor moderne. Tot de
aici pleacă şi principalul dezavantaj: un program poate fi scris astfel încât să se
poată interpreta pe sine ca date, dând posibilitatea să se modifice pe el însuşi.
Sistemele de operare moderne, precum GNU/Linux, interzic aplicaţiilor să se
automodifice. Modelul von Neumann constă din cinci componente majore, ilustrate
în Figura 2.1.

Unitatea de
memorie

Unitatea
Unitatea de Unitatea de
Aritmetică și
intrare ieșire
Logică

Unitatea de
control

Figura 2.1 Arhitectura von Newmann. Săgeţile îngroşate reprezintă căi de date. Săgeţile
subţiri reprezintă trasee de control.
Unitatea de intrare reprezintă poarta de intrare a instrucţiunilor şi datelor în sistem.
Acestea sunt stocate în unitatea de memorie şi prelucrate de unitatea aritmetică şi
logică (ALU – Arithmetic and Logic Unit) sub coordonarea unităţii de control.
Rezultatele sunt trimise la unitatea de ieşire. Structura calculatoarelor poate fi
descompusă în aceste cinci unităţi fundamentale chiar şi în prezent.
2.1.2. Modelul bazat pe magistrală

În modelul von Neumann comunicaţia între componentele sistemului se


realizează prin legături dedicate între perechi de componente. Această abordare
este relativ rigidă, limitând în mare măsură scalabilitatea sistemului. Adăugarea de
noi componente este dificilă şi implică modificarea componentelor deja existente.
De asemenea, proiectarea unei componente noi presupune cunoaşterea în
amănunţime a funcţionării celorlalte componente. Soluţionarea acestei probleme a
venit din partea firmei Digital Equipment Corporation (DEC), care, la sfârşitul
anilor ’60, a lansat pe piaţă primul calculator (PDP 11) construit în jurul unei
magistrale de sistem.

CPU (ALU,
Intrare și
Registre și Memorie
Ieșire (I/O)
Control)
Magistrala de

Magistrala de date
sistem

Magistrala de adrese

Magistrala de control

Figura 2.2 Modelul bazat pe magistrală al unui calculator


Această rafinare a modelului von Neumann combină ALU şi unitatea de
control într-o singură unitate funcţională, numită unitate centrală de prelucrare
(CPU – Central Processing Unit). Totodată, unităţile de intrare şi ieşire sunt de
asemenea combinate într-o singură unitate de intrare şi ieşire. Aşadar, principalele
componente hardware2 ale unui sistem de calcul sunt:
• procesor sau unitate centrală de prelucrare;
• memorie sau unitate de memorare (memoria RAM – Random Access
Memory3);
• unitate de intrare/ieşire (unitate I/O). La această interfaţă sunt conectate
dispozitivele de intrare/ieşire.

Elementul definitoriu al acestui model constă în faptul că toate


componentele sistemului comunică între ele prin intermediul unei magistrale de

2
Componentele fizice ale unui sistem (structuri mecanice, cabluri, cutii, circuite, etc.).
3
„Random” se referă la faptul că timpul de acces la orice unitate de memorie este constant
şi independent de locaţia fizică a acesteia sau de secvenţele accesurilor anterioare.
sistem. Din punct de vedere conceptual, magistrala este un mediu comun de
comunicaţie între componentele unui sistem de calcul; fizic, este alcătuită dintr-un
set de semnale care facilitează transferul de date şi instrucţiuni, precum şi
sincronizarea între componentele sistemului.

2.1.1. Magistrala de sistem

Transferul de informaţii pe magistrală se face pe baza unui set de reguli (un


protocol) care stabileşte cine, când şi cum se comunică pe magistrală. Aşadar,
magistralele sunt ansambluri de conexiuni fizice (fire) prin care circulă informaţie
în conformitate cu un set de reguli, având ca suport semnale electrice. Prin
termenul de magistrală se mai înţelege de regulă şi ansamblul de circuite
electronice care amplifică puterea semnalelor electrice sau aduc nivelul acestora
(de ex., tensiunea) la valoarea standard. În funcţie de informaţie, semnalele
electrice sunt de trei tipuri: de date (reprezintă informaţia propriu zisă), de adrese
(semnale care identifică locaţia la care este transmisă informaţia) şi de control
(descriu aspecte legate de modul şi maniera în care este transmisă informaţia). Nu
este obligatoriu ca o magistrală să implementeze căi fizice separate pentru fiecare
din aceste trei tipuri de semnale. De exemplu, standardul PCI foloseşte acelaşi
suport fizic atât pentru date cât şi pentru adrese, dar la momente diferite. Tipul
informaţiilor (sau direcţia lor) aflate pe magistrală la un moment dat este indicat
prin semnale de control. Din acest punct de vedere putem clasifica magistralele în
unidirecţionale şi bidirecţionale. Magistralele unidirecţionale pot transmite
informaţiile într-un singur sens, cele bidirecţionale în ambele sensuri. Magistrala de
adrese este unidirecţională (de la procesor spre sistem) iar magistralele de date şi
control sunt bidirecţionale.
Un program constă dintr-o secvenţă de instrucţiuni stocată în memorie.
Atunci când procesorul este pregătit să execute următoarea instrucţiune, plasează
adresa locaţiei de memorie care conţine acea instrucţiune pe magistrala de adrese.
Totodată, procesorul plasează un semnal de citire pe magistrala de control.
Unitatea de memorie răspunde prin plasarea instrucţiunii pe magistrala de date, de
unde poate fi citită4 de procesor. Dacă procesorul trebuie să citească date din
memorie, are loc aceeaşi secvenţă de evenimente.
Atunci când procesorul trebuie să scrie5 date în memorie, acesta plasează
datele pe magistrala de date, adresa locaţiei de memorie (la care trebuie acestea
stocate) pe magistrala de adrese, şi un semnal de scriere pe magistrala de control.
Unitatea de memorie răspunde prin copierea pe magistrala de date a datelor
prezente la locaţia specificată.
Dacă o instrucţiune solicită un transfer de date din/către memorie,
următoarea instrucţiune nu mai poate fi citită din memorie pe aceeaşi magistrală

4
Operaţia de citire se traduce prin extragerea unei date stocate anterior.
5
Operaţia de scriere se traduce prin stocarea unei date în memorie.
decât după finalizarea acestuia. Acest conflict a dat naştere la o altă arhitectură
bazată pe tehnica programelor stocate, arhitectura Harvard. În arhitectura Harvard,
programul şi datele sunt stocate în memorii diferite, fiecare conectată la procesor
prin propria magistrală. Acest lucru permite procesorului să acceseze simultan atât
instrucţiunile cât şi datele programului.
În calculatoarele moderne, magistrala care conectează procesorul de
modulele externe de memorie nu poate ţine pasul cu viteza de execuţie a
procesorului. Încetinirea vitezei de transfer pe magistrală poartă numele de
ştrangulare von Neumann (von Neumann bottleneck).
Interacţiunea procesorului cu dispozitivele de intrare/ieşire se face prin
acelaşi mecanism întâlnit la interacţiunea procesorului cu memoria. Dacă
procesorul trebuie să citească date de la un dispozitiv de intrare, plasează adresa
acestuia pe magistrala de adrese şi un semnal de citire pe magistrala de control.
Dispozitivul răspunde prin încărcarea datelor pe magistrala de date.
Când procesorul trebuie să trimită date la un dispozitiv de ieşire, plasează
datele pe magistrala de date, specifică adresa dispozitivului pe magistrala de adrese
şi activează semnalului de scriere pe magistrala de control. Deoarece viteza de
răspuns a diferitelor dispozitive I/O variază drastic faţă de viteza procesorului sau
memoriei, programatorul trebuie să utilizeze tehnici speciale de programare.
O altă caracteristică de bază a magistralelor este dimensiunea acestora,
adică numărul liniilor de conectare; avem magistrale de 8 biţi (8 linii de conectare),
16 biţi (16 linii de conectare), etc.. Dimensiunea fiecărei magistrale este
determinată de tipul de procesor (de 8, 16, 32, 64 de biţi) şi determină la rândul său
numărul de locaţii de memorie ce pot fi adresate (capacitatea memoriei) şi structura
porturilor din dispozitivele de intrare/ieşire. De exemplu, o magistrală de 32 de biţi
poate adresa o memorie RAM de 2!" = 2!" ⋅ 2! = 4!GB.

2.1.2. Unitatea de intrare/ieşire

Majoritatea programelor trebuie să comunice cu dispozitive I/O. Tipul


dispozitivelor I/O variază foarte mult. Unele sunt menite să interacţioneze cu
utilizatorul (de exemplu, tastatura, monitorul, mouse-ul), altele să permită
programelor interacţiunea cu unităţi hardware (de exemplu, stocarea un fişier pe
disc sau citirea unui fişier din reţea). Toate aceste dispozitive se comportă diferit şi
cerinţele lor de sincronizare diferă drastic de la unul la altul. Din moment ce
programarea dispozitivelor I/O este dificilă, dar aproape toate programele au
nevoie de acestea, software-ul care gestionează dispozitivele I/O este inclus în
sistemul de operare. Sistemele Linux includ un set bogat de funcţii care permit
programatorilor de aplicaţii să efectueze operaţii de intrare/ieşire. Într-un capitol
viitor vom apela şi noi la serviciile puse la dispoziţie de sistemul de operare pentru
îndeplinirea unor operaţii de intrare/ieşire.
Aşadar, pe lângă componentele hardware, un sistem de calcul dispune şi de
o componentă software6 fundamentală: sistemul de operare. De fapt, termenul
“arhitectura calculatoarelor” defineşte graniţa dintre hardware şi software, şi putem
afirma, fără să fim acuzaţi de partizanat, că arhitectura calculatorului reprezintă
sistemul de calcul văzut de un programator în limbaj de asamblare.

2.1.3. Memoria

Memoria sistemului, numită şi memorie principală, este folosită la stocarea


informaţiilor (instrucţiuni şi date) în format binar şi reprezintă sursa sau destinaţia
tuturor informaţiilor. Toate informaţiile iniţiale şi rezultatele prelucrărilor sunt
încărcate, generate sau stocate temporar în memorie.
Memoria este organizată ca o colecţie de locaţii de memorare, numerotate
consecutiv, începând cu zero. Unitatea de bază a memoriei este octetul. Ne putem
imagina memoria ca şir liniar de octeţi suprapuşi, asemenea unui dulap cu sertare
numerotate, fiecare sertar având capacitatea de 8 biţi (Figura 2.3). Numărul
fiecărui sertar este ceea ce numim în general adresă fizică, iar sertarul, în
programare, se numeşte locaţie de memorie. Adresele permit identificarea fiecărui
octet din memorie. Mulţimea totală a adreselor fizice constituie spaţiul adreselor
fizice, iar numărul de biţi dintr-o locaţie de memorie reprezintă dimensiunea
locaţiei sau formatul memoriei. În cazul nostru, considerăm dimensiunea locaţiei
ca fiind de 8 biţi.
Figura 2.3 prezintă o memorie adresată printr-o magistrală de 20 de biţi.
Dimensiunea magistralei, adică numărul liniilor de conectare, determină numărul
de locaţii de memorie ce pot fi adresate. În acest caz, adresa fizică a primului octet,
de jos în sus, este reprezentată prin 20 de biţi de 0.

FFFFFH

.
.
.

00003H
00002H
1 0 0 0 1 0 0 1 00001H
00000H
0 7
Figura 2.3 Reprezentarea memoriei principale
În programare întotdeauna numărăm începând cu zero. Următorul octet are adresa
1, ş.a.m.d, până la adresa ultimului octet ce poate fi adresat cu 20 de biţi, adresa cu
toţi biţii de 1. În Figura 2.3 adresele sunt reprezentate în hexazecimal. Numărul
total de octeţi de memorie este 2!" = 1048576. În binar, prefixul kilo este asociat
6
Componentele imateriale (programe de sistem şi aplicaţii).
cu 1024 = 2!" , prefixul Mega şi Giga cu 1048576 = 2!" , respectiv
1073741824 = 2!" . În concluzie, Figura 2.3 ilustratrează o memorie RAM cu
capacitatea de 1 MB.
Pe viitor va fi foarte important să stăpâniţi la perfecţie puterile lui 2
prezentate în Tabelul 2.1. Vă vor ajuta să evaluaţi rapid capacitatea de memorie
adresabilă cu o magistrală de 16 sau 32 de biţi.

Memoria se măsoară în unităţi de ordinul kilobytes (kB), megabytes (MB),


gigabytes (GB).
Tabelul 2.1 Puteri ale lui 2 în binar și zecimal
Binar Puteri ale lui 2 Zecimal
1 2! 1
10 2! 2
100 2! 4
1000 2! 8
10000 2! 16
100000 2! 32
1000000 2! 64
10000000 2! 128
100000000 2! 256
1000000000 2! 512
10000000000 2!" 1024
100000000000 2!! 2048
1000000000000 2!" 4096
10000000000000 2!" 8192
100000000000000 2!" 16384
1000000000000000 2!" 32768
10000000000000000 2!" 65536
100000000000000000 2!" 131072
1000000000000000000 2!" 262144
10000000000000000000 2!" 524288
100000000000000000000 2!" 1048576

2.2. Arhitectura IA-32

În această carte studiem arhitectura procesoarele Intel de 32 de biţi sau


compatibile. Numărul acestora este impresionant. Intel a produs microprocesoare
încă din 1969. Primul lor microprocesor a fost 4004. Era un procesor de 4 biţi
(capacitatea de memorare a registrelor specifică tipul de procesor). A fost urmat
de 8080 şi 8085. Dezvoltarea acestor microprocesoare a dus la apariţia arhitecturii
Intel (IA – Intel Architecture).

2.2.1. Arhitectură şi microarhitectură

Primul procesor al arhitecturii Intel este 8086, apărut în 1978. Avea o


magistrală de adrese de 20 de biţi şi o magistrală de date de 16 biţi. Deoarece
registrele interne şi magistrala de date erau de 16 biţi, 8086 a fost considerat primul
procesor de 16 biţi. Magistrala de adrese de 20 de biţi îi permitea să adreseze 1 MB
de memorie.
8088 a fost versiunea mai ieftină a lui 8086. Reducerea preţului s-a obţinut
prin folosirea unei magistrale de date de 8 biţi. Cu excepţia acestei deosebiri, 8088
este identic cu 8086.
80186 a fost varianta mai rapidă a lui 8086. Dispunea de aceleaşi
magistrale, dar un set îmbunătăţit de instrucţiuni. Însă 80186 nu a fost niciodată
foarte îndrăgit de producătorii de calculatoare.
Adevăratul succesor al lui 8086 a fost 80286, introdus în 1982. Magistrala
de adrese de 24 de biţi îi permitea să adreseze o memorie de 16 MB şi includea
câteva posibilităţi de protecţie a memoriei.
Primul procesor de 32 de biţi, 80386, apare în 1986. Are o magistrală de
adrese de 32 de biţi şi o magistrală de date tot de 32 de biţi. Spaţiul de memorie
adresabil a crescut la 4 GB şi a fost introdus mecanismul paginării (vom prezenta
detalii cu privire la acest aspect ceva mai târziu). Paginarea a permis un model
liniar de memorie - spaţiu liniar, continuu de adrese7 (modelul de memorie
prezentat de noi în secţiunea precedentă) – şi prin el, posibilitatea portării
sistemelor de operare din familia Unix pe arhitecturi Intel.
Intel 80486 apare în 1989 ca variantă îmbunătăţită a lui 80386. Pentru
prima dată un procesor Intel încorpora unitatea de calcul aritmetic în virgulă
mobilă (dă posibilitatea efectuării operaţiilor cu numere reale). Până atunci,
unitatea de calcul în virgulă mobilă era prezentă sub forma unui coprocesor extern.
80486 dispune şi de alte îmbunătăţiri tehnologice: unităţile de decodificare şi
execuţie a instrucţiunilor cu capacitate de lucru în paralel, 8 kB L1 cache, prezenţa
unui cache L2. Versiunile ulterioare includ tehnologii de economisire a consumului
de energie (power saving), utile în special laptop-urilor.
Pentium/Pentium Pro aduc foarte puţine elemente noi. În principal măresc
viteza de execuţie.
Pentium MMX aduce suport MMX (MultiMedia eXtensions) - instrucţiuni
ce măresc viteza operaţiilor grafice.
Pentium II este un Pentium Pro cu suport MMX. Pentium III, în esenţă un
Pentium II mai rapid, introduce arhitectura SIMD (Single Instruction Multiple
Data) pentru execuţia concurentă a mai multor operaţii în virgulă mobilă.

7
Adresele pornesc de la zero şi se succed, într-un mod liniar, fără goluri sau întreruperi,
până la limita superioară impusă de numărul total de biţi dintr-o adresă logică.
Pentium 4 îmbunătăţeşte aceste caracteristici. Tot de la Pentium 4 apar
primele modele de 64 de biţi (arhitectură botezată iniţial EMT64T). Pentium 4 a
fost urmat de Pentium Xeon, Pentium II Xeon, Pentium Core, Celeron, Core Duo,
Core 2 Duo, Core i3, Core i5, Core i7 şi multe altele. Toate acestea au fost
fabricate numai de Intel. Alte companii, în special AMD, au fabricat propriile
procesoare compatibile cu cele produse de Intel.

Arhitectura
În capitolele precedente am afirmat că programarea în asamblare necesită
cunoştinţe legate de procesor. „Legate de” se referă la ce face un procesor, spre
deosebire de cum face un procesor. Perspectiva programatorului include, printre
altele, numărul şi tipul de registre, setul de coduri instrucţiune recunoscut, modul
de adresare a memoriei, prezenţa sau lipsa unor unităţi de uz special precum
coprocesorul matematic (cu propriile instrucţiuni şi registre). Toate aceste lucruri
sunt definite de producător, iar definiţiile, luate împreună, formează arhitectura
procesorului.

“The architecture, as I see it, is the high-level specs of the processor. This consists
of the instructions set, memory structure, memory segmentation, register structure,
I/O space, interrupt mechanism, instruction operands, memory addressing modes,
and so on.”
Stephen Morse8

Cu timpul, arhitectura este îmbogăţită cu noi instrucţiuni, registre sau alte


caracteristici. De obicei, noile caracteristici nu modifică modul de lucru anterior, ci
adaugă noi opţiuni. Acest lucru asigură compatibilitatea noilor platforme cu
programele scrise anterior (backward compatibility). „Anterior” înseamnă că
programe scrise pentru 8086 pot rula şi astăzi pe un Core i7.
Câteodată, pe lângă adăugarea periodică de noi instrucţiuni, arhitecturile
suferă schimbări mai importante. Am văzut că, în 1986, o dată cu apariţia
procesorului 80386, pe lângă introducerea unor noi instrucţiuni şi moduri de
operare, Intel dublează capacitatea registrelor. Se făcea astfel trecerea la arhitectura
de 32 de biţi. Această arhitectură poartă numele de IA-32 (i386, x86-32). Este
arhitectura studiată de noi în această carte. În 2003, arhitectura x86 se extinde
iarăşi, de data aceasta la 64 de biţi; noi instrucţiuni, noi moduri de operare, registre
de capacitate dublă. Părintele acesteia este principalul competitor Intel, AMD, care
o numeşte AMD64. La acea dată, Intel lansase deja propria arhitectură de 64 de
biţi, numită IA-64 (Itanium), dar incompatibilă cu precedentele procesoare x86 (set
de instrucţiuni diferit de x86). Pentru că nu se bucură de foarte mult succes,
compania Intel este nevoită să producă o familie de procesoare compatibilă cu
arhitectura şi specificaţiile AMD. Termenul generic pentru noua arhitectură de 64
de biţi, compatibilă cu x86, este x86-64.

8
Părintele arhitecturii microprocesorului Intel 8086.
Arhitectura x86-64 include arhitectura IA-32, care la rândul său include
vechea arhitectură x86 de 16 biţi. Totuşi, la proiectarea unei aplicaţii,
programatorul trebuie să ştie care sunt procesoare compatibile cu instrucţiunile
folosite. Aplicaţia nu va rula pe procesoarele dinaintea apariţiei instrucţiunilor
respective.

Microarhitectura
Problemele ridicate de păstrarea compatibilităţii cu programele dezvoltate
anterior limitează creşterea performanţelor prin mijloacele arhitecturii. Două din
cele mai importante criterii de performanţă constau în numărul total de instrucţiuni
pe care un procesor este capabil să îl execute într-un anumit interval de timp
(throughput) şi consumul de energie. Numărul de instrucţini pe unitate de timp
trebuie să fie cât mai mare (viteză de procesare mai mare), iar consumul de energie
cât mai mic. În cazul celui din urmă, motivul este mai subtil. O bună parte din
energia utilizată de procesor se pierde sub formă de căldură; aceste pierderi, dacă
nu sunt minimizate, pot leza procesorul şi componentele din vecinătate. Proiectanţii
caută întotdeauna soluţii care să permită efectuarea aceloraşi sarcini cu consum mai
mic de energie. Unele din acestea permit procesorului să intre în starea de repaus
(stand-by) în momentele în care nu este folosit.
Puterea de procesare a crescut prin dezvoltarea unor mecanisme precum
citirea anticipată a instrucţiunilor (prefetching), execuţie în benzi de prelucrare
(pipelining), execuţia paralelă (hyper threading), predicţia salturilor (branch
prediction), execuţia speculativă, memorii intermediare L1 şi L2 (cache), şi multe
altele. Unele tehnici reduc sau elimină blocajele din interiorul procesorului, astfel
încât acesta să lucreze permanent (aici, un rol important îl are mecanismul de lucru
cu memoria), altele permit procesorului să execute mai multe instrucţiuni în acelaşi
timp. Împreună, aceste mecanisme fizice prin care procesorul îndeplineşte
operaţiile codificate în instrucţiuni, formează microarhitectura procesorului. În
vederea creşterii performanţelor, Intel şi AMD reproiectează constant
microarhitecturile procesoarelor. În acest context se înscriu şi eforturile de
îmbunătăţire a tehnicilor de fabricaţie a pastilelor de siliciu, tehnici ce permit
creşterea numărului de tranzistoare plasate pe un singur cip. Toate numele exotice,
NetBurst, Core, Nehalem, Sandy Bridge, Ivy Bridge, indică modificări apărute în
microarhitectură.

2.2.2. Structura de principiu a procesorului

În această secţiune detaliem modul în care programatorul „vede” structura


procesorului şi modul în care procesorul interacţionează cu memoria principală.
Figura 2.4 prezintă schema bloc generală a unui procesor. Unităţile sunt conectate
prin magistrale interne. Reţineţi că aceasta este o diagramă extrem de simplificată.
Procesoarele reale sunt mult mai complexe, dar conceptele generale discutate în
acest capitol se aplică tuturor.
Indicatorul de instrucțiune

Memorie L1 Cache
Registrul de instrucțiune

Unitatea de control

Registre
Unitatea
Aritmetică și
Logică

Interfața cu
Registrul indicatorilor de magistralele
stare

către magistralele de adrese, date și control

Figura 2.4 Structura generală a unui procesor


În continuare descriem pe scurt fiecare unitate în parte. Componentele de
interes special pentru programator sunt prezentate pe larg puţin mai târziu.

Unitatea de interfaţă cu magistrala


Unitatea de interfaţă cu magistrala de sistem este mijlocul prin care
procesorul comunică cu restul componentelor din sistem – memorie şi dispozitive
de intrare/ieşire. Unitatea conţine circuite ce permit plasarea adreselor pe
magistrala de adrese, plasarea semnalelor de citire şi scriere pe magistrala de
control, citirea şi scrierea datelor pe magistrala de date. De obicei, unitatea de
interfaţă cu magistrala este conectată la unităţi externe de control al magistralelor,
care, la rândul lor, sunt conectate la memorie sau la diferite dispozitive I/O (de ex.,
SATA, PCI-E, etc.). Pentru programator, aceste unităţile externe de control sunt
invizibile.

Unitatea de management a memoriei


Începând cu procesoarele de 16 biţi apare noţiunea de adresă logică.
Adresele generate de sistemul de operare sunt considerate adrese logice şi
totalitatea acestora formează spaţiul de adrese logice. Aceste noţiuni constrastează
cu cele de adresă fizică (aflată pe magistrala de adrese) şi spaţiu de adrese fizice
discutate în secţiunea dedicată memoriei principale. Din punct de vedere al
numărului de adrese, cele două spaţii, al adreselor fizice şi logice, pot fi egale sau
inegale. Ca urmare, procesorul trebuie să dispună de un mecanism de conversie a
adreselor (un mecanism de translatare a adreselor logice în adrese fizice). La
procesoarele Intel x86 mecanismul de translatare este inclus pe acelaşi substrat de
siliciu cu microprocesorul.
Datorită mecanismului de translatare a adreselor, modul de organizare al
memoriei poate fi definit prin software, de către sistemul de operare. Dacă
procesorul permite adresarea liniară a întregului spaţiu de adrese fizice (adresele
fizice încep de la 0 şi avansează liniar), atunci şi sistemul de operare poate organiza
memoria ca structură liniară (adresele logice încep de la 0 şi avansează liniar). Pe
de altă parte, atunci când procesorul poate adresa liniar numai anumite segmente de
memorie, segmente cu lungimi mai mici decât capacitatea totală a memoriei,
vorbim de organizare segmentată. În acest caz, spaţiul de adrese logice este
împărţit în mai multe spaţii cu adresare liniară, fiecare de lungime diferită, şi o
adresă logică este calculată ca sumă între adresa de început a unui segment (adresa
de început a unui bloc de memorie) şi un deplasament în cadrul acestuia.

Memorii intermediare
Când procesorul are nevoie de informaţii aflate în memoria principală
trimite către unitatea de management a memoriei o cerere de citire memorie.
Unitatea de management a memoriei trimite cererea respectivă la memorie şi,
atunci când informaţia se află pe magistrala de date, anunţă procesorul. Lungimea
întregului ciclu – procesor, controler de memorie, memorie, (înapoi la) procesor -,
variază în funcţie de viteza memoriei şi a magistralei de date. Aşadar, o memorie
cu timp de acces mai mic contribuie semnificativ la performanţa sistemului.

Performanţa memoriei este cuantificată prin intermediul a doi parametrii:


timpul de acces - perioada de timp necesară memoriei să extragă datele din locaţia
adresată, şi timpul unui ciclu la memorie - timpul minim între două operaţii
succesive de acces la memorie.

Totuşi, viteza de lucru a memoriei şi eforturile de creştere a acesteia reprezintă


numai o faţetă a problemei. Timpul necesar datelor şi intrucţiunilor să parcurgă
traseul dintre memorie şi procesor este mult mai mare decât timpul consumat de
procesor pentru prelucrarea acestora (acest fenomen se numeşte „ştrangulare von
Neumann”). De aceea, între procesor şi memoria principală a fost introdusă o
memorie intermediară, numită cache. Memoria cache este o memorie foarte rapidă,
de mici dimensiuni (tipic, mai puţin de 1MB), plasată foarte aproape de procesor.
Este proiectată să păstreze datele şi instrucţiunile solicitate frecvent de procesor.
Pentru că preluarea datelor din cache are loc numai într-o fracţiune din timpul
necesar accesării memoriei principale, prezenţa memoriei cache salvează foarte
mult timp. Principiul pe care se bazează mecanismul cache este regula 80/20, care
spune că, din toate programele, informaţiile şi datele din calculator, aproximativ
20% sunt utilizate 80% din timp. În consecinţă, este foarte probabil ca datele şi
instrucţiunile folosite de procesor la un moment dat să fie necesare din nou, la
puţin timp după.
Memoria cache este ca o „listă fierbinte” (hot list) de instrucţiuni necesare
procesorului. Unitatea de management a memoriei salvează în cache fiecare
instrucţiune solicitată de procesor; la o nouă cerere similară, procesorul primeşte
instrucţiunea din cache (cache hit). Totodată, acea instrucţiune avansează către
partea superioară a listei. Atunci când memoria cache este plină şi procesorul
solicită o nouă instrucţiune, sistemul suprascrie instrucţiunile care nu au fost
utilizate de cea mai lungă perioadă de timp. Acest mecanism este valabil şi pentru
date. În acest fel, informaţiile cu prioritate ridicată, folosite des, rămân în cache, în
timp ce informaţiile mai puţin utilizate sunt eliminate.
În prezent, memoriile cache sunt încorporate chiar în cipul procesorului.
Cum majoritatea procesoarelor conţin mai multe nuclee de procesare, numite core-
uri, în funcţie de proximitatea sa faţă de acestea, memoria cache are atribuit un
nivel. Memoria cache cea mai aproape de un nucleu se numeşte cache de nivel 1
(L1), următorul nivel de cache este notat L2, apoi L3, ş.a.m.d.. De exemplu, un
procesor Core i7 Quad dispune de 64 kB cache L1 (32 kB pentru instrucţiuni, 32
kB pentru date), 1 MB cache L2, ambele per nucleu, şi de 8 MB cache L3, partajat
între nuclee. Toate pe pastila procesorului. Totodată, majoritatea procesoarelor
moderne folosesc două memorii cache L1, una pentru date şi una pentru
instrucţiuni, în configuraţie arhitecturală tip Harvard.
Conceptul de cache se aplică nu numai memoriei principale, ci şi mediului
de stocare. De exemplu, sistemul de operare poate folosi memoria principală ca
memorie intermediară pentru disc. În acest caz, memoria principală înregistrează
cele mai recente date citite de procesor de pe disc sau stochează date ce vor fi
scrise de procesor pe disc la un moment viitor. Reţinem aşadar că termenul cache
se poate referi atât la memorie cât şi la alte tehnologii de stocare.

Registre
Unele instrucţiuni au nevoie ca datele prelucrate de acestea să fie
memorate chiar în interiorul procesorului. Acest lucru se realizează prin
intermediul unor locaţii de memorie numite registre. Registrele sunt locaţii de
memorie volatilă9 aflate în interiorul procesorului. Procesorul accesează mult mai
rapid datele stocate în registre decât pe cele aflate în memoria principală sau cache.
Pe de altă parte, numărul de registre este limitat. Dimensiunea registrelor, în biţi,
determină şi arhitectura procesoarelor - procesoare de 16, 32 sau 64 de biţi. În
această carte ne referim la arhitectura procesoarelor Intel de 32 de biţi - registrele
interne au capacitatea de 4 octeţi.

Indicatorul de instrucţiune
Indicatorul de instrucţiune este registrul care memorează adresa următoarei

9
În lipsa unei surse de energie, informaţiile sunt pierdute. Memoria RAM este tot o
memorie volatilă.
instrucţiuni din secvenţa de cod executată.

Registrul de instrucţiune
Instrucţiunile sunt simple şabloane (pattern) de biţi. Acest registru conţine
instrucţiunea executată în acel moment. Modelul său de biţi determină unitatea de
execuţie să comande celorlalte unităţi din procesor o anumită operaţie. Odată ce
acţiunea a fost finalizată, biţii instrucţiunii (biţii prin care aceasta a fost codificată)
din registrul de instrucţiune pot fi înlocuiţi şi procesorul va efectua operaţia
specificată de biţii noii instrucţiuni.

Unitatea de execuţie
Biţii din registrul de instrucţiune sunt decodificaţi de unitatea de execuţie.
Aceasta generează semnalele care comandă celorlalte unităţi din procesor
efectuarea acţiunilor specificate de instrucţiune. De obicei este implementată sub
forma unei maşini cu stări finite care conţine decodoare, multiplexoare şi alte
circuite logice.

Unitatea aritmetică şi logică (ALU)


ALU denotă circuitele electronice care efectuează operaţii aritmetice şi
logice pe grupuri de biţi de date.

Registrul indicatorilor de stare


Registrul indicatorilor de stare semnalează anumite evenimente apărute în
cadrul operaţiilor aritmetice şi logice. De exemplu, operaţia de adunare poate
produce un transport. Un anumit bit din registrul indicatorilor de stare va fi setat în
zero (nu există transport) sau unu (există transport) de fiecare dată când ALU a
finalizat o astfel de operaţie.

2.2.3. Funcţionarea procesorului

Setul de instrucţiuni în idiom binar care poate fi executat de un procesor


formează limbajul maşină al acelui tip de procesor. Instrucţiunile maşină sunt
şiruri de numere binare pe care procesorul le decodifică şi execută foarte rapid.
Fiecare tip de procesor are propriul limbaj maşină. Din această cauză, programele
scrise pentru procesoarele Intel x86 nu pot rula pe alte tipuri de procesoare
(POWER, SPARC, Intel Itanium, ARM).
Procesorul poate fi privit ca dispozitiv ce execută la nesfârşit următorul
proces:
• Extragere instrucţiune din memorie (fetch);
• Decodificare instrucţiune (decode);
• Execuţie instrucţiune.
Acest proces este numit ciclu de extragere-decodificare-execuţie sau,
simplu, ciclu de execuţie.

Ciclu de execuţie
Extrage Decodifică Execută Extrage Decodifică Execută
timp

La primele procesoare, de exemplu 8080, etapele ciclului de execuţie se


realizau secvenţial. Începând cu 80286, acestea sunt efectuate în paralel. În plus,
fiecare microarhitectură nouă adăugă diferite tehnici de eficientizare. Totuşi, din
perspectiva programatorului, mecanismul prezentat aici este tot ce interesează.
Extragerea unei instrucţiuni din memorie presupune plasarea adresei sale
pe magistrala de adrese şi activarea semnalului de citire pe magistrala de control
(lucru care indică intenţia de citire a locaţiei respective). După iniţierea cererii de
citire procesorul aşteaptă apariţia instrucţiunii pe magistrala de date.
Decodificarea presupune identificarea instrucţiunii extrase. Pentru
facilitarea acestui proces, instrucţiunile maşină folosesc o schemă particulară de
codificare (un cod de operaţie).
Execuţia instrucţiunii înseamnă prelucrarea operaţiei aritmetice sau logice.
Execuţia instrucţiunilor este sincronizată cu ajutorul unui generator de tact, numit
ceas10. Generatorul de tact pulsează la o frecvenţă fixă cunoscută sub numele de
frecvenţa ceasului (deşi se utilizează uzual şi termenul de frecvenţa procesorului).
Ceasul nu contorizează minute sau secunde, doar “ciclează” la o frecvenţă
constantă. Circuitele electronice din procesor folosesc această frecvenţă pentru a
executa operaţiile corect, aşa cum un dirijor dictează ritmul unei piese. Frecvenţa
impulsurilor de tact determină viteza de execuţie a instrucţiunilor. Fiecare ciclu de
execuţie a unei instrucţiuni poate fi descompus într-o serie de operaţii elementare
efectuate în paralel, pe durata unui singur impuls de tact (ciclu de tact). O
asemenea operaţie elementară (o singură prelucrare numerică a informaţiei sau un
transfer de date între procesor şi memorie) se numeşte micro-operaţie. Numărul de
cicli de tact necesari execuţiei unei instrucţiuni depinde atât de instrucţiune cât şi
de modelul şi generaţia procesorului. O frecvenţă mai mare nu înseamnă neapărat
şi număr de operaţii pe secundă mai mare, depinde de arhitectura procesorului.

2.2.4. Registrele procesorului

Arhitectura IA-32 pune la dispoziţia programatorului zece registre de 32 de


biţi şi şase registre de 16 biţi. Aceste registre sunt grupate în următoarele categorii:
registre de uz general, registre de control şi registre de segment. Registrele de uz
general sunt la rândul lor împărţite în registre de date, registre indicator şi registre

10
Generator electronic pilotat de un cristal de cuarţ, care asigură stabilitatea frecvenţei la
variaţia tensiunii de alimentare şi a temperaturii.
index.

Registre de date

32 de biţi 16 biţi
↓ 31 16 15 8 7 0 ↓
EAX AH AL AX
EBX BH BL BX
ECX CH CL CX
EDX DH DL DX
Figura 2.5 Registre de date
Arhitectura IA-32 pune la dispoziţie patru registre de date de 32 de biţi
fiecare destinate operaţiilor aritmetice şi logice, dar nu numai. Aceste patru registre
pot fi folosite după cum urmează:
• 4 registre de 32 de biţi: EAX, EBX, ECX, EDX; sau
• 4 registre de 16 biţi: AX, BX, CX, DX; sau
• 8 registre de 8 biţi: AH, AL, BH, BL, CH, CL, DH, DL (H = High, L =
Low).

Registrele de date pot fi folosite fără constrângere în majoritatea


instrucţiunilor aritmetice şi logice. Totuşi, când sunt executate anumite instrucţiuni,
unele registre din acest grup au funcţii speciale. De exemplu, registrele EAX (AX),
denumit acumulator, şi EDX (DX), au rol prestabilit în instrucţiunile de înmulţire,
împărţire şi de transfer I/O. ECX (CX) este utilizat drept numărător pentru
operaţiile de deplasare, rotaţie, bucle, repetiţii de instrucţiuni.

Registre index şi indicator

31 16 15 0
ESI SI Source Index
EDI DI Destination Index
Figura 2.6 Registre index

31 16 15 0
ESP SP Stack Pointer
EBP BP Base Pointer
Figura 2.7 Registre indicator
Arhitectura IA-32 înglobează două registre index şi două registre indicator.
Acestea pot fi folosite ca registre de 16 sau 32 de biţi. Registrele index au rol
principal în prelucrarea instrucţiunilor, dar pot fi folosite şi ca registre de date.
Registrele indicator sunt folosite în special pentru lucrul cu stiva.
Stiva este o zonă din memoria principală organizată după principiul LIFO
(Last In First Out), folosită ca mijloc de depozitare a datelor temporare. Stivele
sunt strict necesare în lucrul cu subprograme (proceduri şi funcţii), când registrele
interne trebuie eliberate în vederea execuţiei unei funcţii care va suprascrie
registrele cu propriile sale date. Eliberarea registrelor se face prin salvarea lor în
stivă, într-o anumită ordine, şi refacerea lor din stivă la revenirea în programul
apelant.

Registre de control

31 16 15 0
EIP IP Instruction Pointer
Figura 2.8 Indicatorul de instrucţiune
Acest grup de registre constă din două registre de 32 de biţi: registrul
indicator de instrucţiune şi registrul indicatorilor de stare.
Procesorul foloseşte registrul indicator de instrucţiune ca să memoreze
adresa următoarei instrucţiuni ce va intra în ciclul de execuţie. Câteodată, acest
registru este denumit registru contor de program. Registrul indicator de instrucţiune
poate fi folosit ca registru de 16 (IP) sau 32 de biţi (EIP), în funcţie de mărimea
adreselor. Când o instrucţiune este extrasă din memorie, registrul indicator de
instrucţiune este reiniţializat automat cu adresa următoarei instrucţiuni. Registrul
poate fi modificat şi de o instrucţiune care transferă controlul execuţiei la altă
locaţie în program.
31 16 15 0
0 0 0 0 0 0 0 0 0 0 ID VIP VIF AC VM RF 0 NT IOPL OF DF IF TF SF ZF 0 AF 0 PF 1 CF
Valorile de de 0 şi 1 în gri sunte rezervate Intel.
Indicatori de stare Indicatori de control Indicatori de sistem
CF = Carry Flag DF = Direction Flag TF = Trap Flag
PF = Parity Flag IF = Interrupt Flag
AF = Auxiliary Carry Flag IOPL = I/O Privilege Level
ZF = Zero Flag NT = Nested Task
SF = Sign Flag RF = Resume Flag
OF = Overflow Flag VM = Virtual 8086 Mode
AC = Alignment Check
VIF = Virtual Interrupt Flag
VIP = Virtual Interrrupt Pending
ID = ID flag
Registrul indicatorilor de stare poate fi considerat ca fiind de 16 (FLAGS)
sau 32 de biţi (EFLAGS). Registrul FLAGS este folosit atunci când se execută cod
compatibil 8086.
Registrul EFLAGS conţine 6 indicatori de stare, 1 indicator de control şi
10 indicatori de sistem. Biţii acestui registru pot fi setaţi (1 logic) sau nu (0 logic).
Setul de instrucţiuni IA-32 conţine instrucţiuni capabile să seteze sau să şteargă
valoarea unora dintre indicatori. De exemplu, instrucţiunea clc şterge valoarea
indicatorului de transport (Carry Flag), iar instrucţiunea stc o setează.
Cei 6 indicatori de stare semnalizează un eveniment specific apărut în urma
execuţiei ultimei instrucţiuni aritmetice sau logice. De exemplu, dacă o instrucţiune
de scădere produce rezultat zero, procesorul setează automat indicatorul de zero,
ZF (Zero Flag ia valoarea 1). Vom discuta în detaliu indicatorii de stare atunci când
vom prezenta instrucţiunile aritmetice şi logice. Aici mai amintim faptul că
indicatorul de direcţie (DF) se diferenţiază de ceilalţi indicatori prezenţi în registrul
EFLAGS. Prin intermediul lui, programatorul poate specifica procesorului cum
trebuie să judece anumite instrucţiuni; aşadar, programatorul semnalizează ceva
procesorului şi nu invers. Rolul acestui indicator va fi discutat în detaliu în
secţiunea dedicată instrucţiunilor de operare pe şiruri.
Cei zece indicatori de sistem controlează modul în care operează
procesorul. De exemplu, setarea indicatorului de mod virtual, VM, forţează
procesorul să emuleze un 8086, iar posibilitatea setării şi ştergerii indicatorului ID
indică faptul că procesorul poate furniza programelor informaţie cu privire la
producătorul procesorului, familia din care face parte, modelul, etc., prin
intermediul instrucţiunii CPUID.

2.2.5. Întreruperile

Dispozitivele de intrare/ieşire pun la dispoziţia sistemelor de calcul


mijloacele prin care acestea pot interacţiona cu exteriorul. Un dispozitiv poate fi un
dispozitiv de intrare (de ex., tastatura, mouse-ul), un dispozitv de ieşire (de ex.,
imprimanta, monitorul), sau un dispozitiv de intrare şi ieşire (de ex., discul).
Calculatoarele folosesc dispozitive de intrare/ieşire, numite şi periferice, din două
motive principale: să comunice cu exteriorul şi să stocheze date. Dispozitive ca
imprimanta, tastatura, modemurile, plăcile de reţea sunt folosite în comunicaţia cu
exteriorul, iar discul la stocarea informaţiei. Deşi scopurile sunt diferite, sistemul
comunică cu aceste dispozitive prin intermediul aceleiaşi magistrale de sistem.
Dispozitivele de intrare/ieşire nu sunt conectate direct la magistrala de sistem;
comunicaţia dintre sistem şi periferic este gestionată de un controler de
intrare/ieşire.
Procesoarele accesează registrele interne ale controlerului de intrare/ieşire
prin porturile de intrare/ieşire. Un port I/O reprezintă adresa unui registru din
controlerul I/O. Procesoarele pot mapa porturile I/O la adrese de memorie,
mecanism numit memory-mapped I/O, sau pot folosi un spaţiu de adrese I/O
separat de spaţiul adreselor de memorie, tehnică numită isolated I/O.În primul caz,
scrierea la un port I/O este similară cu scrierea într-o locaţie de memorie. În al
doilea caz, spaţiul de adrese I/O este accesat cu ajutorul unor instrucţiuni speciale,
numite instrucţiuni de intrare/ieşire. Setul de instrucţiuni IA-32 furnizează două
astfel de instrucţiuni: IN şi OUT. Instrucţiunea IN este folosită pentru citirea
portului I/O, iar instrucţiunea OUT pentru scrierea portului I/O.
Procesoarele pot folosi simultan ambele strategii. De exemplu, imprimanta
şi tastatura sunt mapate în spaţiul de adrese I/O folosind strategia isolated I/O, iar
monitorul este mapat la un set de adrese de memorie folosind strategia memory-
mapped I/O.
Arhitectura IA-32 pune la dispoziţie un spaţiu de adrese I/O de 64 kB.
Acest spaţiu de adrese poate fi folosit pentru porturi I/O de 8, 16 şi 32 de biţi.
Totuşi, combinaţia nu poate depăşi 64 kB. Sistemele de operare pun la dispoziţia
aplicaţiilor şi utilizatorilor rutine (secvenţe de instrucţiuni) de acces la dispozitivele
I/O. Sistemele Linux şi Windows implementează un set astfel de rutine. Pe lângă
acestea, se pot folosi şi rutine prezente în BIOS. BIOS-ul este un soft rezident într-
o memorie ROM aflată pe placa de bază şi constă dintr-o colecţie de rutine care
controlează dispozitivele I/O.

2.3. Exerciţii

2.1. Care este diferenţa între microarhitectura şi arhitectura procesorului?

2.2. Descrieţi ciclul de execuţie.

2.3. Ce rol are memoria principal în structura de ansamblu a unui calculator?

2.4. Ce înseamnă arhitectură von Neumann? Care este problema principală a


acesteia?

2.5. Care processor este ultimul reprezentant al arhitecturii IA-32?

2.6. Dacă un procesor poate folosi o magistrală de adrese de 64 de linii, care este
mărimea spaţiului de memorie adresabil? Exprimaţi adresa ultimei locaţii de
memorie în hexazecimal.

2.7. Care sunt registrele de uz general la procesoarele Intel de 32 de biţi?


3. LIMBAJUL DE ASAMBLARE

Acest capitol prezintă nivelurile la care utilizatorul poate interacționa cu


sistemul de calcul și defineşte limbajul de asamblare în contextul acestei
interacțiuni. Totodată se explică pe ce nivel se regăsește limbajul de asamblare în
ierarhia limbajelor de programare. Deoarece reiese că limbajul de asamblare
reprezintă o etapă parcursă de orice compilator în procesul de generare a codului
obiect, prezentăm procesul de compilare a unui program scris în C. Capitolul se
încheie cu o descriere succintă a avantajelor și dezavantajelor asociate cu
programarea în diferite limbaje de programare. Această secțiune are rolul să ofere
motivația necesară studierii limbajului de asamblare și să demonstreze avantajele
acestuia.

Nivel 5

Aplicație

Crește nivelul Nivel 4 Indepentent de


de Limbaj de nivel înalt sistem
abstractizare (C, C++, Java)

Nivel 3

Limbaj de asamblare
Dependent de
Nivel 2
sistem
Limbaj mașină

Nivel 1

Apeluri de sistem

Nivel 0

Hardware

Figura 3.1 Nivelurile de interacțiune cu sistemul de calcul


Figura 3.1 prezintă nivelurile la care utilizatorul poate interacţiona cu un
sistem de calcul.
La nivelul programelor de aplicaţie utilizatorul interacţionează cu sistemul
de calcul prin interfaţa pusă la dispoziţie de o anumită aplicaţie, fie aceasta un
editor de text, un joc, etc.. Utilizatorul nu trebuie să cunoască în detaliu cum
funcţionează sistemul, problema se reduce numai la ştiinţa de a folosi aplicaţia.
Presupunând că este vorba de un editor de text, tot ce trebuie să stăpânească
utilizatorul, din punct de vedere tehnic, sunt combinaţiile de taste care introduc,
şterg, copiază text, etc..
Nivelul următor presupune cunoaşterea unui limbaj de programare de nivel
înalt, precum C sau Java. Un utilizator care interacţionează cu sistemul de calcul la
acest nivel trebuie să deţină cunoştinţe detaliate cu privire la dezvoltarea software.
Aceşti utilizatori sunt programatori de aplicaţii, care cunosc limbajul de
programare folosit la elaborarea aplicaţiei şi modul în care este ea realizată. În
schimb, nu este necesar să cunoască în detaliu cum funcţionează sistemul (doar
dacă nu cumva sunt implicaţi în dezvoltarea de drivere pentru dispozitive hardware
sau de compilatoare, asambloare, etc.).
Ambele niveluri, 4 şi 5, sunt independente de sistem, adică de tipul
particular de procesor folosit. O aplicaţie scrisă în limbaj de nivel înalt poate fi
rulată de sisteme cu arhitecturi diferite fără modificări suplimentare aduse codul
sursă. Această caracteristică a aplicaţiei se numeşte portabilitate, şi tot ce trebuie
făcut este ca programul să fie recompilat cu un compilator nativ sistemului
respectiv. În fapt, de aici provine şi denumirea de limbaj de nivel înalt - denotă
capacitatea limbajului de programare de a oferi o abstractizare a arhitecturii
sistemului de calcul. Unele limbaje pot ascunde complet detaliile specifice unei
anumite arhitecturi: modul de adresare a memoriei, alocarea/dealocarea memoriei,
lucrul cu dispozitivele externe, etc..
În contrast, dezvoltarea de programe sub nivelul 4 este dependentă de
sistem. Limbajul de asamblare se numeşte şi limbaj de programare de nivel scăzut
deoarece necesită cunoştinţe detaliate cu privire la organizarea internă a sistemului:
arhitectura procesorului, organizarea memoriei, ş.a.m.d. Instrucţiunile limbajului
de asamblare sunt native procesorului. Un program scris în limbaj de asamblare
pentru procesoare Intel nu poate fi executat de un procesor ARM sau SPARC.
Limbajul maşină este o rudă apropiată a limbajului de asamblare. Tipic,
este o corespondenţă de unu la unu între instrucţiunile limbajului de asamblare şi
instrucţiunile maşină. Procesorul înţelege numai limbajul maşină, ale cărui
instrucţiuni consistă din şiruri de 0 şi 1.
Vom vedea că, deşi limbajul de asamblare este considerat de nivel scăzut,
programarea în asamblare nu expune toate “măruntaiele” sistemului. Unele detalii
sunt ascunse de sistemul de operare. De exemplu, citirea datelor de la tastatură sau
afişarea lor la monitor se face prin intermediul unor funcţii puse la dispoziţie de
sistemul de operare.
Nivelul hardware execută instrucţiunile limbajului maşină.

3.1. Tipuri de limbaje de programare

3.1.1. Limbajul maşină

Fiecare familie de procesoare (x86, SPARC, Power, MIPS) are propriul


său limbaj, denumit cod maşină, exprimat în idiom binar. Acest limbaj este definit
în etapa de proiectare şi cuprinde totalitatea instrucţiunilor pe care le poate executa
un procesor. Instrucţiunea binară definită de producător în procesul de realizare a
procesorului se numeşte intrucţiune în cod maşină sau instrucţiune maşină. Tipuri
diferite de procesor conţin tipuri diferite de instrucţiuni maşină. Procesoarele sunt
clasificate frecvent pe baza numărului şi tipului de instrucţiuni maşină cunoscute.
Un program în cod maşină este denumit şi cod obiect. Figura 3.2 arată un
fragment de cod maşină scris pentru un procesor cu arhitectură IA-32. Să
presupunem că în memoria principală, reprezentată ca şir liniar de octeţi, avem
secvenţa de biţi prezentată în Figura 3.2.

10110000

10001001
11000010
10001001
11010001
10001001
11001000
7 0
Figura 3.2 Instrucţiuni maşină în memoria principală
În timpul rulării, procesorul citeşte instrucţiuni maşină din memorie.
Fiecare instrucţiune maşină poate conţine unul sau mai mulţi octeţi de informaţie.
Aceştia instruiesc procesorul să efectueze un proces specific: operaţii aritmetice şi
logice, transferul datelor între memorie şi procesor, etc.. Datele prelucrate de
instrucţiunile maşină sunt de asemenea stocate în memorie, iar octeţii
instrucţiunilor maşină nu sunt diferiţi de octeţii datelor utilizate de instrucţiune.
Biţii de date ilustraţi în Figura 3.3 sunt aceeaşi cu biţii de cod prezentaţi în Figura
3.2, numai semnificaţia lor este diferită. Pentru a diferenţia datele de instrucţiuni
sunt utilizaţi indicatori (pointeri) speciali care ajută procesorul să diferenţieze zona
de memorie care stochează date de zona de memorie care stochează instrucţiuni.
10110000

10001001
11000010
10001001
11010001
10001001
11001000
7 0
Figura 3.3 Secvenţă de date în memoria principală
Indicatorul de instrucţiune ajută procesorul să diferenţieze instrucţiunile
deja executate de cele care urmează să fie procesate. Evident, există instrucţiuni (de
ex., instrucţiunea de salt la o anumită adresă din program) capabile să schimbe
locaţia memorată în indicatorul de instrucţiune. În mod similar, există un indicator
de date, care adresează zona de memorie în care sunt stocate datele, şi un indicator
de stivă, care adresează zona de memorie destinată stivei.
Fiecare instrucţiune maşină trebuie să conţină cel puţin un octet denumit
opcode (operation code ), şi unul sau mai mulţi octeţi de informaţie. Opcode-ul,
sau codul de operație, defineşte operaţia ce trebuie realizată de procesor. Fiecare
familie de procesoare are predefinit propriul set de coduri de operaţie care
defineşte toate funcţiile disponibile.
Să traducem ce reprezintă pentru un procesor 80386 codul din zona de
memorie din exemplul nostru:
• primii doi octeţi, 1 0 0 0 1 0 0 1 1 1 0 0 1 0 0 0 (citiţi normal, de la stânga
la dreapta), ”forţează” procesorul să copieze în registrul EAX valoarea din
registrul ECX.
• următorii doi octeţi, 1 0 0 0 1 0 0 1 1 1 0 1 0 0 0 1, ”forţează” procesorul să
copieze valoarea lui EDX în ECX.
• următorii doi octeţi copiază valoarea din EAX în EDX.

Toate aceste operaţii sunt deduse dacă ştim că opcode-ul pentru


operaţiunea de copiere dintr-un registru în altul este 1 0 0 0 1 0 0 1 1 1, iar codul
pentru registrele EAX, ECX, EDX, este 0 0 0, 0 0 1, respectiv 0 1 0.

Reprezentarea hexazecimală a codurilor instrucţiune


Programul scris în limbaj maşină devine mai uşor de înţeles dacă se
recurge la reprezentarea codurilor binare în hexazecimal. Se obţine un aşa numit
hexdump (reprezentare hexazecimală) al programului.

B0

89
C2
89
D1
89
C8
7 0
Figura 3.4 Reprezentarea instrucţiunilor maşină în hexazecimal

3.1.2. Limbajul de asamblare

Codul maşină este limbajul „înţeles” de procesor. Limbajul de asamblare


poate fi definit ca fiind limbajul ce permite programatorului să aibă control asupra
codului maşină. Limbajul de asamblare specifică cod maşină. Instrucţiunile sale
sunt de fapt reprezentări simbolice ale instrucţiunilor maşină. Aceste coduri
simbolice sunt denumite mnemonici.
1 0 0 0 1 0 0 1 1 1 este definit simbolic prin cuvîntul MOV, care primeşte ca
argumente două registre. Mnemonica MOV înseamnă efectuarea unei copieri.
Deoarece sintaxa completă este 1 0 0 0 1 0 0 1 1 1 S S S D D D, unde S S S
este codul pentru registrul sursă, iar D D D este codul pentru registrul destinaţie, 1
0 0 0 1 0 0 1 1 1 0 0 0 0 0 1 înseamnă MOV ECX,EAX.
Secvenţa din exemplul nostru scrisă în limbaj de asamblare arată astfel:

mov eax,ecx
mov ecx,edx
mov edx,eax

Procesul prin care se coverteşte un program din limbaj de asamblare în cod


maşină se numeşte asamblare, iar unealta software care converteşte un program
scris în limbaj de asamblare în limbaj maşină se numeşte asamblor. Alternativ,
procesul de conversie a codului maşină în limbaj de asamblare se numeşte
dezasamblare, iar programul care realizează acest proces se numeşte dezasamblor.
Limbajul de asamblare este proiectat pentru o anumită familie de
procesoare. Limbajul de asamblare destinat unui procesor Intel x86 are altă
mnemonică faţă de limbajul de asamblare destinat unui procesor SPARC, de
exemplu.

3.1.3. Limbaje de nivel înalt

Putem programa în limbaj maşină, dar este foarte dificil. Chiar şi cel mai
simplu program obligă programatorul să specifice o mulţime de coduri de operaţie
şi octeţi de date. În plus, programul ar fi rulat numai de tipul de procesor căruia i-a
fost dedicat programul respectiv.
Limbajele de nivel înalt, de ex. C, au fost concepute pentru a nu ţine cont
de caracteristicile tehnice specifice unui procesor. Instrucţiunile limbajului de nivel
înalt pot fi convertite în cod obiect pentru fiecare familie de procesoare în parte.
Totuşi, codul scris în limbaj de nivel înalt trebuie tradus printr-un mecanism sau
altul în formatul limbajului maşină. Din acest punct de vedere, programele scrise în
limbaj de nivel înalt pot fi clasificate în trei mari categorii:
• compilate
• interpretate
• hibride

Programe compilate
Majoritatea aplicaţiilor sunt create în limbaje compilate. Programatorul
scrie programul folosind sintaxa specifică unui limbaj de nivel înalt. Fişierul care
conţine programul în format text se numeşte fişier sursă sau cod sursă. Acest text
este convertit în cod maşină specific unui tip de procesor. De obicei, ceea ce se
numeşte comun compilare este un proces în doi paşi:
• conversia codului sursă în cod obiect. Programul care realizează acest pas
se numeşte compilator.
• editarea legăturilor între diferite module obiect în vederea obţinerii
executabilului. Programul care efectuează acest pas se numeşte editor de
legături, sau linker.

Compilator

Fișier sursă Fișier obiect

Editor de
legături
Alt$fișier$obiect Fișier$executabil

Biblioteci$de$fișiere$obiect

Figura 3.5 Procesul de compilare


Paşii procesului de compilare convertesc instrucţiunile limbajului de nivel
înalt în instrucţiuni maşină. Fiecărei linii de cod scrise în limbaj de nivel înalt îi
corespunde una sau mai multe instrucţiuni maşină specifice tipului de procesor pe
care va rula aplicaţia. De exemplu, codul
int main()
{
int i=1;
exit (0);
}

este compilat în următoarele instrucţiuni maşină:

55
89 E5
83 EC 08
C7 45 FC 01 00 00 00
83 EC 0C
6A 00
E8 D1 FE FF FF

Acest pas produce un fişier intermediar, numit fişier obiect. Fişierul obiect
conţine cod obiect într-un anumit format înţeles de sistemul de operare, deşi încă
nu poate fi rulat de acesta. Codul obiect conţine numai datele şi instrucţiunile
maşină ale funcţiilor definite în program. Programul poate avea însă nevoie de
componente aflate în alte fişiere obiect (de ex., funcţia exit). Pentru adăugarea
acestor componente este necesar încă un pas. După transformarea codului sursă în
cod obiect, un editor de legături „leagă” fişierul obiect al programului de alte
fişiere obiect necesare acestuia şi crează fişierul executabil. Rezultatul editorului de
legături este un executabil ce poate fi rulat numai de tipul de sistem de operare pe
care a fost compilat programul. Din nefericire, fiecare sistem de operare foloseşte
un format de fişier executabil (sau obiect) diferit. O aplicaţie compilată pe un
sistem de operare tip Windows nu va rula pe Linux, sau viceversa.
Fişierele obiect care conţin funcţii foarte uzuale pot fi combinate într-un
singur fişier, numit bibliotecă. Bibliotecile pot fi legate de aplicaţii în timpul
compilării (biblioteci statice) sau în timpul rulării aplicaţiei (biblioteci partajate).
Bibliotecile statice sunt acele biblioteci ale căror module obiect (componente) sunt
incluse în fişierul executabil în momentul editării de legături. În cazul bibliotecilor
partajate, modulele obiect sunt adresate în momentul lansării în execuţie sau în
momentul rulării.

Programe interpretate
Spre deosebire de programul compilat, care rulează prin forţe proprii,
programul interpretat este citit şi rulat de un program separat, numit interpretor.
De-a lungul prelucrării aplicaţiei, interpretorul citeşte şi decodifică (interpretează)
fiecare instrucţiune în parte. Conversia programului în instrucţiuni maşină specifice
procesorului este realizată de interpretor în timpul rulării programului. Evident,
punctul sensibil al acestor tipuri de programe este viteza. În loc ca aplicaţia să fie
compilată direct în cod maşină, un program intermediar citeşte fiecare linie de cod
sursă şi procesează operaţiile respective. La timpul de execuţie se adaugă timpul de
care are nevoie interpretorul să citească şi să decodifice instrucţiunile aplicaţiei.
Avantajul este convenienţa. Pentru programele compilate, la fiecare
modificare a programului, fişierul sursă trebuie recompilat. La programele
interpretate, fişierul sursă poate fi modificat rapid şi uşor chiar în timp ce
programul rulează. În plus, programul interpretor determină automat funcţiile
adiţionale necesare codului principal (codul obiect al sursei iniţiale).

Programe hibride
Programele hibride combină caracteristicile programelor compilate cu
versatilitatea programelor interpretate. Un exemplu perfect este limbajul Java.
Java este compilat în aşa numitul cod octet (byte code). Codul octet este
similar codului maşină rulat de procesor, dar el însuşi nu este compatibil cu nicio
familie de procesoare. În schimb, codul octet Java trebuie interpretat de o maşină
virtuală (JVM - Java Virtual Machine) care rulează separat. Codul octet Java este
portabil, în sensul că poate fi rulat de către orice JVM, pe orice tip de sistem de
operare sau procesor. Implementările maşinilor virtuale Java pot diferi de la
platformă la platformă, dar toate pot interpreta acelaşi cod octet Java fără a fi
necesară recompilarea sursei originare.

3.2. Procesul de compilare

În concluzie, un program este compus din una sau mai multe instrucţiuni ce
descriu un proces, sau un set de procese, realizat de calculator, iar codul sursă este
o secvenţă de instrucţiuni şi/sau declaraţii scrise într-un limbaj de programare
lizibil. Codul sursă poate fi convertit în cod executabil de către un compilator sau
poate fi executat în timpul rulării cu ajutorul unui interpretor. Executabilul este un
fişier al cărui conţinut este interpretat drept program de către sistemul de operare.
Deşi un fişier sub formă de cod sursă poate fi un executabil (acest tip de fişier se
numeşte script), majoritatea fişierelor executabile conţin reprezentarea binară a
instrucţiunilor maşină (de aceea se mai numeşte şi binar) specifice unui tip de
procesor.
Compilatorul este un program (sau un set de programe) care translatează
textul scris în limbaj de programare de nivel înalt în limbaj maşină. Programul
originar este numit cod sursă, iar programul obţinut este numit cod obiect.
Portabilitatea sistemului de operare Unix a fost în parte consecinţa faptului
că în 1973 a fost rescris într-un limbaj de nivel înalt, şi anume, C. GCC (GNU C
Compiler), prima versiune nonproprietară de compilator C, a fost scris de Richard
Stallman în 1989 pentru proiectul GNU. Numele său, GNU, care provine de la
”GNU's Not Unix”, proclamă independenţa de restricţiile impuse de drepturile de
copiere. Compilatorul GNU C este foarte folosit, nu numai deoarece este gratuit,
dar şi pentru că a impus un standard în ceea ce priveşte utilitatea.
GCC translatează un program scris în C în cod maşină. Realizează în
acelaşi timp cei doi paşi ai procesului de compilare, adică compilarea propriu zisă
şi editarea de legături. Rezultatul este un executabil ce poate fi stocat în memoria
calculatorului şi rulat de procesor. Compilatorul GNU C lucrează în etape, aşa cum
se poate observa din Figura 3.6.

Unitate de Cod în Fișier


Cod sursă translatare asamblare Cod obiect executabil

prog.c prog.i prog.s prog.o a.out

Preprocesare Compilare Asamblare Editarea de legături

gcc -E
gcc -S
gcc -c
gcc

Figura 3.6 Etapele procesului de compilare


Comanda gcc prog.c parcurge toate etapele necesare translatării
codului sursă şi generează un executabil, numit a.out, capabil să ruleze exclusiv
pe sisteme Linux. Pentru a stopa execuţia procesului la un anumit moment, putem
folosi opţiunile din reprezentarea de mai sus.
Pentru exemplificare, considerăm programul C de mai jos. Nu este necesar
să cunoaşteţi limbajul C. Ne interesează numai să înţelegem efectul etapelor
parcurse de compilator.

/* prog.c */
#include <stdio.h>
int main() {
int var1 = 40;
int var2 = 50;
int var3;

var3 = var1;
var1 = var2;
var2 = var3;
return 0;
}

3.2.1. Preprocesarea

Etapa de preprocesare presupune înlocuirea/expandarea directivelor de


preprocesare din fişierul sursă.
Preprocesorul utilizat de GCC în această etapă este un program de sine
stătător numit CPP. Preprocesorul CPP analizează macrourile (cuvintele din sursă
care încep cu caracterul #) şi le expandează. În cazul nostru, directiva #include
copiază conţinutul fişierului stdio.h în fişierul sursă prog.c. Astfel, rezultatul
preprocesării este tot un program sursă, dar care nu mai include directivele
preprocesor, ci rezultatul acestora.

gcc -E prog.c -o prog.i


sau
cpp prog.c prog.i

Din aceste două variante, prima permite rularea preprocesorului cu toate


opţiunile implicite (dezirabilă). Fişierul rezultat .i este generat rar, dar poate fi
interesant de văzut cum sunt expandate macrourile complexe. Opţiunea -o
specifică numele fişierul de ieşire.

3.2.2. Compilarea

Compilarea este etapa în care din fişierul preprocesat se obţine un fişier în


limbaj de asamblare.
Rezultatul compilării este un fişier lizibil, cu sufixul .s, conţinând
programul scris în limbaj de asamblare. Compilatorul propriu-zis folosit de GCC
este CC. CC este un program complex, deoarece nu există o corespondenţă unu la
unu între instrucţiunile C şi instrucţiunile în asamblare. În plus, poate optimiza
codul în funcţie de cerinţe precum „generează varianta mai rapidă” sau „generează
varianta mai compactă”, rezultând secvenţe diferite de instrucţiuni în asamblare.
Totuşi, niciun compilator nu este infailibil, optimizări complexe nu pot fi realizate
decât de un programator în asamblare.

gcc –O0 -S prog.c -o prog.s –m32


sau
cc -S prog.c -o prog.s
Redăm integral rezultatul acestei comenzi:

.file "prog.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $40, -4(%ebp)
movl $50, -8(%ebp)
movl -4(%ebp), %eax
movl %eax, -12(%ebp)
movl -8(%ebp), %eax
movl %eax, -4(%ebp)
movl -12(%ebp), %eax
movl %eax, -8(%ebp)
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4)
4.5.2"
.section .note.GNU-stack,"",@progbits

Aşa arată în asamblare programul C din exemplul nostru. Totuşi, el nu se


regăseşte în forma studiată de noi în această carte. Deşi am fi putut genera cu
uşurinţă varianta dorită (gcc –O0 -S prog.c -o prog.s –m32 –
mintel)am preferat să folosim setarea implicită. Acest lucru ne permite să
demonstrăm faptul că limbajul de asamblare nu este un limbaj de sine stătător, ci
mai degrabă o familie de limbaje.
Limbajul de asamblare pentru arhitecturi Intel nu are o singură sintaxă. În
fapt, sunt două mari familii: sintaxa Intel şi sintaxa AT&T. Toate sistemele de
operare derivate din Unix sau înrudite folosesc nativ sintaxa AT&T. Toate
sistemele cu origini în DOS şi Windows folosesc sintaxa Intel. Sintaxa Intel este
forma limbajului de asamblare specificată în documentaţia Intel. Cele două sintaxe
nu sunt foarte diferite, folosirea uneia sau alteia fiind mai degrabă o “problemă de
gust”. Pe un sistem de operare Linux sintaxa implicită este AT&T, dar putem folosi
oricare din cele două. Dispunem şi de un utilitar capabil să transforme un program
dintr-o sintaxă în alta sau dintr-un idiom specific unui asamblor în altul (deoarece
asambloarele însăşi introduc modificări de sintaxă).

intel2gas -g prog.s -o intel.asm


Rezultat:
;FILE "prog.c"
SECTION .text
GLOBAL main
GLOBAL main:function
main:
push ebp
mov ebp,esp
sub esp,16
mov dword [ebp-4],40
mov dword [ebp-8],50
mov eax, [ebp-4]
mov [ebp-12],eax
mov eax, [ebp-8]
mov [ebp-4],eax
mov eax, [ebp-12]
mov [ebp-8],eax
mov eax,0
leave
ret
GLOBAL main:function (.-main)
;IDENT "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4)
4.5.2"

3.2.3. Asamblarea

Etapa de asamblare translatează codul scris în limbaj de asamblare în cod


binar.
Asamblorul utilizat de GCC se numeşte AS. AS este un asamblor care
cunoaşte sintaxa AT&T. Rezultatul asamblării este un fişier obiect. Fişierul obiect
reprezintă un amestec de cod maşină şi alte informaţii necesare în faza de editare
de legături; conţine o listă de simboluri (de ex., nume de variabile declarate, dar
nedefinite), locurile în care acestea apar în program, informaţii de relocare
(specifică adresele de memorie la care vor fi plasate datele şi instrucţiunile
programului în etapa de editare de legături). Nu în ultimul rând, fişierele obiect pot
conţine informaţie de depanare, inclusă în corpul lor cu opţiunea -g. Această
opţiune este necesară dacă se intenţionează depanarea programului.
gcc -g -c prog.s -o prog.o
sau
as -g prog.s -o prog.o

Fişierul obiect prog.o este generat într-un format binar specific platformelor Linux,
numit ELF (Executable and Linkable Format). Acesta descrie modul în care trebuie
structurat binarul unui program astfel încât să poată fi executat de un sistem de
operare Linux. Sistemele de operare Windows folosesc un format diferit (PE –
Portable Executable). De aceea, o aplicaţie compilată pentru Linux nu rulează pe
Windows, sau invers.
În Linux putem dezasambla conţinutul unui fişier binar (obiect sau
executabil) cu ajutorul unui utilitar ca objdump. Dezasamblarea înseamnă
„recuperarea” mnemonicilor limbajului de asamblare pornind de la codul maşină;
este operaţia inversă asamblării.

objdump -d -M intel prog

prog.o: file format elf32-i386

Disassembly of section .text:

08048394 <main>:
8048394: 55 push ebp
8048395: 89 e5 mov ebp,esp
8048397: 83 ec 10 sub esp,0x10
804839a: c7 45 fc 28 00 00 00 mov DWORD PTR [ebp-0x4],0x28
80483a1: c7 45 f8 32 00 00 00 mov DWORD PTR [ebp-0x8],0x32
80483a8: 8b 45 fc mov eax,DWORD PTR [ebp-0x4]
80483ab: 89 45 f4 mov DWORD PTR [ebp-0xc],eax
80483ae: 8b 45 f8 mov eax,DWORD PTR [ebp-0x8]
80483b1: 89 45 fc mov DWORD PTR [ebp-0x4],eax
80483b4: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc]
80483b7: 89 45 f8 mov DWORD PTR [ebp-0x8],eax
80483ba: b8 00 00 00 00 mov eax,0x0
80483bf: c9 leave

Se observă clar diferenţa dintre limbajul de asamblare şi limbajul maşină. Limbajul


maşină, în format hexazecimal, este reprezentat cu bold, iar în dreapta este
programul în limbaj de asamblare (în format Intel). Observaţi mnemonica
instrucţiunilor în limbaj de asamblare - abrevieri apropiate limbajului uman (NOP –
no operation, MOV – move, INT – interrupt). Din listing reiese echivalentul în cod
maşină al instrucţiunii NOP este 90H, şirul binar 1001 0000.
În concluzie, limbajul de asamblare este un limbaj mnemonic - un cod
format din una sau mai multe litere ce reprezintă un număr, abrevieri, cuvinte care
uşurează memorarea instrucţiunilor complexe şi fac programarea mai uşoară.
Programarea în cod maşină este posibilă, dar predispusă la erori şi mare
consumatoare de timp. Mnemonica uşurează sarcina programatorului, în schimb,
un program scris în limbaj de asamblare trebuie convertit în cod maşină. Acest
lucru este realizat de asamblor. Asamblorul generează translatări unu la unu între
cuvinte mnemonice şi instrucţiuni maşină.
A nu se confunda asamblorul cu compilatorul. Acesta din urmă este folosit
la conversia în cod maşină a unui program de nivel înalt, conversia efectuându-se
în bloc. De asemenea, convertoarele ce translatează linie cu linie se numesc
interpretoare.

3.2.4. Editarea legăturilor

Un fişier obiect este asociat unui singur fişier sursă. Fişierul obiect conţine
datele şi codul funcţiilor proprii unui program. Datele şi funcţiile poartă numele
generic de simboluri. Simbolurile externe modulului (adică funcţiile sau datele
nedefinite local, prezente în alte fişiere obiect) sunt marcate ca nedefinite. Această
etapă este folosită la rezolvarea simbolurilor nedefinite (operaţie denumită şi
rezolvarea simbolurilor) şi la unificarea într-un singur fişier a mai multor fişiere
obiect sau biblioteci. Rezultatul este un fişier binar cu un format apropiat de fişierul
obiect. În Linux este chiar acelaşi format, ELF. Editarea de legături se face cu
ajutorul programului LD, dar care, în cazul programelor C, e preferabil să nu fie
apelat separat (programul poate include numeroase biblioteci şi lista fişierelor
obiect poate fi foarte mare). În cazul programelor scrise în asamblare va fi folosit
de fiecare dată.

3.2.5. Formatul fişierelor executabile

Un fişier executabil este un fişier binar obţinut dintr-un set de fişiere obiect
şi biblioteci în urma operaţiei de editare de legături.
Un fişier executabil conţine codul maşină necesar îndeplinirii operaţiilor,
dar şi antete şi secţiuni de formatare auxiliare care specifică organizarea
executabilului în memorie, modul în care se foloseşte codul, zonele de memorie
alocate pentru acesta şi date, etc.. Un fişier executabil are, aşadar, un format bine
definit şi totodată strâns legat de sistemul de operare. Sistemul de operare este
responsabil cu interpretarea fişierului executabil şi generarea unui proces pe baza
acestuia.
Reţinem că formatul de fişier executabil este acelaşi şi pentru fişierele
obiect sau bibliotecile partajate. Exemple de formate de fişiere obiect/executabile
sunt:
• a.out – primul format folosit de sistemele Unix;
• ELF – folosit în sistemele Unix;
• PE – formatul implicit pe sistemele Windows;
• Mach-O – formatul implicit pe Mac OS X.

3.3. Avantajele limbajului de asamblare

Avantajele limbajelor de nivel înalt:


• Permit dezvoltarea rapidă a programelor. Limbajele de nivel înalt oferă
funcţii, proceduri, structuri de date predefinite care facilitează dezvoltarea
rapidă a programelor. În comparaţie cu echivalentele lor în limbaj de
asamblare, programele în limbaj de nivel înalt au număr relativ mic de linii
de cod şi sunt uşor de scris.
• Programele sunt uşor de revizuit. Programele scrise în limbaj de nivel înalt
sunt mai uşor de înţeles şi, dacă respectă anumite metode de programare,
uşor de revizuit. Putem adăuga cod sau corecta erori de programare foarte
rapid.
• Programe portabile. Programele în limbaj de nivel înalt ascund nivelurile
dependente de sistem. Ca rezultat, ele pot fi folosite pe diferite arhitecturi
de sistem cu puţine, sau fără, modificări. În contrast, programele în limbaj
de asamblare sunt total dependente de procesor.

Avantajele limbajului de asamblare:


• Eficienţa. Eficienţa se referă la cât de “bun” este un program în
îndeplinirea unui obiectiv. Întotdeauna când judecăm calitatea unui
program trebuie să ţinem cont de cantitatea de memorie ocupată şi de
timpul necesar acestuia în îndeplinirea obiectivului pentru care a fost
realizat. Programele scrise în limbaj de asamblare, din perspectiva spaţiului
de memorie pe care îl necesită faţă de echivalentele lor în limbaj de nivel
înalt, sunt mult mai compacte. De asemenea, timpul necesar execuţiei
programului în asamblare este mai mic decât echivalentul său de nivel
înalt.
• Acces eficient la hardware-ul sistemului. Limbajele de nivel înalt, prin
natura lor, furnizează o imagine abstractă a hardware-ului pe care îl
utilizează. Din această cauză este aproape imposibil ca programele să poată
executa sarcini care necesită acces direct la hardware-ul sistemului. De
exemplu, scrierea unui driver pentru un scaner aproape sigur necesită
cunoştinte de programare în limbaj de asamblare.

3.4. Exerciţii

3.1. Ce relaţie există între limbajul de asamblare şi limbajul maşină?

3.2. De ce este considerat limbajul de asamblare limbaj de nivel scăzut iar C limbaj
de nivel înalt?

3.3. De ce este importantă portabilitatea? Dacă doreşti ca un program să fie


portabil, scrii programul în C sau în limbaj de asamblare?

3.4. Care este diferenţa între compilator şi asamblor?

3.5. Ce rol are editorul de legături?

3.7. Ce înseamnă dezasamblare?

3.8. Ce conţine un fişier obiect?

3.9. Care sunt avantajele programelor interpretate?


4. DEZVOLTAREA PROGRAMELOR ÎN
LIMBAJ DE ASAMBLARE

Acest capitol prezintă procesul de dezvoltare a programelor în limbaj de


asamblare. Prima secțiune a capitolului descrie elementele constitutive ale
programelor în asamblare și modul în care acestea definesc un cadru de bază (un
arhetip). Se continuă cu definirea noţiunii de proces şi prezentarea structurii
acestuia în memoria principală. Urmează o secţiune esenţială programării în
limbaj de asamblare, şi anume declararea datelor în memorie. Pe baza tuturor
acestor noțiuni sunt prezentate uneltele de dezvoltare necesare creării
programelor în limbaj de asamblare.

4.1. Sintaxa limbajului de asamblare

Programele scrise în limbaj de asamblare sunt formate din trei categorii


diferite de declaraţii.
Prima categorie spune procesorului ce să facă. Aceste declaraţii sunt
numite instrucţiuni executabile sau simplu, instrucţiuni. Fiecare instrucţiune
conţine un cod de operaţie ce comandă asamblorului generarea unei instrucţiuni în
cod maşină. De obicei, fiecare instrucţiune executabilă generează o instrucţiune în
cod maşină.
A doua categorie furnizează asamblorului informaţii cu privire la diferite
aspecte legate de procesul de asamblare. Aceste instrucţiuni se numesc directive de
asamblare sau pseudoinstrucţiuni. Pseudoinstrucţiunile nu generează instrucţiuni în
cod maşină.
Ultima clasă de declaraţii pune la dispoziţia programatorului un mecanism
de substituţie a textului, numit macrodefiniţie (macro).
Toate declaraţiile limbajului de asamblare sunt scrise linie cu linie în
fişierul sursă şi toate au acelaşi format:

[etichetă] operaţie [operanzi] [;comentariu]

În unele declaraţii, câmpurile scrise între parantezele pătrate sunt opţionale.


Câmpul etichetă reprezintă un nume (litere, cifre sau caractere
speciale), primul caracter fiind literă sau caracter special. Fiecare etichetă are
asociată o valoare - adresa relativă a liniei respective în cadrul segmentului.
Câmpul operaţie reprezintă mnemonica instrucţiunii.
Câmpul operanzi defineşte operandul sau operanzii asociaţi
instrucţiunii, conform sintaxei cerute de codul de operaţie. Pot fi constante,
simboluri sau expresii de simboluri.
Câmpul comentariu reprezintă un text oarecare precedat de caracterul
punct şi virgulă.
Câmpurile unei declaraţii trebuie separate printr-un spaţiu sau un caracter
TAB. Numărul acestor caractere despărţitoare rămâne la discreţia programatorului.
Deşi asamblorul ignoră tot ce depăşeşte un singur caracter TAB, utilizarea mai
multor caractere de acest fel permite evidenţierea structurii programului şi dă
claritate textului.

4.2. Structura programului

Un program, indiferent de limbajul folosit la scrierea sa, este alcătuit din


două părţi importante: cod şi date. Codul, tradus în limbaj maşină, este plasat într-o
zonă de memorie numită segment de cod, iar datele, în funcţie de tipul lor, în zone
de memorie numite segmente de date. Împărţirea programelor scrise în limbaj de
nivel înalt în segmente are loc în procesul de compilare. În cazul programelor
scrise în limbaj de asamblare segmentele sunt vizibile încă din fişierul sursă.
Programatorul în asamblare este obligat să specifice prin directive speciale
începutul fiecărui segment folosit de programul său şi să îl „populeze” în
consecinţă. Denumirile exacte depind de asamblor. NASM (Netwide Assembler),
MASM (Microsoft Assembler), TASM (Borland Turbo Assembler) sunt câteva din
cele mai populare asambloare pentru procesoarele Intel. Asamblorul folosit de noi
delimitează segmentele prin directivele segment sau section.
Până în acest capitol am tot repetat faptul că instrucţiunile şi datele unui
program sunt introduse în segmente de memorie separate, aşadar, poate că nu vă
aşteptaţi să existe mai multe segmente de date. Următorul listing prezintă structura
tipică a unui program scris în asamblare:

section .rodata ;declară datele protejate la scriere


section .data ;declară segmentul de date iniţializate

...

section .bss ;declară segmentul de date neiniţializate


...

section .text ;declară segmentul de cod


global _start
_start:
nop ;instrucţiunea No Operation

... ;instrucţiunile programului

;instrucţiuni care întrerup rularea programului


mov eax,1
mov ebx,0
int 80h

section .rodata
date read-only

section .data
date iniţializate

section .bss
date neiniţializate

section .text
cod

Figura 4.1 Structura programului în limbaj de asamblare


Datele programelor sunt de două tipuri: iniţializate şi neiniţializate.
Section .data declară elemente de date ce dispun de valoare iniţială;
adică elementele de date cărora la momentul rezervării spaţiului de memorie li se
specifică şi valoarea iniţială (valoarea introdusă în locaţia de memorie respectivă).
Aceste elemente de date sunt folosite în programele de nivel înalt ca variabile
globale sau statice, iniţializate.

//variabilă globală iniţializată (în C)


int i = 56;
void main() {
}

Variabilele sunt locaţii de memorie din care se pot scrie şi citi valori. Declararea
variabilei presupune rezervarea unui spaţiu de memorie corespunzător păstrării
unei valori. Adresa la care a fost încărcată în memorie variabila este reprezentată
chiar de numele variabilei. Dacă folosim analogia memoriei ca dulap cu sertare,
declararea unei variabile iniţializate înseamnă introducerea unei valori în sertar şi
etichetarea acestuia cu numele variabilei.
Unele date iniţializate sunt protejate, în sensul că nu pot fi rescrise în
timpul rulării programului. Acestea se găsesc în secţiunea declarată cu section
.rodata. Din această categorie fac parte literalii din C. De exemplu, în cazul
instrucţiunii printf (”Hello World!\n”), şirul Hello World\n este
stocat în secţiunea .rodata.
În segmentul de date neiniţializate, declarat cu section .bss, rezervăm
un anumit număr de locaţii de memorie cărora le asociem o denumire. Etichetăm
sertarul dar nu specificăm nicio valoare. Valoarea este instanţiată automat cu valori
de zero (caracterul NULL). Variabilele neiniţializate se folosesc frecvent la
alocarea unor zone de memorie tampon (buffer).

//variabilă globală neiniţializată (în C)


int i;
void main() {
}

Aşadar, dacă până în prezent ştiam că, în memorie, datele sunt separate de
instrucţiuni, acum aflăm că zona de memorie care conţine date iniţializate este
separată de zona de memorie care conţine date neiniţializate. Listingul dinainte de
Figura 4.1 arată modul tipic de dispunere a segmentelor în program. Segmentul
.bss trebuie plasat întotdeauna înainte de segmentul .text, în timp ce
segmentul .data poate fi plasat şi după. Însă trebuie să ţinem cont că, pe lângă
funcţionalitate, programul trebuie să fie şi lizibil. Gruparea tuturor definiţiilor de
date la începutul fişierului sursă facilitează înţelegerea programului de către alţi
programatori.
Section .text declară zona de memorie ce conţine instrucţiunile
programului. Ca fapt divers, termenul de „program care îşi modifică propriul cod”
(self modifying code) denotă faptul că programul îşi poate modifica singur această
secţiune în timpul rulării. Pe de altă parte, mult mai important de reţinut este faptul
că atunci când editorul de legături converteşte fişierul sursă în fişier executabil,
acesta trebuie să recunoască instrucţiunea din segmentul de text de la care sistemul
de operare trebuie să înceapă rularea programului. Programatorul specifică punctul
de intrare în program (entry point), declarând o etichetă11, un identificator.
Eticheta _start indică instrucţiunea de la care trebuie să înceapă rularea
programului. Dacă editorul de legături nu găseşte această etichetă, va produce un
mesaj de atenţionare (warning: cannot find entry symbol _start;) şi va încerca să
ghicească singur punctul de intrare în program. Însă nu avem nicio garanţie că va
ghici corect.

Nu este obligatoriu ca numele etichetei de intrare în program să fie _start.


În acest caz, în etapa editării de legături trebuie să precizăm numele punctului de
intrare în program printr-un parametru specific editorului de legături.

Programatorul poate să „anunţe” punctul de intrare în program al aplicaţiei


curente şi altor programe. Acest lucru se face cu directiva global. Directiva
global indică etichetele din programul curent accesibile altor programe externe.
Segmentul de cod se încheie cu trei instrucţiuni. Recunoaştem instrucţiunea
MOV, întâlnită în paragrafele anterioare, şi deducem uşor efectul: registrul EAX ia
valoarea 1 iar registrul EBX valoarea 0. Instrucţiunea INT 080H solicită
sistemului de operare să studieze conţinutul registrelor şi să execute operaţia
codificată prin valorile acestora. Codul asociat valorii 1 în registrul EAX cere
sistemului de operare Linux să întrerupă execuţia programului. Aşa este apelată
funcţia de sistem (funcţii implementate la nivelul sistemului de operare)
sys_exit – ieşire din program, în Linux. Vom vorbi pe larg la timpul potrivit,
momentan reţinem că aceste linii permit întreruperea normală a procesului. Un
program aflat în execuţie se numeşte proces.

4.3. Structura procesului

Sistemul de operare, în momentul lansării programului în execuţie şi creării


procesului, alocă memorie pentru segmentele de mai sus. Pe lângă aceste zone
definite în executabil, în cadrul unui proces se alocă şi două zone de memorie

11
Denumiri introduse de programator în program cu scopul de a înlesni accesul la zonele de
memorie specificate de acestea.
foarte importante. Acestea sunt zonele de stivă şi heap. Pentru a înţelege mai bine
structura procesului trebuie să vorbim pe scurt de modul în care sistemul de
operare Linux organizează memoria principală în modul protejat de adresare.

4.3.1. Memoria virtuală în Linux

În modul protejat, procesoarele Intel x86 sunt procesoare de memorie


virtuală. Instrucţiunile nu specifică locaţiile de memorie prin adresele lor fizice, ci
prin adrese virtuale (reţinem aşadar că adresa prezentă în corpul unei instrucţiuni
este o adresă virtuală). Adresa virtuală este un număr alternativ dat unei locaţii
fizice de memorie. Sistemul de operare mapează adresele virtuale la adrese fizice.
Prin acest mecanism sistemul de operare poate executa în acelaşi timp12 mai multe
procese independent unul de altul, astfel încât rularea defectuoasă a unuia să nu
afecteze rularea celorlalte. Niciun proces nu este conştient de faptul că rulează
alături de altele şi nici nu poate interfera cu ele. Mecanismul memoriei virtuale
permite sistemului de operare să aloce o zonă din spaţiul de adrese fizice şi să o
prezinte procesului ca începând de la adresa virtuală 08048000H. Sistemul de
operare face acest lucru pentru toate procesele. Toate procesele consideră că blocul
de memorie alocat lor de către sistemul de operare începe de la adresa 08048000H
şi sfârşeşte aproape de 0BFFFFFFFH (adresa de sfârşit nu e întotdeauna aceeaşi).
Fiecare proces „crede” că rulează în propria zonă de memorie şi totuşi toate zonele
de memorie ale proceselor încep şi sfârşesc la aceleaşi adrese. Sistemul de operare
realizează acest lucru mapând aceeaşi adresă virtuală la o adresă fizică diferită.
Adresa 0BFFFFFFFH înseamnă puţin peste 3 GB. Chiar dacă majoritatea
calculatoarelor au mai puţină memorie fizică, sistemul de operare poate promite
programului 3 GB de memorie prin faptul că nu mapează toate adresele virtuale la
adrese fizice. De fapt, spaţiul de memorie virtuală pus la dispoziţia procesului este
împărţit în două blocuri:
• Blocul inferior - începe de la adresa 08048000H şi conţine secţiunile de
cod, date iniţializate şi neiniţializate. Acestuia îi este alocat numai spaţiul
necesar, în funcţie de mărimea codului şi a datelor definite.
• Blocul superior - începe din partea opusă, dinspre capătul superior al
memoriei, şi se întinde înspre blocul inferior. Adresa de început pentru al
doilea bloc nu este întotdeauna aceeaşi, dar nu poate fi mai mare de
0BFFFFFFFH. Acest bloc reprezintă stiva programului.

Stiva reprezintă o zonă de memorie în care sunt stocate variabilele locale şi

12
Proprietatea sistemului de operare de a executa mai mult de un program simultan se
numeşte multitasking. Se bazează pe capacitatea procesorului de a comuta rapid între
procese, creând iluzia de simultaneitate.
parametrii funcţiilor. La apelul unei funcţii, informaţiile menţionate formează un
cadru de stivă (stack frame). Atunci când se revine din funcţie, cadrul de stivă
asociat este eliberat. În stivă, datele sunt stocate utilizând metoda „ultimul intrat,
primul ieşit” (Last In First Out). Acest lucru înseamnă că spaţiul este alocat şi
dealocat la un singur capăt al memoriei, numit vârful stivei.

Stiva este o secţiune din memoria principală utilizată pentru stocarea temporară a
informaţiilor, în care cel mai recent element introdus este primul extras.

4 GB 0FFFFFFFFH

KERNEL SPACE

3 GB 0BFFFFFFFH

stivă Bloc superior

Acest spaţiu de memorie


virtuală este alocat numai
la cerere. Nu e “gol”, ci
NU EXISTĂ
USER SPACE (HEAP)

segment .bss Bloc inferior

segment .data

segment .text
08048000H

00000000H
7 0
Figura 4.2 Aspectul memoriei pentru un proces în linux

Spaţiul aparent neutilizat dintre cele două blocuri nu există. Dacă


programul are nevoie de memorie adiţională din acea zonă, trebuie numai să
încerce să o acceseze şi sistemul de operare va mapa rapid adresa virtuală la o
adresă fizică. Această zonă se numeşte heap.
Heap denotă o zonă de memorie folosită pentru crearea şi distrugerea
structurilor de date care au timp de viaţă limitat. În acest caz, blocurile de memorie
sunt alocate şi eliberate dinamic. Modelul de alocare şi dimensiunea blocului de
memorie nu sunt cunoscute decât la momentul rulării (runtime). Zona heap este
partajată de toate bibliotecile şi modulele încărcate dinamic de proces. Începe la
sfârşitul segmentului de date neiniţializate şi creşte către adrese mai mari.
Aşadar, în Linux, codul şi datele programului încep de jos, aproape de
08048000H, iar stiva începe de sus, aproape de 0BFFFFFFFH.

4.4. Tipuri de date

Segmentele .data şi .bss conţin directive de date care declară alocarea


unor zone de memorie. Sunt două tipuri de directive de date:
• directive D<x>, pentru date iniţializate (D=”define”)
• directive RES<x>, pentru date neiniţializate (RES=”reserve”)
x se referă la dimensiunea datelor şi se înlocuieşte cu indicatorii din
Tabelul 4.1.
Tabelul 4.1 Dimensiunea tipurilor de date
Unitate Indicator (x) Dimensiune (octeţi)
Byte (octet) B 1
word (cuvânt) W 2
double word D 4
quad word Q 8
ten bytes T 10

4.4.1. Date iniţializate

Declararea unei zone de memorie cu date iniţializate presupune definirea a


trei elemente.
• etichetă: numele acelei zone de memorie.
• D<x>, unde x este litera corespunzătoare dimensiunii datelor declarate.

DB Define Byte ;alocă 1 octet


DW Define Word ;alocă 2 octeţi
DD Define Doubleword ;alocă 4 octeţi
DQ Define Quadword ;alocă 8 octeţi
DT Define Ten Bytes ;alocă 10 octeţi

• valoare iniţială.
Este important să înţelegem că, spre deosebire de limbajele de nivel înalt,
eticheta din limbajul de asamblare nu face altceva decât să indice locaţia datelor în
memorie. Este un indicator, o adresă. Nu o valoare, nu un tip de date. Eticheta nu
specifică o dimensiune. Deşi este tentant să o gândim ca variabilă, eticheta este
mult mai limitată: reprezintă numai adresa virtuală a unui octet aflat undeva în
memorie.
Variabilele limbajelor de nivel înalt sunt nume simbolice asociate cu un
spaţiu de stocare de mărime definită şi cu o valoare cunoscută sau necunoscută. Pe
parcursul programului, atât locaţia, cât şi valoarea, se pot modifica. Dacă la
momentul declarării se atribuie o valoare de început (se defineşte variabila), atunci
vorbim de o variabilă iniţializată. Dacă se alocă spaţiu fără atribuirea unei valori
iniţiale, vorbim de variabilă neiniţializată.
Constanta reprezintă un tip special de variabilă a cărei valoare nu poate fi
alterată în timpul execuţiei programului. Valoarea rămâne fixă. Valoarea unei
constante este specificată o singură dată.
;
;date iniţializate
;
section .data
var db 55h ;octet cu numele var şi valoarea 0x55
vector db 55h ;o succesiune de trei octeţi
db 56h
db 57h
_char db 'a' ;caracterul „a”
cars db 'hello',12,10,'$' ;şir
sir db "hello"
_short dw 1234h
character dw 'a'
chars dw 'abc' ;caracterele 61h, 62h, 63h
_int dd 12345678h
opt dq 0123456789ABCDEFh
zece dt 0FFAAFFAAFFAAFFAAFFAAh

Fiecare element de date este plasat în memorie în ordinea în care este


definit. Elementele cu mai multe valori sunt plasate în memorie exact aşa cum apar
în corpul directivei de date.
Deoarece pentru multe asambloare, inclusiv pentru cel folosit de noi, short
şi int sunt cuvinte rezervate (instrucţiuni), pentru a le folosi ca etichete am adăugat
prefixul underscore. Am reprezentat harta memoriei în Figura 4.3.
Când utilizăm definiţii multiple de date, asamblorul alocă un spaţiu
contiguu de memorie. De aceea, secvenţa de directive vector poate fi abreviată:
vector db 55h,56h,57h

Din acest punct de vedere, secvenţa,

cars db 'hello',12,10,'$'

este abrevierea de la

cars db 'h'
db 'e'
db 'l'
db 'l'
db 'o'
db 12
db 10
db '$'

Valorile de iniţializare pot fi definite şi ca expresii. De exemplu,

_init dw 10*25

este echivalent cu

_init dw 250

Asamblorul evaluează expresia şi atribuie etichetei valoarea rezultată. Deşi


utilizarea unor astfel de expresii nu este indicată, există situaţii în care o expresie
evidenţiază mai bine semnificaţia datei respective.

Ordinea octeţilor

Un octet poate reprezenta o valoare cuprinsă între 0 şi 255. Numerele mai


mari de 255 folosesc mai mult de un octet. O secvenţă de doi octeţi alăturaţi poate
reprezenta orice număr între 0 şi 65535. Totuşi, o dată ce ai o valoare numerică
care nu poate fi reprezentată pe un singur octet, ordinea octeţilor devine crucială.
Luăm ca exemplu numărul zecimal 681. Limbile europene evaluează
numerele de la dreapta la stânga. Numărul 681 constă în 6 sute, 8 de zece şi 1 de
unu, nu din 1 sute, 8 de zece şi 6 de unu. Prin convenţie, cea mai puţin
semnificativă coloană este cea din dreapta şi valorile cresc de la dreapta la stânga.
Dacă transformăm 681 în hexazecimal obţinem o secvenţă de doi octeţi: 02A9H.
Cel mai semnificativ octet (MSB – Most Significant Byte) este cel din stânga
(02H), iar cel mai puţin semnificativ (LSB – Least Significant Byte) este cel din
dreapta (A9H). Dacă am inversa ordinea lor am obţine un număr zecimal diferit
(A902H = 43266). De aceea, trebuie să fim atenţi cum scriem valorile zecimale
reprezentate în hexazecimal şi, mai ales, să ştim cum judecă sistemul de calcul
numerele hexazecimale. Din acest ultim punct de vedere, sistemul de calcul are
două posibilităţi:
• octetul mai puţin semnificativ este introdus la adresă de memorie mai
mică, iar octetul mai semnificativ la adresă de memorie mai mare;
• octetul mai puţin semnificativ este introdus la adresă de memorie mai
mare, iar octetul mai semnificativ la adresă de memorie mai mică.

Posibilităţile se exclud reciproc. Despre procesorul care stochează octetul


cel mai puţin semnificativ la adresă mai mică şi octetul cel mai semnificativ la
adresă mai mare spunem că respectă convenţia little-endian. Procesorul care
stochează octetul cel mai semnificativ la adresă mai mică, lucrează conform
convenţiei big-endian. Am spus procesorul, nu sistemul de operare.

I always regret that I didn't fix up some idiosyncrasies of the 8080 when I had a
chance. For example, the 8080 stores the low-order byte of a 16-bit value before
the high-order byte. The reason for that goes back to the 8008, which did it that
way to mimic the behavior of a bit-serial processor designed by Datapoint (a bit-
serial processor needs to see the least significant bits first so that it can correctly
handle carries when doing additions). Now there was no reason for me to continue
this idiocy, except for some obsessive desire to maintain strict 8080 compatibility.
But if I had made the break with the past and stored the bytes more logically,
nobody would have objected. And today we wouldn't be dealing with issues
involving big-endian and little-endian - the concepts just wouldn't exist.

Stephen Morse

Sistemul de operare Linux utilizează ambele tipuri de „ordine”, în funcţie de


arhitectura hardware pe care este instalat. Întreaga arhitectură x86, de la 8086 până
la Haswell, este little-endian. Alte arhitecturi hardware, ca ARM sau POWER, sunt
big-endian. Unele arhitecturile hardware, MIPS şi Intel Itanium, sunt bi-endian, în
sensul că pot fi configurate să interpreteze valorile numerice ori într-un fel, ori în
celălalt. Dar asta nu e tot. Limbile europene evaluează numerele de la dreapta la
stânga, dar şirurile de caractere sunt evaluate invers, de la stânga la dreapta. Din
această perspectivă, dacă un şir arbitrar de digiţi hexazecimali, ABCD, este
considerat şir de cifre hexazecimale (număr), de exemplu 0ABCDH, atunci MSB
este ABH şi LSB este CDH. În memorie, va apărea în ordinea CD AB. Dacă un şir
arbitrar de digiţi hexazecimali, ABCD, este considerat şir de caractere (cuvânt,
text), de exemplu 'ABCD', atunci MSB este caracterul D, iar LSB caracterul A. În
memoria unui sistem little-endian şirul de caractere va fi ordonat A B C D.
0xFF Etichetă Adresă
0xAA
0xFF 0x80490bb
0xAA 0x80490ba
0xFF 0x80490b9
0xAA 0x80490b8
0xFF 0x80490b7
0xAA zece 0x80490b6
0x01 0x80490b5
0x23 0x80490b4
0x45 0x80490b3
0x67 0x80490b2
0x89 0x80490b1
0xAB 0x80490b0
0xCD 0x80490af
0xEF opt 0x80490ae
_int este număr. Octetul mai puţin 0x12 0x80490ad
semnificativ, 0x78, la adresă mai 0x34 0x80490ac
mică (little-endian) 0x56 0x80490ab
0x78 _int 0x80490aa
0x00 0x80490a9
0x63 0x80490a8
0x62 0x80490a7
0x61 chars 0x80490a6
Caracterul 'a' reprezentat pe un 0x00 0x80490a5
cuvânt. 0x61 character 0x80490a4
0x12 0x80490a3
0x34 _short 0x80490a2
sir este şir de caractere. Octetul mai 0x6F 0x80490a1
puţin semnificativ, 'h', la adresă mai 0x6C 0x80490a0
mică (little-endian). 0x6C 0x804909f
0x65 0x804909e
0x68 sir 0x804909d
caracterul $ 0x24 0x804909c
10 0x0A 0x804909b
12 0x0C 0x804909a
'o' 0x6F 0x8049099
'l' 0x6C 0x8049098
'l' 0x6C 0x8049097
'e' 0x65 0x8049096
'h' 0x68 cars 0x8049095
'a' 0x61 char 0x8049094
0x57 0x8049093
0x56 0x8049092
0x55 vector 0x8049091
0x55 var 0x8049090
7 0
Figura 4.3 Harta memoriei
Harta memoriei din Figura 4.1 precizează şi adresele corespunzătoare
etichetelor. Numele simbolic var este adresa 0x8049090, ş.a.m.d..
Pseudoinstrucţiunile DB, DW, DD, DQ, DT alocă spaţiu de stocare şi asociază
acestuia eticheta prin care poate fi accesat. La momentul asamblării, asamblorul
atribuie fiecărei etichete un deplasament. Aşa cum am specificat anterior,
asamblorul alocă datelor un spaţiu contiguu. De asemenea, asamblorul păstrează
ordinea datelor din fişierul sursă. Ca să deducă deplasamentul unui element de
date, asamblorul nu trebuie decât să numere octeţii alocaţi datelor de dinainte. De
exemplu, deplasamentul lui char este 4 deoarece var şi vector au alocaţi 1,
respectiv 3 octeţi.
Tabelul 4.2 Tabela de simboluri
Nume Deplasament
var 0
vector 1
char 4
cars 5
sir 13
_short 18
character 20
chars 22
_int 26
opt 30
zece 38

Tabelul 4.2 prezintă tabela de simboluri corespunzătoare hărţii de memorie din


Figura 4.3. Odată aleasă adresa de început, celelalte sunt calculate prin adunarea
deplasamentului. Acest lucru permite adresarea oricărui element din segmentul de
date prin intermediul etichetei var.

Constante

Constanta este o etichetă care are asociată o valoarea fixă. O astfel de


etichetă este tratată asemănător constantelor din limbajele de nivel înalt. De fiecare
dată când asamblorul întâlneşte o astfel de etichetă, va înlocui numele cu valoarea.
Reţinem că acest lucru se întâmplă la asamblare. Constantele pot fi definite cu
directivele EQU, %assign şi %define.
Directiva EQU defineşte constante numerice şi nu permite redefinirea
ulterioară în cadrul programului. EQU leagă un nume de un operand (de un singur
operand). Valoarea rămâne aceeaşi pe parcursul întregului program.
De exemplu, următoarea directivă defineşte o constantă numită CR.
Valoarea ASCII pentru carriage-return este atribuită acestei constante prin
directiva EQU.

CR equ 0Dh ;caracterul carriage-return


NR_CARACTERE equ 16
NR_DE_RAND equ 25
NR_DE_COL equ 80
MARIME_VECTOR equ NUM_DE_RAND * NUM_DE_COL

Constantele CR, NR_CARACTERE,NR_DE_RAND, etc., nu pot fi


redefinite mai târziu în cadrul programului. Totuşi, definirea unor constante aduce
programului două avantaje: devine mai lizibil şi mai uşor de modificat. Prin
asocierea valoare - nume descriptiv, instrucţiunea devine mai uşor de înţeles.
Acum ştim că valoarea 0DH din acea instrucţiune reprezintă caracterul carriage-
return. Pe de altă parte, apariţii multiple ale constantei pot fi modificate dintr-un
singur loc. Pentru a modifica numărul de caractere acceptat de la tastatură de la 16
la 160 trebuie să schimbăm numai valoarea din cadrul directivei NR_CARACTERE.
Operandul unei directive EQU poate fi o expresie evaluată la momentul
asamblării. Ultimele trei propoziţii definesc mărimea vectorului la 2000.
Foarte frecvent, constanta definită cu EQU se foloseşte la memorarea
lungimii unui şir.

mesaj db 'Hello world'


lungime equ $-mesaj

Constanta lungime va avea valoarea 11 şi nu va putea fi redefinită pe tot


parcursul programului. Caracterul $ reprezintă un simbol special al asamblorului,
evaluat ca fiind adresa de început a liniei care conţine expresia. Valoarea sa
reprezintă deplasamentul curent în segment. Aşadar, în cazul exemplului anterior,
caracterul $ indică primul octet după şirul 'Hello world'. Cum mesaj este adresa
de început a şirului Hello world (adresa lui 'H'), $-mesaj reprezintă numărul de
octeţi ocupaţi de şir. În cazul de faţă, numărul de octeţi ocupaţi de şir se confundă
cu numărul de elemente al şirului, adică lungimea acestuia. Dacă considerăm şirul
mesaj2, lungimea se află împărţind numărul de octeţi la 4, deoarece fiecare
caracter este reprezentat pe 32 de biţi, adică 4 octeţi.

mesaj2 dd 'Hello world'


octeti equ $-mesaj2
lungime2 equ ($-mesaj2)/4

În cazul EQU, simbolurile cărora le-au fost alocate o valoare nu pot lua alte
valori pe parcursul programului. Dacă sunt necesare redefiniri, trebuie să folosim
directiva %assign. Directiva %assign defineşte tot o constantă numerică, dar
permite redefinirea ulterioară în cadrul programului. De exemplu, definim i ca
fiind j+1 astfel:

%assign i j+1

şi mai târziu, în cadrul programului, putem redefini i la valoarea j+10:

%assign i j+10

Directiva %assign este sensibilă la majuscule. Simbolurile I şi i sunt


tratate distinct. Dacă este un efect nedorit se poate folosi %iassign.
Directivele EQU şi %assign definesc constante numerice. Directiva
%define poate defini atât constante numerice cât şi constante tip şir. La fel ca
directiva precedentă, %define permite redefinirea şi are variantă insensibilă la
majuscule, %idefine.

%define i [EBX+2]

4.4.2. Date neiniţializate

În segmentul datelor neiniţializate nu declarăm valori, ci rezervăm un


anumit număr de locaţii de memorie. Fiecare directivă de rezervare primeşte ca
argument un singur operand - numărul de unităţi de memorie care trebuie rezervat
(octeţi, cuvinte, dublu cuvinte, etc.). Directiva de rezervare RES<x> ia următoarele
forme:

RESB (REServe a Byte) ;rezervă un octet


RESW (REServe a Word) ;rezervă un cuvânt
RESD (REServe a Doubleword) ;rezervă un dublu cuvânt
RESQ (REServe a Quadword) ;rezervă un cuvânt cvintuplu
REST (REServe Ten Bytes) ;rezervă 10 octeţi

Aşadar, RES<x> specifică întotdeauna numărul de elemente de memorie.

Câteva exemple:

tampon: resb 64 ;rezervă 64 de octeţi


cuvânt: resw 1 ;rezervă un cuvânt (2 octeţi)
vector: resq 10 ;rezervă un şir de 10 numere reale (80 octeţi)

Am putea presupune că declaraţiile precedente, introduse în secţiunea de


date neiniţializate a unui program, cresc mărimea executabilului cu 148 de octeţi.
Dar un beneficiu al declarării elementelor în secţiunea datelor neiniţializate este că
acestea nu sunt incluse în programul executabil.
Datele iniţializate sunt incluse în corpul programului executabil deoarece
trebuie iniţializate cu o valoare specifică. Zonele de date declarate în segmentul
BSS nu sunt iniţializate cu valori la creare (sunt iniţializate la momentul rulării),
aşadar nu sunt incluse în fişierul sursă. În capitolul următor, când vom prezenta
operatorul TIMES, vom demonstra acest lucru prin testarea mărimii unui program
ce alocă succesiv 100 de elemente de date iniţializate şi 100 de elemente de date
neiniţializate.

4.5. Procesul de dezvoltare al programelor

Privit în mare, procesul de dezvoltare al programelor în asamblare poate fi


rezumat astfel:
1. Editarea fişierului sursă;
2. Asamblarea fişierului sursă şi obţinerea fişierului obiect;
3. Coversia fişierului obiect (şi a altor module obiect asamblate anterior) într-
un singur fişier executabil cu ajutorul editorului de legături;
4. Testarea programului prin execuţia sa directă sau prin intermediul unui
depanator;
5. Dacă algoritmul trebuie îmbunătăţit se revine la pasul 1 şi se modifică
fişierul sursă;
6. Se repetă paşii anteriori până la obţinerea rezultatului dorit.

4.5.1. Editarea textului

Programele în asamblare (de fapt, orice programe, indiferent de limbajul de


programare utilizat) sunt scrise cu un editor de text. Nu are importanţă care, dar e
bine ca în alegerea acestuia să ţinem cont de câteva lucruri. Procesoarele de text
gen Microsoft Word şi LibreOffice Writer, pe lângă textul propriu zis, includ în
fişiere date suplimentare (de ex., formatarea paginii, tipul de font, mărimea sa,
antete de pagină) de care asamblorul nu are nevoie şi pe care nu le înţelege. E
posibil ca, în timpul asamblării, această informaţie suplimentară să genereze erori.
Pe de altă parte, este dezirabil ca editorul de text să poată evidenţia sintaxa
limbajului în care scriem - în cazul nostru, limbajului de asamblare. Nu toate
editoarele de text au această proprietate. De aici înainte se presupune că lucraţi cu
editorul de text vim.

Vim
Vi este un editor de text prezent în majoritatea sistemelor Unix. Prima
versiune a fost dezvoltată la Universitatea Berkeley în anul 1980. Vim este
acronimul lui „Vi Improved”, o variantă extinsă a lui vi, creată de Bram
Moolenaar în 1991. Vim include toate caracteristicile vi, plus multe altele noi,
destinate să ajute utilizatorul în procesul de editare al codului sursă.
În vim, comenzile sunt introduse numai prin intermediul tastaturii, ceea ce
înseamnă că putem ţine mâinile pe tastatură şi ochii pe ecran. Pentru utilizarea
acestuia avem nevoie de un terminal, aşadar introduceţi combinaţia de taste
CTRL+ALT+T. Cei care nu consideră introducerea comenzilor de la tastatură un
avantaj, pot instala gvim, o versiune grafică a lui vim. Gvim asigură integrarea cu
mouse-ul, prezintă meniuri şi bară de derulare.
Vim este o unealtă plină de funcţionalităţi şi dispune de un manual pe
măsură. Manualul poate fi activat din interiorul editorului prin comanda :help
(man nu conţine foarte multe informaţii). În continuare vom prezenta numai
comenzile principale.
Deschiderea unui fişier se realizează prin comanda vim fisier.txt.
Dacă fişierul nu exista încă, va fi creat. Atenţie, creat înseamnă că a fost selectată o
zonă temporară de memorie (buffer) care va reţine textul introdus de noi până la
salvarea lui pe disc. Dacă închidem fişierul fără să salvăm datele pe disc, am
pierdut tot ce am introdus în fişier, inclusiv denumirea. Numai salvarea datelor
duce la crearea fisier.txt pe disc. Din perspectiva interfeţei, bufferul este fereastra
în care apare textul în curs de editare. Ecranul vim conţine un buffer şi o linie de
comandă aflată în partea de jos a acestuia. În linia de comandă sunt afişate
informaţii de stare şi pot fi introduse comenzi.
Vim operează în mai multe moduri de lucru, ceea ce înseamnă că editorul
se comportă diferit, în funcţie de acestea. În această prezentare vom lucra în două
moduri: comandă şi editare. În mod comandă, tot ce tastăm este interpretat de
editor ca o comandă. Exemple de astfel de comenzi sunt: salvează fişierul,
părăseşte editorul, mută cursorul, şterge, caută, înlocuieşte, selectează porţiuni de
text, trece editorul în modul inserare. Modul editare (inserare) ne permite să
introducem text.
Acestă dualitate de operare înseamnă că orice tastă poate reprezenta o
comandă sau un caracter. De exemplu, tasta i (insert), introdusă în mod comandă,
comută editorul în modul editare, în modul editare este pur şi simplu caracterul i.
Modul editare este indicat prin cuvântul – INSERT – afişat pe linia de jos a
terminalului. În zona bufferului, liniile libere sunt indicate prin caracterul ~ (tilda).
În acest moment putem introduce text.
Salvarea fişierului înseamnă o comandă, aşadar, comutăm în mod comandă
apăsând tasta ESC. Putem ieşi din editor cu următoarele comenzi:

ZZ – salvează bufferul pe disc şi ieşi


:x - salvează bufferul pe disc şi ieşi (la fel ca ZZ)
:wq – salvează bufferul pe disc şi ieşi (la fel ca ZZ)
:q – ieşi din editor (funcţionează numai dacă modificările sunt salvate)
:q! - ieşi din editor fără să salvezi bufferul pe disc

Cu următoarele comenzi putem scrie bufferul pe disc.

:w - salvează bufferul în fişierul curent (fisier.txt)


:w output.txt - salvează bufferul în fişierul output.txt; nu rescrie fişierul
în caz că acesta există
:w! output.txt - salvează bufferul în fişierul output.txt; rescrie fişierul
dacă există.

Prima comandă salvează modificările în fişierul dat ca argument. A doua şi a treia


comandă ne permite să salvăm bufferul într-un fişier nou. Pentru a afla informaţii
suplimentare despre comanda :w, putem folosi :help w. Implicit, ecranul va fi
împărţit în jumătate, cu partea superioară afişând informaţii de ajutor. Închiderea
noii ferestre de face cu :q.
Navigarea prin text se face de regulă cu tastele săgeţi. Dar pentru că este
incomod să mutăm mâna frecvent în colţul din dreapta jos al tastaturii se pot folosi
tastele h, l, k, j (un caracter la stânga, dreapta, sus, jos). În plus, Space
deplasează cursorul cu un caracter la dreapta şi Enter poziţionează cursorul la
începutul liniei următoare. Alte comenzi care pot deplasa cursorul sunt:

G - poziţionează cursorul la ultima linie a documentului


gg - poziţionază cursorul la prima linie a documentului
^ - poziţionează cursorul la începutul liniei curente
$ - poziţionează cursorul la sfârşitul liniei curente
w - poziţionează cursorul un cuvânt înainte
b - poziţionează cursorul un cuvânt înapoi
fx - sări la următoarea apariţie a caracterului x pe linia curentă
Fx - sări la precedenta apariţie a caracterului x pe linia curentă
% - sări la paranteza care corespunde celei de sub cursor.
De asemenea, în mod comandă putem modifica textul:

x - şterge caracterul sub care este poziţionat cursorul


X - şterge caracterul dinaintea cursorului
dG - şterge de la linia curentă până la sfârşitul fişierului
dfx - şterge tot textul începând cu poziţia curentă până la următoarea
apariţei a caracterului x
dd - şterge linia curentă
dw - şterge următorul cuvânt
D - şterge restul liniei curente
u - anulează ultima comandă
U - anulează toate modificările făcute pe ultima linie editată
r - înlocuieşte caracterul curent cu cel introdus

Când ştergem ceva cu x, d (delete), sau altă comandă, textul este salvat într-o
memorie temporară. Îl putem realipi cu p (paste, deşi termenul tehnic în vi este
put). Comanda p are semnificaţii diferite, în funcţie de elementele de text şterse.
Folosită după dw (delete word):

p - plasează textul după cursor


P - plasează textul înainte de cursor

Folosit după dd:

p - plasează textul sub linia curentă


P - plasează textul deasupra liniei curente

Modificarea unui cuvânt sau a unei părţi de cuvânt (de la cursor până la sfârşitul
cuvântului) se face prin poziţionarea cursorului în locul de început şi tastarea
comenzii cw (change word). Editorul trece automat în mod editare. Comanda cw
este o comandă compusă din alte două comenzi; în acest caz, din alăturarea
comenzilor c (change) şi w (word). Alte exemple:

c$ - schimbă începând cu poziţia curentă până la sfârşitul liniei


c^ - schimbă începând cu poziţia curentă până la începutul liniei
4cw - schimbă următoarele 4 cuvinte
5dd - şterge următoarele 5 linii
3x - şterge următoarele 3 caractere
3X - şterge 3 caractere din faţa cursorului
2G - poziţionează cursorul la a doua linie a documentului
Copierea fără ştergere se face cu y (yank). Nu afectează textul, dar poate fi folosit
în conjuncţie cu p. În acest caz, p se comportă la fel ca în cazul lui d.

yw - copiază următorul cuvânt


yy - copiază linia curentă
3yy - copiază următoarele trei linii

Comanda de înlocuire r (replace) plasează vim în mod editare. Caracterul tastat va


înlocui caracterul curent, după care editorul revine automat în mod comandă. Pe
lângă comanda r, vim trece în mod editare şi cu i (insert), a (append) sau o
(open). În aceste cazuri, textul va fi introdus începând cu prima poziţie înainte de
cursor, prima poziţie după cursor, sau la începutul unei linii noi, creată imediat sub
linia curentă.
Pentru a căuta „înainte” într-un text, se foloseşte comanda /. De exemplu,
/text caută şirul text de la poziţia curentă a cursorului către sfârşitul fişierului.
Căutarea „înapoi” se face cu ? în loc de / . De exemplu, ?text caută de la poziţia
curentă a cursorului către începutul fişierului.
Ultima comandă discutată este cea de substituţie, s (substitute). Permite
înlocuirea unor părţi de text.

:[linii] s/text_vechi/text_nou/opţiune

Vechiul text este înlocuit cu cel nou în limita liniilor specificate de domeniul
opţional [linii]. Domeniul este specificat în format „de la, până la”. Dacă nu este
dat un domeniu, schimbările apar numai pe linia curentă, considerată implicit.
Câmpul opţiune modifică comanda. De obicei se foloseşte caracterul g, care
înseamnă substituţie globală. Comanda

:s/test/text

înlocuieşte cu text prima apariţie a cuvântului test în linia curentă. Dacă doreşti să
înlocuieşti toate apariţiile din linia curentă, foloseşti opţiunea g

:s/test/text/g
Comanda

:1,10s/test/text

înlocuieşte prima apariţie a lui test în fiecare din cele 10 linii specificate. Ca să
schimbi toate apariţiile lui text în aceste linii, adaugi opţiunea g la sfârşit.
Am acoperit numai comenzile de bază. Vim permite câteva comenzi foarte
sofisticate.13

Un lucru foarte important, toate uneltele folosite de noi în procesul de


asamblare (editorul de text, asamblorul) recunosc fişierele text ca fişiere scrise în
limbaj de asamblare după extensia .asm. Cu alte cuvinte, fişierele sursă scrise în
limbaj de asamblare trebuie întotdeauna salvate pe disc cu extensia .asm (de ex.,
program.asm).
În continuare, scrieţi programul care urmează într-un fişier intitulat
prog.asm.

section .data
a db 0fh
b db 89
c dw 045E3h
d dw 65535
e dd 001234567h
f dd 1047000
g db 0ffh
section .text
global _start
_start:
nop
;încarcă imediatul 8H în registrul de 8 biţi AL
mov al, 8h
;încarcă imediatul 1239054 în registrul de 32 de biţi EAX
mov eax, 1239054
;copiază valoarea 89 (aflată în locaţia de memorie cu adresa b) în registrul de 8 biţi
BL
mov bl,[b]
;copiază valoarea 45E3H (aflată în locaţia de memorie cu adresa c) în registrul de
16 biţi CX
mov cx,[c]
;copiază valoarea 01234567H în registrul de 32 de biţi EDX
mov edx,[e]
;încarcă registrul de 32 de biţi EAX cu adresa etichetată a
mov eax,a
;încarcă registrul de 32 de biţi EBX cu adresa etichetată b
mov ebx,b

13
ftp://ftp.vim.org/pub/vim/doc/book/vimbook-OPL.pdf
;copiază în locaţia de memorie etichetată a valoarea aflată în registrul de 8 biţi AH.
În segmentul de date, a este declarată ca fiind etichetă la o locaţie de 8 biţi.
mov [a],ah
;copiază în locaţia f de 32 de biţi valoarea aflată în registrul de 32 de biţi ECX.
Evident, vechea valoare adresată de f se pierde.
mov [f],ecx
;copiază în registrul de 8 biţi AH valoarea 0ffh. Observăm parantezele pătrate. Fără
ele, asamblorul ar fi încercat să scrie în AH adresa g, şi cum registrul de 8 biţi AH
este prea mic pentru o adresă de 32 de biţi, asamblorul returnează o eroare
(relocation truncated to fit: R_386_16 against `.data').
mov ah, [g]
;întrerupe rularea programului
mov eax,1
mov ebx,0
int 80h

Programul realizează una din cele mai frecvente activităţi întâlnite în


practică, transferă date dintr-un loc în altul cu ajutorul instrucţiunii MOV.

4.5.2. Asamblarea

După editarea şi salvarea programului putem verifica prezenţa fişierului


sursă cu ajutorul comenzii ls. În urma acestei comenzi ar trebui să vedeţi fişierul
prog.asm. Dacă nu, verificaţi să nu fi omis ceva. Presupunând că fişierul sursă este
prezent, trebuie să îl transformăm în fişier obiect cu ajutorul unui asamblor. În
paragrafele următoarea prezentăm două din cele mai cunoscute asambloare.
NASM (Netwide Assembler) este un asamblor pentru arhitecturi Intel x86,
de la 16 până la 64 de biţi, care suportă toate extensiile acestora până în prezent.
Rulează pe o mare varietate de sisteme de operare (Linux, BSD, Windows).
Foloseşte o sintaxă similară cu cea Intel, dar mai puţin complexă, şi vine cu un
pachet serios de macroinstrucţiuni.
YASM este o rescriere completă a asamblorului NASM, sub o licenţă
diferită14. Suportă mai multe sintaxe de asamblare (de ex. NASM, GAS, TASM,
etc.) şi la fel de multe formate de fişiere obiect. În general, YASM poate fi folosit
alternativ cu NASM.
Avantaje:
• a fost primul care a implementat suport pentru arhitecturi x86_64. Acesta a
fost şi marele său avantaj până la NASM 0.99.00.

14
Licenţă BSD, spre deosebire de NASM licenţiat LGPL.
• fiind un proiect mai dinamic, YASM răspunde mai rapid cererilor venite de
la utilizatori (de informaţii sau noi opţiuni).
• poate asambla fişiere scrise atât în sintaxa Intel cât şi AT&T (gas).
• implementează o interfaţă ce poate fi folosită de către compilatoare.

Dezavantaje:
• NASM a fost utilizat şi depanat intensiv. YASM este un proiect activ, e
posibil să nu fie atât de bine testat ca NASM.
• Datorită resurselor superioare implicate de-a lungul timpului în dezvoltarea
NASM, documentaţia este mai completă.

NASM şi YASM sunt utilitare în linie de comandă. Deoarece în testele


noastre YASM s-a dovedit a suporta mai bine arhitecturi de 64 de biți, în această
carte recomandăm utilizarea acestuia. În schimb, pentru informaţii cu privire la
sintaxa, instrucţiunile, declaraţiile suportate de asamblor, consultaţi manualul
NASM.15

Asamblaţi fişierul sursă introducând următoarea comandă:

yasm -f elf -g stabs prog.asm -l prog.lst

Comanda de asamblare începe chiar cu numele asamblorului. După nume


urmează diverse opţiuni ce guvernează procesul de asamblare. Acestea pot fi găsite
în documentaţia YASM. Cele prezente sunt specifice platformei Linux:
• cuvântul YASM invocă asamblorul;
• -f elf specifică asamblorului faptul că fişierul obiect trebuie generat în
format ELF, propriu sistemelor Linux de 32 de biţi;
• -g specifică asamblorului să includă în fişierul obiect informaţie de
depanare şi indică formatul acesteia (în acest caz STABS);
• prog.asm, numele fişierului sursă pe care dorim să îl asamblăm;
• -l specifică asamblorului să creeze un listing al procesului de asamblare.
Fişierul listing include textul programului şi eventualele erori apărute la
asamblare.

Dacă YASM nu găseşte greşeli de sintaxă, procesul se finalizează cu


succes şi obţinem doar un nou prompt. Dacă aţi scris programul cu greşeli,
asamblorul semnalează atenţionări sau erori. Despre acestea vorbim mai târziu,
deocamdată asiguraţi-vă că fişierul prog.lst corespunde celui de mai jos. Tastaţi

15
http://www.nasm.us/doc/. Totuşi, nu neglijaţi resursele YASM: http://www.tortall.net/
projects/yasm/manual/manual.pdf
comanda cat prog.lst.

1 %line 1+1 prog.asm


2 [section .data]
3 00000000 0F a db 0f
4 00000001 59 b db 89
5 00000002 E345 c dw 045E3
6 00000004 FFFF d dw 65535
7 00000006 67452301 e dd 001234567
8 0000000A D8F90F00 f dd 1047000
9 0000000E FF g db 0ff
10 [section .text]
11 [global _start]
12 _start:
13 00000000 90 nop
15 00000001 B008 mov al, 8
17 00000003 B80EE81200 mov eax, 1239054
19 00000008 8A1D[00000000] mov bl,[b]
21 0000000E 668B0D[00000000] mov cx,[c]
23 00000015 8B15[00000000] mov edx,[e]
25 0000001B B8[00000000] mov eax,a
27 00000020 BB[00000000] mov ebx,b
29 00000025 8825[00000000] mov [a],ah
31 0000002B 890D[00000000] mov [f],ecx
33 00000031 8A25[00000000] mov ah, [g]
35 00000037 B801000000 mov eax,1
36 0000003C BB00000000 mov ebx,0
37 00000041 CD80 int 80

Acesta este rezultatul opţiunii -l din comanda de asamblare. Chiar dacă


procesul de asamblare nu a întâmpinat probleme, informaţia din fişierul prog.lst nu
este lipsită de importanţă. Cele mai interesante informaţii se desprind din coloana a
doua şi a treia. Coloana a doua specifică distanţa liniei curente faţă de începutul
fişierului sursă (dată în octeţi). Din aceste informaţii putem calcula numărul de
octeţi ai instrucţiunilor. A treia coloană prezintă codul maşină pentru fiecare
instrucţiune în limbaj de asamblare. Codul maşină al instrucţiunilor care lucrează
cu memoria sunt incomplete (vezi parantezele pătrate), deoarece în momentul
asamblării încă nu sunt cunoscute adresele la care se vor afla datele în memorie.
Acesta este rolul editorului de legături.
Formatul fişierelor obiect variază în funcţie de platforma pe care dorim să
rulăm programul. YASM este capabil să genereze toate formatele întâlnite pe
platformele suportate. Parametrul -f specifică asamblorului formatul fişierului
obiect. Pentru arhitecturi IA-32 şi sistem de operare Linux i386, formatul este
ELF32, sau simplu, ELF. Pentru arhitecturi x86-64 şi sistem Linux amd64,
formatul este ELF64.
În timpul dezvoltării unui program este dezirabil să includeţi în fişierul
obiect informaţie de depanare. În acest mod se poate verifica execuţia programului
şi depista eventualele erori de programare. Parametrul -g specifică asamblorului să
introducă informaţie de depanare în fişierul obiect. La fel ca în cazul formatului
obiect, YASM poate genera diferite formate de informaţie de depanare. Pentru un
sistem Linux şi o platformă de 32 de biţi se poate folosi formatul STABS sau
DWARF. Formatul STABS este mai simplu şi e tot ce ne trebuie în acest stadiu.
Reţineţi că sistemul de operare Linux diferenţiază literele mici de cele mari
în corpul comenzilor (case sensitive). Parametrul -f este diferit de parametrul -F.
La fel şi numele fişierului sursă: prog.asm este diferit de Prog.asm, sau orice altă
variaţie de caractere. Acelaşi lucru este valabil şi în cazul etichetelor din program.
Asamblorul diferenţiază eticheta b de B. În schimb, numele registrelor pot apărea
în orice variantă: eax, EAX, eAx, Al, aL, toate sunt corecte atât timp cât
succesiunea de litere denotă un registru existent. Să intrăm puţin în detalii.

Mesaje de eroare
Un fişier sursă scris corect este complet comprehensibil asamblorului şi
poate fi translatat în instrucţiuni maşină fără probleme. Dacă asamblorul găseşte
ceva ininteligibil în linia de program pe care o procesează, textul respectiv se
numeşte eroare, şi asamblorul afişează un mesaj de eroare. Erorile pot consta din
scrierea greşită a numelui unui registru sau din asocierea eronată a opcode-ului cu
operanzii. În cazul primei erori, mesajul de eroare afişat este de forma:

prog.asm:5: symbol 'etx' undefined

În acest caz mesajul este destul de clar, asamblorul nu cunoaşte cine este
etx. Pentru noi este un registru scris greşit. Pentru asamblor poate fi orice simbol al
programului, nu contează ce anume (putea fi numele unei variabile), problema este
că nu îl înţelege. Observaţi că numărul liniei care conţine eroarea este plasat
imediat după numele fişierului (:5).
Alăturarea eronată a opcode-ului cu operanzii este semnalizată prin
mesajul:
prog.asm:10: invalid combination of opcode and operands

Eşti avertizat că ai încercat o asociere invalidă între opcode şi operanzi.


Dar, ca să poţi înţelege ce este valid şi ce este invalid trebuie să ştii ce ai greşit.
Aşadar, mesajele de eroare ale asamblorului nu absolvă programatorul de la
cunoaşterea procesorului sau regulilor de asamblare. Atunci când asamblorul
afişează concomitent mai multe erori, este indicat să începeţi cu rezolvarea
primeia. Celelalte erori pot apărea din cauza acesteia.
Mesaje de eroare pot apărea şi în etapa de editare de legături, atunci când
editorul de legături întâmpină probleme în procesul de asociere a mai multor fişiere
obiect. Din fericire, aceste erori sunt mai puţin întâlnite.

Mesaje de atenţionare
Mesajele de eroare fac imposibilă generarea fişierului executabil; când
asamblorul sau editorul de legături întâlneşte o eroare, nu va furniza ca ieşire
niciun fişier.16 Mesajele de atenţionare apar când asamblorul întâlneşte ceva care
violează logica sa internă, dar nu atât de grav încât să oprească procesul de
asamblare. De exemplu, YASM va afişa o atenţionare dacă în program este definită
o etichetă căreia nu îi urmează nicio instrucţiune. În cazul atenţionărilor,
asamblorul se comportă ca un consultant care indică ceva ciudat în codul sursă.
Deşi executabilul va fi generat, este bine să ţinem cont de mesajele de atenţionare
şi să investigăm problemele apărute. Ignoraţi un mesaj de atenţionare numai dacă
ştiţi exact ce înseamnă.

4.5.3. Editarea legăturilor

În acest moment, YASM a generat un fişier obiect cu numele fişierului


sursă şi extensia .o (object). Nu este neapărat necesar ca fişierul obiect să poarte
numele fişierului sursă. Numele fişierului obiect rezultat poate fi specificat prin
parametrul -o. Înainte să edităm legăturile, verificăm existenţa fişierului obiect cu
ls.
Editorul de legături are rolul să „unească” fişierul obiect de altele şi să
aloce efectiv adrese şi resurse de sistem viitorului fişier executabil. Editorul de
legături utilizat este ld (GNU Linker). După ce ne-am asigurat de prezenţa
fişierului obiect prog.o scriem în terminal următoarea comandă:

ld -o prog prog.o

Dacă procesul se finalizează fără erori, în directorul curent apare un fişier


executabil. La fel ca la asamblare, parametrul -o specifică numele fişierului de
ieşire. Dacă nu am fi specificat numele dorit pentru executabil, editorul de legături
ar fi generat un fişier cu numele a.out. În Windows, fişierele executabile se
identifică prin extensia .exe. În Linux, executabilele nu folosesc extensie, dar pot fi

16
Aceste erori sunt fatale. De aici şi termenul „fatal error”.
identificate după drepturile de execuţie pe care le deţin sau după culoare verde din
listingul ls.
Atenţie! Dacă editorul de legături refuză să proceseze comanda anterioară
şi afişează mesajul „ld: i386 architecture of input file `prog.o' is incompatible with
i386:x86-64 output”, înseamnă că sistemul de operare este de tip x86-64 şi editorul
de legături nu poate lega un fişier obiect generat în format ELF de biblioteci de 64
de biţi. O soluţie este să asamblaţi iarăşi programul folosind formatul ELF64. Dar,
pentru că subiectul acestei cărţi este studiul limbajului de programare pe arhitecturi
IA-32, este mai indicat să generaţi un executabil compatibil cu astfel de arhitecturi
folosind în procesul editării de legături parametrul -m, astfel:

ld -o prog prog.o -m elf_i386

4.5.4. Automatizarea sarcinilor cu GNU Make

Probabil deja vă gândiţi cu groază la faptul că de acum înainte, pentru


fiecare program în parte, trebuie să scrieţi o linie complexă de asamblare, plus una,
la fel de complexă, de editare de legături. Dar vă atenţionez că acest lucru se
întâmplă nu după fiecare instanţă de program, ci după fiecare modificare a
programului. Se poate întâmpla să scrieţi o virgulă unde nu trebuie. Procesul de
generare a executabilului trebuie luat de la început: editare, asamblare, editare de
legături. Atunci când aplicaţia dispune de mai multe fişiere sursă, situaţia devine
critică. Asamblarea şi „legarea” acestora devine un proces foarte anevoios.
Utilitarul Make serveşte în Linux la automatizarea compilării programelor.
Cele mai multe programe distribuite în format sursă folosesc acest utilitar. Prima
implementare a GNU Make a fost realizată de Richard Stallman şi Roland
McGrath
Mecanismul Make permite crearea fişierelor executabile din părţile lor
componente. Utilitarul Make execută alte programe în conformitate cu un plan
descris de un fişier text numit makefile. Fişierul makefile seamănă puţin cu un
program de calculator în care se specifică cum trebuie să se facă ceva. Dar, spre
deosebire de un program, nu precizează succesiunea exactă a operaţiilor ci
specifică ce componente din program sunt necesare la crearea altor componente din
program. În cele din urmă, prin această modalitate de acţiune, sunt definite toate
regulile necesare obţinerii executabilului final. Aceste reguli sunt numite
dependinţe.
În practică, codul sursă al aplicaţiilor conţine sute, mii sau chiar milioane
de linii. Problema principală a dezvoltatorilor de aplicaţii constă din cantitatea
imensă de linii de cod care trebuie gestionată. Răspunsul la această problemă
constă în scrierea programelor într-o manieră modulară. Programul este împărţit în
componente mai mici şi fiecare componentă este dezvoltată separat. Desigur, acest
lucru înseamnă o provocare suplimentară, trebuie să ştii cum sunt create
componentele şi modul în care acestea se potrivesc împreună. Pentru asta este
nevoie de un plan.
Noi deja am urmat un astfel de plan. Ca să obţinem fişierul executabil a
trebuit să transformăm mai întâi fişierul sursă în fişier obiect (cu ajutorul unui
asamblor). Existenţa fişierului obiect depinde aşadar de cea a fişierului sursă. În
continuare, fişierul executabil depinde de existenţa fişierului obiect, precum şi de
existenţa altor fişiere bibliotecă. Pentru legarea acestor fişiere obiect împreună am
fost obligaţi să folosim un editor de legături.
Ei bine, fişierul makefile trebuie să descrie chiar acest plan. Crearea
fişierului makefile începe cu determinarea dependinţelor (fişierele necesare pentru
obţinerea fişierului executabil). Fişierul executabil este creat în etapa de editare de
legături, aşadar, prima dependinţă care trebuie definită constă din numele fişierelor
necesare editorului de legături ca să creeze fişierul executabil. Prima linie a
fişierului makefile este:

prog: prog.o

Acest lucru înseamnă că pentru generarea fişierului executabil prog este nevoie de
fişierul obiect prog.o. Această linie se numeşte linie de dependinţe. Probabil este
cea mai simplă linie de dependinţe posibilă: un fişier executabil depinde de un
singur fişier obiect. Dacă sunt necesare fişiere obiect adiţionale, ele trebuie aranjate
unul după altul, separate prin spaţii. Această organizare se numeşte listă de
dependinţe.

exec: main.o swap.o com.o

Această linie ne spune că executabilul depinde de trei fişiere obiect (main.o,


swap.o şi com.o). Toate trei trebuie să existe înainte de a putea genera fişierul
executabil exec, numit ţintă. Liniile de dependinţe descriu fişierele, nu şi acţiunea
necesară obţinerii lor. Aceasta este o parte esenţială a procesului, şi este o linie
cunoscută nouă. Aceste două linii lucrează împreună:

prog: prog.o
ld -o prog prog.o

A doua linie trebuie identată faţă de începutul liniei cu un singur caracter TAB.
Din motive de tradiţie, comenzile dintr-un fişier makefile trebuie precedate de
caracterul TAB. O greşeală frecventă constă în introducerea unor spaţii înainte de
comenzi. Apariţia unui mesaj de forma celui de mai jos înseamnă, destul de
probabil, omiterea folosirii caracterului TAB:
makefile:2: *** missing separator. Stop.

Aşadar, mecanismul Make presupune să specifici numele fişierelor necesare şi ce


trebuie făcut cu aceste fişiere. În plus, pentru a optimiza execuţia acţiunilor, o dată
fişierul compilat sau „legat”, Make nu va repeta procesul decât atunci când este
modificat unul din fişierele sursă sau obiect de care depinde. De asemenea, Make
ştie când au fost modificate ultima oară executabilul şi dependinţele, astfel încât,
dacă fişierul executabil este mai recent decât vreo dependinţă, deduce că
schimbările apărute în codul obiect deja sunt reflectate în executabil (poate fi
absolut sigur de acest lucru, deoarece singura modalitate de a genera executabilul
se face prin prolucrarea fişierului obiect). Make determină data şi ora modificărilor
printr-o facilitate a sistemului de fişiere (amintiţi-vă de rezultatul comenzii ls -
l).
Încă nu am terminat de editat fişierul makefile. Mecanismul Make îşi
dovedească importanţa când fişierul de configurare conţine lanţuri de dependinţe.
Chiar şi cel mai simplu fişier makefile va conţine dependinţe care depind de alte
dependinţe. Până şi programul nostru banal are nevoie de două propoziţii de
dependinţe în fişierul său makefile:

prog: prog.o
ld -o prog prog.o
prog.o: prog.asm
yasm -f elf -g stabs prog.asm

Aceste două propoziţii de dependinţe definesc cei doi paşi necesari


generării fişierului executabil prog.
La apelarea comenzii make, aceasta caută în directorul curent fişierul
makefile (dacă nu îl găseşte, caută Makefile) şi execută secvenţa de operaţii
specificată în el.

make -k

Opţiunea -k instruieşte Make să oprească generarea oricărui fişier în care apare o


eroare şi să păstreze neschimbată copia anterioară a fişierului ţintă (continuă însă
generarea oricărui alt fişier care trebuie creat/recreat). În absenţa opţiunii -k, Make
ar putea suprascrie codul obiect şi executabilul existent cu variante incomplete.
Reamintim că oricând aduceţi fişierului sursă o modificare, indiferent de
mică, trebuie să rulaţi make pentru a reface executabilul. Lucru care nu este atât
de groaznic cum pare. Editoarele de text prezentate includ combinaţii de taste sau
comenzi care vă permit să rulaţi Make din cadrul lor.
Integrarea cu Vim
Comanda :make rulează programul Make în directorul curent. Implicit,
Vim recunoaşte mesajele de eroare şi le listează, facilitând deplasarea prin surse.
Deplasarea între mesajele de eroare se face utilizând comenzile :cnext şi
:cprev. Aceste comenzi funcţionează şi dacă eroarea nu se află în fişierul curent
(deschide automat fişierele care conţin eroarea).
Comanda :copen împarte ecranul pe orizontală şi deschide o fereastră
nouă, dedicată afişării erorilor. Poziţionarea cursorului pe o anumită eroare, urmată
de un Enter sare în programul sursă la linia respectivă. Navigarea între ferestre se
face cu comenzile de deplasare cunoscute (h,j,k,l) precedate de Ctrl-w.
Închiderea ferestrelor se face normal,:q. Fereastra cu erori poate fi creată la
momentul execuţiei Make dacă se foloseşte comanda compusă :make | copen.
Dacă se preferă împărţirea ecranului pe verticală se foloseşte :make | vert
copen.
Aşa cum am spus mai devreme, dacă fişierul executabil este mai nou decât
toate fişierele de care depinde, Make va refuza să îl regenereze. Cu toate acestea,
există situaţii când acest lucru este dezirabil. În special când modificaţi fişierul
makefile şi doriţi să îl testaţi. Linux pune la dispoziţie o comandă, numită touch,
care are un singur rol: actualizează informaţiile de timp ale fişierelor. Dacă
executaţi comanda touch asupra fişierului sursă sau obiect, de exemplu touch
prog.asm, fişierul va deveni „mai nou” decât fişierul executabil şi Make va
repeta procesul de generare. Editorul Vim permite rularea comenzilor din linia sa
de comandă. De exemplu, comanda anterioară poate fi executată din interiorul
editorului tastând :!touch prog.asm.

4.5.5. Execuţia programului

Odată fişierul binar obţinut, îl putem executa prin apelarea sa din linie de
comandă:
./firstProg

Probabil vă aşteptaţi să „nu se întâmple nimic”. Instrucţiunile programului


nu cer date de la tastatură şi nici nu afişează ceva pe ecran. Programul nu face
altceva decât să introducă date în câteva locaţii de memorie şi să transfere valori
între registre şi memorie. Singurul mod în care putem verifica execuţia
programului este să aruncăm o privire în interiorul calculatorului. Nu deschideţi
carcasa, din fericire dispunem chiar de unealta de care avem nevoie: depanatorul
GDB. Dar, până atunci, să vorbim despre problemele care pot fi întâmpinate chiar
dacă procesul de asamblare s-a desfăşurat fără eroare.
Greşeli de programare
Dacă în urma procesului de asamblare se obţine fişierul executabil,
înseamnă că din punct de vedere al sintaxei programul este corect. Aceasta nu
înseamnă însă că programul va face ceea ce intenţiona programatorul să facă. Se
spune că programul care nu lucrează conform planului conţine una sau mai multe
greşeli de programare (bug). O greşeală de programare este acel ceva din program
care nu lucrează aşa cum a gândit programatorul să lucreze.
O eroare denotă prezenţa în fişierul sursă a ceva inacceptabil pentru
asamblor sau editor de legături. O eroare previne finalizarea cu succes a procesului
de generare a fişierului executabil. În contrast, o greşeală de programare este o
problemă descoperită în timpul execuţiei unui program. Greşelile de programare,
sau de algoritm, nu sunt detectate de asamblor sau de editorul de legături. Pot fi
beningne, de exemplu cuvinte scrise greşit într-un mesaj destinat afişării pe ecran,
sau pot cauza întreruperea prematură a programului. În unele cazuri, greşelile de
programare pot întrerupe execuţia programului fără mesaje de avertizare. În
situaţiile în care operaţiile programului afectează sistemul de operare (încearcă să
acceseze zone de memorie protejate, etc.), acesta va întrerupe programul şi va afişa
o eroare:

Segmentation Fault

Acest tip de eroare se numeşte eroare la rulare (runtime error).


De cele mai multe ori însă, programul nu va deranja sistemul de operare,
va rula complet, dar rezultatul nu va fi cel aşteptat.

4.5.6. Depanarea fişierului executabil

Testarea programelor şi eliminarea erorilor de programare se face în cadrul


unui proces numit depanare (debugging). Depanarea se referă la procesul
sistematic prin care sunt localizate şi corectate greşelile de programare.
Depanatorul este un program creat special în scopul localizării şi identificării
greşelilor de programare. Permite încărcarea programului în memorie, execuţia sa
pas cu pas17 şi vizualizarea memoriei şi a registrelor procesorului. În Linux,
depanatorul standard este GDB (GNU Debugger). Autorul este Richard Stallman,
care a început dezvoltarea acestuia în anul 1988. Rulează în linie de comandă şi a
fost creat pentru a depana programe scrise în C/C++.
Pentru a putea fi executat prin intermediul unui depanator, fişierul
executabil trebuie să fie asamblat cu opţiunea -g (include informaţia de depanare).

17
Execuţia programului se opreşte după efectuarea fiecărei instrucţiuni maşină.
Nu orice executabil include această informaţie. În general, trebuie să folosim
informaţii de depanare numai dacă vrem să verificăm aplicaţia respectivă. Chiar
dacă executabilul creat cu opţiunea -g rulează şi se comportă ca orice alt program
asamblat fără, deoarece introduce în fişierul executabil informaţie adiţională,
executabilul devine mai mare şi mai lent.
Presupunând că executabilul conţine informaţia de depanare necesară,
putem rula programul prin intermediul GDB astfel:

gdb executabil

GDB are o interfaţă de tip interpretor de comenzi ce permite execuţia comenzilor


de depanare. Prompt-ul (gdb) arată că se aşteaptă din partea programatorului
introducerea unei comenzi de depanare. Ieşirea din GDB se face prin comanda
quit. Putem lansa în execuţie GDB fără să furnizăm ca argument numele
fişierului executabil. În acest caz, specificarea acestuia se face prin comanda file.

stefan@laptop:~$ gdb
GNU gdb (Ubuntu/Linaro 7.2-1ubuntu11) 7.2
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
(gdb)

GDB poate fi lansat în execuţie cu opţiunea -q (quiet). În acest caz textul de


prezentare nu mai este afişat.

stefan@laptop:~$ gdb -q
(gdb) file prog
Reading symbols from /home/stefan/prog...done.

Comanda help arată clasele de comenzi pe care le poate primi depanatorul.


Afişarea comenzilor conţinute într-o clasă se face prin specificarea numelui
acesteia imediat după comanda help. Descrierea unei comenzi se face prin acelaşi
procedeu, numai că de această dată, după help specificăm numele comenzii.

(gdb) help info variables


All global and static variable names, or those matching REGEXP.
(gdb) help info registers
List of integer registers and their contents, for selected stack frame.
Register name as argument means describe only that register.

Comanda list afişează codul sursă al programului, linia sau funcţia specificată.
Fără argument, afişează zece (numărul implicit) sau mai multe linii aflate după sau
în jurul liniei la care s-a ajuns în urma afişării anterioare.

(gdb) list
1 section .data
2 r db 67
3 a db 0fh
4 b db 89
5 c dw 045E3h
6 d dw 65535
7 e dd 001234567h
8 f dd 1047000
9 g db 0ffh
10 section .text

În cazul în care argumentul specifică o linie, sunt afişate zece linii în jurul acelei
linii.

(gdb) info source


Current source file is prog.asm
Located in /home/stefan/prog.asm
Contains 29 lines.
Source language is unknown.
Compiled with stabs debugging format.
Does not include preprocessor macro info.
(gdb) list 19
14 mov al, 8h
15 mov bl,[r]
16 mov [r],al
17 mov eax, 1239054
18 mov bl,[b]
19 mov cx,[c]
20 mov edx,[e]
21 mov eax,a
22 mov ebx,b
23 mov [a],ah

Împreună cu argumentul – (liniuţă) afişează zece linii dinaintea poziţiei la care s-a
ajuns prin listarea anterioară.

(gdb) list -
4 b db 89
5 c dw 045E3h
6 d dw 65535
7 e dd 001234567h
8 f dd 1047000
9 g db 0ffh
10 section .text
11 global _start
12 _start:
13 nop

Numărul implicit de linii de afişare poate fi schimbat cu ajutorul comenzii set.


(gdb) set listsize 5
(gdb) show listsize
Number of source lines gdb will list by default is 5.
(gdb) list
14 mov al, 8h
15 mov bl,[r]
16 mov [r],al
17 mov eax, 1239054
18 mov bl,[b]

Avem posibilitatea să precizăm exact regiunea dorită. Următoarea comandă


afişează tot programul. Numărul total de linii al programului a fost deja aflat cu
comanda info source. Comanda list a fost abreviată cu l.

(gdb) l 1,29
1 section .data
2 r db 67
3 a db 0fh
4 b db 89
...
7 e dd 001234567h
8 f dd 1047000
9 g db 0ffh
10 section .text
11 global _start
12 _start:
13 nop
...
27 mov eax,1
28 mov ebx,0
29 int 80h

Pentru a executa programul pas cu pas trebuie să specificăm un punct de oprire.


Punctul de oprire (breakpoint) stopează execuţia programului la acel punct din
codul sursă. De acolo putem rula programul pas cu pas sau putem continua
execuţia obişnuită. Punctul de întrerupere ne permite să inspectăm starea
programului la momentul respectiv.
Punctele de întrerupere pot fi specificate folosind numărul de linie al
codului sursă, numele unei funcţii sau o adresă. Comanda este break (se poate
abrevia cu b). Putem obţine informaţii cu privire la punctele de întrerupere cu
info breakpoints.

(gdb) b 15
Breakpoint 1 at 0x8048083: file prog.asm, line 15.
(gdb) b *_start+1
Breakpoint 2 at 0x8048081: file prog.asm, line 14.

Observaţie: b *_start+1 este comanda utilizată de noi pentru toate


exemplele din această carte şi trebuie să indice către o instrucţiune NOP, altfel
punctul de întrerupere setat nu va fi luat în considerare (un bug al depanatorului
GDB).

(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048083 in _start at prog.asm:15
2 breakpoint keep y 0x08048081 in _start at prog.asm:14

Observaţi că fiecărui punct de întrerupere îi este atribuit un număr (indice) care ne


permite să îl adresăm. Coloana Disp (Disposition) descrie decizia care trebuie
îndeplinită de depanator când ajunge în punctul respectiv. Implicit, toate punctele
de întrerupere sunt executate (keep). Punctul de întrerupere mai poate fi marcat cu
dezactivează (disable) sau şterge (delete). Coloana Enb (Enabled) specifică dacă
punctul de întrerupere este iniţializat sau nu. Un „y” indică faptul că este iniţializat.
Adresa de memorie la care se află punctul de întrerupere este dată în coloana
Address, iar adresa din fişierul sursă, afişată ca o combinaţie de nume de fişier şi
număr de linie, este dată de ultima coaloană.
Comanda tbreak (temporary break) setează un punct de întrerupere
temporar. Un punct de întrerupere temporar întrerupe execuţia programului o
singură dată, apoi este şters.

(gdb) tbreak 13
Temporary breakpoint 4 at 0x8048080: file prog.asm, line 13.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048083 in _start at prog.asm:15
2 breakpoint keep y 0x08048081 in _start at prog.asm:14
4 breakpoint del y 0x08048080 in _start at prog.asm:13

Comanda disable dezactivează punctul de întrerupere. Primeşte ca


argument numărul punctului de întrerupere. Fără argumente dezactivează toate
punctele de întrerupere. Activarea unui punct de întrerupere se face cu enable.

(gdb) disable 2
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048083 in _start at prog.asm:15
2 breakpoint keep n 0x08048081 in _start at prog.asm:14
4 breakpoint del y 0x08048080 in _start at prog.asm:13

Ignorarea punctului de întrerupere pentru un anumit număr de ori se


realizează cu comanda ignore. Comanda ignore primeşte două argumente:
numărul punctului de întrerupere ce trebuie ignorat şi numărul de ori pentru care
acesta este sărit.
Ştergerea unui punct de întrerupere se face cu comanda delete.
Argumentul specifică numărul punctului de întrerupere şters. Dacă nu sunt
specificate argumente, şterge toate punctele de întrerupere.

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) info b
No breakpoints or watchpoints.

Lansarea în execuţie a programului se face prin comanda run. Programul


se opreşte la punctul de întrerupere. De acolo putem continua execuţia pas cu pas
prin comanda next (abreviată n) sau step (abreviată s). Deşi în cazul
programului actual next şi step se comportă la fel, ele sunt comenzi diferite.
Pentru o linie de cod care apelează o funcţie, next va „sări” peste funcţie, la
următoarea linie de cod, în timp ce step va „intra” în funcţie.
Execuţia programului depanat poate fi oprită oricând cu comanda kill.
Starea programului poate fi consultată cu info program.

(gdb) b *_start+1
Breakpoint 1 at 0x8048081: file prog.asm, line 14.
(gdb) r
Starting program: /home/stefan/prog

Breakpoint 1, _start () at prog.asm:14


warning: Source file is more recent than executable.
14 mov al, 8h
(gdb) n
15 mov bl,[r]
(gdb) s
16 mov [r],al
(gdb) info program
Using the running image of child process 4717.
Program stopped at 0x8048089.
It stopped after being stepped.

Continuare execuţiei se face cu ajutorul comenzii continue. Ieşirea din


GDB se face prin comanda quit.
Conţinutul registrelor este afişat cu info registers, info
registers $registru, info all-registers. Versiunile recente primesc ca
parametru şi numele simplu al registrului, fără simbolul $. O abreviere utilă este de
genul i r ax. Tot de la versiunile mai noi se poate afişa conţinutul registrelor
mai mici decât cuvântul standard al arhitecturii (32 de biţi). Eu am observat această
posibilitate de la versiunea 7.2.

(gdb) info registers


eax 0x8 8
ecx 0x0 0
edx 0x0 0
ebx 0x4367
esp 0xffffd400 0xffffd400
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x8048089 0x8048089 <_start+9>
eflags 0x202 [ IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
(gdb) info registers $eax
eax 0x8 8
(gdb) info registers $eax $ebx $ecx
eax 0x8 8
ebx 0x4367
ecx 0x0 0
(gdb) i r al

Comanda care afişează valorile locaţiilor de memorie este x (examine). Este o


comandă complexă, care poate primi mai mulţi parametrii opţionali. Apare în
următoarea formă:

x /nyz &etichetă
unde:
• n reprezintă numărul de linii de afişat,
• y descrie formatul în care se doreşte afişarea,
• z descrie mărimea unităţii în care se doreşte afişarea.
Tabel 4.3: Detalii cu privire la parametrii opţionali
n Număr de repetiţie (întreg zecimal)
Specifică numărul de unităţi de memorie (în z) care trebuie afişat.
Valoarea implicită este 1.

f Formatul afişării
x hexazecimal
d zecimal (decimal)
u zecimal fără semn (unsigned decimal)
o octal
t binar (two)
afişează adresa atât în zecimal cât şi ca deplasament faţă de cel
a
mai apropiat simbol precedent.
c afişează sub formă de caractere (character)
s afişează ca şir terminat în caracterul 0
t afişează ca număr în virgulă mobilă
i afişează ca instrucţiune în cod maşină
Valoarea implicită este x. Valoarea implicită se schimbă de fiecare dată
când este folosită comanda x.
u Mărimea unităţii
b octeţi (byte)
h 2 octeţi sau jumătăţi de cuvânt (half-word)
w 4 octeţi sau cuvânt de 32 de biţi (word)
g 8 octeţi sau cuvânt gigant (giant word)
Valoarea implicită este w. Valoarea implicită se modifică automat la
fiecare unitate care este specificată cu comanda x.

(gdb) x /1db &r


0x80490d0 <r>: 67
(gdb) x /1xb &r
0x80490d0 <r>: 0x43
(gdb) x /1dt &r
0x80490d0 <r>: 01000011
(gdb) x /1c &r
0x80490d0 <r>: 67 'C'
(gdb) x /4db &r
0x80490d0 <r>: 67 15 89 -29
(gdb) x /4xb &e
0x80490d7 <e>: 0x67 0x45 0x23 0x01

Comanda care poate afişa atât valoarea unui registru cât şi a unei locaţii de
memorie este print. Comanda print poate folosi parametrii opţionali de
formatare a afişării.
Registrele primite ca argument trebuie precedate de caracterul $ (dollar).

(gdb) print /x $eax


$4 = 0x12e80e
(gdb) print /d $ebx
$5 = 89
(gdb) print /t $ecx
$6 = 100010111100011

În cazul locaţiilor de memorie, o etichetă simplă dată ca argument duce la


afişarea valorii, o etichetă precedată de caracterul & (ampersand) arată adresa
locaţiei de memorie respective.
(gdb) print /x &e
$11 = 0x80490d7
(gdb) print /x e
$12 = 0x1234567

Dacă sunteţi obişnuiţi cu limbajul de programare C, comanda printf vă


este deja cunoscută. Comanda poate executa o expresie de afişare complexă,
utilizând un format şir cu specificatori de conversie exact ca la funcţia ANSI C,
printf().

(gdb) printf "%d\n",r


3
(gdb) printf "%d\n", $eax
8
(gdb) printf "%x\n", &r
80490d0

Comanda backtrace (abreviată bt) afişează lista funcţiilor invocate din


programul principal până în momentul respectiv. Lista conţine câte o linie pentru
fiecare cadru de stivă (stack frame). Comenzile frame şi info frame ne permit
să examinăm conţinutul cadrelor de stivă. Selectarea unui cadru se face prin
specificarea numărului său ca argument pentru comanda frame. Odată selectat,
putem observa ce conţine cu ajutorul comenzii info frame.
Depanatorul poate dezasambla o secţiune de memorie specificată. Poate
primi ca argument o adresă, de exemplu _start. În mod implicit, zona de
memorie este afişată în sintaxa AT&T. Prima comandă din următoarea secvenţă
schimbă modalitatea de reprezentare.

(gdb) set disassembly-flavor intel


(gdb) disassemble _start
Dump of assembler code for function _start:
0x08048080 <+0>: nop
0x08048081 <+1>: mov eax,0x80490c8
0x08048086 <+6>: mov ebx,DWORD PTR ds:0x80490c8
End of assembler dump.

Ca să nu fim nevoiţi să specificăm sintaxa dorită la fiecare deschidere a


depanatorului, scrieţi într-un terminal următoarea comandă:

echo "set disassembly-flavor intel" > ~/.gdbinit


4.6. Întrebări şi exerciţii

4.1. Care este structura unui program în limbaj de asamblare?

4.2. Care este diferenţa dintre program şi proces?

4.3. Care este diferenţa între datele iniţializate şi neiniţializate?

4.4. Conform cărui criteriu aranjează un procesor Intel x86 octeţii de date în
memorie?

4.5. Pentru fiecare din următoarele declaraţii, reprezentaţi harta memoriei.

msg dw “Introduceti primul numar: “


d dd 123
w dw ‘AB’
sir db ‘ABCD’

4.6. Ce înseamnă ELF şi STABS în linia de asamblare?

4.7. Ce este un mesaj de eroare? Prin ce se diferenţiază acesta de mesajele de


atenţionare?

4.8. Ce rol are opţiunea –m elf_i386 din linia editorului de legături?

4.9. Ce rol are un depanator? Cu ce opţiune trebuie să asamblăm un program astfel


încât să îl putem depana?

4.10. Ce este un punct de întrerupere?

4.11. Ce rol are utilitarul Make?

4.12. Cum se execută un program din linie de comandă în Linux?


5. MEMORIA

Acest capitol demonstrează modul de declarare a datelor în program şi


importanța alinierii acestora în memorie. Discută în detaliu diferite metode de
adresare disponibile la procesoarele Intel de 32 de biți şi descrie modul de
implementare și manipulare în limbaj de asamblare a vectorilor. Din perspectiva
unui programator în asamblare, acest capitol, deoarece prezintă metodele de
adresare a memoriei, este unul din cele mai importante, dacă nu cel mai important.

5.1. Declararea datelor în bloc

Prefixul TIMES permite repetarea unei instrucţiuni sau pseudoinstrucţiuni


de un număr specificat de ori. În exemplul de mai jos, TIMES repetă de 100 de ori
pseudoinstrucţiunea db 0, rezultând un buffer de 100 de octeţi de 0. Pentru
obţinerea aceluiaşi rezultat, dar fără prefixul TIMES, ar fi trebuit să scriem după
directiva DB o sută de cifre de zero separate prin virgulă. N-ar fi fost foarte plăcut.

buffer: times 100 db 0

Dar TIMES este mai versatil decât atât. Argumentul lui TIMES (în
exemplul precedent argumentul a fost 100) nu este o constantă numerică, ci o
expresie numerică. În consecinţă, se pot construi propoziţii mult mai complexe.

buffer: db 'Rezervat:'
times 19-$+buffer db '-'

Expresia 19-$+buffer se evaluează la 10, aşadar se repetă de 10 ori


db '-'(caracterul liniuţă). În final, în memorie vom avea secvenţa:
Rezervat:----------.
Sunt şi situaţii în care nu este indicată folosirea prefixului TIMES. De
exemplu, propoziţiile times 100 resb 1 şi resb 100 au acelaşi rezultat,
numai că ultima, din cauza structurii interne a asamblorului, este asamblată de 100
de ori mai rapid.
Totuşi, ca să nu fim acuzaţi de partizanat, programul care demonstrează
faptul că datele neiniţializate sunt alocate la momentul rulării, şi nu asamblării,
foloseşte la rezervarea ambelor tipuri de date prefixul TIMES. În primul rând, să
verificăm mărimea programului iniţial.
;
;testMarime.asm
;
section .text
global _start
_start:
nop
mov eax,1
mov ebx,0
int 80h

Asamblaţi cu make şi observaţi mărimea acestuia.

ls -l testMarime
-rwxr-xr-x 1 ubuntu users 992 2011-02-19 21:59 testMarime

Dimensiunea fişierului executabil este de 992 de octeţi. Adăugăm


programului 8 MB de octeţi neiniţializaţi şi obţinem executabilul.
;
;testMarime.asm
;+8192 octeţi neiniţializaţi
section .bss
buffer: times 8192 resb 1 ;se poate folosi buffer: resb 8192
section .text
global _start
_start:
nop
mov eax,1
mov ebx,0
int 80h

ls -l testMarime
-rwxr-xr-x 1 ubuntu users 1075 2011-02-19 22:05 testMarime

Aşadar, am adăugat un buffer de 8192 de octeţi, dar mărimea


programului a crescut cu numai 83 de octeţi. Să testăm mărimea programului la
care au fost adăugaţi 8192 de octeţi iniţializaţi cu ‘0’.
;
;testMarime.asm
;+8192 octeţi iniţializaţi cu caracterul 0
section .data
buffer: times 8192 db '0'
section .text
global _start
_start:
nop
mov eax,1
mov ebx,0
int 80h

În urma procesului de asamblare şi editare de legături observăm că


buffer-ul de 8192 de octeţi este adăugat fişierului executabil.

ls -l testMarime
-rwxr-xr-x 1 ubuntu users 9271 2011-02-19 22:05 testMarime

5.2. Adresarea memoriei în modul protejat

Principala instrucţiune de care ne-am folosit până acum a fost instrucţiunea


MOV. MOV necesită doi operanzi şi are următoarea sintaxă:

mov destinaţie,sursă

În sintaxa Intel, operandul aflat lângă numele instrucţiunii este operandul


destinaţie. Datele sunt copiate de la sursă la destinaţie, iar operandul
sursă rămâne neschimbat. Observaţi că am evidenţiat cuvântul copiate. Din
nefericire, numele instrucţiunii, prin faptul că pronunţia, şi chiar aspectul, seamănă
cu cel al cuvântului englezesc move, care înseamnă a muta, înşeală pe mulţi. Datele
nu sunt mutate, ci copiate de la sursă la destinaţie. La final, vom avea aceeaşi
valoare în două locuri, în sursă şi în destinaţie.
Instrucţiunea MOV poate copia date cu capacitatea de un octet, un cuvânt
sau dublu cuvânt, între registrele procesorului sau între registre şi memorie.
Atenţie, MOV nu poate copia date între două locaţii de memorie. Pentru aceasta este
nevoie de un registru intermediar şi de două instrucţiuni MOV – una copiază
valoarea din memorie în registru, şi alta parcurge traseul invers.
MOV nu e singura instrucţiune care respectă regulile de mai sus. Aproape
fiecare instrucţiune din setul de instrucţiuni poate opera pe octet, cuvânt sau dublu
cuvânt, iar atunci când primeşte doi operanzi, aceştia nu pot fi în acelaşi timp
locaţii de memorie. Operaţiile cu ambii operanzi în memorie sunt excluse, cu
excepţia operaţiilor pe şiruri şi a operaţiilor cu stiva.

5.2.1. Adresare imediată şi adresare la registre

Până acum am întâlnit două forme ale instrucţiunii MOV:


• transfer de date între registre:

mov eax,ebx ;copiază valoarea din registrul EBX în registrul EAX

• introducere operand imediat în registru:

mov eax,1 ;introducem imediatul 1 în registrul EAX


mov ebx,0 ;introducem imediatul 0 în registrul EBX

Un „imediat” este o valoare specificată direct în corpul instrucţiunii. Acest


fenomen poartă numele de adresare imediată. Modul de adresare specifică
criteriul folosit la calculul adresei unui operand (modul în care sunt specificaţi
operanzii unei instrucţiuni). Numele de adresare imediată provine de la faptul că
operandul este prezent chiar în corpul instrucţiunii maşină. Procesorul nu trebuie să
extragă valoarea operandului separat, dintr-un registru sau dintr-o locaţie de
memorie, imediatul este inclus în instrucţiunea extrasă şi executată.
MOV EAX,EBX semnifică faptul că adresa operandului sursă este adresa
unui registru de uz general, adică operandul se găseşte într-un registru. Acest mod
de adresare se numeşte adresare la registre.
În completarea regulilor deja enunţate, adăugăm şi faptul că dimensiunea
operanzilor sursă şi destinaţie trebuie să corespundă. De exemplu, un imediat de
16 biţi nu poate fi introdus într-un registru de 8 biţi. Instrucţiunea MOV
AL,0FA95H este ilegală. De asemenea, numai operandul sursă poate fi un
imediat. Asamblorul sancţionează toate greşelile de acest fel. Din acest punct de
vedere asamblorul este un pedagog foarte exigent.
Transferul datelor între registre este un proces simplu. Nu se poate spune
acelaşi lucru despre transferul datelor între registre şi memorie. Primul lucru de
care trebuie să ţinem cont este modul de reprezentare al adresei de memorie în
corpul instrucţiunii. Ne amintim că eticheta directivei de date reprezintă adresa
locaţiei de memorie la care este stocată valoarea. Aşadar, o instrucţiune de forma

mov ebx, d ;introduce în registrul EBX adresa d

stochează în registrul EAX adresa d, nu valoarea adresată de aceasta. Deşi


este util, de cele mai multe ori dorim să transferăm valoarea, nu adresa. Pentru a
obţine valoarea, numele etichetei trebuie introdus între paranteze pătrate.

mov eax,[d] ;introduce în registrul EAX valoarea adresată de d


mov ecx,[ebx] ;introduce în registrul ECX valoarea adresată de
registrul EBX

Al doilea lucru care trebuie luat în considerare la transferul de date între


memorie şi registre este dimensiunea datelor transferate. Instrucţiunea de mai sus
extrage de la o adresă de memorie un număr de biţi de date. Numărul de biţi de
date transferat de la adresa d în registrul EAX nu este specificat direct în corpul
instrucţiunii. Conţinutul locaţiei de memorie adresate de eticheta d poate avea
dimensiunea de un octet, un cuvânt, un dublu cuvânt, un quad, etc.. Programatorul
a presupus că valoarea adresată prin eticheta d este reprezentată pe 4 octeţi şi a
folosit registrul EAX. Dacă programatorul dorea să extragă din memorie 8 biţi de
date folosea ca destinaţie un registru de 8 biţi, precum AL. Dimensiunea registrului
specifică numărul de biţi extraşi din memorie, sau, altfel spus, dimensiunea datelor
este dedusă pe baza registrului destinaţie.
;
;MemReg.asm
;
section .data
b db 55h
w dw 0ABCDh
d dd 12345678h
section .text
global _start
_start:
nop
;încarcă în eax adresa variabilei b
mov eax, b
;încarcă în ebx adresa variabilei w
mov ebx, w
;încarcă în ecx adresa variabilei d
mov ecx, d
;încarcă în SI valoarea 0CD55H (16 biţi de date începând cu adresa b)
mov si, [b]
;încarcă în DI valoarea 0ABCDH
mov di, [w]
;încarcă în EDX valoarea 12345678H
mov edx, [d]
;încarcă în EBP valoarea 5678abcdh (32 de biţi de date începând cu adresa w)
mov ebp, [w]

mov eax, 1
mov ebx, 0
int 80h

Transferul datelor din registre în memorie foloseşte o abordare similară.

mov [d],ecx ;încarcă la adresa d octeţii aflaţi în ecx

Instrucţiunea copiază 4 octeţi de date din registrul ECX în locaţia de


memorie specificată de eticheta d. La fel ca înainte, instrucţiunea introduce în
memorie conţinutul unui registru de 4 octeţi, aşadar va folosi 4 octeţi de memorie.
Completaţi programul anterior cu următoarele instrucţiuni şi rulaţi pas cu pas.
Observaţi efectele. Desenaţi noua hartă a memoriei.

mov al, 13
mov [b], al
mov bx, 65535
mov [w], bx
mov ecx, 0aabbccddh
mov [d], ecx
;mov ebp, 0
;mov [b], ebp

Activaţi ultimele două instrucţiuni şi vizualizaţi efectele.

O directivă de date poate introduce în memorie mai mult de o singură valoare. De


exemplu, directiva

valori: dw 10,20,30,40,50,60

introduce în memorie o secvenţă de valori, fiecare valoare ocupă o unitate de


memorie egală cu 2 octeţi. Reprezentarea memoriei, conform convenţiei little-
endian, este dată mai jos. Presupunem că adresa de început, etichetată valori,
este 0x8049094.
Valoare Adresă
0
60 0x804909e
0
50 0x804909c
0
40 0x804909a
0
30 0x8049098
0
20 0x8049096
0
10 valori
7 0

Prima valoare se accesează simplu: adresa este precizată direct în


instrucţiune, prin intermediul etichetei valori. În schimb, extragerea altor valori
din vector, de exemplu, elementul 40, necesită un mod de adresare mai special, în
sensul că adresa fiecărei locaţii de memorie se determină pe baza următoarelor
elemente:
• adresa de început a vectorului, numită adresă de bază (bază);
• un deplasament (în vector), care se adună la adresa de bază (deplasament);
• mărimea elementului de date – numărul de octeţi pe care este reprezentat
elementul (scală);
• un index, care determină elementul selectat (index). Indexul reprezintă
poziţia elementului în şir, numărând de la zero.

Formatul complet al expresiei este:

[bază + index × scală + deplasament]

EAX EAX 1 fără deplasament


EBX EBX 2 deplasament pe 8 biţi
ECX ECX 4 deplasament pe 32 de biţi
EDX EDX 8
ESI ESI
EDI EDI
EBP EBP
ESP

Aşadar, adresa locaţiei de memorie corespunzătoare valorii dorite se obţine


adunând bază + deplasament + index × scală. Sunt valabile
următoarele reguli:
• baza şi indexul pot fi reprezentate de oricare din registrele de uz general de
32 de biţi;
• deplasamentul poate fi reprezentat de orice constantă de 32 de biţi (evident,
valoarea 0, deşi legală, este inutilă);
• scala trebuie să fie una din valorile 1, 2, 4, sau 8 (valoarea 1 este legală, dar
nu aduce niciun beneficiu, aşadar nu este precizată niciodată);
• toate elementele expresiei sunt opţionale şi pot fi utilizate în aproape orice
combinaţie;
• nu pot fi utilizate registre de 16 sau 8 biţi.

Tabelul 5.1 rezumă metodele prin care poate fi specificată o adresă de memorie
în modul protejat. Cu excepţia primelor două, pe care deja le-am întâlnit, toate
celelalte implică un mic calcul aritmetic între doi sau mai mulţi termeni, proces
numit calculul adresei efective. Procesul de calcul se finalizează cu obţinerea
adresei efective. Termenul „adresă efectivă” ilustrează faptul că, în cele din urmă,
aceasta este adresa care va fi folosită la citirea sau scrierea memoriei, indiferent de
modul în care este exprimată (formată). Calculul adresei efective se face la
momentul execuţiei instrucţiunii ce conţine expresia.

Tabelul 5.1 Moduri de adresare a memoriei


Schemă Exemplu Descriere
[bază] [edx] Numai bază
[deplasament] [etichetă] Deplasament, adresă
sau [0x8049094] simbolică sau explicită
[bază + deplasament] [ebx + 0FH] Bază plus constantă
[bază + index] [eax + ebx] Bază plus index
[index × scală] [ecx × 2] Index înmulţit cu scală
[index × scală + deplasament] [eax × 4 + 32] Index înmulţit cu scală plus
deplasament
[bază + index × scală] [ebp + edi × 2] Bază plus index înmulţit cu
scală
[bază + index × scală + [esi + ebp × 4 + 1] Bază plus index îmnulţit cu
deplasament] scală, plus deplasament

Adresarea indirectă
Primul caz presupune că adresa efectivă este reprezentată de valoarea unui
registru de uz general aflat între paranteze pătrate. Am văzut deja că, pe lângă date,
registrele pot conţine adrese de memorie. Registrul care conţine o adresă de
memorie se numeşte indicator (pointer). Accesarea datelor stocate în locaţia de
memorie indicată de adresa din registru se numeşte adresare indirectă. Poate fi
folosit oricare din registrele EAX, EBX, ECX, EDX, EBP, EDI sau ESI.
Modul în care stocăm o adresă într-un registru a fost întâlnit în programul
memReg.asm. Aşadar, instrucţiunea

mov ecx,valori

va avea ca rezultat stocarea adresei 0x8049094 în registrul ECX. Cum


adresele IA-32 sunt reprezentate pe 32 de biţi, registrul trebuie să fie unul de 32 de
biţi. Programul care urmează stochează în registrul AL primul octet din vectorul
valori.
;
;indirecta.asm
;
section .data
valori: dw 10,20,30,40,50,60
section .text
global _start
_start:
nop
mov ecx, valori
mov al, [ecx] ;AL = 0ah (adresare indirectă)
;mov bl, 255 ;BL = 0ffH
;mov [ecx], bl ;primul octet din vector devine 0ffH

mov eax,1
mov ebx,0
int 080h

Instrucţiunile comentate introduc în memorie, la adresa de început a


vectorului, valoarea 255. În locul celor două încercaţi să folosiţi instrucţiunea

mov [ecx], 255

Asamblorul va semnala o eroare:

error: invalid size for operand 1

Ceea ce asamblorul încearcă să ne spună este că nu poate determina dimensiunea


locaţiei de memorie la care trebuie să introducă valoarea 255. I s-a specificat
adresa, dar nu şi numărul de octeţi. Acesta este rolul operatorilor de dimensiune.
YASM pune la dispoziţie 5 operatori de dimensiune: BYTE, WORD,
DWORD, QWORD, TWORD. Aceştia sunt folosiţi atunci când există ambiguităţi cu
privire la numărul de octeţi pe care trebuie reprezentat în memorie un imediat. În
cazul nostru, nu reiese clar dacă introducem în memorie un octet, un cuvânt, etc..

Operator de dimensiune Octeţi adresaţi


BYTE 1
WORD 2
DWORD 4
QWORD 8
TWORD 10

Pentru o valoare de 255, un octet este suficient:

mov [ecx], byte 255

Aşa cum reiese din intrucţiuni, chiar dacă adresarea indirectă nu necesită
vreun calcul al adresei efective, adresa nu apare în clar şi nici codificată simbolic.
De fapt, singura formă sub care este posibil ca adresa efectivă să apară în clar este
aceea de adresă explicită scrisă între paranteze pătrate:

mov eax, [0x8049094]

Acest mod de adresare directă este aproape imposibil, deoarece în


momentul asamblării este extrem de improbabil să cunoşti adresa exactă. Aproape
întotdeauna vei cunoaşte codificarea simbolică a adresei (eticheta). Sistemul de
operare mapează eticheta la adresă în momentul încărcării programului în
memorie.

Adresarea directă prin deplasament


Acesta a fost primul mod de adresare al memoriei întâlnit în această carte.
Adresa efectivă a operandului se obţine din câmpul deplasament. Să ne amintim:

mov [b], al
mov [w], bx

În acelaşi mod,

mov bx, [valori]


introduce în registrul BX valoarea 10. Principala caracteristică a termenului
deplasament este că nu se află într-un registru.
Un caz particular al acestui mod de adresare se întâlneşte atunci când
adunăm la deplasament un număr întreg.

mov ax, [valori + 2]

Pare că adresa efectivă se formează prin adunarea a două deplasamente.


Însă YASM, la momentul asamblării, combină cele două valori, sau mai multe,
într-un singur deplasament. În cazul prezentat, pur şi simplu adaugă 2 la adresa
simbolică valori. Adresa obţinută va fi 0x8049094 + 2 = 0x8049096, adică
adresa primului octet al celui de al doilea element din vector. În registrul AL vom
avea valoarea 20. Am precizat într-un capitol precedent că putem accesa toate
valorile unui şir, sau vector, numai prin cunoaşterea adresei de început. Trebuie
numai să ţinem cont de mărimea elementelor. Cum fiecare element din şirul dat ca
exemplu are 2 octeţi, elementul 6 din vector poate fi extras cu instrucţiunea

mov ax, [valori + 10]

Elementul 6 este numărul 60.


;
;directa.asm
;
section .data
valori: dw 10,20,30,40,50,60
section .text
global _start
_start:
nop
mov di, [valori]
mov ax, [valori+2] ;copiază în ax elementul 2 (20)
mov bx, [valori+4] ;copiază în bx elementul 3 (30)
mov cx, [valori+6] ;copiază în cx elementul 4 (40)
mov dx, [valori+8] ;copiază în dx elementul 5 (50)
mov si, [valori+10] ;copiază în si elementul 6 (60)

mov eax,1
mov ebx,0
int 080h

Să considerăm instrucţiunea:
mov [d], 1 ;validă, adresare directă prin deplasament

În momentul asamblării, YASM va afişa o eroare:

operation size not specified

Asamblorul nu ştie dacă 1 înseamnă 01H, 0001H sau 00000001H. Altfel spus, nu
cunoaşte dimensiunea lui 1. d este o etichetă, o adresă de memorie. O etichetă nu
are tip, nu denotă o dimensiune. De aceea, trebuie să specificăm dimensiunea
operanzilor imediaţi.

mov dword [d], 1 ;1 este reprezentat pe 4 octeţi


mov byte [b], 1 ;1 este reprezentat pe 1 octet
mov [w], dword 'G' ;caracterul ‘G’ este reprezentat pe doi
octeţi şi introdus în locaţia de memorie etichetată cu w.

Adresarea bazată (bază + deplasament)


O altă modalitate simplă de calcul a adresei efective este să adunăm o
constantă la conţinutul unui registru.
;
;bazata.asm
;
section .data
valori: dw 10,20,30,40,50,60
section .text
global _start
_start:
nop
mov ebx, valori
mov ax, [ebx+4] ;copiază în AX elementul 3 (30)
mov cx, [ebx+6] ;copiază în CX elementul 4 (40)

mov edx,2
mov bx,[valori+edx] ;copiază în BX elementul 2 (20)
mov ax,[valori+edx+8] ;copiază în AX elementul 5 (50)

mov eax,1
mov ebx,0
int 080h
Descrierea acestei metode de calcul, „bază + deplasament”, este confuză.
În majoritatea cazurilor, cuvântul deplasament denotă o adresă (gândiţi-vă la
adresarea directă prin deplasament), în schimb, aici are rolul pur şi simplu de o
constantă (o deplasare faţă de adresa de bază). Această constantă poate fi
reprezentată de un întreg sau de numele vectorului.
În programul bazata.asm, primul mod de acces la elementele vectorului
se face prin încărcarea în EBX a adresei de bază şi adunarea sa cu un indice
explicit.
Al doilea mod de acces necesită o scurtă discuţie. Din punct de vedere al
mecanismului de adresare, în EDX se află o adresă de bază, iar valori este o
constantă de 32 de biţi. La toată această construcţie se poate adăuga uşor o altă
constantă explicită (8). Aşadar, din perspectiva mecanismului de adresare, există
un termen bază (EDX) şi un termen „de deplasare” (valori). Însă, din
perspectiva programului care accesează elementele vectorului, semnificaţia este
inversă: valori este adresa de bază, iar EDX un indice (deplasament).

Adresarea indexată
Adresa efectivă se obţine prin adunarea a două registre de uz general, unul
bază şi unul index.
;
;indexata.asm
;
section .data
valori: db 10,20,30,40,50,60
section .text
global _start
_start:
nop
mov ebp, valori
mov ecx, 4
mov ax, [ebp+ecx] ;copiază în AX elementul cu index 4 (50)
mov word [ebp+ecx], 70 ;încarcă 70 în locaţia cu index 4

mov eax,1
mov ebx,0
int 080h

Acest mod de adresare este utilizat de obicei pentru parcurgerea unui


vector octet cu octet. Registrul bază marchează începutul vectorului, iar registrul
index, incrementat într-o buclă, selectează elementul. Denumirea index trebuie
înţeleasă ca „poziţia elementului în vector”, numârând de la zero. Cuvântul vector
este un termen general pentru buffer sau tablou: o secvenţă de elemente de date
aflate în memorie, toate de acelaşi tip şi mărime.
Dar dacă trebuie să parcurgem un vector ale cărui elemente nu sunt
reprezentate pe un singur octet, ci pe cuvinte sau dublu cuvinte? Aici intră în scenă
conceptul de scală. Următorul listing arată modul în care depanatorul
interpretează adresarea indexată în contextul formulei:

[bază + index × scală + deplasament]

(gdb) disassemble _start


Dump of assembler code for function _start:
0x08048080 <+0>: nop
=> 0x08048081 <+1>: mov ebp,0x80490a4
0x08048086 <+6>: mov ecx,0x4
0x0804808b <+11>: mov ax,WORD PTR [ebp+ecx*1+0x0]
0x08048090 <+16>: mov WORD PTR [ebp+ecx*1+0x0],0x46
0x08048097 <+23>: mov eax,0x1
0x0804809c <+28>: mov ebx,0x0
0x080480a1 <+33>: int 0x80
End of assembler dump.

Registrul index este înmulţit implicit cu un factor de scală. Pentru că nu


am specificat în instrucţiune multiplicatorul dorit, asamblorul a presupus că
factorul de scală este egal cu 1. Aşadar, adresa efectivă este suma dintre bază şi
produsul registrului index cu factorul de scală. În mod obişnuit, factorul de scală
este egal cu mărimea elementelor individuale din vector. Dacă vectorul constă din
cuvinte de 2 octeţi, scala ar trebui să fie 2. Dacă vectorul conţine cuvinte de 4
octeţi, scala ar trebui să fie 4. Pentru elementele de tip quad, scala este 8.
Să presupunem că avem un şir de 100 de elemente, fiecare de 4 octeţi, şi că
dorim să extragem elementele 73, 84 şi 98. Cât adunăm la bază? În primul rând,
deoarece elementele sunt cuvinte de 32 de biţi, înseamnă că factorul de scală este
4. Nu mai trebuie decât să specificăm indexul elementelor dorite cu ajutorul
unui registru de uz general (altul decât cel folosit pentru bază). Amintiţi-vă că
indexul începe de la 0. Aşadar, primul element din vector are index 0, al doilea
element index 1, ş.a.m.d.. Programul exemplifică şi altă formă de adresare
indexată, [index × scală + deplasament].
;
;scala.asm
;
section .data
x equ 1
y equ 2
z equ 3
buff:
times 80 dd x ;80 de cuvinte de 32 de biţi cu valoarea 1
times 10 dd y ;10 cuvinte de 32 de biţi cu valoarea 2
times 10 dd z ;10 cuvinte de 32 de biţi cu valoarea 3
section .text
global _start
_start:
nop

;adresarea prin deplasament+index*scală


mov edi, 72
mov esi, [buff+edi*4]

;adresarea prin bază+index*scală


mov eax, buff
mov edi, 83
mov ebx, [eax+edi*4]
mov edi, 97
mov ecx, [eax+edi*4]

mov eax,1
mov ebx,0
int 080h

Adresarea bazată şi indexată


Adresa efectivă se obţine prin adunarea unui registru de bază cu un registru
index, plus o constantă de 32 de biţi.
Acesta este şi ultimul mecanism de calcul al adresei efective amintit (din
cele 11 prezente la procesoarele 80386). În definitiv, deocamdată contează să
înţelegeţi cum se calculează adresa efectivă, puteţi ignora numele metodelor.
Reţineţi că toate metodele folosesc ca punct de plecare aceeaşi formulă.

Instrucţiunea LEA (Load Effective Address)

Adresa efectivă poate fi încărcată într-un registru şi prin instrucţiunea LEA.


Sintaxa acestei instrucţiuni este

lea registru,[expresie]
Instrucţiunea LEA calculează adresa efectivă prin evaluarea expresiei aflată
între parantezele pătrate (dată ca operand sursă) şi încarcă această adresă într-un
registru de uz general, de 32 de biţi, dat ca operand destinaţie. Astfel, în loc de
instrucţiunea

mov ebx, valori

se poate folosi

lea ebx, [valori]

Diferenţa între cele două instrucţiuni este că varianta cu LEA calculează


valoarea deplasamentului la rulare, în timp ce varianta cu MOV, la asamblare. Din
acest motiv, varianta cu MOV se foloseşte de fiecare dată când este posibil. Totuşi,
LEA este mai flexibilă în ceea ce priveşte tipul operanzilor sursă. De exemplu,
pentru a introduce în EBX adresa elementului al cărui index se află în registrul
ESI, putem scrie

lea ebx, [valori+ESI]

Echivalentul

mov ebx, valori+ESI

este ilegal, deoarece conţinutul lui ESI este cunoscut de abia la rulare. Pe
de altă parte, ne amintim că, în memorie, intrările individuale nu au etichetă. În
consecinţă, nu pot fi accesate direct. LEA poate obţine adresa efectivă a oricărui
octet individual din memorie.
Instrucţiunea LEA permite şi realizarea rapidă a unor operaţii aritmetice,
fără a fi nevoie de utilizarea instrucţiunilor de adunare sau înmulţire. De exemplu,
expresia aritmetică x = x + y × 8, presupunând că valoarea lui x se găseşte în
registrul EAX şi valoarea lui y în registrul EBX, poate fi calculată rapid prin

lea eax, [eax + ebx * 8]

Nu numai că operaţia este efectuată mai rapid decât în cazul utilizării


instrucţiunilor aritmetice dedicate, dar este şi mai lizibilă – operaţia efectuată se
deduce imediat. Nu are importanţă că rezultatul final nu reprezintă o adresă. LEA
nu încearcă să acceseze elementul stocat la adresa pe care o calculează. Pur şi
simplu, efectuează operaţia matematică cuprinsă între paranteze şi stochează
rezultatul în operandul destinaţie (rezultatul poate reprezenta o adresă sau nu).
5.3. Optimizarea accesului la memorie

Accesul la memorie este unul din cele mai lente procese îndeplinite de
procesor. Când programele au cerinţe ridicate de performaţă este bine să păstrăm
datele în registre şi să evităm accesul la memorie cât mai mult posibil. Cel mai
rapid mod de tratare a datelor este să le transferăm între registre. Dacă păstrarea
datelor în registre nu reprezintă o opţiune viabilă (nu dispunem de un număr
suficient de registre), trebuie să încercăm să optimizăm procesul de extragere a
acestora din memorie.
Am învăţat să privim memoria ca şir liniar de octeţi. Cândva, memoria
chiar aşa era organizată şi adresată, dar pentru procesoarele familiei 80386 această
imagine nu se mai reflectă în hardware. De fapt, dimensiunea unei locaţii de
memorie de un singur octet este o ficţiune întreţinută în beneficiul dezvoltatorilor
de programe. Deşi memoria este forţată să emuleze un cuvânt de 8 biţi, cuvintele
propriu zise sunt de 32 de biţi (patru octeţi) - dimensiunea magistralei de adrese.
Dar, şi în acest caz, primii doi biţi ai magistralei de adrese nu sunt prezenţi. Liniile
magistralei de adrese sunt de la A31 la A2. A1 şi A0 lipsesc. Din punct de vedere
hardware, un procesor 80386 adresează 2!" cuvinte de 32 de biţi. Memoria de
2!" ×8 biţi este o ficţiune. Din cauza acestui mod de organizare hardware,
procesorul citeşte sau scrie eficient locaţiile de memorie organizate în anumite
blocuri specifice, începând cu prima adresă a segmentului de date. De exemplu, un
procesor de 32 de biţi accesează cel mai repid datele poziţionate la adrese cu primii
doi biţi de 0 (cei „inexistenţi”, liniile A0 şi A1).
Datele “aliniate” sunt datele aflate la adrese de memorie pe care procesorul
le poate accesa într-un singur ciclu de citire sau scriere. Alinierea datelor nu
depinde numai de adresa, ci şi de mărimea lor:
• valorile de un octet sunt întotdeauna aliniate;
• valorile de doi octeţi sunt aliniate numai atunci când sunt localizate la
adrese pare (adrese multiple de 2) – bitul cel mai puţin semnificativ este 0.
• valorile de patru octeţi sunt aliniate atunci când sunt poziţionate la adrese
perfect divizibile cu patru – primii doi biţi mai puţin semnificativi sunt 0.
• valorile de 8 octeţi sunt aliniate atunci când sunt stocate la adrese multiplu
de opt – primii trei biţi mai puţin semnificativi sunt 0. Această aliniere este
importantă pentru procesoarele cu magistrale de date de 64 de biţi, precum
Pentium. La procesoarele 80386, deoarece magistrala de date are 32 de
biţi, o valoare de 64 de biţi va fi citită întotdeauna în două cicluri de citire
şi alinierea la graniţe de 4 octeţi este suficientă.

Deşi procesoarele IA-32 pot folosi atât date aliniate cât şi date nealiniate,
modul de organizare al datelor prezentat mai sus, numit aliniere naturală, permite
unui program să ruleze mai rapid. Mai mult, anumite caracteristici ale procesorului
pot fi întrebuinţate numai cu date aliniate. Aşadar, unele instrucţiuni lucrează mai
bine cu date aliniate, altele chiar necesită aliniere. Unele sisteme de operare
necesită structuri de date aliniate.

Pentru procesoarele care folosesc memoria cache efectele nealinierii sunt


întrucâtva mai reduse. În general, accesul la datele nealinitate aflate într-o linie
cache nu necesită cicluri adiţionale. Totuşi, accesul la datele nealiniate între liniile
de cache includ penalizare de viteză. În plus, accesarea memoriei în ordine
secvenţială ajută la creşterea probabilităţii de cache hit, deoarece mai multe
blocuri de memorie vor fi citite în acelaşi timp în cache.

Să judecăm următorul caz: presupunem că lucrăm cu date de 32 de biţi


aliniate (aflate la adrese perfect divizibile cu patru). Procesorul va extrage datele în
grupuri de patru octeţi (deoarece sunt definite ca DD), de la adrese divizibile cu
patru (tot deoarece sunt definite ca DD).
Datele noastre sunt 1111 2222 3333 4444, aranjate în memorie astfel:
Tabelul 5.2 Date de 32 de biţi aliniate
Valoare Adresă
11 11 22 22 ..00
33 33 44 44 ..00

Pentru a extrage cuvântul de 32 de biți 3333 4444, procesorul trebuie să


afle adresa divizibilă cu patru şi să extragă o singură dată. Dacă datele sunt aranjate
în memorie conform Tabelului 5.3, ca să extragă aceleaşi date, procesorul trebuie:
• să extragă primul cuvânt de 32 de biţi şi să preia trei octeţi din stânga (33
4444),
• să extragă al doilea cuvânt de 32 de biţi şi să preia ultimul octet din dreapta
(33),
• să pună octeţii cap la cap.
Aşadar, două extrageri.
Tabelul 5.3 Date pe 32 de biţi nealiniate
Valoare Adresă
11 22 22 33 ..00
33 44 44 11 ..00

Pe lângă cele menționate, precizăm şi următoarele reguli de aliniere:


• în segmentul de cod este indicată alinierea etichetelor accesate frecvent
(deoarece s-a observat o creştere a vitezei programului). În plus, unele
instrucţiuni necesită alinieri la 32 de octeţi.
• în segmentul de date este necesară cel puţin alinierea datelor la adrese
divizibile cu patru, altfel procesorul citeşte de două ori ca să extragă o
singură dată, ceea ce penalizează serios performanţa.

O regulă generală spune că trebuie să definim întâi datele cu dimensiuni


mai mari – cuvintele de 32 de biţi înaintea celor de 16, cuvintele de 16 biţi înaintea
octeţilor, etc.. Elemente de date de acelaşi tip trebuie plasate împreună, la începutul
secţiunii de date. Acest lucru asigura alinierea corectă. Dacă se lucrează cu şiruri
sau buffere, este indicat, ori ca acestea să fie poziţionate la sfârşitul secţiunii de
date, astfel încât să nu strice alinierea altor elemente, ori să aliniem explicit datele
declarate după ele.

Alinierea datelor în YASM


În YASM, alinierea datelor sau codului la cuvânt, la dublu cuvânt, la
paragraf (16 octeţi), etc., se face prin directivele align şi alignb. Sintaxa este:

align 4 ;aliniază datele la frontiere de 4 octeţi


align 16 ;aliniază datele la frontiere de 16 octeţi
align 16,nop ;linie echivalentă celei precedente
align 8, db 0 ;vezi OBS. II de mai jos
align 4, resb 1 ;aliniază la frontiere de 4 octeţi în segmentul BSS
alignb 4 ;linie echivalentă celei precedente

Obs. I. Pentru ambele directive, argumentul trebuie să fie putere a lui doi (2, 4, 8,
16, 32).
Obs. II. Iniţial, octeţii liberi sunt completaţi cu valoarea 0x90 (codul maşină al
instrucţiunii NOP). Prin opţiunea db 0, octeţii liberi se completează cu 0.

Datele programului alignData.asm sunt nealiniate (directiva align 4


este comentată). După sir, urmează imediat d1 şi d2:
;
;alignData.asm
;
section .data
sir db 10,20,30,40,50,60
;align 4
d1 dd 0ffffffffh
d2 dd 0aaaaaaaah
section .text
global _start
_start:
nop
mov eax,1
mov ebx,0
int 080h

Dacă urmărim ce se întâmplă în segmentul de date, observăm:

Valoare Adresă Ultimul digit


0xaa 0x804909d ...1110
0xaa 0x804909c ...1100
0xaa 0x804909b ...1011
0xaa 0x804909a ...1010
0xff 0x8049099 ...1001
0xff 0x8049098 ...1000
0xff 0x8049097 ...0111
0xff 0x8049096 ...0110
0x3c 0x8049095 ...0101
0x32 0x8049094 ...0100
0x28 0x8049093 ...0011
0x1e 0x8049092 ...0010
0x14 0x8049091 ...0001
0x0a 0x8049090 ...0000
7 0

Dacă aliniem datele prin directiva align 4 (urmează date de tip DD),
obţinem:
Valoare Adresă Ultimul digit
0xaa 0x804909d ...1110
0xaa 0x804909c ...1100
0xff 0x804909b ...1011
0xff 0x804909a ...1010
0xff 0x8049099 ...1001
0xff 0x8049098 ...1000
0x90 0x8049097 ...0111
0x90 0x8049096 ...0110
0x3c 0x8049095 ...0101
0x32 0x8049094 ...0100
0x28 0x8049093 ...0011
0x1e 0x8049092 ...0010
0x14 0x8049091 ...0001
0x0a 0x8049090 ...0000
7 0
6. ARHITECTURA SETULUI DE
INSTRUCŢIUNI

Arhitectura setului de instrucţiuni (ISA – Instruction Set Architecture)


cuprinde setul de instrucţiuni la nivel maşină recunoscut de procesor, tipurile de
date ce pot fi manipulate cu aceste instrucţiuni şi contextul în care aceste
instrucţiuni operează. Arhitectura setului de instrucţiuni este o componentă a
sistemului de calcul vizibilă programatorului în limbaj de asamblare, componentă
care reprezintă interfaţa între software şi hardware. În acest capitol studiem
arhitectura setului de instrucţiuni specifică procesoarelor Intel de 32 de biţi.

6.1. Simboluri „cheie”

Instrucţiunea MOV primeşte ca operanzi trei tipuri de date: date aflate în


memorie (memory data), date aflate în registre (register data) şi date imediate
(immediate data – date specificate direct în corpul instrucţiunilor). De acum înainte
indicăm natura operanzilor prin următoarele simboluri:
• r, registru.
• m, memorie.
• i, imediat.
• b, octet (byte).
• 2, 2 octeţi.
• v, 32 de biţi sau 16 biţi (variable).
• e, înseamnă E când se lucrează cu 32 de biţi şi dispare în logica de 16
biţi (extended).

Aceste simboluri „cheie” au fost utilizate de Stephen Morse în cartea


80386/387 Architecture. Deşi ele nu respectă formatul oficial, sunt de un real ajutor
în explicarea codurilor maşină. Aşadar, instrucţiunea MOV poate primi una din
formele următoare:

mov r, r
mov r, m
mov m, i
mov r, m
mov m, r

Instrucţiunea MOV nu transferă date din memorie în memorie.

Variantele de mai sus pot fi reduse la două reprezentări:

mov r,rmi
mov m,ri

Când nu precizăm capacitatea operanzilor înseamnă că instrucţiunea poate


lucra cu oricare din cele trei variante (octet, cuvânt, dublu cuvânt). În cazul
transferului de date între operanzi trebuie să ţinem cont că nu putem transfera date
între operanzi de dimensiuni diferite. Mulţi sunt înclinaţi să copieze registre de
dimensiuni mai mici în registre de dimensiuni mai mari în virtutea faptului că
„încape”. Numai că instrucţiuni de tipul

mov bx,al

vor produce o eroare de asamblare. Această instrucţiune încearcă să copieze 8 biţi


din registrul AL în registrul BX. În schimb, trebuie copiat conţinutul registrului AX
în registrul BX, ambele de aceeaşi dimensiune. Evident, trebuie să fiţi atenţi ca nu
cumva valoarea lui AX să difere de valoarea lui AL (când AH nu este zero).
Simbolizăm acest lucru prin forme de genul:

mov rv,rmv
mov rb,rmb
mov rmv,rv

Dacă ţinem cont şi de acest ultim aspect, variantele instrucţiunii MOV cresc
considerabil. Setul de instrucţiuni poate fi interpretat la două niveluri: nivelul de
asamblare şi nivelul maşină. Pentru programatorul în limbaj de asamblare,
procesorul are aproximativ o sută de instrucţiuni. Dar unei instrucţiuni în limbaj de
asamblare (de ex., MOV) îi corespunde, de fapt, mai multe formate de instrucţiuni
maşină, în funcţie de tipul şi mărimea operandului. La nivel maşină există
aproximativ trei sute de instrucţiuni. Instrucţiunile nivelului de asamblare
simplifică viziunea programatorului asupra setului de instrucţiuni: programatorul
scrie instrucţiunea în limbaj de asamblare, asamblorul o examinează şi determină
instrucţiunea de nivel maşină ce trebuie generată.
6.2. Codificarea instrucţiunilor

Mecanismul prin care asamblorul generează instrucţiuni maşină este destul


de complex. Totuşi, instrucţiunile folosite de-a lungul exemplelor noastre până în
prezent nu sunt foarte complicate. Codificarea acestora este ilustrată în Tabelul 6.1.
Majoritatea codului maşină este dat în hexazecimal, dar unii octeţi sunt reprezentaţi
în binar.
Tabelul 6.1 Câteva instrucţiuni maşină de 32 de biţi
Primul octet Al doilea octet Încă 4
mov rv,iv 10111DDD
mov rb,ib 10110DDD
mov rmv,rv 89 11SSSDDD
mov rmb,rb 88 11SSSDDD

Instrucţiunile procesorului 80386 sunt codificate pe un număr de octeţi


cuprins între 1 şi 15 (Figura 6.1).

OPCODE ModR/M Imediat


cod operaţie d w mod reg r/m
7 1 0 76 543 210 încă 4 octeţi

Figura 6.1 Formatul general al unei instrucţiuni


Primul octet conţine codul operaţiei (pe 6 biţi), adică tipul prelucrărilor ce
vor fi efectuate la execuţia instrucţiuni (operaţie aritmetică, logică, etc.). În cazul
nostru este vorba de transfer de date. Al doilea octet conţine informaţii cu privire la
registrele care conţin operanzii, adresarea memoriei, etc..
Codul maşină pentru MOV r,r este format din doi octeţi - primul este
reprezentat în hexazecimal, al doilea în binar. Octetul binar încorporează adresele
registrelor. SSS reprezintă codul de trei biţi pentru registrul sursă. DDD
reprezintă codul de trei biţi pentru registrul destinaţie. Dimensiunea
registrelor depinde de bitul 0 al primului octet. Acesta este notat cu w (word) şi
codifică lungimea operanzilor:
• w=0, operand de tip octet;
• w=1, operand de tip cuvânt; la 386, această valoare înseamnă operand de
dimensiune completă (16/32 de biţi în funcţie de modul de lucru).

În aceste condiţii, adresele registrelor sunt prezentate în Tabelul 6.2.


Tabelul 6.2 Adresele registrelor
Registru
Adresă Cuvânt Octet
w=1 w=0
000 EAX AL
001 ECX CL
010 EDX DL
011 EBX BL
100 ESP AH
101 EBP CH
110 ESI DH
111 EDI BH

Tot în primul octet se află bitul d (direction), care indică direcţia


rezultatului operaţiei:
• d=0, câmpul REG indică operandul sursă;
• d=1, câmpul REG indică operandul destinaţie.

REG se află în al doilea octet al instrucţiunii, numit ModR/M (dacă există). Octetul
ModR/M poate lipsi. De exemplu, instrucţiunea MOV r,i nu prezintă octetul
ModR/M. Nu este zero, ci lipseşte. Valoarea imediatului este memorată în cei patru
octeţi adiţionali (de la 1 la 4; în funcţie de dimensiunea destinaţiei, imediatul poate
fi reprezentat pe un 1, 2 sau 4 octeţi).
Considerăm următoarele exemple:

1 B801000000 mov eax,1


2 B89B000000 mov eax,155
3 B800010000 mov eax,256
4 B8FEFF0000 mov eax,65534
5 BB00000000 mov ebx,0
6 B10F mov cl,15
7 89C8 mov eax,ecx
8 89D3 mov ebx,edx
9 88CD mov ch,cl

Primul octet al instrucţiunii MOV EAX,1, B8, este codificat folosind codul
10111DDD, conform formatului MOV rv,iv. Biţii DDD sunt completaţi cu
adresa registrului EAX, rezultând codul binar 1011 1000 (B8H). Valoarea
imediatului, întregul 1, este reprezentată imediat după acest OPCODE, pe următorii
4 octeţi. Observaţi cum valoarea imediatului de 4 octeţi, 00000001H, este stocată
ca 01 00 00 00, conform criteriului little-endian (de aceea valoarea sa în
instrucţiunea maşină pare a fi întoarsă). De exemplu, valoarea 155 în hexazecimal
este 9B; în consecinţă, a doua instrucţiune este codificată B89B000000. Verificaţi
faptul că aţi înţeles mecanismul încercând să deduceţi instrucţiunile maşină pentru
următoarele trei linii.
Linia 6 prezintă o instrucţiune de forma MOV rb,ib. Codificarea folosită
este 10110DDD. Deoarece adresa registrului CL este 001 şi imediatul 15 în
hexazecimal este 0F, instrucţiunea maşină se formează ca B10F.
Linia 7 prezintă o instrucţiune de forma MOV rv,rv, cu registrul EAX ca
destinaţie (biţii DDD = 000) şi registrul ECX ca sursă (biţii SSS = 001). Octetul
OPCODE este 89H, iar forma binară a octetului ModR/M este 11SSSDDD. Pentru
octetul ModR/M, dacă plasăm adresele registrelor pe poziţiile corespunzătoare,
obţinem 1100 1000 în binar (C8H). Rezultă instrucţiunea maşină 89C8.

6.2.1. Spaţiul codurilor operaţionale

Codurile maşină utilizate în ultima secţiune pot asambla o mare varietate


de instrucţiuni în limbaj de asamblare. Pe de altă parte, pe ansamblu, ele reprezintă
numai o mică parte din setul de instrucţiuni Intel x86. În această secţiune facem o
descriere generală a setului de instrucţiuni maşină.
Tabelul 6.3 prezintă informaţiile oferite de primul octet al codului maşină.
Majoritatea intrărilor din tabel conţin informaţii cu privire la mnemonică şi
operanzi. În câteva cazuri această informaţie este specificată complet. De exemplu,
intrarea corespunzătoare octetului 90 (rândul 9, coloana 0) reprezintă instrucţiunea
NOP. În alte cazuri se precizează mnemonica şi o clasă de operanzi admişi (descrişi
prin simbolurile cunoscute).
Informaţiile transmise de primul octet (OPCODE) pot fi împărţite în
următoarele clase:
• Octetul OPCODE exprimă complet instrucţiunea. De exemplu, NOP
specifică instrucţiunea NOP (No Operation), care, aşa cum denotă şi
numele ei, nu realizează nicio prelucrare. NOP ocupă un singur octet şi
durează trei cicluri de ceas.
• Octetul OPCODE determină tipul operaţiei, dar nu specifică operanzii. În
aceste cazuri, de cele mai multe ori, această informaţie este furnizată de cel
de al doilea octet, ModR/M. De exemplu, dacă primul octet este 89H,
intrarea corespunzătoare din tabelă este MOV rmv,rv. Pe lângă faptul că
specifică operaţia MOV, simbolurile operand rmv,rv transmit informaţie
cu privire la modul în care trebuie interpretat octetul ModR/M. Acest caz a
fost deja întâlnit.
Tabelul 6.3 Codurile setului de instrucţiuni IA-32 [13]

• Octetul OPCODE nu specifică o operaţie, ci o categorie de operaţii, iar


octetul ModR/M este folosit atât să completeze informaţia de prelucrare cât
şi să furnizeze informaţia cu privire la operanzi. De exemplu, dacă primul
octet este C1, intrarea în tabel este Shift rmv,ib. În aceste cazuri este
nevoie de interpretarea completă a octetului ModR/M (Figura 6.2). Shift
nu este un cuvânt mnemonic. El este un indicator către Tabelul 6.4. În
Tabelul 6.3, acest lucru este indicat prin faptul că nu toate literele
cuvântului sunt majuscule.
• Dacă primul octet este 0F, atunci se deschide o întreagă pleiadă de
posibilităţi. Informaţia transmisă de 0F este „mergi la Tabelul 6.5”.
• Dacă valoarea primului octet se află în gama D8-DF, atunci este vorba de o
instrucţiune în virgulă mobilă.
• Octeţi prefix. Octeţii prefix modifică comportamentul instrucţiunilor care îi
urmează (de ex., LOCK).
6.2.2. Octetul ModR/M

Octetul ModR/M transmite informaţii cu privire la operanzi în cazurile în


care primul octet este insuficient.

Mod REG R/M


7 6 5 4 3 2 1 0
Figura 6.2 Octetul ModR/M
• Biţii 7 şi 6 sunt biţii Mod. Când biţii Mod sunt 11, câmpul R/M specifică
un registru. În celelalte cazuri, biţii R/M codifică adrese de memorie.
• Biţii 5, 4, 3, formează câmpul REG, sau registru, şi de cele mai multe ori
este desemnat prin notaţia /r. Indică registrul care conţine unul din
operanzi sau, împreună cu cei 6 biţi din primul octet, specifică alte coduri
operaţionale (Tabelul 6.4). Acest lucru înseamnă că REG este determinat
de primul octet (OPCODE) al instrucţiunii.
• Biţii 2,1,0, formează câmpul R/M, sau registru/memorie. Dacă Mod = 11,
atunci R/M indică registrul care conţine al doilea operand. În toate celelalte
cazuri, R/M indică registrul implicat în aflarea locaţiei de memorie ce
conţine al doilea operand.
Tabelul 6.4 Instrucţiuni specificate de REG
/r /0 000 /1 001 /2 010 /3 011 /4 100 /5 101 /6 110 /7 111
Immed ADD OR ADC SBB AND SUB XOR CMP
Shift ROL ROR RCL RCR SHL SHR SAR
Unary TEST i NOT NEG MUL IMUL DIV IDIV
IncDec INC DEC
INC DEC CALL m CALL JMP JMP PUSH
Indir FAR m FAR

Unul din cazurile în care cei 6 biţi de cod operaţional prezenţi în primul
octet sunt insuficienţi pentru a defini complet operaţia, şi este necesară studierea
câmpului REG din octetul ModR/M, este cel al instrucţiunii AND ECX,64.
Înregistrarea din Tabelul 6.3 pentru 83 este

Immed rmv,ib

Deoarece rândul intitulat Immed din Tabelul 6.4 are un AND sub /r = /4,
înseamnă că instrucţiunea

AND rmv,ib
poate fi codificată folosind 83 ca OPCODE şi 100 pentru biţii REG din octetul
ModR/M. Din moment ce primul operand este registrul ECX, biţii Mod trebuie să
fie 11 şi R/M să conţină 001 (adresa registrului ECX). Aşadar, octetul ModR/M
este format din biţii 11 100 001, sau E1H. ib înseamnă imediat de tip octet -
valoarea sa se adaugă la restul codului instrucţiune. În final, pentru AND ECX,64
se obţine 83 E1 40.

6.2.3. Spaţiul codurilor 386 (0F + ...)

La procesorul 8086 octetul 0FH codifică instrucţiunea de extragere din


stivă a registrului CS. La următoarele procesoare această intrucţiune a fost
înlăturată, astfel încât 0FH, acum disponibil, a permis o extindere semnificativă a
setului de instrucţiuni. Octetul 0FH se numeşte prefix de extindere a codurilor de
operaţie. Tabelul 6.5 dezvăluie spaţiul nou creat.
Tabelul 6.5 Spaţiul 386 [13]

Ca şi Tabelul 6.3, Tabelul 6.5 prezintă informaţia transmisă de un singur


octet - în acest caz, octetul care urmează octetului 0F. Majoritatea instrucţiunilor
din acest tabel au apărut pentru prima dată la procesorul 80386. Informaţiile
furnizate de acest octet pot fi incluse în următoarele clase:
• Specifică complet o instrucţiune. De exemplu, 0F CA este codul pentru
BSWAP EDX.
• Determină tipul operaţiei, dar operanzii sunt specificaţi de octetul
ModR/M. De exemplu, instrucţiunea XADD (0F C1).
• Determină o categorie de operaţii, iar octetul ModR/M specifică o anumită
operaţie. De exemplu, dacă primii doi octeţi ai instrucţiunii sunt 0F 01,
operaţia este determinată de biţii REG ai octetului ModR/M. Din Tabelul
6.6 reiese că, atunci când biţii REG din ModR/M sunt 011, instrucţiunea
este LIDT.
• Reprezintă instrucţiuni MMX.
Tabelul 6.6 Instrucţiunile OF specificate de biţii REG
/r /0 000 /1 001 /2 010 /3 011 /4 100 /5 101 /6 110 /7 111
SLDT STR LLDT LTR VERR VERW
LocalT rm2 rm2 rm2 rm2 rm2 rm2
SGDT SIDT LGDT LIDT SMWS LMSW
GlobalT m6 m6 m6 m6 rm2 rm2
BT BTS BTR BTC
Bits rmv,ib rmv,ib rmv,ib rmv,ib

Instrucţiunile prezente în Tabelul 6.5 mai provin, pe lângă procesorul


80386, de la 80486 şi Pentium. De exemplu, XADD şi BSWAP au apărut pentru
prima dată la procesoarele 80486, iar CPUID şi RDTSC la Pentium.

6.2.4. Prefix de dimensiune operand

Până acum am studiat numai instrucţiuni maşină care primesc drept


operanzi registre de 8 sau 32 de biţi. În continuare studiem modul în care sunt
codificate instrucţiunile cu operanzi de 16 biţi. Câteva exemple:

1 89D8 mov eax,ebx


2 6689D8 mov ax,bx
3 B808000000 mov eax,8H
4 66B80800 mov AX,8H

Instrucţiunea MOV EAX,EBX este codificată 89 D8. Echivalentul ei pe 16 biţi


este MOV AX,BX. Surprinzător, codul pentru această comandă este tot 89 D8
(ignoraţi primul octet). Procesorul 80386 a fost proiectat să folosească aceleaşi
codificări atât pentru operaţii cu registre de 32 de biţi cât şi pentru operaţii cu
registre de 16 biţi. Modul de operare implicit al instrucţiunilor, fie pe 16, fie pe 32
de biţi, depinde de bitul D din descriptorul segmentului de cod. Bitul D specifică
dimensiunea implicită atât pentru operanzi cât şi pentru adresa locaţiilor de
memorie. D = 0 indică operaţii pe 16 biţi, D = 1 indică operaţii pe 32 de biţi. Bitul
D determină dacă 89 D8 înseamnă MOV EAX,EBX sau MOV AX,BX.
Acest lucru nu înseamnă că instrucţiunea MOV AX,BX nu este disponibilă
la procesoarele de 32 de biţi. La aceste procesoare, asamblorul semnalizează
procesorului faptul că prezumţia cu privire la numărul de biţi setată implicit de
bitul D este greşită prin adăugarea prefixului 66 înainte de instrucţiune. Prefixul
66 este numit prefix de dimensiune operand. Astfel, pentru un mod de lucru
implicit pe 32 de biţi (D = 1), codificarea 66 89 D8 înseamnă MOV AX,BX.
Similar, pentru pentru un mod de lucru implicit pe 16 biţi (D = 0), codul 66 89
D8 înseamnă MOV EAX,EBX. Prefixul dimensiune operand indică faptul că
dimensiunea operandului nu este cea implicită. În mod implicit, sistemul Linux
setează bitul D (nu acelaşi lucru se poate spune de DOS), aşadar pe sistemele
noastre 66 89 D8 înseamnă MOV AX,BX.
Acum, odată ce am văzut cum aceeaşi codificare poate avea două
semnificaţii, ne este uşor să explicăm de la ce provine simbolul v din instrucţiunile
de genul MOV rmv,rv. Simbolul v din fiecare operand înseamnă variabil, unde
variabil se referă la faptul că dimensiunea operandului poate fi de 16 sau 32 de biţi.
În mod asemănător, simbolul e (extended) din reprezentările registrelor, de
exemplu eAX, indică faptul că, în codificarea de 32 de biţi, ne referim la registrul
EAX, iar în codificarea de 16 biţi, la registrul AX.

6.3. Codificarea adreselor de memorie

Toate intrările din Tabelul 6.3 care includ simbolul m descriu instrucţiuni
de acces la memorie. Observăm că toate instrucţiunile MOV, ADD, ADC, SUB, SBB,
OR, AND şi XOR pot avea ca sursă sau destinaţie operanzi aflaţi în memorie.
Aceştia pot fi adresaţi în diverse moduri. În capitolul precedent am discutat pe larg
formatul general de adresare a memoriei. Aspectul funcţional

[bază + index × scală + deplasament]

rescris structural, arată astfel:

[reg + reg × scală + imediat]


unde,
• scală este un factor de scală, care trebuie să fie 1, 2, 4 sau 8;
• registrul multiplicat de factorul de scală este numit registru index;
• celălalt registru este numit registru de bază;
• imediatul este un număr pe 8, 16 sau 32 de biţi.

Rezultă o mare varietate de formate, majoritatea deja studiate,

[reg]
[imediat]
[reg+reg]
[reg+imediat]
[reg+reg+imediat]
[reg+scală×reg]
[scală×reg+imediat]

în care imediatul reprezintă fie adresa completă a operandului (deplasamentul), fie


o constantă implicată în adresare (o deplasare). Poate este greu de crezut că
instrucţiuni cu asemenea grad de complexitate sunt codificate uşor de procesor.
Costul acestei complexităţi, în termeni de dimensiune (lungimea instrucţiunii care
foloseşte formatul complet), este de un singur octet. Octetul este numit SIB (Scale
Index Base).

6.3.1. Octetul SIB


Prefix
OPCODE ModR/M SIB Deplasament Imediat
instrucţiune
Până la 4 prefixuri Cod 1 octet 1 octet Deplasament de Date imediate
de câte un octet operaţional (dacă este (dacă este 1,2 sau 4 de 1,2 sau 4
(opţionale) de 1, 2 sau 3 necesar) necesar) octeţi octeţi
octeţi (opţional) (opţional)

Mod Reg/Opcode R/M Scală Index Bază


Figura 6.3 Formatul instrucţiunilor IA-32
În engleză, un joc de cuvinte defineşte SIB ca fiind sibling cu octetul
ModR/M, aluzie la faptul că prezenţa sa în corpul instrucţiunii este indicată de
ModR/M (numai în codificările pentru care acesta este insuficient).
Octetul SIB conţine:
• factorul de scală, pe primii doi biţi;
• registrul index utilizat, pe următorii trei biţi (ca registre index pot fi
utilizate 7 din cele 8 registre de uz general, mai puţin registrul ESP);
• registrul de bază utilizat, pe ultimii trei biţi (ca registre de bază
pot fi utilizate toate registrele de uz general).

6.4. Formatul instrucţiunilor

Toate codificările instrucţiunilor IA-32 sunt subseturi ale formatului de


instrucţiune general prezentat în Figura 6.3. Instrucţiunile constau din prefixe de
instrucţiune opţionale (în orice ordine), unul sau doi octeţi de cod operaţie (unele
din instrucţiunile SSE şi SSE2 au trei octeţi de cod operaţie), un identificator al
formatului de adresă (dacă este necesar) - format din octetul ModR/M şi, uneori,
octetul SIB, un deplasament (dacă este necesar), şi un câmp de date imediate.

6.4.1. Prefixele de instrucţiune

Prefixele de instrucţiune sunt valori speciale de un octet care afectează


operaţia instrucţiunii. Prefixele de instrucţiune sunt împărţite în patru grupuri,
fiecare cu un set de coduri admisibil.

• Prefixe de instrucţiune (de blocare sau repetare) (Grup1)


o F0H, LOCK (garantează că instrucţiunea va avea acces exclusiv la
toată memoria partajată, pe toată durata execuţiei sale).
o F2H, REPNE/REPNZ (folosite numai în operaţii pe şiruri).
o F3H, REP sau REPE/REPZ (folosite numai în operaţii pe şiruri).
• Prefixe de segment (Grup 2)
o 2EH, registrul CS.
o 36H, registrul SS.
o 3EH, registrul DS.
o 26H, registrul ES.
o 64H, registrul FS.
o 65H, registrul GS.
• Prefixe de indiciu de ramificare (Grup 2)
o 2EH, ramificare inactivă (folosit numai cu instrucţiuni de salt
condiţionat).
o 3EH, ramificare activă (folosit numai cu instrucţiuni de salt
condiţionat).
• 66H, prefix de dimensiune operand (Grup 3)
• 67H, prefix de dimensiune adresă (Grup 4)

Pentru orice instrucţiune se poate folosi un prefix din fiecare grup, în orice
ordine.
Prefixul LOCK determină activarea semnalului de magistrală omonim pe
durata execuţiei acelei instrucţiuni. Are ca efect interdicţia de cedare a
magistralelor unui alt dispozitiv. Într-un sistem multiprocesor, acest semnal poate fi
utilizat de operaţii atomice18 pentru a obţine acces exclusiv la memoria partajată.
Prefixele de repetare provoacă repetarea unei instrucţiuni pentru fiecare
element al unui şir. Acestea pot fi utilizate numai cu instrucţiuni pe şiruri: MOVS,
CMPS, SCAS, LODS, STOS, INS şi OUTS (vor fi studiate într-un capitol viitor).
Prefixele de segment forţează unitatea de management a memorie să
folosească registrul de segment specificat în loc de cel implicit.
Prefixele de indiciu ramificare permit unui program să indice procesorului
cea mai probabilă cale urmată de o instrucţiune de salt. Aceste prefixe pot fi
utilizate numai cu instrucţiuni de salt condiţionat. Au fost introduse în procesoarele
Pentium 4 şi Intel Xeon ca parte din extensiile SSE2.
Prefixul de dimensiune operand modifică dimensiunea implicită a datelor
(de la 32 la 16 biţi, sau invers).
Prefixul de dimensiune adresă modifică dimensiunea implicită a adreselor
(de la adrese de 32 de biţi comută la adrese de 16 biţi, sau invers).

6.4.2. Modurile de adresare prin octetul ModR/M

Câmpul Mod din registrul ModR/M reprezintă o codificare a modului de


calcul a adresei efective sau lungimea deplasamentului şi, împreună cu câmpul
R/M, este utilizat pentru a determina adresa efectivă. Până acum ne-am concentrat
atenţia asupra instrucţiunilor în care biţii câmpului Mod sunt 11. În aceste situaţii,
câmpul R/M specifică un registru şi utilizează aceeaşi codificare ca şi câmpul
REG. Am putut obţine coduri pentru instrucţiuni de genul MOV EAX,EDX,ce
folosesc adresarea la registre. Ca o scurtă recapitulare, reamintim paşii procesului.
Din Tabelul 6.3 am identificat codul operaţiei ca fiind 89H. Ultimii doi biţi ai
octetului OPCODE, d, respectiv w, dau informaţii cu privire la destinaţia
rezultatului şi la dimensiunea operanzilor. Deoarece bitul d este zero, înseamnă că
în câmpul REG se află codul registrului sursă, aşadar 010 (EDX), iar în câmpul

18
O operaţie atomică este indivizibilă si nu poate fi întreruptă; odată ce operaţia începe nu
va fi oprită sau întreruptă până când nu este finalizată, şi nici o altă operaţie nu-i va lua
locul în timpul acesta.
R/M trebuie să se afle codul registrului destinaţie, 000 (EAX). Valoarea conţinută
în ModR/M se obţine grupând biţii Mod REG R/M împreună. Rezultă 11 010 000
în binar, adică D0H. Instrucţiunea completă este codificată sub forma 89D0.
Tabelul 6.7 Formele de adresare pe 32 de biţi cu ajutorul octetului ModR/M

Tabelul 6.7 face parte din manualul oficial pus la dispoziţie de Intel pentru
dezvoltatorii de programe19 şi prezintă toate formele de adresare pe 32 de biţi care
folosesc octetul ModR/M. Numerele hexazecimale reprezintă valorile octetului

19
Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference
ModR/M în aceste cazuri. Prima coloană arată modul de calcul al adresei efective,
a doua şi a treia, setarea biţilor Mod, respectiv R/M. Coloanele care urmează
prezintă valoarea octetului ModR/M pe ansamblu, dar şi adresa registrului care se
găseşte în câmpul REG. Priviţi rândul corespunzător coloanei Mod = 11 (ultimul
rând) şi căutaţi codul D0. Prima coloană denotă mecanismul de adresare la
registre, coloana R/M arată că în câmpul R/M se află adresa registrului EAX, iar
coloana pe care se află D0 arată că în câmpul REG se află registrul EDX.
Dimensiunea registrelor a fost descifrată anterior cu ajutorul bitului w din octetul
codului de operaţie. Considerăm următorul exemplu

mov ah,ch

Din Tabelul 6.3 codul operaţiei pentru o instrucţiune de genul MOV rmb,rb este
88. Valoarea 0 a bitului w din OPCODE (1000 1000) indică operaţie la nivel de
octet. Valoarea 0 a bitului d (10001000) specifică faptul că sursa (registrul CH) se
află în REG şi destinaţia (registrul AH) în R/M. În concluzie, octetul ModR/M este
format din şirul de biţi 11 101 100, EC. Codul complet al instrucţiunii este 88EC.
În cazul

mov edx,ecx

codul instrucţiunii este 89CA. Dacă în acest cod schimbăm biţii Mod de la 11 la
00, valoarea octetului ModR/M devine 0A (00 001 010). Instrucţiunea devine:

mov [edx],ecx

În acest caz, biţii Mod indică un operand aflat în memorie. Codurile 01 şi 10 sunt
utilizate la codificarea deplasamentelor imediate, precum MOV EDI,[EAX+5],
unde deplasamentul este de tip ib sau iv. Aşadar, codul pentru MOV
EDI,[EAX+5] este 8B78 05. Codul pentru MOV EDI,[EAX+12345678H]
este 8BB8 78563412. Totuşi, dacă această schemă de codificare se folosea
uniform nu ar fi fost disponibil destul spaţiu pentru formatele complexe ale
modurilor de adresare. Din acest motiv, codul pentru registrul ESP a fost
îndepărtat, poziţia acestuia folosindu-se ca intrare într-un nou spaţiu de codificare
ce foloseşte octetul SIB. În următoarele rânduri descriem pe larg posibilităţile
introduse de octetul ModR/M.
Mod = 00 înseamnă mod de adresare indirect (prin registre), direct prin
deplasament (R/M = 101) sau SIB fără deplasament (R/M = 100).
Din Tabelul 6.7 reiese că Mod = 00 înseamnă adresare bazată. Câmpul R/M
specifică un mod de adresare indirectă sau bazată/indexată, mai puţin pentru R/M =
101, care denotă adresare directă prin deplasament, şi R/M = 100, care indică către
octetul SIB. Slotul ocupat de modul de adresare directă prin deplasament aparţinea
adresării indirecte prin registrul EBP. Intel a decis că în locul acesteia,
programatorii pot utiliza adresarea indirectă [EBP+ib], cu ib = 0 (deşi
instrucţiunea este puţin mai lungă). Aşadar, Mod = 00 poate fi folosit pentru
următoarele cazuri:
[reg]
[deplasament]
[deplasament + constantă]

Mod = 01 înseamnă că octetul (octeţii) modului de adresare este urmat de


un deplasament (imediat) de 8 biţi. Atenţie, nu confundaţi deplasamentul cu
dimensiunea datelor. Dimensiunea datelor este specificată în octetul OPCODE prin
bitul w. Un deplasament de 8 biţi nu înseamnă date de 8 biţi, ci o ajustare în
segment curpinsă în gama -128..+127. Câmpul deplasament din formatul general al
instrucţiunilor IA-32 va fi format dintr-un singur octet poziţionat imediat după
octeţii OPCODE (rapiditate).
Mod = 10 înseamnă că octetul (octeţii) modului de adresare este urmat de
un deplasament de 32 de biţi.

6.4.3. Modurile de adresare prin octetul SIB

Toate intrările din Tabelul 6.7 definite prin intermediul câmpului Mod şi
R/M = 100 trimit către octetul SIB şi arată că instrucţiunea foloseşte o formă de
adresare indexată.

[r32 + eax × scală]


[imediat + r8 + eax × scală]
[imediat + r32 + eax × scală]

Aşa cum se desprinde din Tabelul 6.8, octetul SIB specifică registrul de
bază, registrul index şi factorul de scală. Codul factorului de scală precizează
valoarea cu care va fi înmulţit registrul index.
Tabelul 6.8 Formele de adresare pe 32 de biţi cu ajutorul octetului SIB

Tabelul 6.9 Valoarea factorului de scală


Valoarea factorului de scală Valoarea (Registru index × Factor de scală)
00 Index × 1
01 Index × 2
10 Index × 4
11 Index × 8
Pentru fiecare mod de adresare cu octet SIB, câmpul Mod din octetul
ModR/M specifică dimensiunea deplasamentului (constantă de deplasare). Poate fi
zero, unu sau patru octeţi.
Tabelul 6.10 Adresarea cu SIB
Mod R/M Mod de adresare
00 100 SIB
01 100 SIB + deplasament
10 100 SIB + deplasament

Niciun mod de adresare cu SIB nu permite utilizarea registrului ESP ca


registru index.
Un caz special apare când Mod = 00 şi câmpul registrului de bază este 101.
Sunt adresările de tip:

[deplasament + eax × scală]

6.5. Studiu de caz

Primul listing prezintă fişierul prog.lst generat la asamblare, următorul


reprezintă dezasamblarea executabilului prog. În primul, codificările instrucţiunilor
nu includ adresele de memorie la care se vor afla acestea în momentul rulării
programului şi este mai uşor să descifrăm codul operaţional.

Listing 1.
cat prog.lst

2 [section .data]
3 00000000 0A000000140000001E- valori: dd 10,20,30,40,50,60
4 00000000 000000280000003200-
5 00000000 00003C000000
6 [section .text]
7 [global _start]
8 _start:
9 00000000 90 nop
10 00000001 B8[00000000] mov eax,valori
11 00000006 8B1D[00000000] mov ebx,[valori]
12 0000000C 8B08 mov ecx,[eax]
13 0000000E 8B5004 mov edx,[eax+4]
15 00000017 B803000000 mov eax,3
16 0000001C 8BB0[00000000] mov esi,[valori+eax*1]
17 00000022 BB[00000000] mov ebx,valori
18 00000027 8B0C83 mov ecx,[ebx+eax*4]
19 0000002A 41 inc ecx
20 0000002B 6641 inc cx
21 0000002D 0409 add al,9
22 0000002F 83C009 add eax,9
23 00000032 05FF000000 add eax,255
24 00000037 6605FF00 add ax,255
25
26 0000003B B801000000 mov eax,1
27 00000040 BB00000000 mov ebx,0
28 00000045 CD80 int 080

Listing 2.

objdump -d -M intel prog

prog: file format elf32-i386

Disassembly of section .text:

08048080 <_start>:
8048080: 90 nop
8048081: b8 c8 90 04 08 mov eax,0x80490c8
8048086: 8b 1d c8 90 04 08 mov ebx,DWORD PTR ds:0x80490c8
804808c: 8b 08 mov ecx,DWORD PTR [eax]
804808e: 8b 50 04 mov edx,DWORD PTR [eax+0x4]
8048097: b8 03 00 00 00 mov eax,0x3
804809c: 8b b0 c8 90 04 08 mov esi,DWORD PTR [eax+0x80490c8]
80480a2: bb c8 90 04 08 mov ebx,0x80490c8
80480a7: 8b 0c 83 mov ecx,DWORD PTR [ebx+eax*4]
80480aa: 41 inc ecx
80480ab: 66 41 inc cx
80480ad: 04 09 add al,0x9
80480af: 83 c0 09 add eax,0x9
80480b2: 05 ff 00 00 00 add eax,0xff
80480b7: 66 05 ff 00 add ax,0xff
80480bb: b8 01 00 00 00 mov eax,0x1
80480c0: bb 00 00 00 00 mov ebx,0x0
80480c5: cd 80 int 0x80
Codificarea instrucţiunii MOV eAX,iv

Instrucţiunea MOV EAX,valori este codificată în acelaşi mod ca MOV


EAX,1 - încărcarea unui imediat de 32 de biţi în registrul de uz general EAX
(MOV eAX,iv). Diferenţa constă din faptul că valoarea imediatului, 1, este
cunoscută la asamblare şi asamblorul formează instrucţiunea completă. Observaţi
că valorile sunt scrise în little-endian imediat după singurul octet de cod
operaţional.

Instrucţiune: MOV eAX,iv


Codificare: B8 [00 00 00 00]
Binar: 1011 1000 [imediat]
• Opcode = 10111000, indică încărcarea în registrul EAX sau AX a unui
imediat de 32 sau 16 biţi, în funcţie de modul de lucru implicit. Deoarece,
în cazul nostru, modul de lucru implicit este pe 32 de biţi, înseamnă că
instrucţiunea se traduce: MOV EAX,imm32. Imediatul poate fi specificat
explicit sau simbolic (printr-o etichetă).

Adresare directă prin deplasament

Instrucţiune: MOV EBX,[valori]


Codificare: 8b 1d [00 00 00 00]
Binar: 1000 1011 0001 1101 [depl32]
• Opcode = 100010, indică o instrucţiune MOV.
• d = 1, câmpul REG este destinaţie.
• w = 1, operanzi de 32 de biţi.
• REG = 011, registrul EBX.
• combinaţia Mod = 00 şi R/M = 101 indică mod de adresare prin
deplasament (de 32 de biţi). Aşadar, după instrucţiune va urma un astfel de
deplasament.

Deplasamentul este vizibil în al doilea listing şi reprezintă adresa de la care încep


datele în memorie (0x80490c8 ). Registrul DS din faţa deplasamentului arată că
datele se află în segmentul de date implicit (adresat cu DS).
Tabelul 6.11 Registrele de segment implicite utilizate la adresare
Tip referinţă la memorie Segment implicit Deplasament
Citire instrucţiune CS EIP
Date DS Adresa efectivă
Operaţii pe şiruri
- şir sursă DS ESI
- şir destinaţie ES EDI
Operaţii cu stiva SS ESP
EBP utilizat ca registru de bază SS Adresa efectivă

Adresare indirectă prin registre

Instrucţiune: MOV ECX,[EAX]


Codificare: 8B 08
Binar: 1000 1011 0000 1000
• Opcode = 100010, identifică o instrucţiune MOV.
• d = 1, câmpul REG este destinaţie.
• w = 1, operanzi de 32 de biţi.
• Mod = 00, adresare fără deplasament.
• REG = 001, registrul ECX.
• R/M = 000, indică utilizarea modului de adresare [EAX].

Adresare bazată (deplasament de 8 biţi)

Instrucţiune: MOV EDX,[EAX+4]


Codificare: 8B 50 [04]
Binar: 1000 1011 0101 0000 [depl8]
• Opcode = 100010, identifică o instrucţiune MOV.
• d = 1, câmpul REG este destinaţie.
• w = 1, operanzi de 32 de biţi.
• Mod = 01, câmpul deplasament are 8 biţi.
• REG = 010, registrul EDX.
• R/M = 000, indică utilizarea modului de adresare [EAX+depl8].

Deplasamentul de 8 biţi urmează imediat după octeţii de cod de operaţie şi ocupă


un singur octet.
Adresare bazată (deplasament de 32 de biţi)

Instrucţiune: MOV ESI,[valori+EAX*1]


Codificare: 8B B0 [00 00 00 00]
Binar: 1000 1011 1011 0000 [depl32]
• Opcode = 100010, identifică o instrucţiune MOV.
• d = 1, câmpul REG este destinaţie.
• w = 1, operanzi de 32 de biţi.
• Mod = 10, câmpul deplasament are dimensiune completă. Deoarece modul
de lucru implicit este pe 32 de biţi, deplasamentul are 32 de biţi.
• REG = 110, registrul ESI.
• R/M = 000, indică utilizarea modului de adresare [ESI+depl32].

Deplasamentul de 32 de biţi urmează imediat după octeţii de cod de operaţie şi


ocupă patru octeţi.
Observaţi că asamblorul a interpretat ultimele două instrucţiuni în acelaşi
mod. De fapt, prima va produce o eroare de tip Segmentation Fault,
programatorul a specificat o adresă efectivă inexistentă sau la care nu are drept de
acces.

Adresare indexată şi scalată

Instrucţiune: MOV ECX,[EBX+EAX*4]


Codificare: 8B 0C 83
Binar: 1000 1011 0000 1100 1000 0011
• Opcode = 100010, identifică o instrucţiune MOV.
• d = 1, câmpul REG este destinaţie.
• w = 1, operanzi de 32 de biţi.
• REG = 001, registrul ECX.
• R/M = 100 împreună cu Mod = 00, indică faptul că următorul octet este
SIB.
o SS = 10, registrul index se înmulţeşte cu 4.
o Index = 000, registrul index este EAX.
o Bază = 011, registrul de bază este EBX.

Prefix de dimensiune operand

Ambele instrucţiuni, INC ECX şi INC CX, sunt codificate conform


aceluiaşi criteriu, INC eCX. Programele de 32 de biţi nu folosesc foarte des
operanzi de 16 biţi, dar când folosesc, procesorul trebuie înştiinţat. Acesta este
rolul prefixului de dimensiune operand, 66H. Programatorul nu trebuie să specifice
explicit acest lucru, imediat cum găseşte în instrucţiune un operand de 16 biţi
asamblorul adaugă automat acest prefix. Totuşi, reţineţi că oricând folosiţi operanzi
de 16 biţi în programe de 32, dimensiunea instrucţiunii creşte cu un octet. Totodată,
din cauza efectului acestora asupra mecanismului cache, execuţia este mai lentă.

Codificări alternative pentru instrucţiuni

Pentru unele instrucţiuni utilizate frecvent, cu scopul de a scurta codul


programului, Intel a creat codificări alternative (mai scurte). De exemplu, setul de
instrucţiuni Intel x86 oferă opcode de un singur octet pentru instrucţiuni de genul:

add al,constantă
add eax,constantă

Codurile de operaţie sunt 04H şi 05H. De asemenea, aceste instrucţiuni sunt cu un


octet mai mici decât instrucţiunile standard ADD imediat.
Chiar şi instrucţiunea ADD ax,constantă, care necesită un prefix de
dimensiune, este mai mică decât versiunea standard a lui ADD imediat. Un
compilator sau asamblor alege automat instrucţiunea cea mai scurtă.
Intel oferă alternative de codificare numai pentru registrele acumulator:
AL, AX, EAX. Aşadar, suntem încurajaţi să utilizăm aceste registre oricând putem.
7. OPERAŢII CU NUMERE ÎNTREGI

Numerele întregi sunt numerele fără parte fracţionară. Am făcut cunoştinţă


cu ele încă din primul capitol al acestei cărţi, sau, mai degrabă, având în vedere
că am lucrat numai cu întregi fără semn, cu un tip al acestora. Numerele întregi se
împart în două categorii: întregi cu semn şi întregi fără semn. Acest capitol se
deschide cu o discuție referitoare la reprezentarea întregilor cu semn.Un rol foarte
important în operațiile cu numere întregi sunt indicatorii de stare. De aceea, tot
aici vom discuta pe larg modul în care indicatorii de stare afectează operațiile
aritmetice și logice. Începem cu prezentarea instrucțiunilor care permit operații
aritmetice și logice cu numere întregi. Înspre final abordăm operații mai
complexe: execuție condiționată, procesarea șirurilor și lucrul cu stiva.

7.1. Reprezentarea numerelor cu semn

Problema numerelor cu semn constă în reprezentarea semnului. Pe hârtie,


semnul unui număr, de exemplu -15, poate fi reprezentat printr-un simbol distinct:
–1111. Deoarece calculatorul reprezintă informaţia numai ca şiruri de 0 şi 1, cel
mai uşor pentru programatori a fost să considere bitul cel mai semnificativ ca fiind
bit de semn: 0 - număr pozitiv, 1 - număr negativ. Acest mecanism poartă numele
de reprezentare cu bit de semn şi magnitudine. Magnitudinea este formată din
ceilalţi biţi ai reprezentării şi furnizează valoarea propriu zisă a numărului. Însă,
așa cum vom vedea în paragrafele următoare, acest mod de reprezentare are
probleme de consistență. De aceea, în prezent, există trei tehnici generale de
reprezentare a întregilor cu semn:
• magnitudine cu semn;
• complement faţă de unu;
• complement faţă de doi.

Pentru toate, bitul cel mai semnificativ al unei reprezentări poate fi


considerat bit de semn. Dar atenţie la nuanţă, am spus poate fi considerat;
numerele negative sunt reprezentare diferit în cele trei metode de reprezentare.

Pentru că este foarte important, precizăm de la început că arhitectura IA-


32 foloseşte metoda complementului faţă de doi.
7.1.1. Reprezentarea cu bit de semn şi magnitudine

În această reprezentare, numită şi reprezentare în magnitudine cu semn, cel


mai semnificativ bit are rol de semn (1 pentru numere negative, 0 pentru numere
pozitive), iar biţii rămaşi dau valoarea. Astfel, pentru numerele cu semn
reprezentate pe un octet, bitul 7 reprezintă semnul, ceilalți valoarea.

semn magnitudine
7 0
Figura 7.1 Reprezentarea cu bit de semn şi magnitudine
Cel mai mic număr este 1 1111111, adica – 127, cel mai mare număr este 0 111
1111, adica +127. De unde rezultă că domeniul de reprezentare al numerelor cu
semn pe un octet este -127 ..+127.
Se procedează similar pentru numerele reprezentate pe doi octeţi, pe patru
octeţi, etc.. Totuşi, la o privire mai atentă, descoperim lucruri nu tocmai plăcute.
De exemplu, valoarea zero are două reprezentări distincte: 10000000 (-0) şi
00000000 (+0). Lucru care complică unele operații matematice. Mai mult,
operaţiile aritmetice care folosesc reprezentarea cu bit de semn şi magnitudine sunt
complexe. Daca adunăm +1 cu –1, rezultatul este -2; rezultat fals (principiul de
adunare al numerelor binare este similar cu cel al numerelor zecimale: se adună
cifră cu cifră și se ține cont de transport):

0000 0001 +
1000 0001
-------------
1000 0010 = -2

Enumerăm dezavantajele reprezentării în magnitudine şi bit de semn:


• două modalităţi diferite de a reprezenta valoarea zero;
• operaţii aritmetice complexe;
• numerele cu semn necesită instrucţiuni aritmetice diferite faţă de cele ale
numerelor fără semn.

7.1.2. Reprezentarea în complement faţă de unu

Numărul negativ este reprezentat ca fiind complementul faţă de unu al


numărului pozitiv. Complementul faţă de unu înseamnă că se inversează numărul
pozitiv bit cu bit (1 devine 0 şi 0 devine 1). Complementul faţă de unu al numărului
0000 0001 este 1111 1110, al numărului 0101 1100 este 1010 0011, ş.a.m.d.
În această reprezentare, numărul -127 ar fi complementul faţă de unu al
numărului 127. Cum 127 în binar este 0111 1111, complementul lui faţă de unu, şi
implicit numărul -127, va fi 1000 0000. Numărul -1 este complementul faţă de unu
al şirului binar 0000 0001, adică 1111 1110.
Dar şi de această dată avem două reprezentări diferite pentru valoarea 0:
0000 0000 (+0) şi 1111 1111 (-0), cu efecte nedorite în efectuarea unor operaţii
matematice. În plus, aritmetica în complement faţă de unu este la fel de complicată.

7.1.3. Reprezentarea în complement faţă de doi

Am specificat de la început că arhitectura Intel reprezintă numerele întregi


cu semn prin metoda complementului faţă de doi. Această metodă rezolvă
problemele aritmetice ale reprezentărilor cu bit de semn şi magnitudine sau
complement faţă de unu printr-un simplu artificiu matematic: reprezentarea
negativă a unui număr se obține prin adunarea lui 1 la complementul faţă de unu al
reprezentării pozitive.
Asfel, -1 este reprezentarea în complement faţă de unu a lui 0000 0001,
plus 1:
1111 1110 +
1
----------
1111 1111

Aşadar, -1 este reprezentat ca fiind şirul de biţi: 1111 1111.


-2, este complementul faţă de unu al șirului binar 0000 0010, plus 1:

1111 1101 +
1
----------
1111 1110

Pe baza aceluiași principiu obținem:

Întreg cu semn Reprezentare


-3 1111 1101
...
- 127 1000 0001
- 128 1000 0000
...
+127 0111 1111
+126 0111 1110
...
2 0000 0010
1 0000 0001
0 0000 0000

Acest mecanism rezolvă toate problemele care afectează modurile de


reprezentare discutate anterior. De exemplu,

-1 + 1 = 0 1111 1111 +
0000 0001
------------
CF = 1 0000 0000

Carry Flag (CF) este ignorat în aritmetica numerelor cu semn.

În concluzie:
• avem o singură reprezentare pentru valoarea 0,
• adunarea se realizează uşor,
• adunarea şi scăderea numerelor, cu sau fără semn, folosește aceleaşi
circuite hardware (aceleaşi ciruite logice).

Domeniul de reprezentare al numerelor cu semn este dat în Tabelul 7.1.


Tabelul 7.1 Domeniul de reprezentare al întregilor cu semn
Biți de reprezentare Plajă de valori
8 -128 ..+127
16 -32 768 ..+32 767
32 -2 147 483 648 ..+ 2 147 483 647

Nu trebuie să confundați numerele fără semn cu numerele pozitive cu


semn. De exemplu, domeniul de reprezentare pentru numere fără semn, de 32 de
biți, este 0 .. 4 294 967 295, iar domeniul numerelor pozitive cu semn de 32 de biți
este 0 ..+ 2 147 483 647. Reprezentarea întregilor cu semn, ca și cea a întregilor
fără semn, are natură circulară. Pentru un anumit număr de biți, există o graniță la
care reprezentarea numerelor negative se continuă cu cea a numerelor pozitive, sau
invers. De asemenea, observăm că toate valorile negative încep cu bit de 1 şi toate
valorile pozitive cu bit de 0. În consecință, avem posibilitatea să deducem imediat
dacă un număr este negativ sau pozitiv. Din această perspectivă, putem considera
bitul cel mai semnificativ ca fiind bit de semn. Rețineți totuși că acest bit nu este
bit de semn propriu-zis (în sensul reprezentării cu bit semn și magnitudine), nu
semnifică semnul exclusiv, ci intră în procesul de calcul al valorii. Altfel spus,
indică semnul, nu îl reprezintă.
Următorul program ne permite să studiem reprezentarea binară a întregilor
cu și fără semn.
;
;intSemn.asm
;
section .data
b1 db -127 ;10000001
b2 db 127 ;01111111
b3 db -1 ;11111111
w1 dw -32768 ;10000000 00000000
w2 dw 32767 ;01111111 11111111
w3 dw -1 ;11111111 11111111
d1 dd -1 ;11111111 11111111 11111111 11111111
d2 dd -45 ;11111111 11111111 11111111 11010011
section .text
global _start
_start:
nop
mov al,[b1]
mov bl,[b2]
mov cl,[b3]
mov ax,[w1]
mov bx,[w2]
mov cx,[w3]
mov eax,[d1]
mov ebx,[d2]

mov eax,1
mov ebx,0
int 080h

Rulaţi programul prin intermediul depanatorului GDB. Identificaţi fiecare


valoare prezentă în memorie.

(gdb) x /1bd &b1


0x80490c0 <b1>: -127
(gdb) x /1bt &b1
0x80490c0 <b1>: 10000001
(gdb) x /1hd &w1
0x80490c3 <w1>: -32768
(gdb) x /1ht &w1
0x80490c3 <w1>: 1000000000000000

Executaţi prima instrucţiune și afișați registrele AL și EAX prin comenzile


info reg al și info reg eax.

(gdb) i r al
al 0x81 -127
(gdb) i r eax
eax 0x81 129

Verificați reprezentarea binară:

(gdb) p /t $al
$2 = 10000001
(gdb) p /t $eax
$3 = 10000001

Reprezentarea în hexazecimal a şirului 1000 0001 este în ambele cazuri 0x81, dar
valoarea zecimală diferă. Depanatorul interpretează aceeași reprezentare binară ca
fiind -127 în registrul AL și 129 în registrul EAX. Nu este nicio eroare. Aceeaşi
reprezentare binară este interpretată diferit. De fapt, orice reprezentare binară a
întregilor poate fi interpretată în două moduri, cu semn şi fără semn. Când judecă o
valoare, depanatorul se ghidează după dimensiunea reprezentării și valoarea
“bitului de semn”. Am specificat anterior că bitul cel mai semnificativ indică
semnul. În acest caz, instrucţiunea

mov al, [b1]

introduce în registrul AL valoarea binară 1000 0001, interpretat în zecimal


ca fiind –127, deoarece bitul cel mai semnificativ al octetului este 1. Depanatorul
judecă la fel de corect și valoarea prezentă în registrul EAX. Următoarea
reprezentare a registrului EAX ne ajută să înțelegem mai ușor fenomenul cu care
ne confruntăm. Din cei patru octeţi prezenți în EAX, AL reprezintă octetul mai
puţin semnificativ.

0000000000000000 00000000 10000001


31 AH AL 0

Deşi depanatorul, la judecarea valorii din EAX, nu arată biţii de 0 din faţa
registrului AL, aceştia sunt luaţi în considerare. Dacă judecăm din această
perspectivă valoarea zecimală existentă în EAX, obținem chiar 129 (MSB = 0).
Ca regulă generală, valorile registrelor, atunci când sunt afişate în
zecimal, sunt judecate ca fiind cu semn.

Situația discutată semnalizează un lucru extrem de util: are foarte mare


importanţă ceea ce se află anterior în registre. Dacă introducem o valoare în AL,
nu trebuie să considerăm că registrul EAX are acea valoare.

7.2. Extinderea întregilor

În secțiunea precedentă am văzut cum sunt judecate valorile din registre.


Câteodată avem nevoie să exindem valoarea unui întreg (de la octet la cuvânt sau
de la cuvânt la dublu cuvânt).

Extinderea întregilor fără semn

Dacă rulăm programul de mai jos


;
;faraSemn.asm
;
section .data
val db 127
section .text
global _start
_start:
nop
mov eax,0ffffaaaah
mov al,[val]

;movzx eax, al

mov eax,1
mov ebx,0
int 080h

observăm că valoarea din registrul EAX este eronată (presupunând că


dorim să avem în EAX valoarea 127). Pentru a obţine un rezultat corect, ar fi
trebuit în prealabil să iniţializăm registrul EAX cu zero. Însă Intel a pus la
dispoziţie o instrucţiune capabilă să extindă un întreg fără semn, completând cu
zero ceilalţi octeţi. Sintaxa instrucţiunii este
movzx destinaţie,sursă

unde sursa poate fi registru sau locaţie de memorie de 8 sau 16 biţi, iar
destinaţia registru de 16 sau 32 de biţi. Activați instrucţiunea MOVZX din
program şi studiaţi efectul acesteia.

Extinderea întregilor cu semn

;
;cuSemn.asm
;
section .data
val db -127
section .text
global _start
_start:
nop
mov al,[val]
movsx eax,al

mov eax,1
mov ebx,0
int 080h

În programul de mai sus, rezultatul este cel aşteptat numai după folosirea
instrucţiunii
movsx destinaţie,sursă

La fel ca în cazul instrucțiunii MOVZX, sursa poate fi registru sau locaţie


de memorie de 8 sau 16 biţi, iar destinaţia registru de 16 sau 32 de biţi.
Extinderea întregilor cu semn este diferită de extinderea întregilor fără semn. În
cazul numerelor negative, MOVSX completează biţii de semn cu 1, nu cu 0. Biţii de
zero ar schimba valoarea numerelor negative. De exemplu, octetul -1 (11111111)
încărcat într-o locaţie de tip cuvânt unde octetul superior este completat cu biţi de
0, dă 0000000011111111, care în notaţia cu semn este +127, nu -1. Pentru a se
păstra valoarea unui întreg cu semn, biţi introduși trebuie să fie de acelaşi tip cu
bitul de semn. În acest caz, se obține valoarea 11111111111111. În notaţia cu
semn, un şir de biţi de 1 reprezentă -1.
În cazul unui număr pozitiv, extinderea se face cu biţi de 0. Modificați
valoarea lui val în 127.
7.3. Indicatori de stare

Indicatorii de stare au fost menţionați fugitiv în secțiunea dedicată


arhitecturii IA-32, când am discutat rolul registrul EFLAGS. Am afirmat la
momentul respectiv că fiecare bit din registrul EFLAGS are rol de sine stătător şi
se numeşte indicator de stare. Un indicator de stare reprezintă un singur bit de
informaţie, a cărui semnificaţie este independentă de oricare alt bit. Poziţionat
iniţial în 0, la apariţia unui eveniment specific comută în 1, semnalizând astfel o
anumită condiție prezentă la nivelul procesorului. Un program poate testa condiția
respectivă și acţiona în consecință. Totodată, prin modificarea unui indicator de
stare, programatorul poate seta un anumit comportament al procesorului.
Șase din acești indicatori de stare sunt folosiți exclusiv pentru
monitorizarea condițiilor rezultate în urma operațiilor aritmetice și logice, sau
înrudite. Aceștia sunt:
• Indicatorul de zero (ZF – Zero Flag);
• Indicatorul de transport (CF – Carry Flag);
• Indicatorul de depășire (OF – Overflow Flag);
• Indicatorul de semn (SF – Sign Flag);
• Indicatorul de transport la jumătate (AF – Auxiliary Flag);
• Indicatorul de paritate (PF –Parity Flag).

7.3.1. Indicatorul de zero

Rolul indicatorului de zero este să indice dacă rezultatul ultimei operaţii


aritmetice sau logice este nul. Dacă rezultatul este zero, indicatorul ZF se
poziționează automat în 1 (la prima vedere acest lucru este confuz, dar amintiți-vă
că indicatorii de stare sunt inițial poziționați în 0 și comută în 1 numai la apariția
unui eveniment). Rețineți că nu conținutul unui registru modifică indicatorul ZF, ci
numai rezultatul unei operații aritmetice sau logice. De exemplu, o instrucțiune
care încarcă valoarea zero în acumulator

mov eax,0

nu trece ZF în 1, pentru că operația de transfer nu este aritmetică sau logică. În


schimb, operația de scădere afectează acest indicator. Posibilitatea de apariție a
unui rezultat nul în urma unei operații de scădere este destul de evidentă, lucru care
nu se poate spune în cazul altor operații. De exemplu, o adunare pe 8 biți sau
incrementarea/decrementarea unor valori învecinate cu zero.
0000 1111 + 1111 1111 + 0000 0001 -
1111 0001 0000 0001 0000 0001
0000 0000 0000 0000 0000 0000

Indicatorul de zero este folosit în principal în cazuri de testare a egalității unor


valori și numărare până la o valoarea prestabilită. Din acest motiv, este folosit
extensiv de instrucţiunile de salt condiţionat.

7.3.2. Indicatorul de transport

Indicatorul de transport este utilizat în operaţiile aritmetice fără semn.

Indicatorul de transport semnalizează faptul că rezultatul unei operații


aritmetice între numere fără semn a depășit intervalul (prea mare sau prea mic)
corespunzător capacităţii destinației (registru sau locație de memorie). De exemplu,
atunci când se execută o adunare între două numere fără semn, este posibil să
rezulte transport spre rangul superior, care depăşeşte dimensiunea registrului ce
conţine rezultatul. În mod asemănător, la scăderea unor numere fără semn, poate
apărea necesitatea unui împrumut de la rangul superior. În aceste cazuri,
indicatorul CF se poziționează în 1.

0100 1000 + 1000 1000 +


0000 0011 1000 0011
0100 1011 1 0000 1011

adunare fără transport(CF=0) adunare cu transport(CF=1)

Indicatorul de transport este setat atunci când se depășește capacitatea de


reprezentare a destinației în condițiile unei operații cu întregi fără semn.

Capacitate (biți) Domeniu de reprezentare


8 0 .. 255
16 0 .. 65 535
32 0 .. 4 294 967 295

Orice operație care generează un rezultat ce depăşeşte limitele acestor domenii de


reprezentare este semnalizată cu CF = 1. Este evident că orice rezultat negativ se
găsește în afara domeniului de reprezentare. De exemplu, BE – BF = FF și CF = 1:
1 1011 1110 -
1011 1111
1111 1111

Instrucțiunile aritmetice pot opera cu date de 8, 16 sau 32 de biți. Dacă sunt adunați
operanzi mai mari de 32 de biți, se însumează pe rând două numere de 32 de biți.
Următorul exemplu ilustrează cum putem aduna pe arhitecturi de 32 de biți două
numere întregi fără semn de 64 de biți (folosim reprezentarea hexazecimală):

1 ← transport din prima adunare


2610 15E8 1357 9AE7 +
59AC B341 FE70 5324
7FBC C92A 11C7 EE0B

Efectuăm două operații de adunare. Întâi adunăm primii 32 de biți mai puțin
semnificativi ai operanzilor. Se obține jumătatea inferioară a rezultatului. Totodată,
această operație de adunare poate produce transport, lucru care setează indicatorul
de transport. A doua operație însumează următorii 32 de biți ai operanzilor
împreună cu indicatorul de transport generat de prima adunare. Acestă operație
produce jumătatea superioară a rezultatului de 64 de biți.
În mod similar, adunarea a două numere de 128 de biți implică un proces în
patru etape, în fiecare se adună cuvinte de 32 de biți.

7.3.3. Indicatorul de depăşire

Indicatorul de depășire este utilizat în operațiile aritmetice cu semn.

Indicatorul de depășire este echivalentul indicatorului de transport pentru


aritmetica numerelor cu semn. Rolul principal al indicatorului de depășire este să
indice dacă rezultatul unei operații cu numere cu semn a depășit domeniul de
reprezentare. Ne amintim că domeniul de reprezentare al numerelor cu semn pe 8,
16 și 32 de biți este:

Capacitate (biți) Domeniu de reprezentare


8 -128 ..+127
16 -32 768 ..+32 767
32 -2 147 483 648 ..+2 147 483 647

Când se execută operaţii aritmetice între operanzi cu semn, este posibil să


apară bit de transport către bitul de semn, ceea ce face ca rezultatul să fie eronat.
Indicatorul de depăşire semnalizează generarea unui transport de la bitul 6 către 7,
14 către 15, sau 30 către 31, în funcție de numărul de biți ai reprezentării. Altfel
spus, OF semnalizează valorile interzise ale rezultatului în cazul operaţiilor în
complement faţă de doi. De exemplu, adunăm +127 cu +127.

0111 1111 +
0111 1111
1111 1110

Numărul +127 este reprezentat pe un octet cu semn ca 0111 1111. MSB (Most
Significant Bit) este 0. Când adunăm obţinem rezultatul 1111 1110, rezultat eronat
în logica aritmeticii cu semn. MSB este 1 şi nu 0, aşadar 1111 1110 va fi interpretat
ca număr negativ, adică -2. În astfel de situaţii, indicatorul de depășire se
poziționează în 1.

7.3.4. Indicatorul de semn

SF indică semnul rezultatului unei operații. Așadar, este util numai când
efectuăm operaţii aritmetice între numere cu semn. Dacă rezultatul ultimei operaţii
este negativ, SF devine 1. Indicatorul de semn este copia valorii bitului de semn al
rezultatului unei operații aritmetice.
Pe lângă principala funcție a indicatorului de semn, care constă în testarea
semnului pentru rezultatul generat de o operație aritmetică, acesta se mai folosește
la implementarea buclelor de numărare, unde iterațiile sunt efectuate până când
variabila de control este zero.
Din punctul de vedere al utilizatorului, semnul unui număr poate fi testat
printr-o instrucțiune de deplasare logică. Comparativ cu ceilalți trei indicatori de
stare prezentați până acum, indicatorul de semn este utilizat relativ rar în programe.
Totuși, procesorul utilizează bitul de semn în cazul execuției instrucțiunilor de salt
condiționat.

7.3.5. Indicatorul de transport la jumătate

Indicatorul de transport la jumătate basculează în 1 dacă în cadrul unei


operaţii aritmetice există transport de la bitul trei la bitul patru (sau împrumut de la
bitul patru la bitul 3. De exemplu:

1 ← transport 1 → împrumut
0000 1000 + 0010 1011 -
1000 1000 0101 1100
1001 0000 1100 1111
7.3.6. Indicatorul de paritate

Indicatorul de paritate se foloseşte de obicei la controlul de paritate,


metodă elementară de detectare a erorilor ce pot apărea la transferul datelor pe
magistrale sau în comunicaţiile seriale, etc.. Indicatorul PF se poziționează în 1
atunci când numărul biţilor de 1 dintr-un cuvânt este par. De exemplu, dacă
rezultatul unei operaţii aritmetice este 0E3H, deoarece 1110 0011 conţine un număr
impar de biţi de 1, PF devine 0. Similar, rezultatul 33H va seta indicatorul de
paritate în 1, deoarece 0011 0011 conţine un număr par (patru) de biţi de 1.

Trebuie să reţinem că descrierile anterioare sunt numai generalizări şi


sunt influenţate de modul de lucru impus de instrucţiunile individuale. Setarea
indicatorilor de stare variază de la instrucţiune la instrucţiune. De exemplu, sunt
instrucţiuni aritmetice care pot produce o valoare de zero pentru rezultatul final,
dar care nu setează indicatorul de zero. Comportamentul unei instrucţiuni cu
privire la influențarea indicatorilor de stare trebuie verificat prin studierea
manualului de instrucțiuni.

Două exemple elocvente sunt instrucţiunile INC şi DEC. Acestea incrementează,


respectiv decrementează, un operand. Ambele folosesc un singur operand, care
poate fi registru sau locaţie de memorie.

;
;incDec.asm
;
section .text
global _start
_start:
nop
mov eax, 0fffffffh
mov ebx, 0
inc eax
dec ebx

mov eax,1
mov ebx,0
int 80h

Studiaţi efectele instrucțiunilor de incrementare şi decrementare. Comanda


info reg eflags afișează registrul indicatorilor de stare.
Incrementarea registrului EAX transformă valoarea acestuia în 0. În mod
normal, ar fi trebuit ca indicatorul de transport să fie poziționat în 1, dar nu este.
Indicatorul de transport nu este afectat de instrucţiunea INC.
Instrucțiunile INC și DEC nu influențează indicatorul de transport.
Motivele sunt două:
• Instrucțiunile INC și DEC sunt folosite în special la contorizarea
iterațiilor unor bucle. Cu 32 de biți, numărul de iterații maxim este 4
294 967 295. Acest număr este destul de mare pentru majoritatea
aplicațiilor. În plus, dacă acesta este depășit,
• deoarece INC și DEC modifică valoarea numai cu valori de 1, condiția
detectată de indicatorul de transport este detectată și de indicatorul de
zero. De exemplu, presupunem că registrul ECX ajunge la valoare sa
maximă 4 294 967 295 (FFFFFFFFH). Dacă executăm INC ECX ne
așteptăm ca indicatorul de transport să fie setat în 1. Totuși, detectăm
această condiție și prin faptul că ECX = 0, eveniment care setează
indicatorul de zero. În consecință, pentru aceste instrucțiuni,
modificarea indicatorului de transport este redundantă.

7.4. Instrucţiuni de transfer condiţionat

Instrucţiunile de transfer condiţionat au apărut începând cu familia de


procesoare P6 (Pentium Pro, Pentium II, şi mai noi). O instrucţiune MOV
condiţională (CMOV – Conditional Move) este o instrucţiune MOV efectuată numai
dacă sunt satisfăcute anumite condiţii. Sintaxa generală este

CMOV<x> destinaţie,sursă

unde x reprezintă una sau două litere care specifică condiţia ce va declanşa
instrucţiunea MOV. Condiţiile sunt bazate pe valorile curente din registrul
EFLAGS. Biţii folosiţi de instrucţiunile MOV condiţionale sunt CF, OF, PF, SF, ZF.
Instrucţiunile condiţionale sunt împărţite în:
• instrucţiuni de transfer condiţionat pentru operaţii fără semn (la
determinarea diferenţei între doi operanzi sunt folosiţi indicatorii CF, ZF,
PF).
Tabelul 7.2 Instrucţiuni MOV condiţionale pentru operanzi fără semn
Instrucţiune Descriere Condiţie
CMOVA/CMOVNBE above/not below or equal (CF sau ZF) = 0
CMOVAE/CMOVNB above or equal/not below CF = 0
CMOVNC not carry CF = 0
CMOVB/CMOVNAE below/not above or equal CF = 1
CMOVC carry CF = 1
CMOVBE/CMOVNA below or equal/not above (CF sau ZF) = 1
CMOVE/CMOVZ equal/zero ZF = 1
CMOVNE/CMOVNZ not equal/not zero ZF = 0
CMOVP/CMOVPE parity/parity even PF = 1
CMOVNP/CMOVPO not parity/parity odd PF = 0

• Instrucţiuni de transfer condiţionat pentru operanzi cu semn (la


determinarea condiţiei folosesc indicatorii SF şi OF).

Tabelul 7.3 Instrucţiuni MOV condiţionale pentru operanzi cu semn


Instrucţiune Descriere Condiţie
CMOVGE/CMOVNL greater or equal/not less (SF xor OF) = 0
CMOVL/CMOVNGE less/not greater or equal (SF xor OF) = 1
CMOVLE/CMOVNG less or equal/not greater ((SF xor OF) or ZF) = 1
CMOVO overflow OF = 1
CMOVNO not overflow OF = 0
CMOVS sign (negative) SF = 1
CMOVNS not sign SF = 0

Instrucţiunile de transfer condiţionat se găsesc în perechi, deoarece o


valoare val1 poate fi mai mare decât o valoarea val2 (CMOVA), dar la fel de bine
val1 nu este mai mică sau egală cu val2 (CMOVNBE).
;
;transfCond.asm
;
section .data
val dd 105,106,107,100,110,103
section .text
global _start
_start:
nop
mov ebx,[val]
mov edi,1
mov eax,[val+edi*4]
cmova ebx,eax ;dacă EAX > EBX, atunci EBX = EAX = 106
inc edi
mov ecx,[val+edi*4]
cmovnbe ebx,ecx ;dacă ECX ! ≤ EBX, atunci EBX = ECX = 107
mov eax,1
mov ebx,0
int 080h

7.5. Operaţii aritmetice

Instrucţiunile aritmetice realizează operaţiile aritmetice elementare


(adunare, scădere, înmulţire, împărţire). Fiecare instrucţiune afectează o parte din
indicatorii de stare SF, ZF, AF, CF, OF, numiţi din acest motiv şi indicatori
aritmetici.

7.5.1. Instrucţiuni de adunare

Operația de adunare a întregilor se efectuează cu instrucţiunea ADD.

add destinaţie,sursă

unde sursa poate fi un imediat, un registru sau locaţie de memorie de 8,


16, 32 de biţi, iar destinaţia registru sau locaţie de memorie de 8, 16 sau 32 de
biţi. Rezultatul adunării se păstrează în destinaţie. Indicatorii de stare sunt setaţi în
concordanţă cu rezultatul operaţiei.
Observaţii:
• operanzii nu pot fi simultan locaţii de memorie;
• operanzii trebuie să aibă aceeaşi dimensiune.
;
;adunare1.asm
;
section .text
global _start
_start:
nop
xor ax,ax
mov al,100
add al,[val]
movsx ecx,al ;numărul din AL este considerat cu semn
mov bx,50
add bx,45
add bx,-1
movsx ebx,bx ;instrucțiune redundantă (explicați de ce)
add ebx,10

mov eax,1
mov ebx,0
int 080h

section .data
val db 132

Indiferent de semnul întregilor, instrucţiunea ADD execută adunarea corect,


ceea ce înseamnă că poate fi folosită atât pentru întregi cu semn cât şi pentru
întregi fără semn.
Dacă rulăm programul de mai jos:
;
;adunare2.asm
;
section .text
global _start
_start:
nop
xor eax,eax
xor ebx,ebx
mov al,254
mov bl,1
add bl,al

mov eax,1
mov ebx,0
int 080h

în registrul BL vom avea rezultatul 255. Încărcăm în BL valoarea 2 şi


reasamblăm. Când rulăm pas cu pas suntem foarte atenţi la registrul indicatorilor
de stare. Înaintea instrucţiunii de adunare ADD BL,AL, singurul indicator setat
este IF. În urma adunării, rezultatul din BL este 0, iar registrul indicatorilor de stare
are setat CF. Indicatorul de transport semnalizează că a fost depăşit domeniul de
reprezentare al numerelor fără semn pe un octet. După cum ştim, valoarea maximă
a unui număr fără semn pe un octet este 255. Rezultatul adunării 254 + 2 este 256.
Indicatorul de transport a semnalat depăşirea capacităţii registrului care trebuia să
conţină rezultatul, adică apariţia unui transport spre rangul superior.

Când lucrăm cu întregi fără semn, CF semnalizează faptul că rezultatul


adunării a depăşit limita domeniului de reprezentare. Dacă nu suntem siguri că
adunarea operanzilor se încadrează în domeniu trebuie întotdeauna să verificăm
indicatorul de transport.

În cazul întregilor cu semn, indicatorul de transport este lipsit de


importanţă - acesta va fi setat ori de câte ori rezultatul este număr negativ (vezi
scăderea), indiferent dacă este valid sau nu. În schimb, atunci când folosim întregi
cu semn, trebuie să ţinem evidenţa indicatorului de depăşire (OF). Introduceți în
program următoarele instrucțiuni și observați efectul:

mov bl,2
mov al,127
add bl,al

În urma urma operațiri de adunare între AL = 127 și BL = 2, indicatorul de


depășire este setat, indicând depăşirea limitei superioare a domeniului de
reprezentare pentru numere cu semn: [-128 ..+127].

Adunarea numerelor mai mari de 32 de biți


Instrucțiunea ADD permite adunarea operanzilor de 8, 16 sau 32 de biți.
Presupunem că dorim să adunăm două numere de 64 de biţi fiecare:
401D0219D18D50E1H şi 4016EDECE09C1528H. Deoarece sunt reprezentate
pe 64 de biţi, nu le putem aduna direct (nu avem la dispoziţie registre de 64 de
biţi). De aceea, fiecare număr va fi reprezentat pe câte două registre de 32 de biţi.

EAX EBX
401d0219 d18d50e1

ECX EDX
4016edec e09c1528

Mecanismul a fost prezentat în secțiunea dedicată indicatorului de transport.


Presupune două etape. În prima etapă se adună cuvintele de 32 de biți din dreapta.
În a doua etapă se adună cuvintele de 32 de biți din stânga, plus eventualul
transport generat de adunarea anterioară. Pentru acest lucru, pe lângă instrucţiunea
ADD, Intel a pus la dispoziție și instrucţiunea ADC (Add with Carry).

adc destinaţie,sursă

unde destinaţia şi sursa respectă aceleași reguli menționate la ADD. Numai


că, spre deosebire de instrucţiunea ADD, ADC adună la suma celor doi operanzi şi
valoarea lui CF (0 sau 1).
Așadar, vom aduna registrele EBX şi EDX, apoi registrele EAX şi ECX,
rezultatul final regăsindu-se în EAX:EBX. Începem prin adunarea registrelor EBX,
EDX folosind ADD. Deoarece este posibil ca suma celor două valori să depăşească
domeniul de reprezentare – adică să fie setat CF, următoarele două registre: EAX şi
ECX, vor fi adunate cu ADC.
;
;add64bit.asm
;
section .data
alfa dq 0401d0219d18d50e1H
beta dq 04016edece09c1528H
section .bss
rez resd 2
section .text
global _start
_start:
nop
mov ebx,[alfa]
mov eax,[alfa+4]
mov edx,[beta]
mov ecx,[beta+4]

add ebx,edx
adc eax,ecx

mov dword [rez],ebx


mov dword [rez+4],eax

mov eax,1
mov ebx,0
int 080h

Se rulează urmârindu-se evoluţia CF după prima adunare.

Adunarea unor valori de dimensiune diferită


Când adunăm două valori de dimensiuni diferite trebuie să fim atenţi la
conversia acestora.
;
;adunare3.asm
;
section .data
b db 100
w dw 300
d dd 65800
section .text
global _start
_start:
nop
mov al,[b]
mov bx,[w]
movsx ax,al ;convertim octet la cuvânt
add bx,ax

mov ax,[w]
mov ecx,[d]
movsx eax,ax ;convertim cuvânt la dublu cuvânt
add ecx,eax

mov eax,1
mov ebx,0
int 080h

Programul folosește instrucţiunea MOVSX. Alte instrucţiuni de extindere


sunt:
• CBW (Convert Byte to Word) – extinde AL la AX; fiecare bit din AH ia
valoarea celui mai semnificativ bit (bitul de semn) din AL.
• CWD (Convert Word to Double word) – extinde AX la DX:AX; fiecare bit
din DX ia valoarea MSB-ului din AX.
• CWDE (Convert Word to Double EAX) – extinde AX la EAX
• CDQ (Convert Double word to Quad) – extinde EAX la EDX:EAX; fiecare
bit din EDX ia valoarea MSB-ului din EAX.

Another thing I regret is that some of my well-chosen instruction mnemonics were


renamed when the instruction set was published. I still think it's catchier to call the
instruction SIGN-EXTEND, having the mnemonic of SEX, than to call it
CONVERT-BYTE-TO-WORD with the boring mnemonic CBW.

Stephen Morse

Toate aceste instrucţiuni extind o valoare mai mică la una mai mare prin
replicarea ”bitului de semn” al valorii originare.
;
;intExt.asm
;
section .data
b1 db 100
b2 db -100
w dw 300
d1 dd 65800
d2 dd -354059
section .text
global _start
_start:
nop
mov al,[b2]
cbw ;AH = FFh - bitul de semn a lui AL

mov al,[b1]
cbw ;AH = 00H - bitul de semn a lui AL
mov bx,[w]
add ax,bx

mov ax,[w]
cwde
mov ecx,[d1]
add eax,ecx

mov eax,[d1]
cdq ;EDX = 0000H - bitul de semn a lui EAX

mov eax,[d2]
cdq ;EDX = FFFFh - bitul de semn a lui EAX

mov eax,1
mov ebx,0
int 080h

7.5.2. Instrucțiuni de scădere

Instrucţiunea SUB scade sursa din destinaţie, rezultatul fiind stocat în


destinaţie:
sub destinaţie,sursă

unde sursa poate fi un imediat, un registru sau locaţie de memorie de 8,


16, 32 de biţi, iar destinaţia registru sau locaţie de memorie de 8, 16 sau 32 de
biţi. Indicatorii de stare sunt setaţi în concordanţă cu rezultatul operaţiei.
Observaţii:
• operanzii nu pot fi simultan locaţii de memorie;
• operanzii trebuie să aibă aceeaşi dimensiune.
;
;scadere1.asm
;
section .text
global _start
_start:
nop
xor ax,ax
mov al,100
sub al,[val]
movsx ecx,al ;numărul din AL este considerat cu semn

mov bx,50
sub bx,45
sub bx,-1
movsx ebx,bx ;instrucțiune redundantă (explicați de ce)

sub ebx,10

mov eax,1
mov ebx,0
int 080h

section .data
val db 90

Instrucţiunea SUB execută corect scăderea indiferent de semnul întregilor,


ceea ce înseamnă că poate fi folosită atât pentru întregi cu semn cât şi pentru
întregi fără semn.
Dacă rulăm programul următor
;
;scadere2.asm
;
section .text
global _start
_start:
nop
xor eax,eax
xor ebx,ebx
mov al,4
mov bl,2
sub bl,al

mov eax,1
mov ebx,0
int 080h

în registrul BL vom avea rezultatul -2. Rulăm pas cu pas şi suntem foarte
atenţi la registrul indicatorilor de stare. Înaintea instrucţiunii de scădere SUB
BL,AL, singurul indicator setat este IF. După scădere, rezultatul din BL devine 0
iar registrul indicatorilor de stare are setat CF. Indicatorul de transport
semnalizează că a fost depăşit domeniul de reprezentare al numerelor fără semn pe
un octet. După cum ştim, valoarea minimă a unui număr fără semn pe un octet este
0. Rezultatul scăderii 2 – 4 este -2. Indicatorul de transport a semnalat depăşirea
limitei inferioare a domeniului de reprezentare a numerelor fără semn.

Când scădem două numere fără semn, CF semnalizează trecerea


rezultatului sub valoarea zero. În cazul numerelor considerate cu semn, acest lucru
se întâmplă foarte des şi CF este lipsit de importanţă. În schimb, trebuie să ţinem
cont de indicatorul OF.

Să judecăm următorul exemplu:


;
;scadere3.asm
;
section .text
global _start
_start:
nop
mov eax,7
mov ebx,3
sub ebx,eax

jc sfarsit

mov eax,1
int 080h

sfarsit:
mov eax,1
mov ebx,0
int 080h

Programul scade două numere fără semn: 3 şi 7 (ambele pozitive).


Deoarece rezultatul scăderii este un număr negativ (3 – 7 = - 4), instrucţiunea de
salt va fi executată. La sfârşitul programului vom avea în EBX valoarea 0 (în cazul
unui rezultat pozitiv, saltul nu s-ar fi executat).
Cănd scădem două numere fără semn, 3 şi 7, un rezultatul mai mic de zero
este considerat invalid (pentru că limita minimă de reprezentare a numerelor fără
semn este 0). Totuşi, în ciuda faptului că valorile se ”presupuneau” a fi fără semn şi
rezultatul invalid, procesorul trece rezultatul -4 în EBX. De ce? Deoarece
procesorul nu ştie ce fel de numere „gândim” noi, cu sau fără semn, el ia în calcul
ambele variante şi setează indicatorii de stare CF şi OF în concordanţă. Programul
trebuie să determine dacă valoarea este în afara domeniului de reprezentare al
întregilor cu/fără semn. În cazul scăderii unor întregi fără semn, CF indică faptul că
aceasta s-a soldat cu rezultat negativ. În ce priveşte întregii cu semn, din moment
ce rezultatul poate fi negativ și CF nu este semnificativ, trebuie să ne bazăm pe OF,
care semnalizează depăşirea domeniului de reprezentare a numerelor cu semn.
În concluzie, procesorul nu „ştie” dacă numerele sunt cu semn sau fără
semn. El setează CF sau OF luând în calcul ambele posibilităţi.
• CF – setat dacă a fost depăşit domeniul de reprezentare al
numerelor fără semn.
o [0 ..255] pentru 8 biţi
o [0 ..65535] pentru 16 biţi
o [0 ..4294967295] pentru 32 de biţi

În cazul unui octet putem reprezenta astfel:

CF = 1 fără semn CF = 1

0 255

La adunarea numerelor fără semn, CF = 1 când rezultatul depăşeşte


valoarea maximă a reprezentării folosite (255, 65535, etc.). La scăderea numerelor
fără semn, CF = 1 când rezultatul este sub zero.

• OF – setat dacă a fost depăşit domeniul de reprezentare al numerelor


cu semn.
o [-128 ..+127] pentru 8 biţi
o [-32768 ..+32767] pentru 16 biţi
o [-2147483648 ..+ 2147483647] pentru 32 de biţi

În cazul unui octet putem reprezenta astfel:

OF = 1 cu semn OF = 1

- 128 +127

OF este setat atunci când adunăm sau scădem doi întregi de acelaşi semn şi
se obţine un rezultat de semn diferit.

Scăderea numerelor mai mari de 32 de biți


Pentru a scădea două numere de 64 de biţi, împărţim numerele în două
părţi de 32 de biţi fiecare şi efectuăm operația de scădere pe rând. La a doua
operație se scade și valoarea lui CF. Instrucţiunea necesară se numește SBB
(Subtract with Borrow).
;
;sub64bit.asm
;
section .data
alfa dq 0401d0219d18d50e1h
beta dq 04016edece09c1528h
section .bss
rez resd 2
section .text
global _start
_start:
nop
mov ebx,[alfa]
mov eax,[alfa+4]
mov edx,[beta]
mov ecx,[beta+4]

sub ebx,edx
sbb eax,ecx

mov dword [rez],ebx


mov dword [rez+4],eax

mov eax,1
mov ebx,0
int 080h

O instrucţiune înrudită cu SUB este instrucţiunea NEG. Instrucţiunea NEG


calculează complementul faţă de doi al unei valori. Acelaşi rezultat se poate obţine
prin scăderea valorii respective din zero, cu instrucţiunea SUB. Instrucţiunea NEG
se execută însă mai rapid.

7.5.3. Instrucțiuni de comparare

Compararea a doi operanzi se efectuează cu instrucţiunea CMP (CoMPare).


CMP realizează aceeași operație ca instrucțiunea SUB, dar nu salvează rezultatul.
Instrucțiunea
cmp destinaţie,sursă

simulează scăderea destinaţie – sursă, fără a genera rezultat. În schimb,


sunt modificați indicatorii de stare. Instrucţiunea se foloseşte pentru testarea unei
condiţii:
• dacă destinaţie > sursă, rezultă ZF=0, CF=0;
• dacă destinaţie = sursă, rezultă ZF=1, CF=0;
• dacă destinaţie < sursă, rezultă ZF=0, CF=1.

7.5.4. Incrementare şi decrementare

Instrucţiunile INC și DEC au fost deja prezentate. Am văzut că sunt


folosite la incrementarea şi decrementarea unui întreg fără semn şi nu afectează
indicatorul de transport. În acest fel se poate incrementa sau decrementa valoarea
unui contor fără afectarea operaţiilor de adunare sau scădere din interiorul unei
bucle.
Formatul instrucţiunilor este:

inc destinaţie
dec destinaţie

unde destinaţia poate fi registru sau locaţie de memorie de 8, 16 sau 32 de


biţi.

Instrucţiunile INC şi DEC privesc valoarea din destinaţie ca fiind întreg


fără semn. Dacă decrementăm valoarea 0 reprezentată pe 32 de biţi, noua valoare
va fi FFFFFFFF judecată 4294967295 (indicatorul de transport nu va fi setat) şi
nu -1 (aşa cum ar fi judecat din perspectiva numerelor cu semn). Atenţie când
folosiţi aceste instrucţiuni asupra unor întregi consideraţi ca fiind cu semn.

7.5.5. Instrucţiuni de înmulţire

Înmulţirea numerelor fără semn se realizează cu instrucţiunea MUL.

mul sursă

unde sursa este registru sau locaţie de memorie de 8, 16 sau 32 de biţi.


Celălalt operand al înmulţirii şi rezultatul au locaţie implicită, după cum urmează:
• MUL r/m8, AL este multiplicat cu r/m8 și rezultatul este stocat în AX.
• MUL r/m16, AX este multiplicat cu r/m16 și rezultatul este stocat în
DX:AX.
• MUL r/m32, EAX este multiplicat cu r/m32 și rezultatul este stocat în
EDX:EAX.
Tabelul 7.4 Instrucţiunea de înmulţire MUL
Sursă Operand implicit Rezultat
8 biţi AL AX
16 biţi AX DX:AX
32 biţi EAX EDX:EAX

Înmulţirea numerelor cu semn se realizează cu instrucţiunea IMUL. Spre


deosebire de instrucţiunea anterioară, întâlnim trei tipuri de instrucţiune IMUL:

1. IMUL cu un singur operand. Are sintaxa identică cu instrucţiunea MUL.

imul sursă
Tabelul 7.5 Instrucţiunea de înmulţire IMUL cu un singur operand
Sursă Operand implicit Rezultat
8 biţi AL AX
16 biţi AX DX:AX
32 biţi EAX EDX:EAX

2. IMUL cu doi operanzi

imul destinaţie,sursă
cu următoarele cazuri:

imul r16,r/m16
imul r32,r/m32
imul r16,imm8
imul r16,imm16
imul r32,imm8
imul r32,imm32

Această variantă înmulţeşte cei doi operanzi şi stochează rezultatul în


operandul destinaţie.

3. IMUL cu trei operanzi

imul destinaţie,sursă,imediat

cu următoarele cazuri:

imul r16,r/m16,imm8
imul r16,r/m16,imm16
imul r32,r/m32,imm8
imul r32,r/m32,imm16

Această variantă înmulţeşte cei trei operanzi şi stochează rezultatul în


operandul destinaţie.

Observaţii:
• Instrucţiunea cu doi operanzi este o variantă prescurtată a instrucţiunii
cu trei operanzi.
• În cazurile 2 şi 3 destinaţia are acelaşi ordin de mărime ca sursa. În
cazul înmulţirii este foarte întâlnită situaţia în care rezultatul are număr
dublu de biţi faţă de operanzii înmulţiţi. De aceea, trebuie să fim atenţi
ca înmulţirea celor doi, respectiv trei, operanzi să nu depăşească
capacitatea destinaţiei (se verifică cu ajutorul lui CF şi OF).

7.5.6. Instrucţiuni de împărţire

La împărţire, deîmpărţitul va avea întotdeauna un număr dublu de biţi faţă


de împărţitor, iar în urma operaţiei vom obţine un cât şi un rest, fiecare de mărimea
împărţitorului.
Împărţirea numerelor fără semn se realizează cu instrucţiunea DIV.
div divizor
Tabelul 7.6 Instrucţiunea de împărţire DIV
Deîmpărţit Divizor Cât Rest
AX 8 biţi AL AH
DX:AX 16 biţi AX DX
EDX:EAX 32 biţi EAX EDX

Numerele cu semn se împart cu instrucțiunea IDIV, care respectă în totalitate


sintaxa DIV.

7.6. Instrucţiuni de interschimbare a datelor

Câteodată este necesar să schimbăm între ele valorile a două registre. Un


dezavantaj al instrucţiunilor MOV este chiar faptul că nu poate interschimba valorile
a două registre direct, fără a folosi un alt registru intermediar. De exemplu, pentru a
interschimba valorile registrelor EAX şi EBX trebuie să folosim ca intermediar un
alt registru, sau o locaţie de memorie.

mov ecx,eax
mov eax,ebx
mov ebx,ecx

Sunt necesare trei instrucţiuni, precum şi un registru liber; consum semnificativ de


resurse pentru această operaţie simplă. De aceea, setul de instrucţiuni cuprinde
câteva instrucţiuni care interschimbă date fără intermediar. Cea mai simplă este
XCHG.
Instrucţiunea XCHG poate interschimba date între două registre sau între un
registru şi o locaţie de memorie. Operanzii nu pot fi în acelaşi timp locaţii de
memorie. Formatul instrucţiunii este:

xchg operand1,operand2

Instrucţiunea primeşte operanzi de 8, 16 sau 32 de biţi, dar întotdeauna de aceeaşi


dimensiune. Cu ajutorul acestei instrucţiuni, interschimbarea valorilor din registrele
EAX şi EBX necesită o singură linie de cod:

xchg eax,ebx
Dacă unul din operanzi se află în memorie, procesorul activează în mod automat
semnalul LOCK (în cazul unei structuri multiprocesor, acest lucru asigură accesul
exclusiv la memorie pentru un singur procesor). Deşi util în unele situaţii mai
speciale, acest proces este mare consumator de timp şi penalizează performanţa
programelor.
Instrucţiunea XCHG este foarte utilă în operaţiile de sortare. De asemenea,
este la fel de utilă în interschimbarea octeţilor unui cuvânt, procedură care
corespunde conversiei între formatele little-endian şi big-endian. De exemplu,

xchg al,ah

converteşte valoarea din registrul AX dintr-o formă în cealaltă. Gândiţi-vă cum


puteţi converti dintr-un format în altul valoarea unui registru de 32 de biţi. Nu este
o problemă lipsită de semnificaţie, în unele situaţii ordinea octeţilor are mare
importanţă. De exemplu, transferul datelor în reţea se face conform convenţiei big-
endian. Să facem un experiment. Am văzut în capitolele precedente că asamblorul
permite forme interesante de adresare imediată. De exemplu, următoarea
instrucţiune este perfect legală şi înseamnă introducerea unui şir de caractere în
registrul EAX:

mov eax,'ABCD'

Introduceţi instrucţiunea într-un program şi afişaţi conţinutul registrului


EAX imediat după execuţia acesteia:

(gdb) i r eax
eax 0x44434241 1145258561

31 15 0
'D' (44H) 'C' (43H) 'B' (42H) 'A' (41H)
Figura 7.2 Registrul EAX în reprezentare little-endian
În registrul EAX sunt caracterele ASCII: 'A' (41H), 'B' (42H), 'C' (43H) şi 'D'
(44H). Deoarece caracterele ASCII sunt reprezentate pe 8 biţi, cele patru elemente
ale şirului încap perfect în cei 32 de biţi ai registrului. Chiar dacă la prima vedere
pare că sunt introduse în ordine inversă, acest lucru nu este adevărat. Ne aducem
aminte că arhitectura IA-32 lucrează conform convenţiei little-endian, care
stochează octetul mai puţin semnificativ la adresa mai mică. Acest criteriu se aplică
şi la registre. În registrul EAX, AL se află pe poziţia celui mai puţin semnificativ
octet, urmat de AH şi de ceilalţi doi octeţi. Într-un şir de caractere, caracterul mai
puţin semnificativ este cel din extrema stângă. Aşadar, 'A' este introdus în registrul
AL, 'B' în registrul AH, ş.a.m.d.. Rezultatul este cel menţionat. Dacă aveţi încă
dubii, introduceţi valoarea 'ABCD' în memorie şi comparaţi ordinea octeţilor.
Presupunem că trebuie să transmitem conţinutul registrului EAX prin reţea
şi trebuie să-l convertim în format big-endian.

31 15 0
'A' (41H) 'B' (42H) 'C' (43H) 'D' (44H)
Figura 7.3 Registrul EAX în reprezentare big-endian
Operaţia de conversie între little- şi big-endian presupune interschimbarea octeţilor
1 cu 4 şi 2 cu 3. Este important de reţinut că ordinea biţilor din octeţii individuali
nu se modifică. Procesoarele Pentium au introdus o instrucţiune care efectuează
această operaţie. Formatul acesteia este:

bswap registru

Instrucţiunea BSWAP comută numai octeţii unui registru de 32 de biţi.

(gdb) i r eax
eax 0x41424344 1094861636

Următorul program exemplifică comportamentul instrucţiunilor XCHG şi


BSWAP.
;
;swapBig.asm
;
section .text
global _start
_start:
nop
mov bx,'AB'
xchg bl,bh

mov eax,'ABCD'
bswap eax

mov eax,1
mov ebx,0
int 80h

Alte instrucţiuni din această familie sunt prezentate în Tabelul 7.7.


Tabelul 7.7 Instrucţiuni de interschimbare
Instrucţiune Descriere
XCHG Comută valori între două registre sau între un registru şi o locaţie
de memorie.
BSWAP Converteşte octeţii unui registru de 32 de biţi din ordine little-
endian în big-endian, sau invers.
XADD Comută două valori şi stochează suma în operandul destinaţie.
CMPXCHG Compară acumulatorul cu o valoare externă şi schimbă operanzi
în funcţie de rezultatul comparaţiei.
CMPCHG8B Compară două valori de 64 de biţi fiecare şi le schimbă în funcţie
de rezultatul comparaţiei.

Instrucţiunea XADD comută valorile între două registre sau între un registru
şi o locaţie de memorie, apoi adună valorile şi stochează rezultatul în destinaţie.

xadd destinaţie,sursă

unde sursa trebuie să fie obligatoriu un registru de 8, 16 sau 32 de biţi, iar


destinaţia registru sau locaţie de memorie de dimensiunea corespunzătoare.
Instrucţiunea XADD este disponibilă începând cu procesoarele 80486.
Instrucţiunea CMPXCHG compară operandul destinaţie cu o valoare din AL,
AX sau EAX.

cmpxchg destinaţie,sursă

Dacă valorile sunt egale, valoarea operandului sursă se încarcă în destinaţie. Dacă
valorile sunt diferite, operandul destinaţie este încărcat în EAX, AX sau Al.
Operandul destinaţie poate fi registru sau locaţie de memorie de 8, 16 sau 32 de
biţi. Operandul sursă trebuie să fie un registru de dimensiune corespunzătoare.
Instrucţiunea CMPXCHG este disponibilă începând cu procesoarele 80486.
;
;cmpxchg.asm
;
section .data
val dd 5
section .text
global _start
_start:
nop
mov eax,5
mov ebx,3
cmpxchg [val],ebx

mov eax,1
mov ebx,0
int 80h

Instrucţiunea CMPXCHG8B are acelaşi efect cu instrucţiunea CMPXCHG,


numai că lucrează cu valori de 8 octeţi (de aici şi 8B de la sfârşit). Această
instrucţiune a apărut odată cu procesorul Pentium. Formatul instrucţiunii primeşte
un singur operand:

cmpxchg8b destinaţie

Operandul destinaţie adresează o locaţie de memorie de 8 octeţi. Aceştia sunt


comparaţi cu valoarea conţinută în perechea de registre EDX:EAX (EDX registrul
superior şi EAX registrul inferior). Dacă valorile sunt egale, valoarea de 64 de biţi
aflată în perechea de registre ECX:EBX este compiată în locaţia de memorie
adresată de destinaţie. Dacă nu sunt egale, valoarea din locaţia de memorie este
încărcată în perechea de registre EDX:EAX.
;
;cmpxchg8b.asm
;
section .data
val dq 1122334455667788h
section .text
global _start
_start:
nop
mov edx,11223344h
mov eax,55667788h
mov ecx,ffffffffh
mov ebx,aaaaaaaah
cmpxchg8b [val]

mov eax,1
mov ebx,0
int 80h
7.7. Instrucțiuni de prelucrare la nivel de bit

Instrucțiunile de control din limbajele de nivel înalt, de selecție sau iterație,


sunt implementate pe baza evaluării unor expresii logice sau booleene. Majoritatea
acestora sunt traduse în limbaj de asamblare ca operații de manipulare a
operanzilor la nivel de bit. În setul de instrucțiuni există trei grupuri de instrucțiuni
pentru manipularea biților:
• instrucțiuni logice;
• instrucțiuni de deplasare;
• instrucțiuni de rotație.
Pe lângă aceastea, există și câteva instrucțiuni de testare și modificare sau scanare
pe bit.

7.7.1. Instrucţiuni logice

Operațiile logice pot avea ca rezultat o singură valoare (numită valoare de


adevăr), din două posibile: adevărat sau fals. Pentru reprezentarea acesteia
este suficient un singur bit. Din acest motiv, toate instrucțiunile logice discutate în
această secțiune operează la nivel de bit. Prin convenție, valoarea logică fals este
asociată valorii 0, orice valoare diferită având semnificația adevărat.
Există patru operații logice de bază: AND (conjucție), OR (disjuncție),
XOR (disjuncție exclusivă) şi NOT (negație). Folosim denumirile din engleză
deoarece acestea reprezintă și numele instrucțiunilor în limbaj de asamblare
echivalente. Fiecare operație logică are un tabel de adevăr. Repet, operaţiile logice
se realizează bit cu bit, între biţii de acelaşi rang.
AND 0 1 Rezultatul unei operaţii AND este 1 numai atunci când
0 0 0 ambii biţi sunt 1.
1 0 1

OR 0 1
0 0 1 Rezultatul unei operaţii OR este 0 numai atunci când ambii
1 1 1 biţi sunt 0.

XOR 0 1 Rezultatul unei operaţii XOR este 0 atunci când ambii biţi
0 0 1 sunt egali.
1 1 0

NOT
0 1
Funcţia NOT schimbă valoarea de adevăr. Aplicată asupra
1 0 unui operand, rezultă complementul faţă de unu al
acestuia.
Exemple:

0000 1111 AND 1100 1001 OR 0011 0011 XOR


1001 0110 0100 1010 1111 0000
0000 0110 1100 1011 1100 0011

1100 1100 NOT


0011 0011

Cu excepția operatorului NOT, toți ceilalți operatori logici necesită doi operanzi.
Ca de obicei, instrucțiunile logice primesc operanzi de 8, 16 sau 32 de biți. Sintaxa
generală este de forma

OP destinaţie,sursă

care are ca efect

destinaţie = destinaţie OP sursă

unde OP este operaţia care se doreşte a fi efectuată: AND, OR, XOR.


Destinaţia poate fi un registru sau o locaţie de memorie iar sursa un registru, o
locaţie de memorie sau o constantă de un anumit număr de biţi.
În continuare prezentăm operaţiile tipice în care sunt utilizate instrucţiunile
logice.

Ştergerea rapidă a unui registru


xor eax,eax ;eax = 0
xor al,al ;al = 0

XOR între operanzi identici duce la obţinerea valorii 0.

1110 1100 XOR


1110 1100
0000 0000

Forţarea unor biţi în 0, restul rămânând neschimbaţi

masca equ 00001111


mov al, 0E3h
and al, masca ; al = 0000 0011

În urma operaţiei ŞI logic, biţii din registru aflaţi pe poziţia celor cu


valoarea 0 din mască, vor deveni 0, cei aflaţi pe poziţia cu valoarea 1 din mască vor
rămâne neschimbaţi.

Forţarea unor biţi în 1, restul rămânând neschimbaţi

masca equ 00001111


mov al,0E3h
or al,masca ; al = 1110 1111

În urma operaţiei SAU logic, biţii din registrul AL aflaţi pe poziţia celor cu
valoarea 0 din mască vor rămâne neschimbaţi, cei aflaţi pe poziţie 1 din mască vor
fi setaţi.

Pe lângă instrucţiunile echivalente funcţiilor logice, există şi instrucţiunea


TEST. TEST realizează operaţia logică ŞI bit cu bit între doi operanzi, dar nu
modifică nici sursa nici destinaţia. TEST nu generează rezultat, dar modifică
indicatorii de stare. Este echivalentul logic al instrucțiunii CMP.
Instrucţiunea TEST ne permite să testăm un singur bit dintr-un operand sau
să poziţionăm indicatorii de stare fără a modifica operandul. De exemplu, în cazul
instrucțiunii

test al,00000100b

indicatorul de zero se poziționează în 1 dacă bitul din registru aflat pe poziția


echivalentă a celui marcat cu 1 în mască este 0.
În urma instrucțiunii
test ax,ax

indicatorii de stare sunt poziţionaţi ca şi când a fost efectuată o instrucțiune AND,


dar conţinutul registrului AX nu se modifică.

7.7.2. Instrucţiuni de deplasare

Deplasările pot fi logice sau aritmetice.

Instrucţiuni de deplasare logică


Acest grup de instrucţiuni realizează operaţii de deplasare la nivel de bit,
spre stânga sau spre dreapta. Sunt folosite la înmulţirea sau împărţirea întregilor
fără semn cu puteri ale lui doi.
De exemplu, 4 în binar este 0100. Deplasăm un bit la stânga, 1000,
obţinem 8. Dacă reprezentăm numărul 4 pe un octet: 0000 0100 şi deplasăm doi
biţi la stânga, rezultă: 00010 0000, adică 16. Întotdeauna, deplasările logice cu un
bit înspre stânga trec bitul cel mai semnificativ în CF şi completează bitul b! cu 0.

CF
0 0 0 0 0 1 0 0 0

CF
0 0 0 0 0 1 0 0 0 0

CF
0 0 0 0 1 0 0 0 0

Instrucţiunea care efectuează operaţii de deplasare logică spre stânga este


SHL (Shift logic Left). Are trei formate diferite:

shl destinaţie
shl destinaţie,CL
shl destinaţie,imm8

unde destinaţia poate fi registru sau locaţie de memorie de 8, 16 sau 32 de


biţi. Varianta cu un singur operand presupune că deplasarea se face cu un singur
bit (înmulţim destinaţia cu 2). Celelalte două variante permit declararea numărului
de biţi cu care va fi deplasată destinaţia. Numărul poate fi specificat direct, ca
imediat de 8 biți (imm8), sau prin intermediul registrului CL.
Deplasarea logică spre dreapta împarte destinaţia (întreg fără semn) cu
puteri ale lui doi. Instrucţiunea este SHR (Shift Logic Right); cel mai puţin
semnificativ bit va trece în CF, iar biţii vacanţi se vor completa cu zero.

shr destinaţie
shr destinaţie,CL
shr destinaţie,imm8

Operaţiile de deplasare logică la stânga sau la dreapta sunt folosite la


înmulţirea/împărţirea întregilor fără semn. În cazul unor operanzi cu semn nu dau
rezultatul scontat.

Unele limbaje de nivel înalt pun la dispoziția programatorului operatori


logici pe biți. De exemplu, limbajul C include următorii operatori logici: ~ (NOT),
& (AND), | (OR), ^ (XOR). Aceștia sunt implementați cu instrucțiunile logice din
limbajul de asamblare. De asemenea, limbajul C dispune de operatori de deplasare
la stânga (<<), respectiv la dreapta (>>), echivalentul instrucțiunilor în limbaj de
asamblare SHL și SHR.

Instrucţiuni de deplasare aritmetică


Aceste instrucţiuni permit înmulţirea sau împărţirea rapidă cu puteri ale lui
doi a întregilor cu semn. Ele conservă bitul cel mai semnificativ (care, aşa cum am
precizat la reprezentarea în complement faţă de doi, poate fi „gândit” ca fiind bit de
semn).
sal destinaţie,1 sar destinaţie,1
sal destinaţie,CL sar destinaţie,CL
sal destinaţie,imm8 sar destinaţie,imm8

Deoarece adăugarea unor valori de zero în partea dreaptă nu schimbă bitul


cel mai semnificativ, SAL (Shift Arithmetic Left) este un alias pentru SHL (acelaşi
cod maşină). Toate numerele negative reprezentate în complement faţă de doi au
MSB = 1. Cât timp deplasarea nu modifica bitul cel mai semnificativ rezultatul este
corect. Problema apare atunci când domeniul de reprezentare este depăşit. De
exemplu, pentru un octet, -128 deplasat la stânga cu 1 dă 0.

Chiar dacă este vorba de acelaşi cod maşină, YASM face diferenţa între
denumirile instrucţiunilor; în schimb, dezasamblorul NDISASM întotdeauna
dezasamblează acel cod ca fiind SHL.

;
;deplAritm.asm
;
section .text
global _start
_start:
nop
mov al,-64
shl al,1
movsx ebx,al
shl al,1
movsx ecx,al

mov al,-64
sal al,1
movsx ebx,al
sal al,1
movsx ecx,al

mov eax,1
mov ebx,0
int 080h

Instrucţiunea SAR (Shift Arithmetic Right) este diferită de instrucţiunea


SHR, deoarece completarea biţilor vacanţi cu zero ar duce la suprascrierea celui
mai semnificativ bit (bitul de semn). Astfel, instrucţiunea SAR introduce bitul cel
mai puţin semnificativ în CF şi deplasează cu o poziţie spre dreapta ceilalţi biţi, dar
conservă bitul cel mai semnificativ.
CF
S 1 0 0 0 0 0 1

Astfel, dacă deplasăm un octet spre dreapta cu instrucţiunea SAR, numai


cei 7 biţi mai puţin semnificativi sunt deplasaţi. Cel mai semnificativ bit se
păstrează.
;
;deplAritm2.asm
;
section .text
global _start
_start:
nop
mov al,-4
sar al,1
movsx ebx,al

mov al,-4
shr al,1
movsx ecx,al

mov eax,1
mov ebx,0
int 080h

Instrucțiuni de deplasare dublă


Setul de instrucțiuni cuprinde și două instrucțiuni pentru deplasări de 32 și
64 de biți. Aceste două instrucțiuni primesc ca operanzi de intrare cuvinte sau
dublu cuvinte și generează un rezultat de aceeași lungime (cuvânt sau dublu
cuvânt). Instrucțiunile de deplasare dublă au trei operanzi:

shld destinație,sursă,contor
shrd destinație,sursă,contor

unde destinația și sursa pot fi cuvinte sau dublu cuvinte. Operandul


destinație poate fi registru sau locaţie de memorie, iar operandul sursă obligatoriu
registru. Contorul numărului de biți deplasați poate fi specificat direct, ca valoarea
imediată, sau de registrul CL.
O diferență semnificativă față de instrucțiunile de deplasare este că biții
introduși în destinație prin deplasament provin de la operandul sursă.

15/31 0 15/31 0
shld
CF destinație(reg/mem) sursă(reg)

15/31 0 15/31 0
shrd
sursă(reg) destinație(reg/mem) CF

Observați că biții deplasați în exteriorul operandului sursă sunt introduși în


operandul destinație. Totuși, operandul sursă nu se modifică, el rămâne la valoarea
avută anterior deplasării. Numai operandul destinație este actualizat. La fel ca în
cazul celorlalte instrucțiuni de deplasare, ultimul bit care părăsește destinația este
memorat în indicatorul de transport.
7.7.3. Instrucţiuni de rotație

În cazul instrucțiunilor de deplasare, biții deplasați în afara operandului


destinație sunt pierduți. Există situații în care este dezirabil să păstrăm acești biți.
Instrucţiunile de rotație deplasează operandul spre stânga sau spre dreapta, exact ca
instrucţiunile de deplasare, numai că biţii care depăşesc octetul, cuvântul sau dublu
cuvântul sunt reintroduşi în octet, cuvânt sau dublu cuvânt prin celălalt capăt al
valorii. De exemplu, rotaţia unui octet la stânga cu un bit ia valoarea bitului 7 şi o
plasează pe poziţia bitului 0, iar ceilalţi biţi sunt deplasaţi la stânga cu o poziţie.
Instrucţiunile care realizează rotaţii sunt: ROL (Rotate Left), ROR (Rotate
Right), RCL (Rotate Left and include CF), RCR (Rotate Right and include CF).
Ultimele două instrucţiuni folosesc indicatorul CF ca bit adiţional, lucru care
permite deplasarea a 9, 17 sau 33 de biţi. Formatul instrucţiunilor de rotaţie este
acelaşi cu formatul instrucţiunilor de deplasare. De exemplu:

rol destinaţie,1
rol destinaţie,CL
rol destinaţie,imm8

7.7.4. Instrucţiuni de testare şi modificare a unui bit

Setul de instrucțiuni conține patru instrucțiuni de testare și modificare a


unui bit specificat în instrucțiune prin deplasamentul său față de bitul cel mai puțin
semnificativ al operandului. Ca de obicei, bitul cel mai puțin semnificativ este
considerat bitul de pe poziția 0. Toate instrucțiunile au același format. Dăm ca
exemplu instrucțiunea:
bt operand,poziție

Operandul este un cuvânt sau dublu cuvânt aflat în memorie sau registru. Poziția
specifică indexul bitului testat. Poate fi o valoare imediată sau un registru de 16 sau
32 de biți. Toate cele patru instrucțiuni copiază bitul respectiv în CF și îl modifică
conform operației descrise de numele lor:

Instrucțiune Efect asupra bitului selectat


BT (Bit Test) niciun efect
BTS (Bit Test and Set) bitul selectat este poziționat în 1
BTR (Bit Test and Reset) bitul selectat este poziționat în 0
BTC (Bit Test and Complement) bitul selectat este negat

Instrucțiunile din acest grup modifică numai indicatorul de transport. Ceilalți cinci
indicatori de stare rămân nemodificați.

7.7.5. Instrucţiuni de scanare pe bit

Instrucțiunile de scanare pe bit returnează într-un registru poziția primului


bit de 1 întâlnit. Sunt două instrucțiuni – una pentru scanare directă (începe
scanarea de la bitul mai puțin semnificativ), cealaltă pentru scanare inversă.
Formatul instrucțiunilor este:

bsf destinație,operand ;Bit Scan Forward


bsr destinație,operand ;Bit Scan Reverse

unde operandul este un cuvânt sau dublu cuvânt aflat în memorie sau registru.
Destinația va reține indexul primului bit de 1 întâlnit în operand în timpul scanării
directe sau inverse. Destinația trebuie să fie obligatoriu un registru de 16 sau 32 de
biți. Dacă toți biții sunt zero, indicatorul de zero se poziționează în 1; în caz
contrar, ZF = 0 și registrul destinație memorează poziția. Instrucțiunile de scanare
pe bit afectează numai indicatorul de zero. Ceilalți cinci indicatori de stare rămân
nemodificați.

7.7.6. Instrucţiuni de setare condiţionată

Instrucțiunile din această categorie permit poziționarea unui octet pe zero


sau unu, în funcție de una din cele 16 condiții definite de indicatorii de stare.
Forma generală a instrucțiunii este:

set<condiţie> operand

unde operandul, de tip octet, poate fi registru sau locație de memorie. Condițiile
sunt următoarele:

Instrucțiune Condiție
SETE/SETZ ZF = 1
SETNE/SETNZ ZF = 0
SETL/SETNGE SF < > OF, valori cu semn
SETLE/SETNG SF < > OF sau ZF = 1, valori cu semn
SETNL/SETGE SF = OF, valori cu semn
SETNLE/SETG SF = OF și ZF = 0, valori cu semn
SETB/SETNAE/SETC CF =1, valori fără semn
SETBE/SETNA CF = 1 sau ZF = 1, fără semn
SETNB/SETAE/SETNC CF = 0, valori fără semn
SETNBE/SETA CF = 0 și ZF = 0, fără semn
SETO/SETNO OF = 1 / respectiv OF = 0
SETP/SETPE PF = 1, paritate pară
SETNP/SETPO PF = 0, paritate impară
SETS/SETNS SF = 1 / respectiv SF = 0

Aceste instrucțiuni sunt folosite pentru implementarea expresiilor booleene din


limbajele de nivel înalt. Nu modifică nici un indicator de stare.

7.8. Instrucțiuni de transfer al controlului

Rulați, prin intermediul depanatorului, oricare program din cele prezentate


până acum, de exemplu deplAritm2.asm, și monitorizați la fiecare pas
valoarea registrului EIP. Observați cum execuţia unei instrucţiuni modifică
conţinutul acestuia. Ei bine, conţinutul registrului EIP reprezintă adresa
instrucţiunii ce urmează a fi executată. EIP se numeşte contor de instrucţiune și
memorează adresa următoarei instrucţiuni din program. Pentru că instrucţiunile
sunt formate de obicei din mai mulţi octeţi, valoarea indicatorului de instrucţiune
creşte în concordanţă cu numărul acestora şi nu trebuie să ne aşteptăm să crească
cu unu la fiecare instrucţiune executată.
Nici programatorul, nici programul nu poate modifica direct registrul EIP
(nici nu este de dorit). Altfel spus, nu putem folosi instrucţiunea MOV ca să
încărcăm în EIP adresa unei anumite locații de memorie (de ex., adresa unei
anumite instrucțiuni). Pe de altă parte, există instrucţiuni care modifică indirect
valoarea EIP. Acestea sunt instrucţiunile de ramificare (salt) şi iterative (buclele).
Asfel de instrucţiuni pot altera valoarea registrului EIP necondiţionat sau
condiţionat (modificarea se face numai dacă este îndeplinită o anumită condiţie
bazată pe starea indicatorilor din registrul EFLAGS). Pe baza acestui criteriu
putem împărți instrucțiunile de transfer al controlului programlui în două categorii.
• Instrucţiuni de ramificare necondiţionată:
1. instrucţiuni de salt necondiţionat
2. instrucţiuni de apel (call)
3. instrucțiuni de întrerupere
• Instrucţiuni de ramificare condiţionată:
4. instrucţiuni de salt condiţionat

În continuare vom studia numai instrucţiunile de salt necondiţionat şi


condiţionat (1 si 4), urmând ca instrucţiunile de apel şi întrerupere să facă obiectul
unor capitole viitoare.

7.8.1. Instrucţiuni de salt necondiţionat

Formatul general al instrucţiunii de salt necondiţionat este:

jmp adresă

unde adresa specifică adresa de memorie de la care se va continua


rularea programului (adresa care va fi trecută în registrul EIP). Adresa de memorie
este declarată în cadrul codului sursă ca etichetă (asamblorul sau editorul de
legături modifică numele etichetei cu adresa corectă).
;
;saltNcond.asm
;
section .data
val dd 105,106,107,100,110,103
section .text
global _start
_start:
nop
mov ebx,[val]
mov edi,1
mov eax,[val+edi*4]
cmova ebx,eax
jmp sfarsit

inc edi
mov ecx,[val+edi*4]
cmovnbe ebx,ecx
sfarsit:
mov eax,1
mov ebx,0
int 080h

Rulaţi programul pas cu pas şi observaţi efectul instrucțiunii de salt


necondiționat asupra registrului EIP.
Dacă dezasamblăm programul cu comanda

objdump -D saltNcond -j .text -M intel


vedem adresa în clar:

08048080 <_start>:
8048080 90 nop
8048081: 8b 1d b0 90 04 08 mov ebx,DWORD PTR ds:0x80490b0
8048087: bf 01 00 00 00 mov edi,0x1
804808c: 8b 04 bd b0 90 04 08 mov eax,DWORD [edi*4+0x80490b0]
8048093: 0f 47 d8 cmova ebx,eax
8048096: eb 0b jmp 80480a3 <sfarsit>
8048098: 47 inc edi
8048099: 8b 0c bd b0 90 04 08 mov ecx,DWORD [edi*4+0x80490b0]
80480a0: 0f 47 d9 cmova ebx,ecx

080480a3 <sfarsit>:
80480a3: b8 01 00 00 00 mov eax,0x1
80480a8: bb 00 00 00 00 mov ebx,0x0
80480ad: cd 80 int 0x80

JMP indică procesorului că următoarea instrucţiune care trebuie executată se află la


adresa specificată sub formă de etichetă. În momentul execuției sale, valoarea
indicatorului de instrucţiune este schimbată cu adresa respectivă de memorie.

7.8.2. Instrucţiuni de salt condiţionat

Instrucţiunile de salt condiţionat transferă execuţia programului la altă


adresă numai dacă este satisfăcută o anumită condiţie. Formatul general este:

j<condiţie> adresă

unde <condiţie> rezultă din starea indicatorilor de stare CF, OF, PF, SF, ZF,
iar adresa este o etichetă în cadrul codului.
Tabelul 7.8 Instrucţiuni de salt condiţionat
Instrucţiune Descriere Condiţie
JA Jump if Above CF=0 and ZF=0
JAE Jump if Above or Equal CF=0
JB Jump if Below CF=1
JBE Jump if Below or Equal CF=1 or ZF=1
JC Jump if Carry CF=1
JCXZ Jump if CX Zero CX=0
JE Jump if Equal ZF=1
JG Jump if Greater (signed) ZF=0 and SF=OF
JGE Jump if Greater or Equal (signed) SF=OF
JL Jump if Less (signed) SF != OF
JLE Jump if Less or Equal (signed) ZF=1 or SF != OF
JMP Unconditional Jump unconditional
JNA Jump if Not Above CF=1 or ZF=1
JNAE Jump if Not Above or Equal CF=1
JNB Jump if Not Below CF=0
JNBE Jump if Not Below or Equal CF=0 and ZF=0
JNC Jump if Not Carry CF=0
JNE Jump if Not Equal ZF=0
JNG Jump if Not Greater (signed) ZF=1 or SF != OF
JNGE Jump if Not Greater or Equal (signed) SF != OF
JNL Jump if Not Less (signed) SF=OF
JNLE Jump if Not Less or Equal (signed) ZF=0 and SF=OF
JNO Jump if Not Overflow (signed) OF=0
JNP Jump if No Parity PF=0
JNS Jump if Not Signed (signed) SF=0
JNZ Jump if Not Zero ZF=0
JO Jump if Overflow (signed) OF=1
JP Jump if Parity PF=1
JPE Jump if Parity Even PF=1
JPO Jump if Parity Odd PF=0
JS Jump if Signed (signed) SF=1
JZ Jump if Zero ZF=1

Rezultatul saltului condiţionat depinde de starea registrului EFLAGS la


momentul execuţiei instrucţiunii de salt. Fiecare instrucţiune de salt condiţionat
testează condiţia specifică prin examinarea unor anumiţi indicatori de stare. La
prima vedere putem crede că unele instrucţiuni de salt condiţionat sunt redundante.
De exemplu, JA şi JG. Diferenţa constă în faptul că unele lucrează cu valori cu
semn, altele cu valori fără semn. Instrucţiunile de salt care folosesc cuvintele cheie
above şi below sunt folosite pentru evaluarea valorilor întregilor fără semn, în
timp ce greater şi lower se referă la relaţia dintre două valori cu semn.
În funcţie de distanţa la care se poate efectua saltul, sunt trei tipuri de
instrucţiuni de salt (condiţionat sau nu):
• SHORT – „saltul” se poate efectua numai într-o rază de 128 de octeţi de
memorie (înainte sau înapoi). Avantajul principal faţă de următoarele
două tipuri prezentate este că instrucţiunea de salt ocupă mai puţină
memorie. Pentru stocarea indexului de salt foloseşte un singur octet cu
semn. Indexul de salt este numărul de octeţi adunaţi sau scăzuţi din
adresa aflată în registrul EIP la momentul respectiv. Un astfel de salt
este specificat prin cuvântul cheie SHORT introdus imediat după
instrucţiunea de salt, înainte de etichetă: jmp SHORT adresă.
• NEAR – este tipul standard, atât pentru salturile condiţionate cât şi
pentru cele necondiţionate. Poate fi folosit la selectarea oricărei locaţii
din segment. Arhitecturile de 32 de biţi suportă două tipuri de salturi
NEAR. Unul memorează indexul de salt pe doi octeţi (permite saltul,
înainte sau înapoi, într-o rază de aproximativ 32 000 de octeţi), celălalt
pe patru octeţi (lucru care permite adresarea oricărei locaţii din
segmentul de cod, înţeles ca segment de 4 GB). În modul protejat, tipul
de salt implicit este NEAR cu patru octeţi de adresă. Tipul NEAR cu doi
octeţi poate fi specificat prin cuvântul cheie WORD, astfel: jmp WORD
adresă.
• FAR – este folosit în cazul modelului de memorie segmentat (Modul
REAL sau Virtual). Permite adresarea unei locaţii aflate în alt segment
de cod. Instrucţiunile de salt condiţionat nu suportă acest tip de salt.

7.8.3. Instrucţiuni de ciclare

Scriem şi rulăm pas cu pas programul de mai jos.


;
;bucla.asm
;
section .text
global _start
_start:
nop
mov eax,5
mov ebx,2
mov edx,0
bucla:
inc edx
dec eax
cmp eax,ebx
jnz bucla

mov eax,1
mov ebx,0
int 080h

Atunci când valoarea din EAX devine 2, comparaţia va seta indicatorul de


stare ZF şi instrucţiunea de salt condiţionat nu se va efectua. Astfel, ceea ce este
cuprins între eticheta bucla şi instrucţiunea JNZ bucla este o secvenţă de
program care se repetă de un anumit număr de ori. Putem presupune că scopul este
să incrementăm registrul EDX cu 3.
Deşi putem folosi acest cod, Intel a pus la dispoziţie un set de instrucţiuni
dedicate realizării de bucle repetitive.
Tabelul 7.9 Instrucţiuni iterative
Instrucţiune Descriere
LOOP Repetă bucla până când registrul ECX devine 0
LOOPE/LOOPZ Repetă bucla până când registrul ECX devine 0 sau ZF = 0
LOOPNE/LOOPNZ Repetă bucla până când registrul ECX devine 0 sau ZF = 1

Sintaxa acestora este:

loop adresă

unde adresa este o etichetă din segmentul de cod la care se va efectua


saltul. Din păcate, instrucţiunile LOOP suportă numai un deplasament de 8 biţi, în
consecinţă pot fi realizate numai salturi de tip SHORT. Instrucţiunea LOOP
foloseşte în calitate de contor registrul ECX, căruia îi decrementează automat
valoarea. Când acesta devine 0, LOOP nu se mai execută. Înainte de începutul
instrucţiunii LOOP trebuie să setăm în ECX numărul de iteraţii dorite.

<instrucţiuni înainte de buclă>


mov ecx,100
etichetă:
<instrucţiuni în buclă>
loop etichetă
<instrucţiuni după buclă>

Observaţii:
• Dacă codul din interiorul buclei alterează valoarea registrului ECX,
această modificare este luată în consideraţie.
• Instrucţiunile LOOP nu schimbă valoarea indicatorilor de stare. Când
ECX ajunge la zero, ZF nu este setat.
• Înainte să execute o instrucţiune LOOP, procesorul decrementează
valoarea din ECX cu 1, apoi verifică daca aceasta este zero.
Programul poate fi rescris astfel:
;
;bucla.asm
;
section .text
global _start
_start:
nop
mov edx,0
mov ecx,3
bucla:
inc edx
loop bucla

mov eax,1
mov ebx,0
int 080h

Modificaţi valoarea de iniţializare a registrului ECX cu 0. Explicaţi ce s-a


întâmplat. De câte ori s-a repetat bucla?

7.9. Procesarea şirurilor

Şirul reprezintă o secvenţă de caractere. Există cinci operaţii de bază care


operează pe şiruri, numite şi primitive: mutare, comparare de şiruri, scanarea unui
şir pentru o valoare, transfer la/de la acumulator. Pentru fiecare operaţie există o
instrucţiune dedicată.

7.9.1. Instrucţiuni de transfer

Presupunem că avem un şir în memorie şi trebuie să copiem unul sau mai


multe elemente, poate chiar tot şirul, în altă zonă de memorie. Ne amintim că
instrucţiunea clasică de copiere, MOV, nu poate primi în acelaşi timp ca argumente
două locaţii de memorie, aşadar nu poate copia date din memorie în memorie.
Pentru acest lucru, Intel a pus la dispoziţie instrucţiunea MOVS. MOVS permite
programatorului să transfere părţi sau şiruri întregi dintr-o locaţie de memorie în
alta. Sintaxa generală a instrucţiunii este:
MOVS<x>

unde x reprezintă dimensiunea locaţiei de memorie ce va fi tranferată la


execuţia instrucţiunii: B, W sau D. Astfel, instrucţiunea îmbracă trei forme:

• MOVSB (MOVe String Byte) – copiază un singur octet.


• MOVSW (MOVe String Word) – copiază un singur cuvânt.
• MOVSD (MOVe String Double) – copiază un dublu cuvânt.

Chiar dacă este considerată intrucţiune dedicată prelucrărilor pe şiruri,


instrucţiunea MOVS, prin cele trei variante ale sale, copiază unul, doi sau patru
octeţi o dată.

Fiecare din acestea are la rândul său două variante, în funcţie de numărul
de biţi ai procesorului.
Pentru procesoarele de 16 biţi, MOVSB copiază un singur octet de la locaţia
de memorie adresată de registrele DS:SI în locaţia de memorie adresată de
registrele ES:DI (modul real de adresare a memoriei) şi incrementează automat SI
şi DI.
Pentru procesoarele de 32 de biţi, MOVSB copiază un singur octet de la
locaţia de memorie adresată de registrele DS:ESI în locaţia de memorie adresată de
registrele ES:EDI (modul protejat de adresare a memoriei) şi incrementează
automat ESI (adresa operandului sursă) şi EDI (adresa operandului destinaţie).

Instrucţiunile MOVSW şi MOVSD au aceeaşi sintaxă, numai că vor copia


câte 2, respectiv 4 octeţi.

IA-32
Sursă

ES:EDI

Destinaţie

DS:ESI
7 0 7 0
Figura 7.4 Comportamentul instrucţiunii MOVSB
;
;sir.asm
;
section .data
sir1 db 10,20,30,40,50,60,70,80
section .bss
sir2 resb 8
section .text
global _start
_start:
nop
mov esi, sir1 ;sau lea esi,[sir1]
mov edi, sir2 ;sau lea edi,[sir2]

movsb
movsb
movsb
movsb
movsb
movsb
movsb
movsb ;sau putem folosi 4 instr. movsw sau 2 instr. movsd

mov eax,1
mov ebx,0
int 080h

Instrucţiunea LEA (Load Effective Address) calculează adresa efectivă al celui de


al doilea operand şi o stochează în registrul specificat ca prim operand. În cazul de
mai sus, rezultatul este identic cu cel al instrucţiunii MOV ESI,sir1 (în acest caz
sir1 nu este inclus în paranteze pătrate deoarece ne interesează tocmai adresa).
Spre deosebire de instrucţiunea MOV, LEA necesită paranteze pătrate pentru cel de
al doilea operand al său:

lea edi,[sir1] ;EDI va conţine adresa de început a sir1.

Instrucţiunea calculează adresa unui operand. În cazul exemplului


precedent, adresele şirurilor s-au aflat uşor prin instrucţiunea MOV, dar, aşa cum am
specificat într-un capitol anterior, instrucţiunrea LEA este mai flexibilă. LEA poate
determina adresa unui operand prin metode mai sofisticate:

lea eax,[ebx+ecx*4+100]
Observaţii:
• Pentru a copia un şir de 8 octeţi, avem nevoie de 8 instrucţiuni MOVSB.
MOVSB nu face decât să copieze un singur octet de la adresa dată de
ESI la adresa dată de EDI şi apoi să incrementeze automat ESI şi EDI.
• Putem micşora numărul de linii ale programului înlocuind cele 8
instrucţiuni MOVSB cu 4 instrucţiuni MOVSW sau cu 2 instrucţiuni
MOVSD.
De asemenea, putem copia şirul începând cu primul către ultimul element
al şirului (de la adresă mai mică către adresă mai mare) sau începând de la ultimul
către primul element (de la adresă mai mare către adresă mai mică). Direcţia de
execuţie a instrucţiunii MOVS poate fi setată prin intermediul indicatorul de direcţie
(DF - Direction Flag) din registrul EFLAGS.
• dacă DF = 0 (iniţial), instrucţiunile MOVS incrementează ESI şi EDI cu
1, 2 respectiv 4.
• daca DF = 1, instrucţiunile MOVS decrementează ESI şi EDI cu 1, 2,
respectiv 4.
Poziţionarea indicatorului de direcţie se face cu ajutorul instrucţiunilor
CLD (CLear Direction) şi STD (SeT Direction). Când copiem şirul de la sfârşit la
început trebuie să fim atenţi ca în ESI şi EDI să avem adresele de sfărşit. În acest
caz, exemplul trebuie modificat astfel:

lea esi, [sir1+7]


lea edi, [sir2+7]
std

Dacă copiem un şir foarte mare, de exemplu un şir cu 1578 de octeţi,


trebuie să folosim foarte multe instrucţiuni MOVS. În acest caz este indicat să găsim
un mecanism de repetare automată a instrucţiunii MOVS. De exemplu, putem folosi
o buclă realizată cu ajutorul instrucţiunii LOOP:

mov ecx,8
bucla:
movsb
loop bucla

Codul rezultat funcţionează. Prin folosirea instrucţiunii de buclare, MOVSB se


execută de opt ori şi şirul este copiat în întregime. Totuşi, instrucţiunea LOOP nu
este foarte rapidă (de fapt, se mai foloseşte din motive de compatibilitate, codul
DEC reg; JNZ este mai rapid), în plus, pentru astfel de cazuri, Intel a pus la
dispoziţie o instrucţiune repetitivă dedicată, numită REP.

rep instrucţiune

Atenţie, după REP urmează întotdeauna o instrucţiune.

Aceasta repetă o instrucţiune de un număr de ori specificat în registrul


ECX. REP repetă instrucţiunea care îi urmează până când ECX = 0. Schimbăm în
program şi rulăm:
rep movsb

La rulare observăm că instrucţiunea REP MOVSB a fost executată o


singură dată, nu de 8 ori precum LOOP. Un singur pas, dar după acel pas, toţi cei 8
octeţi ai şirului sursă sunt copiaţi la destinaţie.

În fapt, REP nu este atât o instrucţiune, cât un prefix al lui MOVSB.

Pe lângă instrucţiunea REP, care monitorizează numai valoarea din


registrul ECX, mai sunt şi alte variante care, pe lângă ECX, dau atenţie şi la
indicatorul de stare ZF.
Tabelul 7.10 Variantele instrucţiunii REP
Instrucţiune Descriere
REPE Repeat while equal
REPNE Repeat while not equal
REPZ Repeat while zero
REPNZ Repeat while not zero

Instrucţiunile REPE şi REPZ denotă aceeaşi instrucţiune (aliasuri). La fel şi


REPNE cu REPNZ.
Pe lângă faptul că putem copia şiruri dintr-o locaţie de memorie în alta,
avem la dispoziţie şi instrucţiuni capabile să transfere elementele şirului între
memorie şi acumulator.

7.9.2. Instrucţiuni de transfer la/de la acumulator

Instrucţiunea LODS (LOaD String) încarcă conţinutul unei locaţii de


memorie în acumulator. La fel ca instrucţiunea MOVS, are trei variante:
• LODSB (Load String Byte) – încarcă un octet din memorie în registrul
AL;
• LODSW (Load String Word) – încarcă un cuvânt din memorie în
registrul AX;
• LODSD (Load String Double) – încarcă un dublu cuvânt din memorie
în registrul EAX.

Fiecare din aceste forme are două variante, în funcţie de numărul de biţi ai
procesorului.
Pentru procesoarele de 16 biţi, LODSB copiază un singur octet de la locaţia
de memorie adresată de registrele DS:SI în registrul AL (modul real de adresare a
memoriei) şi automat, în funcţie de starea indicatorului DF, incrementează sau
decrementează SI.
Pentru procesoarele de 32 de biţi, LODSB copiază un singur octet de la
locaţia de memorie adresată de registrele DS:ESI în registrul AL (modul protejat de
adresare a memoriei) şi automat, în funcţie de starea indicatorului DF,
incrementează sau decrementează ESI.

Instrucţiunile LODSW şi LODSD au aceeaşi sintaxă, numai că vor copia


câte 2, respectiv 4 octeţi, în registrul AX, respectiv EAX, şi vor incrementa sau
decrementa corespunzător ESI.

IA-32

AL
7
0

DS:ESI
7 0
Figura 7.5 Comportamentul instrucţiunii LODSB
Instrucţiunea STOS (STOre String) stochează într-o locaţie de memorie
conţinutul registrului acumulator. Are trei forme:
• STOSB (Store String Byte) – stochează în memorie octetul din
registrul AL.
• STOSW (Store String Word) – stochează în memorie cuvântul din
registrul AX.
• STOSD (Store String Double) – stochează în memorie dublu cuvântul
din registrul EAX.
ES:EDI

IA-32

AL
7
0
IA-32

7 0
Figura 7.6 Comportamentul instrucţiunii STOSB
Fiecare din aceste trei forme are două variante, în funcţie de numărul de
biţi ai procesorului, similar instrucţiunilor MOVS şi LODS.

Instrucţiunile STOSW şi STOSD au aceeaşi sintaxă, numai că vor copia în


memorie câte 2, respectiv 4 octeţi, din registrul AX, respectiv EAX, şi vor
incrementa sau decrementa EDI.

Următorul program exemplifică comportamentul instrucţiunilor LODSD şi


STOSD.
;
;addElem.asm
;
section .data
vector1 dd 10,20,30,40,50,60,70,80,90
section .bss
vector2 resd 9
section .text
global _start
_start:
nop
lea esi,[vector1]
lea edi,[vector2]
mov ecx,9
L1:
lodsd
add eax,5
stosd
dec ecx
jnz L1

mov eax,1
mov ebx,0
int 80h

7.9.3. Instrucţiuni de comparare

Instrucţiunea CMPS (CoMPare String) compară două valori de tip octet,


cuvânt sau dublu cuvânt, ambele aflate în memorie. Seamănă cu instrucţiunea
clasică de comparare a două valori (CMP) – o scădere fără rezultat, dar cu setarea
indicatorilor de stare. Aşa cum ne-am obişnuit, prezintă trei forme:
• CMPSB (Compare String Byte)
• CMPSW (Compare String Word)
• CMPSD (Compare String Double)

IA-32
Sursă

ES:EDI

Destinaţie

DS:ESI
7 0 7 0
Figura 7.7 Comportamentul instrucţiunii CMPSB
La fel ca la toate celelalte instrucţiuni de operare pe şiruri, fiecare formă de
COMPS are două variante, în funcţie de numărul de biţi ai procesorului.

Instrucţiunile CMPSW şi CMPSD au aceeaşi sintaxă, numai că vor


compara 2, respectiv 4 octeţi, şi vor incrementa sau decrementa ESI şi EDI.

Următorul program compară două şiruri de caractere. În cazul în care sunt


egale, valoarea din registrul EBX este 0. Dacă nu sunt egale, valoarea din registrul
EBX indică numărul de caractere care mai erau de verificat.
;
;cmpStr.asm
;
section .data
sir1 db "Sir de caractere"
sir2 db "Sir decaractere"
section .text
global _start
_start:
nop
mov eax,1
lea esi,[sir1]
lea edi,[sir2]
mov ecx,16
cld
repe cmpsb
je egale
mov ebx,ecx
int 80h
egale:
mov ebx,0
int 80h

Nu este necesar să rulaţi programul prin intermediul depanatorului.


Valoarea din registrul EBX poate fi obţinută din linie de comandă cu:

./cmpStr
echo $?

7.9.4. Instrucţiuni de parcurgere

Instrucţiunea SCAS (SCAn String) permite scanarea şirului pentru o


valoare dată. Cele trei forme sunt:
• SCASB (Scan String Byte)
• SCASW (Scan String Word)
• SCASD (Scan String Double)

La fel ca la toate celelalte instrucţiuni de operaţii pe şiruri, fiecare formă


are două variante, în funcţie de numărul de biţi ai procesorului.

Instrucţiunile SCASW şi SCASD au aceeaşi sintaxă, numai că vor compara


2, respectiv 4 octeţi, cu valorile din registrele AX, respectiv EAX, şi vor incrementa
sau decrementa EDI.

Următorul program determină primul caracter egal cu un caracter dat


(caracterul spaţiu) prin parcurgerea şirului în sens direct.
;
;scanStr.asm
;
section .data
sir db "abcde jklmnoprst"
section .text
global _start
_start:
nop
lea edi,[sir]
mov ecx,20
cld
mov al,' '
repne scasb
je L1
mov ebx,0
mov eax,1
int 80h
L1:
mov eax,1
mov ebx,ecx
int 80

7.10. Operaţii cu stiva

Stiva este o zonă specială a memoriei principale. Caracteristica stivei


constă în modul în care sunt introduse şi extrase datele. În mod obişnuit, datele sunt
plasate în memoria principală secvenţial, începând cu locaţia de la adresa mai mică
către locaţia cu adresa mai mare. Stiva se comportă exact invers. Memoria stivă
este rezervată la sfârşitul segmentului de memorie şi, introducând date în stivă,
vârful acesteia ”creşte” în jos. Adresa de început a stivei este memorată de registrul
indicator de stivă ESP. ESP conţine adresa primei locaţii libere din stivă. La
introducerea unui element în stivă, adresa scade, la extragerea unui element, adresa
creşte. Primul element extras din stivă va fi ultimul intrat, ceea ce se numeşte LIFO
(Last In First Out).
7.10.1. Introducerea şi extragerea datelor

Datele sunt introduse în stivă cu instrucţiunea PUSH. Sintaxa acesteia este:

push sursă

unde sursa poate fi registru de uz general sau locaţie de memorie de 16


sau 32 de biţi, sau imediat de 8, 16 sau 32 de biţi. sursa poate fi şi registru de
segment.
Extragerea datelor din stivă se face cu instrucţiunea POP.

pop destinaţie

unde destinaţia poate fi registru de uz general sau locaţie de memorie


de 16 sau 32 de biţi. destinaţia poate fi şi registru de segment.
;
;stiva1.asm
;
section .data
val dd 125
section .text
global _start
_start:
nop
mov ecx,244420
mov bx,350
mov eax,100
push ecx
push bx
push eax
;introduce în stivă adresa lui val
push val
;introduce în stivă valoarea lui val, valoare reprezentată pe patru octeţi
push dword [val]

pop eax
pop eax
pop eax
pop ax
pop eax

mov eax,1
mov ebx,0
int 080h

Asamblăm şi rulăm programul pas cu pas. Adresa de început a stivei se află


în registrul ESP. La momentul acestei rulări adresa conţinută de ESP este
0xbfc7b4f0. În urma introducerii registrului ECX în stivă, adresa devine
0xbfc7b4ec, ceea ce înseamnă o scădere cu 4. Putem afişa valorile din stivă
prin comanda:

(gdb) x /4bx $esp

Reprezentăm introducerea lui ECX în stivă:

ESP = 0xbfc7b4f0
00 0xbfc7b4ef
03 0xbfc7b4ee
ba 0xbfc7b4ed
c4 ESP = 0xbfc7b4ec

7 0

Executăm următoarele două instrucţiuni. Până aici am introdus în stivă un


număr de 10 octeţi (doi de la BX şi câte patru EAX, ECX). Stiva arată astfel:

ESP = 0xbfc7b4f0
00 0xbfc7b4ef
03 0xbfc7b4ee
ba 0xbfc7b4ed
c4 ESP = 0xbfc7b4ec
01
5e ESP = 0xbfc7b4ea
00
00
00
64 ESP = 0xbfc7b4e6

7 0
Instrucţiunile PUSH VAR şi PUSH DWORD [VAR] exemplifică faptul că
putem stoca în stivă atât o valoare cât şi adresa unei valori.
Continuaţi rularea şi observaţi cum se modifică adresa din registrul ESP
pentru fiecare instrucţiune POP.

7.10.2. Introducerea şi extragerea registrelor

Următoarele perechi de instrucţiuni salvează sau extrag din stivă starea


curentă a tuturor registrelor de uz general.
Tabelul 7.11 Introducerea sau extragerea registrelor în stivă
Instrucţiuni Descriere
PUSHA/POPA Introduce/extrage din stivă toţi regiştrii de uz general de 16 biţi
PUSHAD/POPAD Introduce/extrage din stivă toţi regiştrii de uz general de 32 de
biţi
PUSHF/POPF Introduce/extrage din stivă primii 16 biţi mai puţin
semnificativi din reg. EFLAGS
PUSHFD/POPFD Introduce/extrage din stivă registrul EFLAGS

Registrele salvate în stivă de către instrucţiunea PUSHAD sunt: EAX, ECX,


EDX, EBX, ESP, EBP, ESI, EDI. Primul registru care poate fi extras este EDI.
Instrucţiunea POPAD extrage din stivă în ordinea inversă a salvării lor cu PUSHAD.
;
;stiva2.asm
;
section .bss
val resd 1
section .text
global _start
_start:
nop
mov eax,0ffffaaaah
mov ebx,012345678h
mov ecx,055556666h
mov edx,011112222h
mov ebp,0bbbbcccch
mov esi,0ddddeeeeh
mov edi,088889999h

pusha
pushf
popf
popa

pushad
pop dword [val]
pushfd
popad
popfd

mov eax,1
mov ebx,0
int 080h

7.10.3. Exemple de lucru cu stiva

Instrucţiunile PUSH şi POP nu sunt singurele care pot introduce sau extrage
date din stivă. Putem introduce sau extrage date din stivă prin adresare bazată,
folosind ca adresă de bază adresa memorată în registrul ESP. De cele mai multe ori
însă, în locul utilizării directe a lui ESP, se copiază adresa din ESP în EBP.
Instrucţiunile care accesează parametrii stocaţi în stivă folosesc ca adresă de bază
valoarea lui EBP. Dar nu intrăm acum în detalii, acesta este subiectul unui capitol
viitor.
Stiva este utilizată în trei scopuri principale:
• spaţiu de stocare a datelor temporare;
• transferul controlului în program;
• transmiterea parametrilor în timpul unui apel de procedură.
În paragrafele următoare dezbatem primul caz, cel al salvării temporare a datelor,
celelalte două sunt studiate pe larg în capitolul intitulat Funcţii.
De exemplu, să presupunem că dorim să interschimbăm conţinutul a două
locaţii de memorie de 32 de biţi. Nu putem interschimba conţinutul acestora direct,
deoarece instrucţiunea XCHG nu poate primi ca operanzi două locaţii de memorie.
Însă putem folosi un registru intermediar. Secvenţa de instrucţiuni

mov eax,[val1]
xchg [val2],eax
mov [val1],eax

îndeplineşte sarcina propusă, dar are nevoie de un registru suplimentar. Din cauza
numărului limitat de registre de uz general, acesta nu este tocmai uşor de găsit. De
obicei conţine date care trebuie salvate înainte de secvenţă şi refăcute după, ca în
următorul exemplu:
;
;swap.asm
;
section .data
val1 dd 125
val2 dd 225
section .text
global _start
_start:
nop
mov eax,0ffffffffh
push eax ;salvăm conţinutul registrului EAX
mov eax,[val1]
xchg eax,[val2]
mov [val1],eax
pop eax ;aducem registrul EAX la starea iniţială

mov eax,1
mov ebx,0
int 080h

Codul accesează memoria de cinci ori. Vă amintiţi că instrucţiunea XCHG,


în cazul în care accesează memoria, este mare consumatoare de timp? Din
considerente de performanţă, probabil ar fi fost mai indicat să folosim patru
instrucţiuni MOV, astfel:

mov eax,[val1]
mov ebx,[val2]
mov [val1],ebx
mov [val2],eax

În acest caz, programul complet accesează memoria de opt ori (4 instrucţiuni MOV
plus 4 instrucţiuni necesare salvării şi restaurării registrelor intermediare). Dar stiva
este o structură de tip LIFO, secvenţa de instrucţiuni POP realizează procesul
invers al secvenţei de instrucţiuni PUSH. O modalitate elegantă de interschimbare a
unor valori este să folosim numai stiva.
;
;swapWithStack.asm
;
section .data
var1 dd 125
var2 dd 225
section .text
global _start
_start:
nop
mov eax,0ffffffffh

push [var1]
push [var2]
pop [var1]
pop [var2]

mov eax,1
mov ebx,0
int 080h

De această dată nu este necesară salvarea conţinutului vreunui registru (nu mai
folosim registre intermediare). Observaţi şi faptul că instrucţiunile PUSH şi POP
permit transferul datelor de la memorie la memorie.
Stiva este folosită frecvent atunci când trebuie eliberat un set de registre
utilizat de secvenţa de cod care urmează.

7.11. Exerciţii
8. OPERAŢII CU NUMERE ÎN VIRGULĂ
MOBILĂ

În capitolele anterioare am lucrat numai cu numere întregi. Aşa cum ştiţi,


unele relaţii numerice nu pot fi definite prin intermediul numerelor întregi şi a fost
introdus conceptul de fracţie. Astfel, între două numere întregi putem avea un
număr infinit de valori. Pe lângă numărul infinit de valori între două numere
întregi, în sistemul de numeraţie există un număr infinit de numere întregi. Toate
aceste numere combinate formează domeniul numerelor reale. Numerele reale pot
conţine orice valoare numerică de la minus infinit la plus infinit, cu orice număr de
cifre zecimale după virgulă. Acest capitol descrie standardele de reprezentare a
numerelor reale, arhitectura unității de calcul în virgulă mobilă pentru
arhitecturile Intel de 32 de biți și operațiile principale pe care procesoarele de
acest tip le pot efectua asupra numerelor reale.

8.1. Reprezentarea numerelor reale

Reprezentarea numerelor reale într-un sistem de calcul este o provocare,


mai ales că acestea au dimensiuni diferite (magnitudine). Primele procesoare Intel,
de la 8086 până la 80386 inclusiv, nu puteau opera cu numere reale. Pentru
reprezentarea acestora Intel punea la dispoziţie coprocesoarele aritmetice separate
(8057, 80287 şi 80387). De abia începând cu 486 operaţiile în virgulă mobilă sunt
realizate de o unitate integrată, denumită prescurtat FPU (Floating Point Unit).

8.1.1. Formatul în virgulă mobilă

Formatul în virgulă mobilă a fost dezvoltat ca metodă standard pentru


reprezentarea numerelor reale în sistemele de calcul. Formatul în virgulă mobilă
reprezintă numerele reale utilizând notaţia ştiinţifică (exponenţială).
În notaţia ştiinţifică zecimală numerele sunt exprimate prin două părţi: o
parte fracţională cu semn, numită mantisă, şi o parte exponenţială ce indică puterea
lui 10 la care trebuie ridicată mantisa astfel încât să obţinem valoarea numărului.
Aşadar, pentru a exprima 0.125 în notaţie ştiinţifică, putem scrie 1.25×10!! .
Sistemele de calcul folosesc numere binare. Deoarece numerele sunt în
format binar, mantisa şi exponentul trebuie reprezentate ca valori binare, nu
zecimale. Exemplul nostru în formatul exponenţial binar devine 1.0×2!! .
Ne amintim conversia din zecimal în binar, este un proces în doi paşi:
• convertim partea întreagă în binar prin împărţire succesivă la 2;
• convertim partea fracţionară multiplicând succesiv cu 2 până când
ajungem la zero, şi reţinând partea întreagă.

Convertim 23.25 din zecimal în binar:


• convertim 23
23: 2 = 11 rest 1
11: 2 = 5 rest 1
5: 2 = 2 rest 1
2: 2 = 1 rest 0
1: 2 = 0 rest 1
• convertim .25
.25 × 2 = 0.50
.50 × 2 = 1.0
• Rezultat final: 10111.01

Convertim 0.85:
• convertim .85
.85 × 2 = 1.7
.7 × 2 = 1.4
.4 × 2 = 0.8
.8 × 2 = 1.6
.6 × 2 = 1.2
.2 × 2 = 0.4
.4 × 2 = 0.8

Nu ajungem niciodată la zero. Acest număr are o reprezentare finită în zecimal, dar
infinită în binar. Echivalentul lui 1/6 în zecimal (0.1666666...). Calculatorul va
putea numai să-l aproximeze. O caracteristică cheie a operaţiilor cu numere întregi
este că rezultatul este întotdeauna precis. De exemplu, dacă adunăm doi întregi,
obţinem întodeauna rezultatul exact. În contrast, operaţiile cu numere reale sunt
întotdeauna predispuse la aproximări.
Numerele reale reprezentate în notaţie ştiinţifică au trei componente

N = (−1)! ×M×2!
unde,
• S reprezintă bitul de semn
• M, mantisa
• E, exponent

Sistemele digitale reprezintă aceste componente sub forma unor câmpuri


de dimensiune fixă:

S E M

Câmpul S are întotdeauna un singur bit şi reprezintă semnul mantisei. În schimb,


numărul de biţi utilizaţi pentru exponent şi mantisă depinde de optimizările dorite.
Mărimea câmpului E determină dimensiunea domeniului de reprezentare, iar
mărimea câmpului M precizia reprezentării.

Un format oarecare
În această secţiune presupunem un format de reprezentare în virgulă mobilă de 14
biţi, cu un exponent de 5 biţi, o mantisă de 8 biţi şi un bit de semn.

1bit 5 biţi 8 biţi


Semn Exponent Mantisă

Mantisa numărului în virgulă mobilă conţine valoarea fracţionară în binar şi este


întotdeauna precedată de un punct binar implicit. Exponentul reprezintă puterea lui
2 la care este ridicată mantisa.
Reprezentăm numărul 32. Ştim că 32 este 2! , aşadar, în notaţia ştiinţifică
binară 32 = 1.0×2! = 0.1×2! .!Folosind această informaţie, introducem 110 (= 6
în zecimal) în câmpul exponent şi 1 în mantisă, astfel:

0 0 0 1 1 0 1 0 0 0 0 0 0 0

O problemă evidentă a acestui model este că nu poate reprezenta exponenţi


negativi. Nu avem nicio posibilitate să reprezentăm 0.125 de exemplu, deoarece
0.125 este 2!! şi exponentul -3 nu poate fi reprezentat. Am putea rezolva problema
prin adăugarea unui bit de semn pentru exponent, dar s-a dovedit că este mult mai
eficient să deplasăm (adunăm la) valoarea iniţială a acestuia cu o valoare
predefinită (bias). Ideea din spatele valorii predefinite este de a transforma fiecare
valoare exponent într-un întreg pozitiv. Valoarea predefinită este un număr aflat la
mijlocul intervalului exponent şi reprezintă zero. În cazul nostru, valoarea
predefinită este 16 deoarece este la mijlocul intervalului 0 şi 31 (exponentul nostru
are 5 biţi, aşadar permite 32 de valori). Orice număr mai mare ca 16 în câmpul
exponent indică o valoare pozitivă. Numerele mai mici de 16 indică valori
negative. Acest mod de reprezentare se numeşte reprezentare în exces cu 16
(excess-16) deoarece pentru a obţine valoarea reală a exponentului trebuie să
scădem 16. Reţineţi că exponenţii care au toţi biţii de zero sau unu sunt rezervaţi
pentru numere speciale (precum zero sau infinit).
Revenind la reprezentarea numărului 32, valoarea iniţială a exponentului, 6, se
adună cu valoarea predefinită, 16, şi rezultă 22:

0 1 0 1 1 0 1 0 0 0 0 0 0 0

Dacă am dori să reprezentăm numărul 0.125 = 1.0×2!! = 0.1×2!! , am avea 16


+ (-2) = 14.

0 0 1 1 1 0 1 0 0 0 0 0 0 0

Dar mai întâmpinăm o problemă deloc de neglijat. Numerele pot avea mai
multe reprezentări. Altfel spus, sistemul nu garantează unicitatea reprezentării.
Toate formele următoare reprezintă numărul 32 (0.1×2! , 0.01×2! , 0.001×2! ,
etc.).
0 1 0 1 1 0 1 0 0 0 0 0 0 0

0 1 0 1 1 1 0 1 0 0 0 0 0 0

0 1 1 0 0 0 0 0 1 0 0 0 0 0

0 1 1 0 0 1 0 0 0 1 0 0 0 0

Nu numai că aceste sinonime ocupă spaţiu suplimentar, dar pot provoca


confuzii. În consecinţă, s-a stabilit ca virgula să fie plasată după primul digit diferit
de 0. Acesta se numeşte normalizare. În acest mod, mantisa stochează numai
partea fracţională a numărul normalizat.
Astfel, dacă 32 este în binar 100000, normalizat înseamnă că mutăm
virgula în dreapta primei cifre de 1. Valoarea noastră binară devine 1.00000×2!
sau 1.0×2! . În consecinţă, aceasta este valoarea care va fi reprezentată. Bitul din
stânga punctului binar nu se mai reprezintă, fiind întotdeauna 1, şi convenţia oferă
efectiv un bit suplimentar de precizie (creşte precizia cu o putere a lui 2). Totuşi,
reprezentarea 0.0 are nevoie de atenţie specială.
Reprezentarea lui 32 este:

0 1 0 1 0 1 0 0 0 0 0 0 0 0

Formatul acestui număr normalizat poate fi reprezentat astfel:

N = (−1)! ×(1. M)×2!!!"#$


unde, ca să aflăm exponentul, scădem valoarea prestabilită din câmpul E.

Operaţii aritmetice cu numere în virgulă mobilă

Adunarea şi scăderea în virgulă mobilă se face în mod obişnuit. Primul


lucru de care trebuie să ţinem cont este să exprimăm ambii operanzi în aceeaşi
putere exponenţială, apoi se adună numerele din mantisă păstrând exponentul. În
caz că este nevoie, exponentul se ajustează la final. În următorul exemplu calculăm
suma numerelor 12 şi 1.25 folosind modelul de reprezentare pe 14 biţi.
12 = 1.1×2! , iar 1.25 = 0.101×2! = 0.00101×2!

0 1 0 0 1 1 1 0 0 0 0 0 0 0 +

0 1 0 0 1 1 0 0 1 0 1 0 0 0

0 1 0 0 1 1 1 0 1 0 1 0 0 0

Aşadar, suma este 1.10101×2! = 1101.01 = 13.25. Observaţi că pentru 12 am


folosit forma normalizată. 1.25 a trebuie adus la forma exponentului lui 12.
Înmulţirea este la fel de simplă, se înmulţesc valorile din mantisă şi se
adună exponenţii. Dacă exponentul are nevoie, se ajustează la final. Înmulţim 12 cu
1.25. Nu folosim formele normalizate: 0.11×2! şi 0.101×2! . În plus, de această
dată nu este nevoie să aducem exponenţii la aceeaşi valoare.

0 1 0 1 0 0 1 1 0 0 0 0 0 0 ×

0 1 0 0 0 1 1 0 1 0 0 0 0 0

0 1 0 1 0 1 0 1 1 1 1 0 0 0

Înmulţim mantisa 0.110×0.101 = 0.01111şi adunăm exponenţii 2! + 2! = 2! .


Obţinem 0.01111×2! = 1111.0 = 15.

Definiţii şi erori
Când discutăm de numerele în virgulă mobilă este important să înţelegem termeni
ca domeniu de reprezentare, precizie, exactitate.
Domeniul unui format numeric întreg reprezintă diferenţa între valoarea
cea mai mare şi cea mai mică care poate fi reprezentată. Acurateţea se referă la cât
de exact aproximează o reprezentare numerică valoarea reală (cât de aproape este
reprezentarea de valoarea corectă). Precizia unui număr indică cantitatea de
informaţie de care dispunem pentru reprezentarea unei valori. Precizia se referă la
numărul de biţi pe care îi are mantisa la dispoziţie ca să reprezinte un număr. Un
număr poate fi precis, dar nu exact. De exemplu, spunem că înălţimea cuiva este
1.7402 m, lucru foarte precis (precizie de aproximativ 1/1000). Totuşi, poate fi
inexact, deoarece înălţimea persoanei poate fi cu totul alta. În ştiinţă, precizia este
definită de numărul de biţi ai mantisei. De exemplu, 2.34×10!" are aceeaşi
precizie cu 2.34×10!!" , deşi a două valoare este mult, mult mai mică decât prima.
Acesta este o precizie diferită de cea cu care probabil suntem obişnuiţi. De
exemplu, dacă avem un cântar cu precizie de 1kg, eroarea poate fi de +/- 1/2 kg.
Majoritatea consideră precizia ca fiind valoarea minimă care poate fi măsurată.
Indiferent de câţi biţi avem la dispoziţie pentru a reprezenta un număr real,
modelul nostru trebuie să fie finit. Când folosim sistemele de calcul pentru operaţii
în virgulă mobilă, nu facem decât să modelăm un sistem infinit de numere reale
într-un sistem finit de numere întregi. Ceea ce obţinem în mod real, este o
aproximare a sistemului numerelor reale. Cu cât folosim mai mulţi biţi, cu atât
aproximaţia este mai exactă. Totuşi, la un moment dat, orice model de reprezentare
îşi atinge limitele şi calculele sunt afectate de erori. Prin creşterea numărului de biţi
vom putea reduce numărul acestora, dar nu vom putea niciodată să le eliminăm.
Rolul nostru este să reducem posibilităţile de apariţie a erorilor sau cel puţin să fim
conştienţi de magnitudinea acestora în calculele noastre. Erorile pot creşte prin
operaţii aritmetice repetitive. De exemplu, este evident că modelul nostru de 14 biţi
nu poate reprezenta valori ce depăşesc capacitatea de 8 biţi a mantisei, dar mai
puţin evident este faptul că nu poate reprezenta exact nici valori din interiorul
domeniului de reprezentare. De exemplu, 128.5, convetit în binar rezultă
10000000.1, 9 biţi. Cum mantisa poate reprezenta 8 biţi, în mod obişnuit, bitul cel
mai mai puţin semnificativ este eliminat şi rotunjit la următorul bit. Deja am
introdus o eroare în sistemul nostru.
Erorile pot fi evidente, subtile sau neobservate. Erorile evidente, precum
depăşirea valorii maxime sau minime a domeniului duc la întreruperea
programului. Erorile subtile pot genera rezultate eronate greu de detectat până nu
încep să producă probleme.

8.1.2. Standardul IEEE 754

Modelul de reprezentare al numerelor în virgulă mobilă din secţiunea


anterioară are rol educativ, dar putem extinde acest model cu orice număr dorit de
biţi. Până în anii '80 deciziile de acest tip erau des întâlnite, fiecare producător de
sisteme avea propriul format. Acestea foloseau un număr diferit de biţi pentru
reprezentarea mantisei şi exponentului, diferite modalităţi de tratare a erorilor
cauzate de depăşirea domeniului de reprezentare sau a condiţiilor speciale
(împărţirea la 0, extragerea rădăcinii pătrate dintr-un număr negativ, etc.). În 1985,
IEEE a publicat un standard de reprezentare a numerelor în virgulă mobilă în
simplă şi dublă precizie. Acest standard este cunoscut oficial ca IEEE-754 (1985).

Reprezentarea în simplă precizie


Standardul IEEE-754 în simplă precizie utilizează un exponent de 8 biţi şi
o valoare predefinită (bias) egală cu 127 (excess-127). Mantisa are 23 de biţi şi este
normalizată cu un bit „ascums” 1.M. Împreună cu bitul de semn, dimensiunea
totală a formatului este de 32 de biţi.

N = (−1)! ×2!!!"# ×(1. M)

Reţinem că 1 < E < 254. Valorile 0 şi 255 reprezentând cazuri speciale.


Domeniul de reprezentare valid pentru formatul în simplă precizie poate fi
definit ca fiind:
2!"#$%& ⩽ !"#$"%& ⩽ (2 − 2!!!"#"$%&'%(")"% )×2!"#$#% , pentru valori pozitive
−2!"#$%& ⩾ !"#$"%& ⩾ −(2 − 2!!!"#"$%&'%(")"% )×2!"#$#% , pentru valori negative
unde,
• minexp este valoarea legală minimă a exponentului pentru numere
normalizate. Aceasta este egală cu -126 care, atunci când se adună
valoarea predefinită de 127, va rezulta un exponent deplasat de valoare
1 (00000001b).
• maxexp este valoarea legală maximă a exponentului pentru numere
normalizate. Aceasta este 127 care, adunată cu valoarea predefinită
127, va rezulta un exponent deplasat de valoare 254 (11111110b).
• bitideprecizie este numărul biţilor din mantisă. Pentru precizia simplă,
acestă valoare este 24 (23 plus bitul implicit).

Introducând valorile enumerate în expresii, rezultă următoarele domenii de valori:

2!!"# ⩽ !"#$"%& ⩽ (2 − 2!!" )×2!"# , pentru numere pozitive


−2!!"# ⩾ !"#$"%& ⩾ −(2 − 2!!" )×2!"# , pentru numere negative

ceea ce înseamnă, cu aproximaţie, de la 1.18×10!!" până la 3.40×10!" .


Numărul 0 este un caz special a lui E = 0 (şirul binar 00000000) şi are două
reprezentări. La fel, când E = 255 (şirul binar 11111111), valoarea reprezentată
este ±∞ (mantisa este egală cu zero) sau NaN (mantisa este diferită de zero).
Valoarea NaN („Not a Number”) reprezintă un număr invalid folosit de obicei ca
indicator de eroare. Valorile speciale sunt redate în tabelul următor:
Tabelul 8.1 Valorile speciale ale reprezentării în simplă precizie
Condiţie Valoare N Reprezentări
E = 255 şi M ≠ 0 NaN Not a Number
E= 255 şi M = 0 (−1)! ∞ ±∞
E = 0 şi M ≠ 0 (−1)! ×2!!"# ×(0. M) Număr denormalizat
E= 0 şi M = 0 (−1)! 0 ±0

Zero

Zero nu poate fi reprezentat direct. Deoarece avem bit de semn, pentru 0 există
reprezentare pozitivă şi negativă.

Infinit

Avem reprezentare separată pentru plus şi minus infinit. Un număr diferit de zero
împărţit la zero dă infinit. De exemplu, 1.0/0.0 produce infinit.

NaN

De obicei apare în cazurile unor operaţii definite greşit. Exemplul standard este
împărţirea 0.0/0.0, care nu are o valoare definită. Ele sunt identificate printr-un
exponent care are toţi biţii de 1 şi o mantisă diferită de cea pentru configuraţia
valorilor de tip infinit. Există două tipuri de astfel de configuraţii, denumite NaN
de semnalizare (SNaN – Signaling NaN) şi NaN tăcut (QNaN – Quiet NaN).
SNaN are mantisa de forma 1.0xx ... x, unde x poate avea orice
valoare (dar nu pot fi toţi zero, deoarece reprezintă valoarea infinit). Când
întâlneşte o astfel de configuraţie FPU generează o excepţie ce semnalizează
efectuarea unei operaţii invalide. Programatorul o poate folosi pentru a indica unele
condiţii de eroare, cum ar fi o variabilă în virgulă mobilă neiniţializată.
Cealaltă configuraţie, QNaN, are mantisa de forma 1.1xxx...x. Primul
1 este cel implicit. O astfel de configuraţie apare când rezultatul unei operaţii
aritmetice este nedefinit matematic.
Orice instrucţiune care are un operand de unul din cele două tipuri de NaN
va genera un rezultat de tip NaN.

Numere denormalizate

Faţă de numerele normalizate, acestea sunt numere cu mai puţini biţi de precizie şi
valori mai mici. Să presupunem că permitem tuturor biţilor dintr-un număr
normalizat să fie 0 (în fapt este chiar reprezentarea validă pentru 0). Un şir de 32 de
biţi de zero ar fi 1.0×2!!"# . O valoare mică, dar am putea reprezenta numere mult
mai mici dacă pentru un E = 0000 0000b:
• renunţăm la bitul de 1 „ascuns”
• şi fixăm valoarea exponentului la -126.
Vă amintiţi că exponentul este scris ca valoare adunată cu 127? În
consecinţă, v-aţi aştepta ca din moment ce biţii câmpului E sunt 0000000, aceştia
trebuie să reprezinte un exponent de – 127, nu -126. Totuşi, e un motiv întemeiat
pentru care este -126. Vom explica puţin mai târziu. Deocamdată acceptăm faptul
că valoarea exponentului este -126 ori de câte ori şirul de biţi din câmpul exponent
este 0! (8 biţi de zero).
Aşadar, cel mai mare număr pozitiv denormalizat este format din şirul de
biţi (semn, exponent, mantisă):

0 0000 0000 1111 1111 1111 1111 1111 111

care este mapat la numărul 0. (1!" )×2!!"# , unde 1!" înseamnă un şir de 23 de biţi
de 1. Din moment ce punctul binar este urmat de 23 de biţi, acest număr are
precizie de 23 de biţi.
Cel mai mic număr denormalizat este format din şirul de biţi:

0 0000 0000 0000 0000 0000 0000 0000 001

care se traduce prin 0.0!! 1×2!!"# , sau 1.0×2!!"# . Acest număr are un singur bit
de precizie. Cei 22 de zero nu afectează precizia numărului. Dacă nu credeţi,
gândiţi-vă că numărul zecimal 256 are trei digiţi de precizie, iar 00256 are tot trei
digiţi de precizie. Cifrele de zero nu afectează numărul digiţilor de precizie. În mod
similar, dacă avem 0.000256, cifrele de zero ne ajută să plasăm 256 corect, dar nu
sunt digiţi care trebuie plasaţi în mantisă. 0.02560 are patru digiţi de precizie
deoarece cifra 0 din dreapta se adaugă preciziei. Aşadar, în urma punctului binar
avem 22 de biţi de zero urmaţi de un 1, şi cei 22 de zero nu au legătură cu numărul
de biţi ai mantisei.
Prin denormalizare am obţinut 1.0×2!!"# ca fiind cel mai mic număr, spre
deosebire de 1.0×2!!"# , cea mai mică valoare pe care am fi obţinut-o dacă
numărul ar fi fost normalizat. S-au sacrificat totuşi 22 de biţi de precizie.
Un şir de biţi 0! în câmpul exponent este mapat la un exponent -126.
Totuşi, pentru numerele în virgulă mobilă reprezentate cu standardul IEEE-754,
valoarea predefinită este -127. De ce este -126 în loc de -127?
Ca să răspundem la această întrebare trebuie să ne uităm la cel mai mic
număr pozitiv normalizat,
0 0000 0001 0000 0000 0000 0000 0000 000

adică 1.0×2!!"# .
Să studiem cele două posibilităţi pe care le avem pentru a reprezenta cel
mai mare număr pozitiv denormalizat.
• 0. (1!" )×2!!"# (bias 127)
• 0. (1!" )×2!!"# (bias 126 - acesta este folosit de IEEE 754 în simplă
precizie)
Amândouă sunt mai mici decât valoarea minimă a numerelor normalizate,
1.0×2!!"# . Acesta este un lucru bun, deoarece dorim să evităm suprapunerea
numerelor normalizate şi denormalizate. De asemenea, observaţi că numărul cu
exponent -126 este mai mare decât numărul cu exponent -127. Aşadar, prin
alegerea lui -126, diferenţa dintre cel mai mic număr normalizat şi cel mai mare
număr denormalizat este mai mică. Aceasta este şi raţiunea pentru care standardul
în simplă precizie IEEE-754 foloseşte ca cel mai mic număr denormalizat
0. (1!" )×2!!"# .

Reprezentarea în dublă precizie


Numerele în dublă precizie folosesc un cuvânt cu semn format dintr-un
exponent de 11 biţi şi o mantisă de 52 de biţi, pentru un total de 64 de biţi.
Valoarea predefinită este 1023 (excess-1023).
Valorile pe care le poate lua acest format sunt prezentate în Tabelul 8.2.
Tabelul 8.2 Valorile reprezentării în dublă precizie
Condiţie Valoare N Reprezentări
E = 2047 şi M ≠ 0 NaN Not a Number
E= 2047 şi M = 0 (−1)! ∞ ±∞
0 < E < 2047 (−1)! ×2!!!"#$ ×(1. M) Număr normalizat
E = 0 şi M ≠ 0 (−1)! ×2!!"## ×(0. M) Număr denormalizat
E= 0 şi M = 0 (−1)! 0 ±0

Această combinaţie produce un domeniu de valori între 2.23×10!!"# şi


1.79×10!"# .

8.1.3. Valori în virgulă mobilă specifice IA-32

Platformele IA-32 folosesc formatele standardului IEEE 754, în simplă şi


dublă precizie, împreună cu propriul format de 80 de biţi, numit formatul
numerelor în precizie extinsă, destinat uzului intern. În C, folosim directiva
float, pentru numere în simplă precizie şi double, pentru numere în dublă
precizie, dar, aşa cum vom vedea în secţiunile următoare, toate registrele interne
ale unităţii de calcul în virgulă mobilă sunt de 80 de biţi.
Formatul numerelor în virgulă mobilă cu precizie extinsă foloseşte 64 de
biţi pentru mantisă şi 15 biţi pentru exponent. Valoarea predefinită este 16.383,
generând exponenţi între -16382 şi +16383, pentru un domeniu de valori cuprins
între 3.37×10!!"#$ şi 1.18×10!"#$ .

8.2. Arhitectura unităţii în virgulă mobilă

Unitatea în virgulă mobilă conţine registrele din Figura 8.1. Acestea sunt
împărţite în trei grupuri:
• registre de date,
• registre de control şi stare,
• registre indicator.

Ultimul grup conţine registrul indicator de instrucţiune şi registrul indicator


de date şi oferă informaţii necesare în scrierea rutinelor de întrerupere care tratează
erorile apărute în timpul operaţiilor. Din moment ce acest subiect depăşeşte
obiectivele acestei cărţi, nu intrăm în detalii.

79 64 63 0 1 0 15 0
0
1 Control
2 15 0
3
4 Stare
5
6
7
S Exponent Mantisă Tag
Figura 8.1 Registrele unităţii în virgulă mobilă

8.2.1. Registre de date

Unitatea în virgulă mobilă dispune de 8 registre de date, de câte 80 de biţi,


organizate ca o stivă de registre. Deşi pot fi adresate individual pe baza numelui,
deoarece sunt organizate ca stivă de registre, aceste nume nu sunt atribuite static.
Adresarea registrelor se face relativ la registrul din vârful stivei. ST0 nu se referă la
un registru specific, ci la registrul aflat în acel moment în vârful stivei (TOS – Top
Of Stack). Registrul următor este numit ST1 ş.a.m.d., până la ultimul registru,
ST7. Vârful curent al stivei este indicat de un câmp de trei biţi aflat în registrul de
stare, TOP (stack TOP). Operaţiile de încărcare decrementează TOP şi introduc
valoarea în noul vârf al stivei, operaţiile de stocare extrag valoarea din vârful
curent al stivei şi incrementează TOP.
Fiecare registru de date reprezintă valoarea în precizie extinsă. Aceste
registre reţin de obicei rezultate intermediare şi utilizarea formatului extins
îmbunătăţeşte acurateţea rezultatului final.
Starea şi conţinutul fiecărui registru de date este indicată printr-o etichetă
de doi biţi. Deoarece sunt 8 registre de date, avem nevoie de 16 biţi. Aceşti 16 biţi
sunt stocaţi în registrul de etichete (tag register).

8.2.2. Registre de control şi stare

Acest grup conţine trei registre de câte 16 biţi:


• registrul de control,
• registrul de stare
• registrul de etichete.

Registrul de stare
Acest registru de 16 biţi păstrează informaţii de stare cu privire la
funcţionarea unităţii de calcul în virgulă mobilă.

15 0
C C C C E S P U O Z D I
B TOP
3 2 1 0 S F E E E E E E
Figura 8.2 Structura registrului de stare FPU
Codurile de condiţii C0 – C3 indică rezultate ale operaţiilor de comparaţie
şi aritmetice în virgulă mobilă. Unii sunt similari anumitor indicatori de stare din
registrul EFLAGS. Tabelul 8.3 arată corespondenţa dintre aceştia şi unii indicatorii
de stare:
Tabelul 8.3 Corespondenţa indicatori FPU indicatori CPU
Indicator FPU Indicator CPU
C0 CF
C2 PF
C3 ZF
Ei sunt utilizaţi pentru salturi condiţionate, asemenea biţilor CPU echivalenţi.
Pentru a-i putea testa trebuie mai întâi copiaţi în registrul EFLAGS. Copierea se
face în două etape. Mai întăi se încarcă cuvântul de stare în registrul AX cu ajutorul
instrucţiunii FSTSW, apoi copiem aceste valori în registrul EFLAGS cu
instrucţiunea SAHF. Odată încărcaţi, putem folosi în mod obişnuit instrucţiunile de
salt condiţionat. Acesta este considerat vechiul mecanism de efectuare a salturilor
condiţionate pentru valori în virgulă mobilă, deoarece, începând cu familia de
procesoare P6, mecanismul salturilor condiţionate pentru valori în virgulă mobilă a
fost simplificat. Intel a pus la dispoziţie o familie de instrucţiuni care compară
numerele în virgulă mobilă şi setează direct indicatorii ZF, PF şi CF din EFLAGS.
Astfel, o singură instrucţiune a noului mecanism înlocuieşte trei instrucţiuni
necesare vechiului mecanism.
Câmpul TOP, de trei biţi, indică registrul din vârful stivei de registre.
Valoarea 000 semnalează stiva goală, valoarea 111 stiva plină. Primul element
încărcat are indice 0. O nouă încărcare incrementează indicii registrelor de date, o
extragere decrementează indicii. Pentru a permite adresarea circulară a celor 8
registre, rezultatul incrementării/decrementării este trunchiat la trei biţi (modulo 8).

Excepţii în virgulă mobilă

În timpul efectuării unor operaţii în virgulă mobilă pot apărea câteva tipuri
de erori, începând de la simple erori logaritmice până la erori provenite din limitele
reprezentării. Erorile în virgulă mobilă se numesc excepţii. Proiectanţii au clasificat
excepţiile în şase clase. Biţii 0 – 5 din registrul de stare corespund acestor situaţii
deosebite.

Operaţie invalidă – operandul este invalid pentru operaţia care trebuie


efectuată. De obicei sunt erori de algoritm: un rezultat nedefinit ca urmare a unei
împărţiri zero la zero sau infinit la infinit. În acest caz, rezultatul este reprezentat
printr-o secvenţă QNaN. FPU setează bitul IE (Invalid operation Exception).
Tot din clasa excepţiilor de operaţii invalide face parte şi depăşirea
superioară sau inferioară a stivei de registre. Depăşirea superioară înseamnă că o
instrucţiune încearcă să introducă un operand din memorie într-un registru de date
deja plin. Un registru plin este definit ca fiind registrul care conţine o valoare de 0
(eticheta 01 din registrul de etichetare), o valoare validă (eticheta 00) sau o valoare
specială (eticheta 10). Depăşirea inferioară se referă la cazul în care o instrucţiune
foloseşte ca operand sursă un registru de date gol (incluzând încercarea scrierii în
memorie a unui registru de date gol). Un registru gol (neutilizat) are o etichetă
egală cu 11. Atunci când apar condiţii de depăşire, programul nu poate continua.
Aşadar, excepţia de operaţie invalidă poate apărea ca urmare a operaţiilor
aritmetice sau a celor cu stiva. Bitul SF (Stack Fault) dă informaţii cu privire la
cauza operaţiei invalide. Dacă acest bit este 1, operaţia invalidă a fost rezultatul
unei depăşiri a stivei de registre; în caz contrar, o instrucţiune aritmetică a întâlnit
un operand invalid. Atenţie, FPU setează bitul SF la detecţia unei condiţii de
depăşire, dar nu-l şterge când detectează o condiţie de operand invalid. Ca rezultat,
dacă nu a fost şters explicit de la ultima condiţie de depăşire a stivei de registre,
starea indicatorului SF poate fi 1 şi în cazul unei excepţii aritmetice. În acest caz,
natura erorii este indicată de codul de condiţie C1: depăşire superioară (C1 = 1) sau
depăşire inferioară (C1 = 0).

Împărţire cu zero – deîmpărţitul este un număr finit nenul şi împărţitorul


este zero; sau, mai general spus, când rezultatul unei operaţii cu numere finite este
infinit. Rezultatul este reprezentat corect, ca infinit cu semn. FPU setează bitul ZE
(Zero divide Exception).

Depăşire superioară – rezultatul unei operaţii este prea mare pentru a


putea fi reprezentat (depăşeşte marginea superioară a domeniului de reprezentare).
Rezultatul este reprezentat ca infinit. FPU setează bitul OE (Overflow Exception).

Depăşire inferioară – rezultatul unei operaţii este prea mic pentru a putea
fi reprezentat (depăşeşte marginea inferioară a domeniului de reprezentare).
Rezultatul este reprezentat ca zero. FPU setează bitul UE (Underflow Exception).

Operand denormalizat – unul din operanzi nu este normalizat sau rezultatul


nu se poate reprezenta normalizat (de exemplu, rezultatul este atât de mic încât este
imposibilă normalizarea lui). FPU setează bitul DE (Denormal Exception).

Rezultat inexact – rezultatul operaţiei este inexact din cauza unei rotunjiri.
Această excepţie de precizie apare când FPU nu poate reprezenta exact rezultatul
unei instrucţiuni în virgulă mobilă. Cazuri de genul împărţirii 1/3, ce furnizează un
rezultat care se repetă la infinit (0.33..3), sau când un număr real este convertit la
un format cu o precizie mai mică şi se pierd biţi prin această conversie. FPU
setează bitul PE (Precision Exception). De obicei, această excepţie este mascată (în
registrul de control) deoarece rotunjirea sau trunchierea rezultatului este
satisfăcătoare.

Biţii de excepţie se păstrează până când programatorul încarcă registrul de


stare cu o valoare nouă. Acest lucru permite programatorului să scrie o secvenţă de
instrucţiuni şi să plaseze un singur test de detecţie al erorilor la sfârşitul acesteia, în
loc să scrie un test după fiecare instrucţiune.
Registrul de control
Registrul de control permite programatorului să controleze câteva opţiuni
de prelucrare.

15 0
P U O Z D I
RC PC
M M M M M M
Figura 8.3 Structura registrului de control
Atunci când apare o excepţie unitatea în virgulă mobilă are două
posibilităţi: să genereze o întrerupere sau să o trateze intern. Modul intern de
tratare a excepţiilor a fost deja specificat în secţiunea anterioară. Întreruperea este
generată ca urmare a unei opţiuni specificate de programator prin intermediul
primilor şase biţi mai puţin semnificativi din acest registru:
• PM (Precision Mask) – întrerupere pentru semnalarea rotunjirii.
• UM (Underflow Mask) – întrerupere pentru semnalarea depăşirii
inferioare.
• OM (Overflow Mask) – întrerupere pentru semnalarea depăşirii
superioare.
• ZM (Zero divide Mask) – întrerupere pentru semnalarea împărţirii cu
zero.
• DM (Denormalized operand Mask) – întrerupere pentru semnalarea
unui operand denormalizat.
• IM (Invalid operation Mask) – întrerupere pentru semnalarea operaţiei
invalide.

Dacă aceşti biţi nu sunt mascaţi (valoare 1), se generează întreruperea


corespunzătoare; altfel, nu se generează excepţia şi programul continuă cu execuţia
următoarei instrucţiuni.

Metoda de rotunjire

RC (Rounding Control) controlează modul de rotunjire a valorilor ce nu pot fi


reprezentate exact:
• 00 – rotunjire la cel mai apropiat număr reprezentabil;
• 01 – rotunjire inferioară (către -);
• 10 – rotunjire superioară (către +);
• 11 – trunchiere.
Valoarea implicită pentru acest câmp este 00.
Precizia de calcul

PC (Precision Control) specifică formatul utilizat pentru reprezentarea rezultatului


operaţiilor de +, -, ×, / şi extragerea rădăcinii pătrate:
• 00 – simplă precizie;
• 01 – neutilizat;
• 10 – dublă precizie;
• 11 – precizie extinsă, utilizată implicit.

Registrul de etichete
Acest registru conţine 8 câmpuri de câte doi biţi, fiecare câmp
corespunzând unui registru de date. T0 este câmpul pentru registrul 0, nu pentru
ST0, care, la un moment dat, poate fi oricare dintre cele 8 registre. Fiecare câmp
oferă informaţii despre conţinutul registrelor respective:
• 00 – registrul conţine un număr corect în virgulă mobilă;
• 01 – registrul conţine valoarea 0.0;
• 10 – registrul conţine valoarea infinit, un număr denormalizat sau
incorect;
• 11 – registrul este gol (neutilizat).

Registrul de etichete nu poate fi utilizat direct de către programator, dar poate fi


folosit depanatoare de exemplu, ca să examineze şi să interpreteze adecvat
conţinutul registrelor de date.

8.3. Instrucţiuni în virgulă mobilă

FPU conţine un set de instrucţiuni în virgulă mobilă pentru transferul de


date, operaţii aritmetice, operaţii de comparare şi transcedentale. Pe lângă acestea
sunt prezente instrucţiuni care încarcă în stivă constante uzuale precum π sau
cuvinte de control.

8.3.1. Transferul datelor în virgulă mobilă

Setul instrucţiunilor de transfer conţine instrucţiuni capabile să transfere


operanzi între registrele stivei şi între vârful curent al stivei şi memorie. Putem
împărţi aceste instrucţiuni în două categori: de încărcare şi de memorare.
Formatul general al instrucţiunilor de încărcare este:
fld sursă

Această instrucţiune încarcă (depune) operandul sursă în registrul din vârful


stivei. Acest lucru are loc prin decrementarea indicatorului stivei şi copierea
conţinutului sursei în noul vârf al stivei. Operandul sursă poate fi un registru al
stivei sau orice tip de număr real din memorie. Înainte de încărcarea în ST0,
operanzii din memorie în simplă sau dublă precizie sunt convertiţi automat la
formatul temporar de 80 de biţi. Instrucţiunea FLD ST0 va copia valoarea din
vârful curent al stivei în noul vârf al stivei.
Unele instrucţiuni de acest tip nu primesc niciun operand. Ele introduc în
stivă anumite constante uzuale.

Instrucţiune Descriere
FLDZ Introduce valoarea +0.0 în ST0
FLD1 Introduce valoarea +1.0 în ST0
FLDPI Introduce valoarea π în ST0
FLDL2T Introduce valoarea !"#210 în ST0
FLDL2E Introduce valoarea !"#2! în ST0
FLDLG2 Introduce valoarea !"#102 în ST0
FLDLN2 Introduce valoarea ln 2 în ST0

Datorită faptului că reprezentarea acestora se face pe 10 octeţi, iar instrucţiunile se


reprezintă numai pe doi octeţi, rezultă că, pe lângă faptul că simplifică
programarea, acestea economisesc spaţiu de memorie şi îmbunătăţesc viteza de
execuţie.
Programul floatDec.asm arată cum putem definit în limbajul de asamblare
valori în virgulă mobilă.
;
;floatDef.asm
;
section .data
fs1 dd 0.0
fs2 dd 1.0
fs3 dd 12.34
fd1 dq 3.333333333
fd2 dq 4.444444444
fd3 dq 5.0
fd4 dq 1.234567e20 ;dublă precizie
fd5 dq 1.e10 ;10.000.000.000
fd6 dq 1.e+10 ;sinonim cu 1.e10
fd7 dq 1.e-10 ;0.000 000 000 1
fd8 dq 12.34
fd9 dq 3.14
fd10 dq 3.141592653589793238462
fe1 dt 3.141592653589793238462 ;pi
fe2 dt 12.34
section .bss
m32 resd 1
m64 resq 1
section .text
global _start
_start:
nop
finit
fld dword [fs3]
fld dword [fs2]
fld qword [fd1]
fld qword [fd2]
fld qword [fd3]
fld qword [fd5]
fld tword [fe1]
fld tword [fe2]
fst dword [m32]
fst qword [m64]

mov eax,1
mov ebx,0
int 80h

YASM acceptă constante în virgulă mobilă numai ca argumente pentru directivele


DD, DQ şi DT. Ele sunt exprimate în forma tradiţională: digiţi, urmaţi de punct, alţi
digiţi opţionali, apoi un E opţional urmat de exponent. Punctul este obligatoriu,
astfel încât asamblorul să poată diferenţia între DD 1, care declară un întreg, de DD
1.0 care declară un număr în virgulă mobilă. În YASM, aproape orice instrucţiune
care adresează operanzi din memorie trebuie să indice mărimea corespunzătoare cu
unul din prefixele DWORD, QWORD sau TWORD.
În GDB, valorile zecimale pot fi afişate cu opţiunea f a comenzii x:

(gdb) x /1wf &fs1


0x80490cc <fs1>: 0
(gdb) x /1wf &fs2
0x80490d0 <fs2>: 1
(gdb) x /1wf &fs3
0x80490d4 <fs3>: 12.3400002

Valorile fs1, fs2, fs3 sunt valori în simplă precizie. Din ultima comandă se
observă că erorile de rotunjire îşi fac apariţia chiar şi atunci când depanatorul
încearcă să calculeze valorile. Ca să afişăm valori în dublă precizie trebuie să
utilizăm opţiunea g, destinată valorilor de 8 octeţi.

(gdb) x /1gf &fd1


0x80490d8 <fd1>: 3.3333333330000001
(gdb) x /1gf &fd2
0x80490e0 <fd2>: 4.4444444440000002
(gdb) x /1gf &fd3
0x80490e8 <fd3>: 5
(gdb) x /1gf &fd4
0x80490f0 <fd4>: 1.234567e+20
(gdb) x /1gf &fd5
0x80490f8 <fd5>: 10000000000
(gdb) x /1gf &fd6
0x8049100 <fd6>: 10000000000
(gdb) x /1gf &fd7
0x8049108 <fd7>: 1e-10
(gdb) x /1gf &fd8
0x8049110 <fd8>: 12.34
(gdb) x /1gf &fd9
0x8049118 <fd9>: 3.1400000000000001
(gdb) x /1gf &fd10
0x8049120 <fd10>: 3.1415926535897931

Obsevaţi modul în care au fost rotunjite ultimele două valori ale lui π. Valorile în
precizie extinsă nu pot fi afişate direct.
Instrucţiunea FINIT iniţializează unitatea în virgulă mobilă. FINIT
setează registrele de stare şi control la valorile implicite şi modifică câmpurile de
etichete astfel încât să indice faptul că registrele de date FPU sunt goale. Dar,
întotdeauna este un „dar”, deşi modifică etichetele, instrucţiunea FINIT nu
alterează datele deja existente în aceste registre. Este obligaţia programatorului să
ţină evidenţa registrelor de date folosite de program şi dacă conţin date valide sau
nu.
După execuţia primei instrucţiuni, valoarea din registrul ST0 poate fi
afişată cu comanda print $st0 sau info reg $st0.
(gdb) print $st0
$5 = 12.340000152587890625
(gdb) info reg $st0
st0 12.340000152587890625 (raw 0x4002c570a40000000000)

Valoarea locaţiei de memorie fs1 a fost plasată în registrul ST0. A doua


instrucţiune înlocuieşte conţinutul lui ST0 cu valoarea fs2 şi coboară fs1 în
registrul ST1.

(gdb) p $st0
$6 = 1
(gdb) p $st1
$7 = 12.340000152587890625

Instrucţiunea fst dword [m32]stochează în locaţia de memorie de 32 de biţi


m32 valoarea aflată în prezent în vârful stivei ST0. Acesta este şi formatul general
al instrucţiunilor de memorare:

fst destinaţie

unde destinaţie poate fi un alt registru din stivă sau o locaţie de memorie de
32 sau 64 de biţi (conversia de la precizia extinsă se face automat). Dacă vârful
stivei este etichetat special (adică conţine o valoare de tip infinit, NaN sau număr
denormalizat) atunci nu se face convesia nici pentru exponent, nici pentru mantisă.
Pentru a păstra valoarea specială, acestea sunt trunchiate la dreapta, la dimensiunea
destinaţiei. Un lucru foarte important de reţinut, demonstrat de următoarele
comenzi, este că această instrucţiune nu extrage valoarea din stivă; doar o copiază.
Dacă doreşti să copiezi şi să extragi, adaugi sufixul p.

(gdb) p $st0
$8 = 12.340000000000000000138777878078145
(gdb) x /1wf &m32
0x804913c <m32>: 12.3400002
(gdb) p $st0
$8 = 12.340000000000000000138777878078145
(gdb) p $st1
$9 = 3.1415926535897932380791280904119844
(gdb) n
35 mov eax,1
(gdb) x /1gf &m64
0x8049140 <m64>: 12.34
(gdb) p $st0
$10 = 12.340000000000000000138777878078145

În continuare prezentăm alte instrucţiuni de transfer.


• FILD sursă - converteşte operandul din memorie de la formatul
întreg binar la formatul real temporar şi depune rezultatul în stivă.
• FBLD sursă – converteşte operandul din memorie de la formatul
zecimal împachetat la formatul real temporar şi depune rezultatul în
stivă.
• FSTP destinaţie – realizează o operaţie asemănătoare cu FST, cu
deosebirea că extrage valoarea din stiva de registre. De asemenea,
FSTP permite plasarea în memorie a unui număr în precizie extinsă, în
timp ce FST nu acceptă asta. Instrucţiunea FSTP ST0 realizează
extragerea din stivă fără transfer în memorie.
• FIST destinaţie – rotunjeşte conţinutul registrului din vârful
stivei la un întreg (în conformitate cu biţii RC din registrul de control)
şi transferă rezultatul la destinaţie. Destinaţia poate fi de 32 sau 64 de
biţi. Zero negativ este memorat cu aceeaşi codificare ca zero pozitiv.
• FISTP destinaţie – instrucţiune similară cu instrucţiunea
anterioară, dar în plus, glisează o dată stiva de registre. Destinaţia
poate fi de orice tip întreg.
• FBSTP destinaţie – converteşte valoarea din vârful stivei la un
întreg zecimail împachetat, depunde rezultatul în memorie şi glisează
stiva de registre. Conversia se face prin rotunjire întreagă, prin
adunarea valorii 0.5 şi apoi trunchierea rezultatului.
• FXCH destinaţie – interschimbă destinaţia cu vârful stivei. Dacă
destinaţia nu este specificată explicit, se va utiliza implicit ST1.
Instrucţiunea constituie un mijloc eficient de utilizare a instrucţiunilor
în virgulă mobilă pe elementele existente în stivă. De exemplu,
secvenţa următoare extrage rădăcina pătrată din al treilea registru din
stivă:
FXCH ST3
FSQRT
FXCH ST3

Următorul program demonstrează cum pot fi utilizate valorile constante


prestabilite.
;
;fpuconsts.asm
;
section .text
global _start
_start:
nop
fld1
fldz
fldpi
fldl2t
fldl2e
fldlg2
fldln2

mov eax,1
mov ebx,0
int 80h

(gdb) info all


st0 0.6931471805599453094286904741849753
st1 0.30102999566398119522564642835948945
st2 1.4426950408889634073876517827983434
st3 3.3219280948873623478083405569094566
st4 3.1415926535897932385128089594061862
st5 0 (raw 0x00000000000000000000)
st6 1 (raw 0x3fff8000000000000000)
st7 0 (raw 0x00000000000000000000)

Valorile sunt plasate în ordinea inversă introducerii lor.

8.3.2. Operaţii aritmetice în virgulă mobilă

Aşa cum era de aşteptat, unitatea de calcul în virgulă mobilă pune la


dispoziţie şi instrucţiuni pentru realizarea funcţiilor matematice de bază cu valori în
virgulă mobilă. Funcţiile matematice de bază sunt descrise în următorul tabel.
Tabelul 8.4 Operaţii aritmetice în virgulă mobilă
Instrucţiune Descriere
FADD Adunare în virgulă mobilă
FSUB Scădere în virgulă mobilă
FSUBR Scădere inversă în virgulă mobilă
FMUL Îmnulţire în virgulă mobilă
FDIV Împărţire în virgulă mobilă
FDIVR Împărţire inversă în virgulă mobilă

Aceste instrucţiuni permit minimizarea referinţelor la memorie şi optimizează


utilizarea stivei de registre. Fiecare poate îmbrăca o varietate de forme.

Adunarea

Instrucţiunea FADD poate fi utilizată în următoarele forme:


• FADD sursă – adună la registrul ST0 un operand în simplă sau dublă
precizie aflat în memorie. Rezultatul rămâne în ST0.
• FADD ST(i),ST0 – adună ST0 la ST(i) şi stochează rezultatul în
ST(i).
• FADD ST0,ST(i) – adună ST(i) la ST0 şi stochează rezultatul în
ST0.
• FADDP ST(i),ST0 – adună ST0 la ST(i), stochează rezultatul în
ST(i) şi glisează (pop) ST0.
• FADDP – adună ST0 cu ST1, stochează rezultatul în ST1 şi glisează
stiva de registre. Rezultatul se va afla în ST0. Valoarea iniţială din ST1
este pierdută, valoarea din ST0 se va afla în ST7.
• FIADD întreg – adună un întreg de 16 sau 32 de biţi la ST0 şi
stochează rezultatul în ST0.

Următorul program demonstrează afirmaţiile anterioare.


;
;fadd.asm
;
section .data
fa dd 2.5
fb dd 5.5
fc dd 8.0
fd dd 7.0
a dd 12 ;întreg de 32 de biţi
section .text
global _start
_start:
nop
fld dword [fa]
fld dword [fb]
fld dword [fc]
fld dword [fd]
faddp
fadd dword [fc]
faddp st2,st0
fadd st0,st2
faddp
fiadd dword [a]

mov eax,1
mov ebx,0
int 80h

Executaţi primele patru instrucţiuni şi observaţi poziţia valorilor în stiva de


registre.

(gdb) info all


st0 7 (raw 0x4001e000000000000000)
st1 8 (raw 0x40028000000000000000)
st2 5.5 (raw 0x4001b000000000000000)
st3 2.5 (raw 0x4000a000000000000000)
st4 0 (raw 0x00000000000000000000)

Instrucţiunea FADDP adună ST0 cu ST1 (7 + 8), păstrează rezultatul în ST1


(pierdem valoarea 8) şi glisează ST0 (7). Registrul ST0 devine ST7 (7). ST1 (15)
devine vârful stivei de registre şi îşi modifică denumirea în ST0 (15). În urma
acestor operaţii stiva de registre arată astfel:

st0 15 (raw 0x4002f000000000000000)


st1 5.5 (raw 0x4001b000000000000000)
st2 2.5 (raw 0x4000a000000000000000)
st3 0 (raw 0x00000000000000000000)
st3 0 (raw 0x00000000000000000000)
st4 0 (raw 0x00000000000000000000)
st5 0 (raw 0x00000000000000000000)
st6 0 (raw 0x00000000000000000000)
st7 7 (raw 0x4001e000000000000000)

Următoarea instrucţiune adună la ST0 un operand din memorie.

st0 23 (raw 0x4003b800000000000000)


st1 5.5 (raw 0x4001b000000000000000)
st2 2.5 (raw 0x4000a000000000000000)
st3 0 (raw 0x00000000000000000000)

Instrucţiunea FADDP ST2,ST0 adună ST0 la ST2, rezultatul rămâne în ST2, şi


glisează ST0. Aşadar, ST2 devine ST1, iar ST0 devine ST7.

st0 5.5 (raw 0x4001b000000000000000)


st1 25.5 (raw 0x4003cc00000000000000)
st2 0 (raw 0x00000000000000000000)
st3 0 (raw 0x00000000000000000000)
st4 0 (raw 0x00000000000000000000)
st5 0 (raw 0x00000000000000000000)
st6 7 (raw 0x4001e000000000000000)
st7 23 (raw 0x4003b800000000000000)

Instrucţiunea FADD ST0,ST2 adună ST2 la ST0 şi păstrează rezultatul în ST0.


Nu descarcă niciun registru. Ne dăm seama din faptul că instrucţiunea nu este
urmată de sufixul P(OP).

st0 -nan(0xc000000000000000) (raw 0xffffc000000000000000)


st1 25.5 (raw 0x4003cc00000000000000)
st2 0 (raw 0x00000000000000000000)

Dacă ne-am fi aşteptat să rezulte o valoare de 5.5 ne-am înşelat. Registrul ST2 nu
avea valoarea 0, ci era neutilizat (eticheta acestuia din registrul de etichete este 11).
Operaţia de adunare între o valoare şi un registru neutilizat a întors o excepţie de
tip operaţie invalidă; de aici rezultă valoarea NaN în registrul ST0.
Următoarele două instrucţiuni, folosind NaN ca operand, au ca rezultat aceeaşi
valoare.
Este foarte important să ţinem evidenţa stării registrelor de date. Fiecare
variantă a instrucţiunilor aritmetice specifică registrul, rolul său în operaţie şi dacă
stiva de registre glisează sau nu. Nu trebuie să facem presupuneri, de cele mai
multe ori sunt greşite.

Scăderea

Instrucţiunea FSUB are un format similar cu instrucţiunea de adunare.


Instrucţiunea de scădere
fsub sursă
execută operaţia
ST0 = ST0 – sursă
Numai că, spre deosebire de adunare, scăderea nu este comutativă (ST0 –
sursă nu dă acelaşi rezultat ca sursă – ST0). Dacă avem nevoie de operaţia
inversă, sursă – ST0, folosim instrucţiunea FSUBR (FSUB Reverse).

fsubr sursă
execută operaţia
ST0 = sursă – ST0

Similar instrucţiunii de adunare, FSUB are şi versiuni cu doi operanzi de tip


registru, cu sau fără posibilitate de glisare a stivei de registre.
• FSUB ST(i),ST0 – scade ST0 din ST(i) şi stochează rezultatul în
ST(i).
• FSUB ST0,ST(i) – scade ST(i) din ST0 şi stochează rezultatul în
ST0.
• FSUBP – scade ST0 din ST1, reţine rezultatul în ST1 şi glisează ST0.
• FSUBP ST(i),ST0 – scade ST0 din ST(i), stochează rezultatul în
ST(i) şi glisează ST0.
• FSUBRP ST(i),ST0 – scade ST(i) din ST0, stochează rezultatul în
ST(i) şi glisează ST0.

Pentru a scădea un întreg, putem folosi FISUB, pentru scăderea standard, sau
FISUBR, pentru cea inversă. Întregul de 16 sau 32 de biţi trebuie să fie în
memorie.

Înmulţirea

Instrucţiunea de înmulţire are versiuni similare cu instrucţiunea de adunare:


• FMUL sursă – înmulţeşte la registrul ST0 un operand în simplă sau
dublă precizie aflat în memorie. Rezultatul rămâne în ST0.
• FMUL ST(i),ST0 – înmulţeşte ST0 la ST(i) şi stochează rezultatul
în ST(i).
• FMUL ST0,ST(i) – înmulţeşte ST(i) la ST0 şi stochează rezultatul
în ST0.
• FMULP ST(i),ST0 – înmulţeşte ST0 la ST(i), stochează rezultatul
în ST(i) şi glisează ST0.
• FMULP – înmulţeşte ST0 cu ST1, stochează rezultatul în ST1 şi
glisează ST0.
• FIMUL întreg – înmulţeşte un întreg de 16 sau 32 de biţi la ST0 şi
stochează rezultatul în ST0.
Împărţirea

Instrucţiunea de împărţire are versiuni similare instrucţiunii de scădere.


Câteva din acestea sunt:
• FDIV sursă – împarte conţinutul registrului ST0 la sursă şi
păstrează rezultatul în ST0 (ST0 = ST0/sursă). Operandul sursă
poate fi o valoare în simplă sau dublă precizie aflată în memorie.
• FDIVR sursă –împarte conţinutul sursei la registrul ST0 şi
păstrează rezultatul în ST0 (ST0 = sursă/ST0).
• FDIV ST(i),ST0 – împarte ST(i) la ST0 şi stochează rezultatul în
ST(i).
• FDIV ST0,ST(i) – împarte ST0 la ST(i) şi stochează rezultatul în
ST0.
• FDIVP – împarte ST1 la ST0, reţine rezultatul în ST1 şi glisează ST0.
• FDIVRP – împarte ST0 la ST1, reţine rezultatul în ST1 şi glisează
ST0.
• FDIVP ST(i),ST0 – împarte ST(i) la ST0, stochează rezultatul în
ST(i), şi glisează ST0.
• FDIVRP ST(i),ST0 – împarte ST0 din ST(i), stochează rezultatul
în ST(i) şi glisează ST0.
• FIDIV întreg – împarte ST0 la un întreg de 16 sau 32 de biţi aflat
în memorie.

8.3.3. Instrucţiuni transcedentale

Unitatea în virgulă mobilă pune la dispoziţie mult mai multe funcţii


matematice în virgulă mobilă decât simpla adunare, scădere, înmulţire şi împărţire.
Grupul instrucţiunilor transcedentale realizează calcule mari consumatoare de timp.
Cuprinde toate funcţiile trigonometrice, hiperbolice, inversele acestora, logaritmice
şi exponenţiale. Toate aceste instrucţiuni operează asupra primului sau primelor
două elemente din vârful stivei şi returnează rezultatul lor în stivă. Toţi operanzii
trebuie să fie normalizaţi, ceilalţi sunt consideraţi invalizi. Dacă un operand este
invalid, instrucţiunea va furniza un rezultat nedefinit, fără semnalarea unei excepţii.
Următorul tabel prezintă câteva din aceste funcţii avansate.
Tabelul 8.5 Instrucţiuni transcedentale
Instrucţiune Descriere
FSIN Calculează sinus din valoarea lui ST0
FCOS Calculează cosinus din valoarea lui ST0
FSINCOS Calculează atât sin cât şi cos din valoarea lui ST0
FABS Calculează valoarea absolută din valoarea lui ST0
FCHS Schimbă semnul valorii din ST0
FSCALE Calculează ST0 la puterea ST1
FSQRT Calculează rădăcina pătrată a valorii din ST0
FRNDINT Rotunjeşte la cel mai apropiat întreg valoarea lui ST0
F2XM1 Calculează 2 la puterea ST0, minus 1
FPATAN Înlocuieşte ST1 cu arctan (ST1/ST0) şi descarcă ST0
FPTAN Înlocuieşte ST0 cu tangenta sa şi introduce 1 în stiva de registre
FYL2X Calculează ST1 = ST1 * log2 ST0 şi descarcă (extrage) stiva
FYL2XP1 Calculează ST1 = ST1 * log2 (ST0+1.0)

8.3.4. Instrucţiuni de comparare

Din nefericire, compararea numerelor în virgulă mobilă nu este atât de


facilă precum aceea a numerelor întregi. Când lucrăm cu întregi şi dorim să aflăm
dacă o valoare este mai mare decât, egală cu, sau mai mare decât, este uşor să
folosim instrucţiunea CMP şi să evaluăm valorile din registrul de stare EFLAGS. În
cazul numerelor în virgulă mobilă nu putem folosi instrucţiunea CMP. Unitatea
FPU oferă propriile instrucţiuni pentru compararea acestora. Una este

fcom sursă

Fcom compară valoarea din ST0 cu sursa şi setează indicatorii de stare FPU.
Operandul sursă poate fi în memorie sau într-un registru. Dacă nu se dă niciun
operand, instrucţiunea FCOM compară registrul ST0 cu registrul ST1. Mai multe
variante ale instrucţiunii sunt date în Tabelul 8.6.
Tabelul 8.6 Instrucţiuni de comparare în virgulă mobilă
Instrucţiune Descriere
FCOM sursă Compară ST0 cu o valoare de 32 sau 64 de biţi din memorie
FCOM Compară registrul ST0 cu registrul ST1
FCOM ST(i) Compară registrul ST0 cu alt registrul de date
FCOMP Compară ST0 cu ST1 şi glisează stiva de registre
FCOMP ST(i) Compară ST0 cu alt registru de date şi glisează stiva de registre
FCOMP sursă Compară ST0 cu o valoare din memorie şi glisează stiva
FCOMPP Compară ST0 cu ST1 şi glisează stiva de două ori
FTST Compară registrul ST0 cu valoarea 0.0
FICOM întreg Compară ST0 un întreg de 16 sau 32 de biţi

Rezultatul comparaţiei setează biţii codurilor de condiţii C0, C2 şi C3, din registrul
de stare.

Condiţie C3 C2 C0
ST0 > sursă 0 0 0
ST0 < sursă 0 0 1
ST0 = sursă 1 0 0

Aşa cum am menţionat înainte, C1 indică condiţia de depăşire a stivei. Totuşi,


acesta este utilizat de o instrucţiune din această familie, instrucţiunea FXAM.
Aceasta examinează numărul din ST0 şi încarcă bitul de semn în indicatorul C1 (0
pozitiv, 1 negativ). În plus, specifică tipul numărului prin intermediul celorlalţi trei
biţi de condiţie.

Tip C3 C2 C0
Neacceptat 0 0 0
NaN 0 0 1
Normalizat 0 1 0
Infinit 0 1 1
Zero 1 0 0
Gol 1 0 1
Denormalizat 1 1 1

Tipul neacceptat este un format care nu face parte din standardul IEEE 754.
Celelalte tipuri au fost deja întalnite pe parcurs.
Pentru a determina starea biţilor de condiţie (rezultatul comparaţiei) trebuie
să copiem valoarea registrului de stare în registrul AX sau la o locaţie de memorie
cu instrucţiunea FSTSW, şi apoi să o încărcăm în registrul EFLAGS cu
instrucţiunea SAHF.
;
;fcom.asm
;
section .data
fs1 dd 1.923
fs2 dd 4.5532
section .text
global _start
_start:
nop
fld dword [fs1]
fcom dword [fs2]
fstsw AX
sahf
ja greater
jb lessthan
mov eax,1
mov ebx,0
int 80h
greater:
mov eax,1
mov ebx,2
int 80h
lessthan:
mov eax,1
mov ebx,1
int 80h

Instrucţiunea SAHF mută biţii 0, 2, 4, 6 şi 7 din AH pe poziţia biţilor CF, PF, AF,
ZF şi SF din registrul EFLAGS. Acestă mapare a biţilor din registrul de stare FPU
la respectivii biţi EFLAGS este una intenţionată. Aşadar, instrucţiunea FSTSW
urmată de SAHF are ca rezultat:
• mutarea bitului C0 pe poziţia CF
• mutarea bitului C2 pe poziţia PF
• mutarea bitului C3 pe poziţia ZF

În acest moment putem determina rezultatul comparaţiei prin aceleaşi folosite


pentru numerele întregi, JA, JB şi JZ.
Programul fcom.asm produce rezultate diferite în funcţie de valorile setate
în memorie. Codul rezultat poate fi observat cu ajutorul comenzii echo:

./fcom
echo $?
1

Rezultatul 1 indică faptul că prima valoare fs1 este mai mică decât valoarea fs2.
Puteţi modifica valorile din program astfel încât să vă asiguraţi că acesta
funcţionează corect.
Începând cu procesoarele Pentium P6, Intel a pus la dispoziţie un nou
mecanism de comparare: familia de instrucţiuni FCOMI. Instrucţiunea FCOMI şi
variantele sale realizează comparaţii în virgulă mobilă şi indică rezultatul acestora
direct prin biţii CF, PF şi ZF din registrul EFLAGS.
Tabelul 8.7 Instrucţiuni din familia FCOMI
Instrucţiune Descriere
FCOMI Compară registrul ST0 cu registrul ST(i)
FCOMIP Compară ST0 cu registrul ST(i) şi glisează stiva de registre
FUCOMI Verifică corectitudinea valorilor înainte de comparare
FUCOMIP Verifică corectitudinea valorilor înainte de comparare şi glisează
stiva

Aşa cum reiese din Tabelul 8.7, o limitare a instrucţiunilor FCOMI este că pot
compara numai valori din registrele de date FPU, nu şi un registru de date cu o
valoare din memorie. Însă ultimele două instrucţiuni oferă un serviciu indisponibil
instrucţiunilor din familia FCOM. FUCOMI şi FUCOMIP verifică faptul că
valorile ce vor fi comparate se regăsesc într-un format valid (folosesc registrul de
etichetare). Dacă este prezentă o valoare nenormalizată se întoarce o excepţie.
Rezultatul instrucţiunilor FCOMI asupra biţilor din registrul EFLAGS au
următoarele semnificaţii:

Condiţie ZF PF CF
ST0 > ST1 0 0 0
ST0 < ST1 0 0 1
ST0 = ST1 1 0 0

;
;fcomi.asm
;
section .data
fs1 dd 1.4444
fs2 dd 4.5532
section .text
global _start
_start:
nop
fld dword [fs2]
fld dword [fs1]
fcomi st0,st1
ja greater
jb lessthan
mov eax,1
mov ebx,0
int 80h
greater:
mov eax,1
mov ebx,2
int 80h
lessthan:
mov eax,1
mov ebx,1
int 80h

Deoarece instrucţiunea FCOMI compară numai valori din registre FPU, valorile
din memorie sunt încărcate în ordine inversă, astfel încât, la momentul comparării,
valoarea fs1 să se afle în registrul ST0.

8.3.5. Instrucţiuni FPU de transfer condiţionat

Similar instrucţiunilor de transfer condiţionat pentru întregi (CMOV),


instrucţiunile FCMOV permit transferul valorilor în virgulă mobilă în funcţie de
anumite condiţii. Toate instrucţiunile din familia FCMOV mută un registru ST(i) în
registrul ST0 pe baza condiţiilor de adevăr prezente în registrul EFLAGS.
Deoarece operaţia se bazează pe registrul EFLAGS, se obişnuieşte ca instrucţiunea
FCMOV să fie precedată de o instrucţiune FCOMI. Tabelul 8.8 prezintă
instrucţiunile familiei FCMOV.
Tabelul 8.8 Instrucţiuni în virgulă mobilă de transfer condiţionat
Instrucţiune Descriere
FCMOVB Mută dacă ST0 este mai mic decât ST(i) (CF=1)
FCMOVE Mută dacă ST0 este egal cu ST(i) (ZF=1)
FCMOVBE Mută dacă ST0 este mai mic sau egal cu ST(i) (CF=1 sau ZF=1)
FCMOVU Mută dacă ST0 este neordonat (PF=1)
FCMOVNB Mută dacă ST0 nu este mai mic decât ST(i) (CF=0)
FCMOVNE Mută dacă ST0 nu este egal cu ST(i) (ZF=0)
FCMOVNBE Mută dacă ST0 nu este mai mic sau egal cu ST(i) (CF=0 sau ZF=0)
FCMOVNU Mută dacă ST0 nu este neordonat (PF=0)

Instrucţiuni de control
Aceste instrucţiuni se folosesc pentru activităţi de iniţializare, gestionare a
excepţiilor şi comutare de proces. Ele permit salvarea stării curente a unităţii de
calcul în virgulă mobilă (contextul FPU) şi revenirea la aceasta după încheierea
altui proces.
Una din aceste instucţiuni este FSTENV, utilizată pentru stocarea într-o
zonă de memorie a întregului context FPU. Sunt salvate următoarele informaţii:
• registrul de control,
• registrul de stare,
• registrul de etichete,
• valoarea indicatorului de instrucţiune FPU,
• valoarea indicatorului de date FPU,
• valoarea ultimului cod operaţional FPU executat.

Valorile sunt stocate într-un bloc de memorie de 28 de octeţi. Instrucţiunea


FLDENV efectuează procesul invers, încarcă valorile din blocul de memorie înapoi
în unitatea FPU. Următorul program demonstrează efectul acestor instrucţiuni.
;
;fpuenv.asm
;
section .data
fs1 dd 34.78
fs2 dd 78.34
fs3 dd 100.1
fs4 dd 200.1
w dw 0b7fH
section .bss
buffer resb 28
section .text
global _start
_start:
nop
finit
fld dword [fs1]
fld dword [fs2]
fldcw [w]
fstenv [buffer]

finit
fld dword [fs3]
fld dword [fs4]
fldenv [buffer]

mov eax,1
mov ebx,0
int 80h

Programul fpuenv.asm iniţializează unitatea FPU, încarcă câteva valori în registrele


de date FPU, modifică câmpul biţilor de rotunjire din registrul de control şi
stochează contextul FPU în cei 28 de octeţi ai locaţiei de memorie buffer.
Înainte de instrucţiunea FSTENV registrele FPU arată astfel:

(gdb) info all


st0 78.339996337890625 (raw 0x40059cae140000000000)
st1 34.779998779296875 (raw 0x40048b1eb80000000000)
st2 0 (raw 0x00000000000000000000)
st3 0 (raw 0x00000000000000000000)
st4 0 (raw 0x00000000000000000000)
st5 0 (raw 0x00000000000000000000)
st6 0 (raw 0x00000000000000000000)
st7 0 (raw 0x00000000000000000000)
fctrl 0xb7f 2943
fstat 0x3000 12288
ftag 0xfff 4095

Observaţi valorile registrelor de control, stare şi etichete. Aceleaşi valori sunt


prezente în buffer după execuţia instrucţiunii FSTENV:

(gdb) x /28xb &buffer


0x80490d4 <buffer>: 0x7f 0x0b 0xff 0xff 0x00 0x30 0xff
0xff
0x80490dc <buffer+8>: 0xff 0x0f 0xff 0xff 0x8a 0x80 0x04
0x08
0x80490e4 <buffer+16>: 0x00 0x00 0x05 0x01 0xc4 0x90
0x04 0x08
0x80490ec <buffer+24>: 0x00 0x00 0xff 0xff

După salvarea stării curente, unitatea FPU este iniţializată şi se introduc din nou
câteva valori de date. Priviţi valorile înregistrate de registrele FPU înainte şi după
execuţia instrucţiunii FLDENV. Observaţi că în urma execuţiei FLDENV valorile
registrelor de date ST0...ST7 nu au fost restaurate, dar registrele de control, stare şi
etichete indică valorile dinainte de instrucţiunea FSTENV.
Instrucţiunea FSTENV stochează contextul FPU, dar nu şi valorile
registrelor de date. Salvarea context FPU plus date se face cu instrucţiunea FSAVE.
Instrucţiunea FSAVE copiază într-o locaţie de memorie de 108 octeţi toate
registrele interne ale unităţii FPU (inclusiv registrele de date), după care o
reiniţializează. La restaurarea cu instrucţiunea FRSTOR, toate registrele sunt
readuse la valoarea dinaintea execuţiei FSAVE.
;
;fpusave.asm
;
section .data
fs1 dd 34.78
fs2 dd 78.34
fs3 dd 100.1
fs4 dd 200.1
w dw 0b7fH
section .bss
buffer resb 108
section .text
global _start
_start:
nop
finit
fld dword [fs1]
fld dword [fs2]
fldcw [w]
fsave [buffer]

fld dword [fs3]


fld dword [fs4]
frstor [buffer]

mov eax,1
mov ebx,0
int 80h

În urma execuţiei FSAVE, zona de memorie nu conţine numai valorile din


registrele de control, stare şi etichete, ci şi pe cele ale registrelor de date. De
asemenea, FSAVE realizează automat iniţializarea unităţii FPU (de aceea acest
program nu conţine al doilea FINIT). După execuţia instrucţiunii FRSTOR puteţi
observa că toate registrele sunt readuse la starea dinaintea FSAVE.
Alte instrucţiuni din această categorie sunt prezentate pe scurt în Tabelul
8.9. Multe din instucţiunile de control ale unităţii FPU au două forme:
• formă wait, prefixată numai cu litera F, de exemplu FINIT.
• formă non-wait, prefixată cu FN, de exemplu FNINIT.

Termenii wait şi non-wait se referă la modul în care instrucţiunile tratează


excepţiile în virgulă mobilă. Excepţiile au fost discutate anterior în secţiunea
dedicată registrului de stare. Instrucţiunile în virgulă mobilă pot genera şase tipuri
de excepţii. De obicei, acestea semnalizează apariţia unei erori aritmetice.
Majoritatea instrucţiunilor aşteaptă (sunt întârziate) să verifice dacă nu
cumva instrucţiunea anterioară returnează o excepţie. Dacă este prezentă o
excepţie, aceasta trebuie tratată înainte de execuţia următoarei instrucţiuni.
Alternativ, unele instrucţiuni au versiuni non-wait, care nu aşteaptă să verifice
apariţia unei excepţii în virgulă mobilă. Aceste instrucţiuni permit programului să
salveze sau să reseteze starea curent a unităţii FPU fără a trata excepţii.
Tabelul 8.9 Instrucţiuni de control FPU
Instrucţiune Descriere
FINIT/FNINIT Iniţializează unitatea FPU
FCLEX/FNCLEX Şterge toţi indicatorii de excepţie din cuvântul de stare
FSAVE/FNSAVE Salvează în memorie contextul complet al unităţii FPU
FRSTOR Încarcă din memorie contextul complet al unităţii FPU
FSTENV/FNSTENV Salvează în memorie contextul FPU
FLDENV Încarcă contextul FPU din memorie
FSTCW/FNSTCW Salvează în memorie cuvântul de stare curent
FSTSW/FNSTSW Salvează cuvântul de stare curent în AX sau într-o locaţie de
memorie
FLDCW Încarcă cuvântul de stare din memorie
FDECSTP Decrementează câmpul TOP din cuvântul de stare. Dacă
TOP este 0, prin decrementare devine 7.
FINCSTP Incrementează câmpult TOP. Dacă TOP este 7, devine 0.
FFREE ST(i) Marchează ST(i) ca neutilizat
FNOP Echivalentul NOP de la numere întregi

8.4. Exerciţii

8.1. Convertiţi manual următoarele numere în format IEEE 754 de 32 de biţi.

a) 1.1 e) -2015.3125
b) -0.1 f) 0.33
c) 2005.0 g) -0.67
d) 0.0039 h) 3.14

8.2. Convertiţi manual în numere zecimale următoarele valori date în format IEEE
754 simplă precizie.

a) 4000 0000 e) c180 4000


b) bf80 0000 f) 42f6 e666
c) 3d80 0000 g) 3f99 999a
d) c259 48b4 h) 42c8 1000

8.3. Următorul program calculează rădăcinile unei ecuaţii de gradul al II-lea,

!! ! + !" + ! = 0.

Cele două rădăcini sunt definite astfel:

−! + ! ! − 4!"
!1 = ,
2!
−! − ! ! − 4!"
!1 = ,
2!

Rădăcinile sunt reale dacă ! ! ≥ 4!", în caz contrar, rădăcinile sunt imaginare.
Rulaţi programul şi observaţi efectul fiecărei instrucţiuni. Modificaţi valorile
coeficienţilor şi, pentru fiecare caz, la finalul programului, afişaţi rădăcinile cu
ajutorul comenzii x /1gf &r1, respectiv &r2.

section .data
a dq 2.0
b dq -3.0
c dq 1.0
section .bss
r1 resq 1
r2 resq 1
real resb 1
section .text
global _start
_start:
nop
finit
fld qword [a]
fadd ST0
fld qword [a]
fld qword [c]
fmulp ST1
fadd ST0
fadd ST0
fchs
fld qword [b]
fld qword [b]
fmulp ST1
faddp ST1
ftst
fstsw AX
sahf
jb no_real_roots
fsqrt
fld qword [b]
fchs
fadd ST1
fdiv ST2
fstp qword [r1]
fchs
fld qword [b]
fsubp ST1
fdivrp ST1
fstp qword [r2]
mov al,1
mov [real],al
jmp sfarsit
no_real_roots:
mov al,0
mov [real],al
sfarsit:
mov eax,1
mov ebx,0
int 80h
9. FUNCŢII

Programele prezentate până acum au fost alcătuite dintr-un singur modul


(prin modul înțelegând o singură unitate logică de prelucrare). Acest lucru a fost
posibil deoarece problemele au fost simple. Numai problemele simple pot fi
rezolvate într-o singură secvenţă de cod. De obicei, pentru rezolvarea unor
aplicaţii complexe, trebuie să descompunem problema în subprobleme mai simple,
relativ independente, scriind module de program distincte pentru fiecare din
acestea. De asemenea, dacă o aplicaţie necesită rularea repetată a unei secvenţe
de instrucţiuni, în loc să scriem de mai multe ori acelaşi cod este indicat să
introducem secvenţa respectivă într-un modul separat ce poate fi apelat de oriunde
din program. Modulele prelucreză valori de intrare primite din program (numit
program principal) și returnează rezultate. Acest capitol prezintă posibilităţile de
care dispune un programator în limbaj de asamblare pentru a crea module
dedicate unor procese simple, regulile pe care trebuie să le respecte în elaborarea
acestora, precum și modul de interacțiune a modulelor cu programul principal.

9.1. Modularizarea programelor

În matematică, putem obține funcții oricât de complexe prin compunerea


unor funcții elementare (funcția polinomială, funcția de ridicare la putere sau de
extragere radical, etc.). În programare, putem construi programe oricât de
complexe prin tehnica de modularizare, adică prin înlănțuirea logică a unor “unități
elementare” de program: citirea datelor de intrare (modulul de citire a datelor),
prelucrarea lor (modulul de prelucrare), afișarea rezultatelor obținute în urma
prelucrării (modulul de afișare a rezultatelor). Mulțimea valorilor care constituie
datele de intrare și datele de ieșire ale subprogramului formează lista parametrilor
de intrare, respectiv de ieșire. Din acest punct de vedere, în programare întâlnim
două categorii de subprograme:
• proceduri – subprograme care primesc ca date de intrare oricâte valori și
returnează ca rezultat o singură valoare, mai multe valori, sau niciuna
(poate efectua o prelucrare care să nu aibă ca efect obținerea unei valori);
• funcții – subprograme care primesc ca date de intrare oricâte valori, dar
returnează ca rezultat o valoare și numai una.
Teoretic, o funcție returnează întotdeauna o valoare. Cu toate acestea,
unele limbaje de programare, cum ar fi C, dispun și de funcții “fără tip”, care nu
returnează nicio valoare. În aceste condiții, diferențele dintre proceduri și funcții
încep să devină discutabile. În continuare, pentru consistența cu limbajul C, vom
folosi noțiunea de funcție, singura admisă de acesta. Puteți considera procedura ca
fiind un termen generic pentru funcție.

9.2. Apelarea funcţiilor

În C, însuși programul principal este o funcție cu nume rezervat (main).


Deoarece nu numai programul principal admite funcţii, ci şi o funcţie oarecare
poate avea, la rândul său, alte funcţii, orice program care apelează o funcţie se
numeşte program apelant. Prin apelarea unei funcţii de către un program apelant
înţelegem o comandă pe care programul apelant o trimite funcţiei, comandă prin
care îi cere acesteia să execute operaţiile din blocul său de instrucţiuni.
O funcţie poate fi apelată ori de câte ori este nevoie şi conţine tot codul
necesar realizării complete a unui proces. Nu necesită asistenţă din partea
programului apelant. Parametrii (datele de intrare) cu care lucrează funcţia pot fi
diferiţi de la apel la apel şi sunt furnizaţi acesteia din interiorul programului
apelant.

_start:

call <nume_funcţie>

exit

<nume_funcţie>:

ret
Figura 9.1 Apelarea funcțiilor
La apelul unei funcţii, execuţia programului sare la prima instrucţiune a acesteia.
Procesorul execută instrucţiunile funcţiei până când întâlneşte instrucţiunea RET.
RET redă controlul programului apelant din locul în care a fost apelată funcţia.

9.3. Definirea funcţiilor

Crearea unei funcţii în limbajul de asamblare presupune trei etape:


• definirea valorilor de intrare necesare acesteia,
• definirea operaţiilor care trebuie efectuate asupra valorilor de intrare,
• definirea modului de obținere și transmitere către programul apelant a
valorilor de ieşire.

Modul în care se defineşte o funcţie depinde de asamblor. În YASM, trebuie numai


să dăm un nume secvenţei de instrucţiuni respective. Odată creată, rutina poate fi
accesată de oriunde din programul principal prin instrucţiunea CALL. Instrucţiunea
CALL are un singur operand, numele funcţiei.
Următorul program foloseşte o funcţie pentru a interschimba valorile din
două registre.
;
;functie1.asm
;
section .data
temp dd 0
section .text
global _start
_start:
nop
mov eax,3
mov ebx,4
call _functie

mov eax,1
mov ebx,0
int 080h
_functie:
mov [temp],ebx
mov ebx,eax
mov eax,[temp]
ret
9.4. Transferul controlului

În programul anterior, codul funcţiei a fost plasat la sfârşitul programului


principal. Textul funcţiei poate fi poziţionat la fel de bine şi la început, imediat
după directiva section .text. Când am discutat despre rolul etichetei
_start am precizat că editorul de legături foloseşte această etichetă pentru
aflarea instrucţiunii de început a programului (instrucţiunea cu care trebuie să
înceapă execuţia programului). Înainte de _start putem avea orice număr de
funcţii fără ca execuţia programului să fie afectată în vreun fel. În plus, spre
deosebire de unele limbaje de nivel înalt, în asamblare, funcţiile nu trebuie definite
înainte de apelare. Tot ceea ce caută instrucţiunea CALL este eticheta care specifică
adresa de început a funcţiei, adresă ce va fi încărcată în registrul indicator de
instrucţiune.
Funcţia din programul următor calculează complementul faţă de doi pentru
valorile 3 şi 5, rezultatul regăsindu-se în registrul EDX. Complementul faţă de doi
se obţine cu instrucţiunea NEG – inversează biţii şi adună 1. Dacă vrem să obţinem
complementul faţă de unu, folosim instrucţiunea NOT, actualmente comentată.
;
;funcţie2.asm
;
section .bss
mem resd 1
rez resd 1
section .text

functie:
; not dword [mem] ;complementul faţă de unu
neg dword [mem] ;complementul faţă de doi
mov edx,[mem]
ret
global _start
_start:
nop
mov dword [mem],3
call functie

mov dword [mem],5


call functie

mov eax,1
mov ebx,0
int 080h

Instrucţiunea CALL, la apelare, salvează în stivă conţinutul registrului EIP


şi îl iniţializează cu adresa de început a funcţiei. Numele acesteia este înlocuit de
asamblor cu deplasamentul calculat între locaţia lui CALL și locaţia funcţiei.
Aşadar, în programul anterior, numele functie este un deplasament. Acest
deplasament, după salvarea conţinutului registrului EIP în stivă, este adunat la
valoarea EIP. Se ajunge astfel la începutul funcţiei. Evident, valoarea
deplasamentului poate fi negativă sau pozitivă, în funcţie de poziția funcţiei în
program. Instrucţiunea RET restabileşte în EIP adresa salvată de instrucţiunea
CALL.

9.5. Metode de transfer al parametrilor

De obicei, funcțiile au nevoie de parametri de intrare. O funcție poate primi


parametri de intrare din programul apelant prin trei metode:
• Prin intermediul registrelor;
• Prin intermediul variabilelor globale;
• Prin intermediul stivei.

9.5.1. Prin registre

Aşa cum reiese din nume, metoda transferului prin registre presupune
folosirea unor registre de uz general. Înainte de apelul funcţiei, programul
principal introduce toţi parametrii necesari acesteia în anumite registre.
;
;regTr.asm
;
section .data
d1 dd 3
d2 dd 5
section .bss
temp resd 1
section .text
;definim functiile
global _functie
_functie:
mov [temp],ebx
mov ebx,eax
mov eax,[temp]
;revenire din funcţie în programul principal
ret
;începutul programului principal
global _start
_start:
nop

mov eax,[d1]
mov ebx,[d2]
;intrare în funcţie
call _functie
;revenire din funcţie
mov [d1],eax
mov [d2],ebx

mov eax,1
mov ebx,0
int 080h

Se observă cum funcţia primeşte parametri de intrare de la programul


principal prin registrele EAX şi EBX. Rezultatul este returnat programului prin
aceleaşi registre.
Această metodă este rapidă, toate argumentele se găsesc în registre. Însă,
dacă folosim registre şi în corpul funcţiei, nu există nicio garanţie că valoarea
acestora va fi identică cu cea de la intrarea în funcţie. Altfel spus, e posibil ca
valorile registrelor de la ieşirea din funcţie să difere de cele de până la intrarea în
funcţie. Din acest motiv este indicat să urmărim registrele folosite de funcţie pentru
procesul ei intern. Atât registrele cât şi locaţiile de memorie folosite în interiorul
funcţiei pot avea altă valoare la reîntoarcerea în program. Dacă o funcţie modifică
registrele folosite de programul principal este crucial să salvăm conţinutul lor
înainte de apelul funcţiei. Salvarea se poate face cu instrucţiunile PUSH, PUSHA.
Restaurarea cu POP, POPA. De asemenea, trebuie să fim atenţi care sunt registrele
folosite de funcție pentru returnarea rezultatului. Dacă acestea sunt suprascrise,
funcţia a fost efectuată inutil.
Din moment ce procesorul nu dispune de foarte multe registre de uz
general, un dezavantaj implicit al metodei este numărul mic de argumente care
poate fi furnizat funcţiei.
Dezavantajele modalităţii de transmitere a parametrilor prin intermediul
registrelor:
• neconvenabilă - dacă se transmite un număr mare de parametrii,
numărul registrelor poate fi insuficient;
• neeconomică sub aspectul timpului de execuţie - necesită salvări şi
restaurări frecvente de registre.

9.5.2. Prin variabile globale

În această metodă, parametrii de intrare sunt transmişi funcţiei prin


intermediul unor variabile ce pot fi accesate atât de programul principal cât şi de
funcţie. Toate funcţiile au acces la locaţiile de memorie definite în programul
principal. Deoarece aceste locaţii sunt accesibile tuturor funcţiilor, ele se numesc
variabile globale. Funcţiile pot folosi variabilele globale în orice scop, incluzând
transferul datelor între program şi funcţii.
;
;memTr.asm
;
section .bss
mem resd 1
rez resd 1
section .text
functie:
; not dword [mem]
neg dword [mem]
mov edx,[mem]
ret
global _start
_start:
nop
mov dword [mem],3
call functie

mov dword [mem],5


call functie

mov eax,1
mov ebx,0
int 080h

Programul memTr.asm demonstrează cum pot fi interschimbate date între


funcţie şi programul principal prin intermediul variabilei globale mem.
Transmiterea parametrilor prin variabile globale presupune rezervarea de
memorie pentru fiecare din aceștia, aşadar poate fi neeconomică din punct de
vedere al spaţiului de memorie ocupat (în special dacă se folosesc structuri mari de
date necesare numai temporar).

9.5.3. Prin stivă

Stiva este accesibilă programului principal, dar şi tuturor funcţiilor folosite


în acel program, permiţând schimbul facil de date între programul apelant şi
funcţii. Prin folosirea stivei ca mijloc de tranfer al parametrilor sunt evitate
problemele ridicate de transferul datelor prin registre sau memorie. Aceasta este şi
modalitatea de transfer a parametrilor preferată de toate funcţiile limbajului C. Din
acest motiv, metoda se mai numeşte „apelul funcţiei în stilul C” (C style function
calling).
Dacă este folosită stiva, metoda obişnuită de returnare a rezultatelor în
programul apelant se face prin:
• registrul EAX – pentru rezultate de 32 de biţi;
• alăturarea registrelor EDX:EAX – pentru rezultate de 64 de biţi;
• FPU ST(0) – pentru valori în virgulă mobilă (floating point).

Ne amintim că stiva este alcătuită din locaţii de memorie rezervate la


sfârşitul spaţiului de memorie alocat programului. Datele pot fi introduse sau
extrase numai de la vârful stivei. Vârful stivei este indicat de registrul ESP.

În mod obişnuit, datele sunt introduse în stivă cu instrucţiunea PUSH şi


extrase cu instrucţiunea POP. PUSH plasează elementul de date la adresa indicată
de registrul ESP şi totodată decrementează automat valoarea acestuia astfel încât să
indice următoarea locaţie liberă. POP extrage din stivă elementul de date (într-un
registru sau locaţie de memorie) şi incrementează valoarea registrului ESP astfel
încât acesta să indice valoarea precedentă.
Rulaţi următorul program.

; Afişăm valoarea registrului ESP:


;cStyleCall.asm (gdb) print /x $esp
; $1 = 0xbfc65cd0
section .bss (gdb) n
rez1 resd 1 9 call functie
rez2 resd 1 (gdb) print /x $esp
section .text $2 = 0xbfc65ccc
global _start
_start: Afişăm conţinutul stivei:
nop (gdb) x /1bd 0xbfc65ccc
push 5 0xbfc65ccc: 5
call functie (gdb) n
mov [rez1],eax 20 pop eax
(gdb) print /x $esp
push 10 $3 = 0xbfc65cc8
call functie (gdb) x /xw 0xbfc65cc8
mov [rez2],eax 0xbfc65cc8: 0x0804808b
(gdb) x /2xw 0xbfc65cc8
mov eax,1
0xbfc65cc8: 0x0804808b 0x00000005
mov ebx,0
(gdb) n
int 080h
functie: 21 neg eax
pop eax (gdb) print /x $esp
neg eax $4 = 0xbfc65ccc
(gdb) print /x $eax
ret $5 = 0x804808b
(gdb) n
23 ret
(gdb) print /x $eax
$6 = 0xf7fb7f75
(gdb) n
0x00000005 in ?? ()

Teoretic, programul cSyleCall.asm ar fi trebuit să funcţioneze corect.


Programul introduce paramentrul 5 în stivă şi apelează funcţia. Funcţia extrage
paramentrul din stivă, îi calculează complementul faţă de doi şi returnează
rezultatul în registrul EAX. Dacă nu aţi făcut-o deja, rulaţi programul pas cu pas,
privind în stivă. Într-adevăr, programul principal introduce numărul 5 în stivă (de
unde ar trebui preluat de funcţie cu ajutorul instrucţiunii POP). Dar am uitat că la
apelare, instrucţiunea CALL introduce automat în stivă adresa de revenire în
programul apelant. Aşadar, ceea ce extrage POP-ul funcţiei este adresa de revenire,
căreia îi calculează complementul faţă de doi. Pentru că adresa a fost extrasă din
stivă, instrucţiunea RET furnizează programului ca adresă de revenire valoarea 5.
Programul nu mai poate continua. Am pierdut valoarea adresei de revenire.
Aspectul stivei la apelul funcţiei poate fi reprezentat astfel:
Stivă

Paramentrul funcţiei
Adresa de revenire ESP
31 0

Recapitulăm paşii care apar în cazul furnizării parametrilor funcţiei prin


intermediul stivei.
1. Înainte de apelul funcţiei (prin instrucţiunea CALL), programul apelant
plasează la vârful stivei paramentrii de intrare necesari acesteia. Dacă
funcţia trebuie să primească mai mulţi parametrii, standardul C stabilește
ca aceştia să fie introduşi în stivă în ordinea inversă extragerii lor de către
funcţie (lucru normal din moment ce nu putem extrage din stivă decât în
ordinea inversă introducerii).
2. Când este executată, instrucţiunea CALL plasează adresa de revenire în
programul apelant tot la vârful stivei, astfel încât funcţia să ştie de unde să
redea controlul acestuia.

În urma acestor paşi, reprezentarea stivei arată astfel:

Stivă

Paramentru 3 funcţie
Parametru 2 funcţie
Paramentru 1 funcţie
Adresa de revenire ESP
31 0

Registrul ESP indică locaţia adresei de revenire în program. Toţi parametrii


de intrare ai funcţiei se găsesc „deasupra” adresei de revenire în program. Dacă
extragem parametrii de intrare cu instrucţiunea POP, adresa de revenire s-ar putea
pierde (aşa cum s-a întamplat în programul nostru anterior). Aşadar trebuie să
recurgem la o metodă diferită de extragere a parametrilor din stivă.
Într-un capitol anterior am prezentat adresarea bazată a memoriei.
Adresarea bazată pune la dispoziţie o metodă de acces la locaţii de memorie pe
baza unei adrese aflate într-un registru. Deoarece ESP indică vârful stivei, pentru a
accesa parametrii de intrare fără a fi nevoie de extragerea lor cu POP, funcţia poate
folosi adresarea bazată cu registrul ESP drept registru de bază. Fiecare paramentru
de intrare poate fi accesat indirect printr-un deplasament adunat la adresa din
registrul ESP (Atenţie, în figură, fiecare căsuţă reprezintă 4 octeţi).

Stivă

Paramentru 3 funcţie ESP + 12


Parametru 2 funcţie ESP + 8
Paramentru 1 funcţie ESP + 4
Adresa de revenire ESP
31 0

Luând în considerare modul în care arată stiva, putem schimba funcţia


programului nostru astfel:

functie:
mov eax,[esp+4]
neg eax
ret

Rulăm programul şi vedem că de această dată adresa de revenire este


păstrată şi totul funcţionează corect. Totuşi, nu am scăpat complet de neplăceri.

Problemă I:

Este posibil ca, în timpul rulării funcţiei, aceasta să introducă date în stivă. Dacă se
întâmplă acest lucru, valoarea lui ESP se modifică şi adresarea bazată nu dă
rezultatul scontat. Presupunem că funcţia trebuie să introducă în stivă conţinutul
registrului EBX. Se rulează programul schimbând secvenţa de cod a funcţiei astfel:
functie:
push ebx
mov eax,[esp+4]
neg eax
pop ebx
ret

Prin introducerea lui EBX în stivă, ESP + 4 indică adresa de revenire. Iarăşi
extragem adresa de revenire şi îi calculăm complementul faţă de doi. De această
dată însă, din moment ce nu am extras-o cu POP, ci numai am adresat-o indirect,
nu pierdem valoarea adresei de revenire în program. Însă rezultatul funcţiei este
eronat - am calculat complementul față de doi al adresei de revenire, nu al
parametrului de intrare.

Rezolvare I:

Prima instrucțiune a funcției trebuie să salveze vârful inițial al stivei. Se copiază


astfel valoarea registrului ESP în registrul EBP. Acest mecanism garantează că
există întotdeauna un registru care indică corect vârful iniţial al stivei (existent la
momentul apelării funcţiei). Datele introduse de funcţie nu afectează EBP, doar
ESP.

functie:
mov ebp,esp
push ebx
mov eax,[ebp+4]
neg eax
pop ebx
mov esp,ebp
ret

Problemă II:

Registrul EBP poate fi folosit de programul principal. Salvarea adresei de început a


stivei în EBP îi suprascrie acestuia valoarea anterioară şi poate deregla execuţia
corectă a programului apelant.

Rezolvare II:

Înainte de copierea valorii din ESP în EBP, salvăm registrul EBP în stivă. De
acum, codul de început și sfârșit al funcţiei seamănă cu un prolog, respectiv un
epilog.

functie:
push ebp
prolog
mov ebp,esp

<corpul funcţiei>

mov esp,ebp epilog


pop ebp

ret

Stivă

EBP + 16
Paramentru 3 funcţie
Parametru 2 funcţie EBP + 12
Paramentru 1 funcţie EBP + 8
Adresa de revenire EBP + 4
ESP Vechiul EBP EBP
31 0

Datele locale funcţiei

Ultima reprezentare a stivei rezistă până când funcţia însăşi are nevoie să
folosească stiva pentru stocarea variabilelor locale (variabilele din interiorul
funcţiei).
Funcţia ar putea folosi registre pentru stocarea propriilor variabile, dar
acestea pun la dispoziţie un spaţiu limitat de manevră, sau ar putea folosi variabile
globale, ceea ce ar însemna să le creăm noi în programul principal (să punem la
dispoziţie elemente de date dedicate funcţiei). Aşadar, tot stiva rămâne cea mai
bună soluţie.
Să ne amintim: o dată ce am fixat EBP la vârful stivei, orice variabilă
folosită în funcţie (variabilă locală) poate fi plasată în stivă după acel punct fără să
afecteze modul de acces la paramentrii de intrare. Ele pot fi adresate uşor prin
intermediul registrul EBP. De exemplu, presupunând valori de 4 octeţi, prima
variabilă locală poate fi adresată cu [EBP - 4], a doua cu [EBP - 8], etc..Funcţia
care calculează complementul faţă de doi şi adună doi, devine:

functie:
push ebp
prolog
mov ebp,esp

push ebx
mov eax,[ebp+4]
mov [ebp-4],2 ;variabilă locală 1
mov [ebp-8],6 ;variabilă locală 2
mov [ebp-12],7 ;variabilă locală 3
neg eax
add eax,[ebp-4]
pop ebx

mov esp,ebp
pop ebp epilog

ret

Stivă

Paramentru 3 funcţie EBP + 16


Parametru 2 funcţie EBP + 12
Paramentru 1 funcţie EBP + 8
Adresa de revenire EBP + 4
ESP Vechiul EBP EBP
Variabilă locală 1 EBP - 4
Variabilă locală 2 EBP - 8
Variabilă locală 3 EBP - 12
31 0
Problemă III:

Așa cum reiese din ultima figură, ESP încă indică vechea adresă a EBP. Dacă
funcţia plasează în stivă variabile locale (ca în exemplul anterior – Variabila locală
1,2,3), dar în corpul ei mai conţine şi instrucţiuni PUSH, acestea, folosind ESP,
suprascriu valorile variabilelor locale. Pierdem variabilele funcţiei.

functie:
push ebp
Prolog (intrarea în funcție)
mov ebp,esp

push ebx
mov eax,[ebp+4]
mov [ebp-4],2 ;variabilă locală 1
mov [ebp-8],6 ;variabilă locală 2
mov [ebp-12],7 ;variabilă locală 3
push 20
push 60 Suprascriu variabilele locale
push 70
neg eax
add eax,[ebp-4]
pop ebx

mov esp,ebp
pop ebp Epilog (ieșirea din funcție)

ret

Rezolvare III:

La începutul funcţiei, imediat după secvenţa de intrare în funcţie (după prolog),


rezervăm în stivă un anumit spaţiu pentru variabilele locale, scăzând din ESP
valoarea dorită. Astfel, dacă vor fi introduse în stivă date suplimentare, acestea vor
fi plasate sub variabilele locale. În aceste condiţii, registrul ESP poate fi folosit în
mod obişnuit, cu instrucţiunea PUSH, fără să afecteze variabilele funcţiei.

La sfârşitul funcţiei, registrul ESP este rescris cu valoarea sa de început


(în epilog) şi variabilele funcţiei vor fi pierdute din stivă – de aici şi denumirea de
variabile locale.

Funcţia programului nostru devine:


functie:
push ebp
prolog
mov ebp,esp
sub esp,12 ;rezervă 12 octeţi pentru variabilele locale

push ebx
mov eax,[ebp+4]
mov [ebp-4],2 ;variabila locală 1
mov [ebp-8],6 ;variabila locală 2
mov [ebp-12],7 ;variabila locală 3
push 20
push 60 variabilele locale nu sunt suprascrise
push 70
neg eax
add eax,[ebp-4]
pop ebx

mov esp,ebp
pop ebp epilog
ret

9.5.4. Structura funcţiei

Structura de ansamblu a funcţiei, cu noul prolog, este:

functie:
push ebp
mov ebp,esp Prolog (intrarea în funcție)
sub esp,12

Corpul funcției

mov esp,ebp
pop ebp Epilog (ieșirea din funcție)
ret

Aspectul final al stivei este arătat în Figura 9.2. Informaţia stocată în stivă
– parametrii de intrare, adresa de revenire în programul principal, vechea adresă
EBP şi variabilele locale – formează ceea ce poartă numele de cadru de stivă (stack
frame). Valoarea curentă a registrului EBP se numeşte indicator de cadru (frame
pointer). Odată cunoscută, toate elementele cadrului de stivă pot fi accesate prin
intermediul ei.

Stivă

Paramentru 3 funcţie EBP + 16


Parametru 2 funcţie EBP + 12
Paramentru 1 funcţie EBP + 8
Adresa de revenire EBP + 4
Vechiul EBP EBP
Variabilă locală 1 EBP - 4
-12 Variabilă locală 2 EBP - 8
ESP Variabilă locală 3 EBP - 12
31 0

Figura 9.2 Aspectul stivei


Procesoarele Pentium au introdus două instrucţiuni care facilitează alocarea şi
eliberarea cadrelor de stivă. Instrucţiunea ENTER se utilizează pentru alocarea
cadrului de stivă la intrarea în funcţie. Formatul este

enter octeţi,nivel

unde octeţi specifică numărul de octeţi dorit pentru stocarea variabilelor locale,
iar nivel precizează nivelul de imbricare al funcţiei. Dacă specificăm un nivel
diferit de zero, instrucţiunea copiază în noul cadru de stivă nivel indicatori de
cadru, plecând de la începutul cadrului de stivă precedent. Asfel, instrucţiunea

enter <octeţi>, 0

este echivalentă cu secvenţa de instrucţiuni

push ebp
mov ebp,esp
sub esp, <octeţi>

Instrucţiunea LEAVE eliberează cadrul de stivă alocat cu instrucţiunea ENTER. Nu


primeşte niciun operand. Instrucţiunea LEAVE se traduce prin

mov esp,ebp
pop ebp

Instrucţiunea LEAVE trebuie să apară întotdeauna înaintea lui instrucţiunii RET.

Eliberarea stivei

Am văzut că programul principal plasează parametrii de intrare ai funcţiei


în stivă înainte de apelul acesteia. Când se iese din funcţie, aceşti parametrii sunt
tot în stivă, deoarece funcţia i-a accesat prin adresarea indirectă, nu prin
instrucţiunea POP.
Stivă

Parametru 3 funcţie
Paramentru 2 funcţie
Parametru 1 funcţie ESP
31 0

POP extrage din stivă, elementul extras cu POP nu mai rămâne în stivă.
Adresarea bazată îl copiază, elementul rămâne în stivă.

Totuşi, dacă programul principal foloseşte stiva şi în alte scopuri este de


preferat ca acesta să regăsească stiva exact în stadiul în care se afla înainte de
apelul funcţiei (eliberată de parametrii de intrare ai unei funcţii care s-a sfârşit).
Procesul de eliberare a stivei de parametrii nedoriţi poate fi realizat de
către:
• funcţia apelată;
• programul apelant (programul principal).

Dacă funcţiile primesc un număr fix de parametri, se preferă prima metodă.


În acest caz, codul de îndepărtare a parametrilor este scris o singură dată, în corpul
funcţiei, chiar dacă funcţia este apelată de mai multe ori. Aveţi în vedere că nu
puteţi folosi o metodă de genul

functie:
<corpul_funcţiei>
leave
add esp,12
ret

deoarece la execuţia lui RET, ESP trebuie să indice către adresa de revenire în
programul principal. Soluţia este reprezentată de operandul opţional care poate fi
specificat imediat după RET.

ret operand

care rezultă în următoarea secvenţă de instrucţiuni:

EIP ← SS:ESP
ESP ← ESP + 4 + operand

Operandul trebuie să fie un imediat de 16 biţi. Din moment ce scopul acestei valori
opţionale este să descarce parametrii introduşi în stivă, operandul are întotdeauna o
valoare pozitivă.
Totuşi, dacă o funcţie primeşte un număr variabil de parametri, trebuie să
folosim a doua metodă. Aceasta este şi metoda utilizată de compilatoarele C. Din
programul principal, parametrii de intrare pot fi extraşi cu instrucţiunea POP, dar
cel mai indicat este să reiniţializăm registrul ESP cu adresa anterioară apelului
funcţiei. Pentru aceasta se adună la ESP mărimea parametrilor de intrare introduşi
în stivă. De exemplu, dacă introducem în stivă, ca parametri de intrare pentru o
funcţie, trei întregi a câte 4 octeţi fiecare, pentru îndepărtarea acestora din stivă, la
sfârşitul textului funcţiei din programul principal, imediat după instrucţiunea de
apel, trebuie să adunăm valoarea din ESP cu 12.
Tabelul 9.1 Exemple de eliberare a stivei din interiorul programului principal

push eax push eax push eax


push ebx push ebx call functie
push ecx call functie add esp,4
call functie add esp,8
add esp,12
9.6. Mecanisme de transfer al parametrilor

Partea cea mai importantă din procesul de apelare a funcţiilor este


reprezentată de transferul parametrilor la apel. Există două mecanisme de transfer
al parametrilor:
• transfer prin valoare (call-by-value)
• transfer prin referinţă (call-by-reference).

9.6.1. Transfer prin valoare

Mecanismul de transfer al parametrilor prin valoare furnizează funcţiei


apelate numai valoarea curentă a parametrului de care are nevoie. Din această
cauză, programatorul trebuie să aloce două locaţii de memorie: una accesibilă
programului – definită în segmentul de date - pe care funcţia nu o poate modifica,
şi una accesibilă funcţiei – definită pe un nivel de stivă – pe care funcţia o poate
modifica. În acest caz, spunem că transmitem acel parametru prin valoare, şi
valoarea nu poate fi modificată de funcţia apelată.
Următorul program exemplifică transferul parametrilor prin valoare.
Funcţia înmulţeşte două valori primite prin stivă şi tot ea eliberează stiva la final.
Rezultatul este returnat conform standardului de apel C, în perechea de registre
EDX:EAX (fiind valoare de 64 de biţi).
;
;callByValue.asm
;
section .data
val1 dd 150
val2 dd 10
section .text
global _start
_start:
nop
push dword [val1]
push dword [val2]
call multiply
mov eax,1
mov ebx,0
int 80h
multiply:
enter 0,0
mov eax,[ebp+8]
mul dword [ebp+12]
leave
ret 8

Rulăm programul prin intermediul depanatorului. Imediat după comanda de rulare


afişăm conţinutul registrului ESP (vârful stivei). Apoi introducem valorile în stivă.

(gdb) i r esp
esp 0xffffd7d0 0xffffd7d0
(gdb) n
13 push dword [val2]
(gdb)
14 call multiply
(gdb) i r esp
esp 0xffffd7c8 0xffffd7c8

Dacă scădeţi adresa curentă a ESP din adresa iniţială rezultă că au fost introduşi în
stivă 8 octeţi. Nu intraţi încă în funcţie. Studiaţi conţinutul cadrului de stivă curent
cu ajutorul comenzilor backtrace şi info frame. Acesta este cadrul de stivă
al programului principal.
Intraţi în funcţie cu instrucţiunea nexti.

(gdb) ni
0x080480a2 in multiply () at sum.asm:19
19 enter 0,0

Instrucţiunea ENTER a fost deja executată. Puteţi afişa cadrele de stivă prezente şi
detalia cadrul de stivă al funcţiei.

(gdb) bt
#0 0x080480a2 in multiply () at sum.asm:19
#1 0x08048092 in _start () at sum.asm:14
(gdb) info frame
Stack level 0, frame at 0xffffd7c8:
eip = 0x80480a2 in multiply (sum.asm:19); saved eip 0x8048092
called by frame at 0xffffd7cc
source language unknown.
Arglist at 0xffffd7c0, args:
Locals at 0xffffd7c0, Previous frame's sp is 0xffffd7c8
Saved registers:
eip at 0xffffd7c4
(gdb) i r ebp
ebp 0xffffd7c0 0xffffd7c0
(gdb) i r esp
esp 0xffffd7c0 0xffffd7c0

Înainte de intrarea în funcţie, adresa registrului ESP era 0xffffd7c8. După


parcurgerea prologului, adresa din registrul ESP este 0xffffd7c0. O diferenţă de 8
octeţi – 4 octeţi ocupaţi cu adresa de revenire în program (introdusă automat de
instrucţiunea CALL) şi 4 octeţi ocupaţi de valoarea registrului EBP, salvat în stivă
în cadrul prologului (PUSH EBP). Registrul EBP are aceeaşi valoare cu registrul
ESP deoarece a fost executată instrucţiunea MOV EBP,ESP. Aşadar, indicatorul
de cadru EBP are valoarea 0xffffd7c0. Din acest motiv, în descrierea cadrului de
stivă se specifică faptul că lista argumentelor şi a variabilelor locale începe de la
această adresă.

(gdb) x /4wx $ebp


0xffffd7c0: 0x00000000 0x08048092 0x0000000a 0x00000096

Am afişat patru cuvinte de 32 de biţi începând de la adresa EBP. Prima valoare din
stânga, 0x00000000, reprezintă adresa anterioară a registrului EBP, a doua,
0x08048092, reprezintă adresa de revenire în programul apelant (adresa care va fi
încărcată în registrul EIP de instrucţiunea RET), a treia şi a patra valoare sunt
datele de intrare în funcţie - 10, respectiv 150, în hexazecimal.
Următoarele două instrucţiuni efectuează înmulţirea. Prima copiază în
registrul EAX un parametru din stivă prin adresare bazată. Rezultatul rămâne în
combinaţia de registre EDX:EAX. Deoarece valoarea este mică, registrul EDX
rămâne 0.

(gdb) si
0x080480a5 20 mov eax,[ebp+8]
(gdb) i r eax
eax 0xa 10
(gdb) si
0x080480a8 21 mul dword [ebp+12]
(gdb) i r eax
eax 0x5dc 1500
(gdb) i r edx
edx 0x0 0

La ieşirea din funcţie, instrucţiunea RET repoziţionează indicatorul de stivă la


adresa acestuia dinaintea introducerii parametrilor.
(gdb) i r esp
esp 0xffffd7d0 0xffffd7d0

9.6.2. Transfer prin referinţă

Mecanismul de transfer al parametrilor prin referinţă furnizează funcţiei


apelate adresa parametrului de care are nevoie. În acest caz se foloseşte o singură
locaţie de memorie, dar zona de memorie respectivă trebuie să fie vizibilă funcţiei
apelate. Funcţia poate modifica conţinutul parametrului – şi aceste modificări sunt
vizibile din programul apelant – prin manipulearea directă a valorii sale. În acest
caz, spunem că transmitem acel parametru prin adresă.
Următorul program exemplifică transferul parametrilor prin adresă.
Deoarece este echivalentul programului anterior, în continuare ne mărginim să
specificăm numai diferenţele.
;
;callByRef.asm
;
section .data
val1 dd 150
val2 dd 10
section .text
global _start
_start:
nop
push dword val1
push dword val2
call multiply
mov eax,1
mov ebx,0
int 80h
multiply:
enter 0,0
push ebx
mov ebx,[ebp+8]
mov eax,[ebx]
mov ebx,[ebp+12]
mul dword [ebx]
pop ebx
leave
ret 8
Adresa iniţială a indicatorului de stivă este tot 0xffffd7d0.

(gdb) i r esp
esp 0xffffd7d0 0xffffd7d0
(gdb) n
9 push dword val2
(gdb)
10 call multiply
(gdb) x /2wx $esp
0xffffd7c8: 0x080490b4 0x080490b0

Observaţi că în stivă sunt introduse adresele parametrilor, nu valorile acestora.


Valorile sunt găsite prin intermediul acestor adrese.

(gdb) x /1wd 0x080490b4


0x80490b4 <val2>: 10
(gdb) x /1wd 0x080490b0
0x80490b0 <val1>: 150

După executarea instrucţiunii ENTER, indicatorul de cadru poate reprezenta adresă


de bază pentru

(gdb) x /4wx $ebp


0xffffd7c0: 0x00000000 0x08048090 0x080490b4 0x080490b0

vechea valoare a registrului EBP, adresa de revenire în program, şi adresele


parametrilor (nu valorile parametrilor ca în programul precedent).

(gdb) si
0x080480a4 17 mov ebx,[ebp+8]
(gdb) i r ebx
ebx 0x80490b4 134516916
(gdb) i r eax
eax 0x0 0
(gdb) si
18 mov eax,[ebx]
(gdb) i r eax
eax 0xa 10

Valoarea celui de al doilea parametru este încărcată în registrul EAX prin adresare
indirectă cu registrul EBX. Celălalt parametru se accesează tot prin adresare
indirectă.
De aici până la final, comportamentul programului este cunoscut.

9.7. Conservarea stării registrelor

Totuşi, în funcţie se petrece un lucru foarte important. Înainte de utilizarea


registrului EBX în adresarea indirectă, instrucţiunea

push ebx

salvează conţinutul acestuia în stivă. Există posibilitatea ca registrul EBX să fie


folosit de programul apelant şi funcţia se asigură că operaţiile sale nu modifică
conţinutul acestuia. Conservarea stării registrelor de-a lungul execuţiei unei funcţii
este un concept foarte important. Funcţia apelată trebuie să salveze toate registrele
pe care le foloseşte şi să le refacă înainte de a reda controlul programului apelant.
Acesta este un principiu important al programării modulare.
Pentru a evita selecţia individuală a registrelor ce trebuie salvate, am putea
salva toate registrele la începutul funcţiei apelate şi le-am putea restaura la sfârşit.
De exemplu, am putea utiliza instrucţiunile PUSHAD şi POPAD. Instrucţiunea
PUSHAD este utilă în anumite situaţii, dar nu în toate.
În primul rând, unele registre salvate de PUSHAD sunt folosite la returnarea
valorilor de ieşire (EAX – pentru rezultate întregi de 32 de biţi, sau EDX - în cazul
unor rezultate întregi de 64 de biţi). Instrucţiunea POPAD distruge rezultatele
returnate de funcţia apelată.
În al doilea rând, PUSHAD consumă cinci cicluri de tact, în timp ce o
singură instrucţiune PUSH consumă numai unul. Instrucţiunea PUSHAD devine
eficientă numai dacă sunt salvate mai mult de cinci registre.
În plus, nu uitaţi că o instrucţiune PUSHAD modifică semnificativ
deplasamentul la care pot fi găsiţi în stivă parametrii de intrare ai funcţiilor apelate.

9.8. Scrierea funcţiilor în fişiere separate

Scrierea funcţiilor conform standardului C (folosirea stivei) mai prezintă


un avantaj: funcţia îşi este suficientă sieşi, în sensul că nu este nevoie să definim
locaţii globale de memorie pentru accesul la date (variabile globale). Altfel spus,
nu avem nevoie de directive section .data sau .bss în funcţii (vezi
variabile locale). Acest fapt aduce la rândul său şi alt beneficiu: nu este imperios
necesar ca funcţia să se afle în acelaşi fişier cu programul principal. În continuare
arătăm cum putem crea fişiere separate pentru funcţii, cum trebuie să le asamblăm
şi cum trebuie să le „legăm” de fişierul programului principal.
Structura funcţiei scrise într-un fişier separat este similară structurii unui
program obişnuit, numai că, în loc de directiva _start, trebuie să declarăm ca
etichetă globală numele funcţiei.

section .text
global _functie
_functie:

De asemenea, în fişierul programului principal trebuie să folosim directiva


extern <nume_funcţie>.
Din perspectiva procesului de asamblare, fiecare fișier .asm separat este
considerat un modul, indiferent dacă conține o etichetă _start (caz în care este
considerat program principal) sau dacă reprezintă o simplă funcție. Fiecare modul
conține cod și, posibil, câteva definiții de date. Directiva extern indică
asamblorului că funcția respectivă se află într-un alt modul. Directiva global
atenționează asamblorul că funcția respectivă poate fi adresată din exteriorul
modului. Convenția extern – global se aplică și declarațiilor de date. Putem
declara orice etichetă de date ca global și ea poate fi utilizată de orice modul în
care numele etichetei respective apare declarat ca extern. Mai mult, fișierele de
funcții pot partaja între ele date și funcții, în orice combinație, atât timp cât toate
declarațiile globale și externe sunt realizate corect. Un modul care conține funcții
sau date declarate global se spune că exportă acele elemente. Similar, un modul
care utilizează funcții sau date externe se spune că importă acele elemente.
Modulele asamblate separat au structură similară cu programele normale, cu o
excepție importantă: modulele externe nu conțin un program principal, deci nu au
adresă de început. Acest lucru înseamnă că nu există o etichetă _start care să
indice editorului de legături punctul de la care trebuie să înceapă execuția
programului. Modulele de funcții nu sunt destinate să ruleze singure, astfel încât
prezenta etichetei _start este inutilă. Editorul de legături va genera o eroare ori
de câte ori întâlnește mai mult de o etichetă _start în modulele pe care le
procesează la un moment dat. Dacă toate declarațiile sunt corecte, modulele pot
comunica unul cu altul prin intermediul apelurilor de funcție, și orice funcție poate
adresa orice definiție de date din oricare din fișierele “legate” de editorul de
legături.
Scriem codul funcţiei care calculează complementul faţă de doi într-un
fişier separat de cel al programului principal. Vor rezulta două fişiere:
functie.asm, pentru corpul funcţiei, şi main.asm, pentru programul principal.
; ;
;functie.asm ;main.asm
; ;
section .bss section .bss
mem resd 1 rez1 resd 1
section .text rez2 resd 1
global _functie section .text
_functie: extern _functie
push ebp global _start
mov ebp,esp _start:
sub esp,8 nop
push 5
mov eax,[ebp+8] call _functie
neg eax add esp,4
mov [rez1],eax
leave
ret push 10
call _functie
add esp,4
mov [rez2],eax

mov eax,1
mov ebx,0
int 080h

Nu există o limită cu privire la numărul de directive extern prezent într-


un modul. Un modul (program sau funcție) poate primi oricâte directive extern,
corespunzător numărului de funcții externe apelate. Pentru un aspect compact,
funcțiile pot fi declarate şi pe o singură linie, separate prin virgulă.

extern f1, f2, f3

Totuși, declarațiile care trec de limita liniei nu sunt recunoscute.


Directivele globale trebuie declarate înaintea definirii lor în codul sursă. În
practică, acest lucru înseamnă că directivele global apar în codul sursă la
începutul secțiunii TEXT, înaintea oricărei funcții. Similar, toate elementele de date
globale sunt declarate ca atare în segmentul DATA înaintea definirii lor. Elementele
care nu sunt declarate global sunt private, în sensul că nu pot fi accesate decât
din interiorul modulului care le conține.
Asamblăm cu următoarele comenzi:
yasm -f elf -g stabs main.asm
yasm -f elf -g stabs functie.asm
ld -o program main.o functie.o –melf_i386
gdb program

Ordinea modulele în linia editorului de legături este foarte importantă.


Vom discuta acest fapt într-un capitol viitor.
Reasamblăm programul folosind modulul functie2.asm. Vedem că
funcţia foloseşte stiva, dar totul funcţionează corect din moment ce am rezervat un
spaţiu de 8 octeţi cu instrucţiunea SUB ESP,8.
;
;funcţie2.asm
;
section .bss
mem resd 1
;variabila mem se găseşte în memorie la adresa imediat următoare variabilelor
;rez1 şi rez2 definite în fişierul programului principal
section .text
global _functie
_functie:
push ebp
mov ebp,esp
sub esp,8

mov eax,[ebp+8]
mov [mem],eax
add eax,1
push eax
neg dword [mem]
mov eax,[mem]
pop eax

leave
ret

Presupunem că funcţia foloseşte variabile locale, adresate cu [ebp-4], [ebp-8]:


;
;functie3.asm
;
section .bss
mem resd 1
section .text
global _functie
_functie:
push ebp
mov ebp,esp
sub esp,8

mov eax,[ebp+8]
mov [mem],eax
mov eax,0ffffffffh
mov ebx,0aaaaaaaah
mov ecx,022222222h
push eax
push ebx
mov [ebp-4],ecx
mov dword [ebp-4],033333333h
neg dword [mem]
mov eax,[mem]
pop eax

leave
ret

Rulaţi programul pas cu pas şi observaţi cum se modifică conţinutul stivei.


Poate aţi observat că în aceste ultime funcţii nu am folosit instrucţiunea
ENTER, ci am preferat secvenţa clasică de intrare în funcţie. Majoritatea
compilatoarelor au aceeaşi preferinţă. Acest lucru se explică prin faptul că
instrucţiunea ENTER are probleme de performanţă. Procesoarele moderne
decodează ENTER în 10 până la 20 de microoperaţii, în timp ce secvenţa de trei
instrucţiuni este decodată în 4 până la 6, în funcţie de arhitectură. Diferenţa de
viteză este destul de mare. În plus, secvenţa de trei instrucţiuni poate fi optimizată
de asamblor.

9.9. Exerciţii

9.1. Scrieţi următoarea funcţie C:

/* f.c */
int f(void) {
return 0;
}

Afişaţi echivalentul ei în asamblare cu comanda:

gcc -S f.c -o - -m32 -masm=intel -O0

Comparaţi rezultatul cu cel al următoarelor două funcţii:

int g(void) { int h(void) {


} return 56;
}

9.2. Scrieţi următorul program C:

#include <stdio.h>
int main(){

int x = 1;
double y,z;

y = 1.23;
z = (double)x +y;

return 0;
}

Folosiţi comanda prezentată la exerciţiul anterior pentru afişarea echivalentului său


în limbaj de asamblare. Identificaţi secvenţa de instrucţiuni în limbaj de asamblare
care realizează adunarea z = (double)x +y. Descrieţi operaţiile efectuate.

9.3. Scrieţi în fişiere separate următoarele două programe şi studiaţi echivalentul


acestora în limbaj de asamblare.

#include <stdio.h> #include <stdio.h>


void swap( int num1, int num2 ) ; void swap( int *num1, int *num2 ) ;

int a = 256; int a = 256;


int b = 128; int b = 128;

void main( ) void main( )


{ {
swap( a, b ) ; swap( &a, &b ) ;
} }

void swap( int num1 , int num2 ) void swap( int *num1 , int *num2 )
{ {
int temp ; int temp ;

temp = num2 ; temp = *num2 ;


num2 = num1 ; *num2 = *num1 ;
num1 = temp ; *num1 = temp ;
} }

Generaţi executabilele cu comanda:

gcc -g f.c -o - -m32 -masm=intel -O0 -o f

şi depanaţile cu GDB. Care sunt valorile variabilelor a şi b la sfârşitul celor două


programe? Descrieţi operaţiile care au loc în fiecare caz în parte.
10. INTERFAŢA CU SISTEMUL DE
OPERARE

Odată lansat în execuţie, niciun program întâlnit în capitolele precedente


nu interacţionează în vreun fel cu utilizatorul. Programele „interactive”, care
acceptă date de la tastatură sau afişează informaţii la monitor, folosesc servicii
puse la dispoziţie de sistemul de operare. Ştim deja că sistemul de operare
gestionează şi controlează de o maniera strictă toate elementele hardware ale
maşinii de calcul: discurile, imprimanta, tastatura, diferite porturi (USB, Ethernet,
etc..), monitorul. Accesul unui program la aceste componente se face numai prin
intermediul funcţiilor puse la dispoziţie de sistemul de operare. Modul prin care
atragem atenţia sistemului de operare că avem nevoie de suportul său în realizarea
unei operaţii se realizează prin apeluri de sistem (o categorie specială de
întreruperi software). În acest capitol studiem toate aspectele legate de modul în
care sistemul de operare ne permite să interacţionăm cu sistemul de calcul.

10.1. Întreruperi software

Noţiunea de întrerupere o luăm ca atare, în sensul de „a suspenda temporar


cursul, desfăşurarea unei acţiuni, a unei activităţi”. În cazul de faţă, cel care
efectuează activitatea (de ex., rularea unui program) este procesorul, iar
întreruperea poate fi declanşată de o componentă hardware care are nevoie de
atenţia sa. În acest caz, procesorul suspendă procesul curent şi rulează secvenţa de
cod (rutina) specifică acelei componente. De exemplu, dacă semnalul de
întrerupere provine de la controlerul portului serial, procesorul permite acestuia să
transfere un caracter. Deoarece întreruperea a fost iniţiată de o componentă
hardware, se numeşte întrerupere hardware, iar secvenţa de instrucţiuni maşină
centrată pe o anumită sarcină poartă numele de rutină de tratare a întreruperii (ISR
– Interrupt Service Routine). Fiecare secvenţă de acest gen, specifică fiecărui
sistem de operare, realizează ceva util – citeşte un fişier, scrie un fişier, extrage
timpul curent, citeşte un port de reţea, ş.a.m.d..
Sistemul de operare foloseşte secvenţele de întrerupere în scop propriu, dar
le poate pune şi la dispoziţia programatorului. Însă programatorul nu le poate apela
direct, şi asta din două motive întemeiate. În primul rând, accesul nerestricţionat al
programelor din spaţiul utilizator la componentele intime ale sistemului de operare
este periculos. Persoane rău intenţionate le-ar putea modifica sau folosi în scopuri
improprii. În al doilea rând, aceste secvenţe se modifică odată cu îmbunătăţirea şi
evoluţia sistemului de operare. Evoluţia presupune adăugarea, modificarea sau
ştergerea unor instrucţiuni maşină din motive de securitate sau de optimizare.
Din aceste motive, apelul funcţiilor de sistem se face prin ceea ce în limbaj
de specialitate se numeşte poartă de apel (call gate) şi anume, o poartă de acces
aflată între spaţiul utilizator, acolo unde rulează programele obişnuite, şi spaţiul
kernel, acolo unde îşi desfăşoară activitatea componenta sistemului de operare care
gestionează resursele hardware ale sistemului. Poarta de apel este implementată
printr-o întrerupere software.
În sistemele x86, chiar la începutul memoriei RAM, la adresa 0, se află un
tabel special format din 256 de intrări. Fiecare intrare reprezintă o adresă de
memorie de 4 octeţi. Astfel, primii 1024 de octeţi de memorie sunt rezervaţi acestui
tabel. Fiecare intrare (adresă) din tabel se numeşte vector de întrerupere. Din acest
motiv, tabelul este denumit tabelul vectorilor de întrerupere. În funcţie de poziţia
sa în tabel, fiecare vector are un număr de identificare cuprins între 0 şi 255. La
iniţializarea calculatorului, BIOS-ul şi sistemul de operare20 încarcă locaţiile din
tabel cu adresele rutinelor de întrerupere specifice lor. O actualizare a sistemului de
operare poate modifica atât rutinele cât şi adresele prezente în tabelul vectorilor de
întrerupere, în schimb numărul (indexul) întreruperii care conţine respectiva adresă
rămâne nemodificat. Cu alte cuvinte, rolul locaţiilor din tabelul vectorilor de
întrerupere este standard, secvenţa de întrerupere şi adresa ei în memorie se poate
modifica (de ex., adăugarea unor noi instrucţiuni la o rutină de întrerupere creşte
dimensiunea acesteia şi trebuie încărcată la altă adresă de memorie), dar
întotdeauna va fi apelată de la vectorul cu acelaşi număr. Din această cauză, încă de
la prima apariţie a sistemului Linux şi până în prezent, numărul de întrerupere 80H
a indicat întotdeauna către un dispecer de apel (servicii), un fel de manager al
tuturor funcţiilor de sistem puse la dispoziţie de kernelul Linux. Adresa
dispecerului diferă de la distribuţie la distribuţie sau chiar de la o versiune de Linux
la alta, dar indiferent de acest lucru, programele îl pot apela prin intermediul
locaţiei 80H din tabelul vectorilor de întrerupere. Din motive de securitate, tabelul
vectorilor de întrerupere aparţine sistemului de operare şi programatorul nu are
acces direct la adresele sale. Totuşi, programatorul dispune de o instrucţiune ce
poate interoga respectivul tabel - instrucţiunea INT (INTerrupt). Singura diferenţă
reală între întreruperile hardware şi întreruperile software constă în evenimentul
care declanşează saltul procesorului la tabela vectorilor de întrerupere. În cazul
unei întreruperi hardware, evenimentul declanşator este un semnal electric aplicat
pe un pin al procesorului. După ce recunoaşte semnalul electric, procesorul
introduce în stivă adresa de revenire în program şi execută rutina de tratare a
respectivei întreruperi. Pentru întreruperile software, evenimentul declanşator este

20
De fapt, sistemele de operare folosesc o adresă specifică pentru vectorul de întreruperi.
intrucţiunea INT. Când este executată instrucţiunea INT 80H, procesorul extrage
adresa găsită la locaţia 80H a tabelei vectorilor de întrerupere şi execută secvenţa
de instrucţiuni indicată de acea adresă. În acest mod se efectuează şi se controlează
tranziţia din spaţiul utilizator în spaţiul kernel. Procesul este ilustrat în Figura 10.1.

Prima dată, instrucţiunea Stiva


INT 80H introduce în stivă
adresa următoarei instrucţiuni
Adresa de revenire

Codul programului ... apoi sare la adresa stocată


în vectorul 80h
INT 80h
(Următoarea instrucţiune)
SPAŢIUL UTILIZATOR
SPAŢIUL KERNEL Linux

Dispecer de apel

Tabela vectorilor de
întrerupere

Vector 80h

Figura 10.1 Apelul vectorului de întrerupere 80h


Instrucţiunea INT 80H introduce în stivă adresa următoarei instrucţiuni din
program (adresa care urmează imediat instrucţiunii INT 80H) şi apoi sare în spaţiul
kernel la adresa vectorului 80H. De aici, dispecerul de apel controlează accesul la
aproximativ 200 de rutine de sistem individuale. Ca să ştie pe care din ele să o
execute dispecerul caută în registrul EAX un identificator de apel. Înainte de apelul
dispecerului (cu INT 80H), programatorul trebuie să specifice în EAX un
identificator de apel. Pe lângă acesta, programatorul trebuie să specifice şi alte
informaţii, furnizate aproape întotdeauna tot prin intermediul unor registre. După
finalizarea secvenţei de întrerupere, instrucţiunea IRET foloseşte adresa salvată
anterior în stivă pentru a reveni în program. Acest proces seamănă cu cel prin care
funcţiile studiate în capitolul precedent folosesc perechea de instrucţiuni CALL şi
RET. CALL introducea în stivă adresa următoarei instrucţiuni şi sărea la funcţie,
instrucţiunea RET de la sfârşitul funcţiei extrăgea adresa din stivă şi permitea
continuarea execuţiei programului de la prima instrucţiune sub CALL. Este timpul
să înţelegem rolul ultimelor trei instrucţiuni din toate programele studiate în
această carte până în prezent.
;
;întrerupe execuţia programului şi redă controlul sistemului de operare
;
mov eax,1 ;specifică funcţia de sistem Exit
mov ebx,0 ;cod de întoarcere zero (SUCCESS)
int 80h ;efectuează apel de sistem în vederea întreruperii
execuţiei

Programatorul, pentru a întrerupe execuţia programului, trebuie să plaseze


identificatorul de apel sys_exit, 1, în registrul EAX şi un cod de întoarcere în EBX,
apoi specifică instrucţiunea INT 80H. Codul de întoarcere este o valoare numerică
aflată la discreţia sa. Tehnic, nu sunt restricţii (trebuie numai să „încapă” într-un
registru de 32 de biţi) dar, prin convenţie, o valoare de întoarcere 0 înseamnă că
operaţia s-a finalizat cu succes (procesul a fost întrerupt normal). O valoare diferită
de 0 indică o eroare (întâmpinată de kernel în procesul de întrerupere): fişierul nu
poate fi găsit, discul este la capacitate maximă, etc..
Fiecare program trebuie întrerupt în acest mod. Chiar dacă un program nu
implementează funcţia sys_exit, până la urmă va fi întrerupt, dar sistemul de
operare va afişa o eroare tip Segmentation fault şi nu putem ştii cum a fost
finalizat procesul de întrerupere.

10.2. Formatul apelurilor de sistem

Utilizarea funcţiilor de sistem în programele scrise în limbaj de asamblare


poate fi complicată. Spre deosebire de funcţiile C, unde valorile de intrare erau
plasate în stivă, apelurile de sistem necesită ca valorile de intrare să se afle în
registre. Locul acestora în registre este prestabilit. Plasarea unei valori de intrare
într-un registru greşit produce rezultate neaşteptate sau poate duce la întreruperea
prematură a programului.
Deja aţi aflat că registrul EAX trebuie să conţină identificatorul de apel.
Cum registrele EIP, EBP şi ESP nu pot memora valori de intrare, deoarece ar
afecta operarea normală a programului, rămân disponibile numai cinci registre. Aşa
cum am menţionat, ordinea în care valorile de intrare sunt plasate în registre este
foarte importantă. Dispecerul de apel aşteaptă datele de intrare în ordinea
următoare:
• EBX, primul parametru;
• ECX, al doilea parametru;
• EDX, al treilea parametru;
• ESI, al patrulea parametru;
• EDI, al cincilea parametru.

Funcţiile de sistem care necesită mai mult de şase parametrii de intrare


folosesc o metodă diferită de preluare a acestora: dispecerul adresează şi citeşte
parametrii prin intermediul registrului EBX. În acest caz, registrul EBX conţine
adresa unei locaţii de memorie de la care parametrii sunt stocaţi în ordine
secvenţială.
O altă problemă constă în stabilirea corectă a numărului de parametrii şi a
registrului corespunzător unui anumit parametru. Semnificaţia parametrilor, la fel
ca şi numărul acestora, diferă de la funcţie la funcţie. Asemenea oricărei comenzi
Linux, odată ce ai aflat numele funcţiei de sistem poţi găsi definiţia acesteia în
paginile manual. Secţiunea 2 a paginilor de manual conţine definiţiile tuturor
funcţiilor de sistem disponibile. Accesul la definiţia unei funcţii de sistem se face
cu ajutorul comenzii man urmată de numărul 2:

man 2 exit

Numărul 2 specifică secţiunea paginilor de manual. Deoarece unele funcţii de


sistem au acelaşi nume cu unele comenzi listate în secţiunea 1, implicită pentru
paginile de manual, nu uitaţi să includeţi 2 explicit. Altfel, în loc să obţineţi
definiţia funcţiei de sistem, vor fi afişate opţiuni de comandă. Pagina de manual
conţine patru părţi:
• Nume: arată numele funcţiei de sistem;
• Rezumat: arată modul în care putem utiliza funcţia de sistem
respectivă;
• Descriere: o scurtă descriere a funcţiei de sistem;
• Valoarea returnată: valoarea returnată la sfârşitul funcţiei de sistem.

Rezumatul este scris pentru programatori de C, dar pot beneficia de aceste


informaţii şi cei care programează în limbaj de asamblare. Rezumatul funcţiei de
sistem EXIT arată că aceasta primeşte un singur parametru de intrare (valoarea
între paranteze) şi nu returnează nicio valoare.
Funcţia de sistem WRITE este utilizată la scrierea datelor într-un descriptor
de fişier. Dacă afişăm pagina sa de manual, rezumatul se prezintă astfel:
SYNOPSIS
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

Parametrii sunt menţionaţi de la stânga la dreapta, aşa cum apar în rezumat.


Primul parametru (fd) este un întreg care reprezintă descriptorul de fişier pentru
dispozitivul de ieşire, un identificator de fişier. Al doilea parametru (buf) este
adresa (pointer) de la care începe şirul ce trebuie scris la dispozitiv. Al treilea
parametru (count) reprezintă dimensiunea în octeţi a şirului care trebuie scris.
Utilizând această convenţie, valorile de intrare sunt atribuite următoarelor registre:
• EBX, descriptorul de fişier;
• ECX, adresa de memorie a şirului (pointer-ul);
• EDX, numărul de octeţi care trebuie scris din memorie în fişier.

Un exemplu de utilizare a acestei funcţii de sistem este dat în programul


hello.asm.
;
;afişează la monitor mesajul „Hello, World!”
;
section .data
mesaj db ”Hello, World”, 0xa ;0xA = ASCII newline
lungime equ $ - mesaj
section .text
global _start
_start:
nop ;pentru a putea seta punctul de întrerupere în depanator
mov eax,4 ;EAX = identificatorul funcţiei de sistem write
mov ebx,1 ;EBX = descriptorul de fişier 1 (STDOUT)
mov ecx,mesaj ;ECX = adresa de început a şirului care va fi trimis la
STDOUT
mov edx,lungime ;EDX = numărul de octeţi trimişi la STDOUT
int 80h ;specifică kernelului să execute funcţia indicată în EAX
;WRITE returnează în EAX numărul de caractere scris la monitor
mov eax,1 ;EAX = identificatorul funcţiei de sistem EXIT
mov ebx,0 ;EBX = codul de întoarcere din SO în caz de succes
int 80h ;specifică kernelului să execute funcţia indicată în EAX

Probabil vă este cunoscut cam tot ce se întâmplă în programul hello.asm.


Identificatorul de apel pentru funcţia de sistem WRITE (4) este plasat în registrul
EAX. Valorile de intrare necesare sunt plasate în registrele predefinite. Însă pentru
a înţelege ce reprezintă valoarea plasată în registrul EBX trebuie să vorbim despre
fişiere standard în Linux.

Fişiere standard
Principiul fundamental de proiectare a sistemelor Unix şi înrudite este
„everything is a file”. Sintagma „orice este un fişier” trebuie înţeleasă ca: „orice
este reprezentat ca fişier”. Un fişier poate fi o colecţie de date pe disc dar, într-un
sens mai general, un fişier este punctul final al unui traseu parcus de date. Când
scrii într-un fişier, trimiţi date de-a lungul unui traseu într-un punct final. Când
citeşti un fişier, preiei date dintr-un punct final. În cazul unui transfer între fişiere,
calea parcursă de date se poate afla în totalitate în interiorul unui calculator sau
poate traversa o reţea de calculatoare, datele pot suferi modificări de-a lungul
traseului sau nu, ş.a.m.d.. Ceea ce trebuie să înţelegem este că, în sistemele Unix,
totul este reprezentat ca fişier şi toate fişierele, indiferent de natura lor, sunt tratate
de către mecanismele interne ale sistemului de operare mai mult sau mai puţin
identic. În acest sens, fişierele nu sunt reprezentate numai de colecţiile de date
stocate pe disc, ci de toate componentele hardware care pot juca rol de sursă sau
destinaţie pentru date. Tastatura este un fişier: un punct final care generează date şi
le trimite undeva. Monitorul este un fişier: un punct final care primeşte date de
undeva şi le afişează. Fişierele Unix nu sunt neapărat fişiere text. Fişierele binare
au parte de acelaşi tratament. Tabelul 2.5 prezintă cele trei fişiere standard definite
de sistemele Unix sau înrudite, cum este Linux. Aceste fişiere sunt deschise şi
disponibile întotdeauna în timpul rulării programelor.
Tabelul 10.1 Cele trei fişiere standard în Unix
Fişier Identificator C Descriptor de fişier Hardware
Standard Input STDIN 0 Tastatură
Standard Output STDOUT 1 Monitor
Standard Error STDERR 2 Monitor

Pentru sistemul de operare, un fişier este reprezentat printr-un descriptor


de fişier. Descriptorul de fişier este un număr întreg folosit ca identificator pentru
acel fişier. Primele trei numere aparţin celor trei fişiere standard. Dacă un program
deschide un fişier existent sau crează unul nou, sistemul Linux va returna un
descriptor de fişier unic. Programul va gestiona fişierul folosind descriptorul de
fişier respectiv. Tabelul 2.5 prezintă şi numele de identificare specifice limbajului
de programare C. De obicei, când cineva spune STDOUT înţelegem că se referă la
descriptorul de fişier 1, adică la monitor. Pe de altă parte, STDERR semnifică
destinaţia la care programele trimit mesajele de eroare. Descriptorii de fişier 1 şi 2
au aceeaşi destinaţie, monitorul, şi mesajele standard şi cele de eroare vor fi afişate
în acelaşi mod. Totuşi, în sistemele Unix putem separa ieşirile standard ale
programului de mesajele de eroare (sau alte mesaje specifice modului în care se
comportă programul) printr-un mecanism numit redirecţionare I/O.

Programul hello.asm foloseşte descriptorul de fişier STDOUT ca să afişeze


un text pe ecranul terminalului. Următoarea valoare de intrare specifică adresa de
la care începe şirul care trebuie afişat. Observaţi că specificarea locaţiei de
memorie se face prin adresare directă. Aceasta presupune ca în registrul ECX să se
afle adresa explicită a locaţiei de memorie de la care începe textul mesajului.
Ultimul parametru de intrare specifică lungimea şirului care trebuie afişat. În loc să
specifice în clar numărul de octeţi ai mesajului, programul determină lungimea
acestuia printr-un mic artificiu prezentat deja în capitolul dedicat structurilor de
date. Aşa cum reiese din pagina de manual a funcţiei de sistem WRITE, valoarea
returnată reprezintă numărul de octeţi scrişi sau o valoare negativă (-1) - dacă
apelul s-a încheiat cu o eroare. Valoarea returnată este plasată în registrul EAX.
Programatorul trebuie să verifice acestă valoare, în special de posibilitatea apariţiei
vreunei erori. Întotdeauna trebuie să ţinem cont de tipul de date al valorii returnate.
Unele funcţii de sistem folosesc tipuri de date exotice. Acesta este şi cazul nostru:
funcţia WRITE returnează o valoare de tip ssize_t. Tipul de date ssize_t nu
este unul din cele întâlnite în limbajul de asamblare. De fapt, nu ţine de limbajul de
programare, ci este un sinonim pentru o valoare de tip întreg folosit de sistemul de
operare Linux. Reprezintă numărul de caractere scris la un descriptor de fişier sau
valoarea -1 dacă apare vreo eroare.
Următorul program demonstrează modul în care putem gestiona valorile
returnate de o funcţie de sistem.
;
;CallReturn.asm
;
section .bss
pid resb 4
uid resb 4
gid resb 4
section .text
global _start
_start:
nop
mov eax,20
int 80h
mov [pid],eax

mov eax,24
int 80h
mov [uid],eax

mov eax,47
int 80h
mov [gid],eax
end:
mov eax,1
mov ebx,0
int 80h

Programul CallReturn.asm foloseşte trei apeluri de sistem separate:

Indicatorul de apel Funcţia de sistem Descriere


Specifică identificatorul programului
20 getpid
care rulează
Specifică identificatorul persoanei care
24 getuid
rulează programul
Specifică identificatorul de grup al
47 getgid
persoanei care rulează programul

În urma plasării identificatorilor de apel în registrul EAX şi al execuţiei


instrucţiunii INT pentru fiecare apel în parte, valoarea returnată în registrul EAX
este introdusă într-o locaţie de memorie cu nume sugestiv. Eticheta end oferă un
punct de întrerupere uşor de accesat, astfel încât să putem afişa valorile de ieşire
grupat.

(gdb) b *end
Breakpoint 1 at 0x80480a5: file return.asm, line 19.
(gdb) r
Starting program: /home/stefan/return

Breakpoint 1, end () at return.asm:19


(gdb) x /d &pid
0x80490b4 <pid>: 3975
(gdb) x /d &uid
0x80490b8 <uid>: 1000
(gdb) x /d &gid
0x80490bc <gid>: 1000

Identificatorul de proces este unic programului care rulează. Ceilalţi doi


identificatori se pot afla prin comanda Linux id.
id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)

Următorul program permite introducerea datelor de la tastatură prin funcţia


de sistem READ.
;
;readInput.asm
;
section .data
lung_nume dd 0
;;bufferul este zona de memorie în care vom stoca numele introdus, 40 de
caractere ASCII 0x2d
buffer db "---------------------------------
------- "
msg_cerere db ”Introduceţi numele: ”,0xa
msg_cerere_lung equ $ - msg_cerere

msg_salut db "Salut, "


msg_salut_lung equ $ - msg_salut_lung
;;dacă doriţi să folosiţi pentru buffer memorie neiniţializată, comentaţi linia
buffer din .data şi activaţi următoarele două linii.
;section .bss
; buffer resb 40

section .text
global _start
_start:
nop

mov eax,4
mov ebx,1
mov ecx,msg_cerere
mov edx,msg_cerere_lung
int 80h

mov eax,3
mov ebx,0
mov ecx,buffer
mov edx,40
int 80h
;read returnează în EAX numărul de caractere introduse de la tastatură
mov [lungime_nume],eax

mov eax,4
mov ebx,1
mov ecx,msg_salut
mov edx,msg_salut_lung
int 80h

mov eax,4
mov ebx,1
mov ecx,buffer
mov edx,[lungime_nume]
int 80h

mov eax,1
mov ebx,0
int 80h

Funcţia de sistem READ este identificată prin cifra 3. Are nevoie ca


descriptorul de fişier să fie specificat în registrul EBX, iar adresa şi dimensiunea
zonei de memorie la care vor fi stocate datele trebuie introdusă în registrul ECX,
respectiv în EDX. Va returna numărul de caractere citit din fişier sau, în cazul în
care operaţiunea nu s-a putut desfăşura (de ex., nu există fişierul), un cod de eroare.
Codurile de eroare sunt întodeauna reprezentate prin numere negative (-1).
Observaţi că funcţia de sistem pentru scriere necesită aceeaşi parametrii ca funcţia
de sistem pentru citire, cu excepţia faptului că datele care vor fi scrise trebuie să se
afle deja în zona de memorie tampon (buffer).

10.2.1. Alocarea statică a bufferelor

În programul readInput.asm am folosit o variabilă buffer fără să


explicăm ce reprezintă. Un buffer este un bloc continuu de octeţi folosit la
transferul datelor. La apariţia unei cereri de citire fişier, sistemul de operare trebuie
să aibă la dispoziţie o zonă de memorie în care să stocheze datele citite. Această
zonă de memorie se numeşte buffer. De obicei, bufferele sunt utilizate numai
pentru stocarea temporară a datelor, date care ulterior sunt citite şi convertite în
diferite formate necesare. De exemplu, în programul anterior am citit o linie de text
de la STDIN fără să ştim exact lungimea acesteia. Am presupus că aceasta va avea
maxim 40 de caractere, de aceea am setat lungimea bufferului la 40 de caractere.
Dacă textul ar fi depăşit 40 de caractere, surplusul de caractere ar fi fost ignorat.
Bufferele au mărime fixă, setată de programator.
Buffer-ul este creat prin rezervarea statică sau dinamică a unui spaţiu de
memorie. Rezervarea dinamică nu face obiectul acestei cărţi. În program am folosit
două modalităţi statice de rezervare a spaţiului. O primă modalitate a fost
declararea acestuia în zona datelor iniţializate (prin directive standard – DB, DW,
DD, DQ, DT). Totuşi, pe lângă faptul că este destul de incomod să iniţializezi cu o
valoare numărul respectiv de octeţi, o astfel de declaraţie creşte numărul de octeţi
ai executabilului. În exemplul anterior, mărimea este infimă, dar presupunând că
avem nevoie de un buffer de 1025 de octeţi, ambele probleme devin semnificative.
Soluţia a fost deja dezvăluită: rezervarea unui spaţiu temporar de stocare în zona
datelor neiniţializate. Soluţia este foarte utilă deoarece nu trebuie să iniţializăm
zona respectivă cu o valoare şi nici nu creştem suplimentar mărimea executabilului.
Directiva

buffer resb 40

rezervă 40 de octeţi pe care îi putem folosi ca buffer.

10.2.2. Macroinstrucţiuni

Observăm că programele care folosesc multe apeluri de sistem tind să


devină foarte lungi. Pentru fiecare apel de sistem în parte folosim un număr
considerabil de instrucţiuni. În capitolul precedent am văzut că putem împărţi
programul în mai multe module prin intermediul funcţiilor. Mecanismul de apelare
şi de revenire din funcţii este implementat la nivelul procesorului şi este
independent de asamblor. Dar asambloarele oferă şi altă modalitate de management
a complexităţii: macroinstrucţiunile.

Programele scrise în limbaj de asamblare conţin trei tipuri de propoziţii:


instrucţiuni, directive de asamblare şi macroinstrucţiuni. Macroinstrucţiunile dau
posibilitatea substituirii unei secvenţe de program cu un nume generic.

În timp ce funcţiile folosesc instrucţiuni CALL şi RET, macroinstrucţiunile


reprezintă un artificiu pus la dispoziţie de asamblor şi nu depind de o anumită
instrucţiune sau de un anumit set de instrucţiuni. O macroinstrucţiune este o
etichetă, un nume sub care se ascunde un bloc de text. Aceste linii de text pot
reprezenta, deşi nu este neapărat necesar, o secvenţă de instrucţiuni. Când
asamblorul întâlneşte în fişierul sursă o astfel de etichetă, substituie numele cu
secvenţa de text asociată, proces numit extinderea macroinstrucţiunii.
Macroinstrucţiunile sunt definite cu directivele %macro şi %endmacro,
sintaxa fiind următoarea:
%macro <nume_macro> <nr_parametri>
bloc de text
%endmacro

Nume_macro reprezintă numele macroinstrucţiunii, iar nr_parametri


specifică numărul de parametrii utilizaţi de aceasta. Apelul macroinstrucţiunii se
face prin specificarea numelui şi valorii parametrilor. Pentru o macroinstrucţiune
fără parametrii, nr_parametri poate fi 0 sau poate lipsi. De exemplu,
următoarea macroinstrucţiune fără parametrii întrerupe execuţia programului:

%macro Exit
mov eax,1
mov ebx,0
int 80h
%endmacro

O macroinstrucţiune cu parametrii poate transfera date dintr-o locaţie de


memorie în altă locaţie de memorie. Ştim că transferul de date între locaţii de
memorie nu este permis. De aceea, recurgem la un registru intermediar.

%macro mxchg 2
xchg eax, %1
xchg eax, %2
xchg %1, eax
%endmacro

Această macroinstrucţiune se invocă

mxchg valoare1, valoare2

şi interschimbă valorile fără modificarea registrului EAX. Parametrii


macroinstrucţiunii sunt reprezentaţi prin numere precedate de semnul % (procent).
Astfel, %1 indică primul parametru, %2 indică al doilea parametru. Valorile
propriu-zise primite de parametrii se numesc argumente. Nu confundaţi parametrii
cu valorile efective ale acestora. Parametrii sunt etichetele care urmează numelui în
linia în care este definită macroinstrucţiunea. Argumentele sunt valorile specificate
în linia care invocă macroinstrucţiunea.
Următorul program cere de la tastatură numele utilizatorului şi afişează un
mesaj de salut.
;
;salutMacro.asm
;
%macro Write 2
mov eax,4
mov ebx,1
mov ecx, %1
mov edx, %2
int 80h
%endmacro
%macro Read 2
mov eax,3
mov ebx,0
mov ecx,%1
mov edx,%2
int 80h
%endmacro
%macro Exit 0
mov eax,1
mov ebx,0
int 80h
%endmacro
section .data
cerere_nume db "Introduceti numele: "
cerere_size equ $-cerere_nume
salut db "Salut, "
salut_size equ $-salut
buffer db "01234567890123456789"
lungime dd 0
nr_caractere db 20
section .text
global _start
_start:
nop
Write cerere_nume,cerere_size
Read buffer,[nr_caractere]
mov [lungime],eax
Write salut,salut_size
Write buffer,[lungime]
Exit

Macroinstrucţiunile din programul salutMacro.asm sunt simple şi uşor de


interpretat. Niciuna nu conţine intrucţiuni de salt. Dar codul din macroinstrucţiune,
la fel ca orice funcţie, poate utiliza salturi condiţionate sau necondiţionate. Aici
apare însă o problemă importantă: etichetele din programele de asamblare trebuie
să fie unice şi, cum un macro este reintrodus în codul sursă o dată cu fiecare
invocare, la asamblare vor apărea erori care semnalizează reutilizarea etichetelor.
Din această cauză, asamblorul YASM permite tratarea etichetelor din interiorul
macroinstrucţiunilor ca etichete locale. Etichetele locale nu au semnificaţie decât în
cadrul macroinstrucţiunii care le-a definit. Faptul că etichetele locale unui macro
nu sunt vizibile din exteriorul său înseamnă că ele nu pot fi adresate decât din
interiorul graniţelor %macro ... %endmacro.
Toate etichetele definite în cadrul unui macro sunt considerate locale şi
interpretate special de asamblor. Următorul program citeşte de la tastatură
caractere scrise cu literă mică şi le converteşte în majuscule.
;
;majuscule.asm
;
%macro Majuscule 2
mov edx,%1 ;%1 = Adresa buffer-ului
mov ecx,%2 ;%2 = Numărul de caractere din buffer
%%LC: cmp byte [edx+ecx-1],'a' ;Sub 'a'?
jb %%Next
cmp byte [edx+ecx-1],'z' ;Deasupra lui 'z'?
ja %%Next
sub byte [edx+ecx-1],20h ;Transformă octetul din buffer în
majusculă
%%Next: dec ecx
jnz %%LC
%endmacro
%macro Write 2
mov eax,4
mov ebx,1
mov ecx, %1
mov edx, %2
int 80h
%endmacro
%macro Read 2
mov eax,3
mov ebx,0
mov ecx,%1
mov edx,%2
int 80h
%endmacro
%macro Exit 0
mov eax,1
mov ebx,0
int 80h
%endmacro

section .bss
BUFLEN equ 1024
buffer resb BUFLEN
section .text
global _start
_start:
nop
;citim un buffer de la stdin
citeste:
Read buffer,BUFLEN
mov esi,eax
cmp eax,0
je sfarsit

Majuscule buffer,eax

Write buffer,esi
jmp citeste
sfarsit: ;întrerupem execuţia programului
Exit

Eticheta din interiorul unei macroinstrucţiuni precedată de două simboluri procent


%% devine locală. Dacă eticheta marchează o anumită poziţie din macroinstrucţiune
trebuie urmată de două puncte. Dacă este folosită ca operand pentru instrucţiuni de
apel sau de salt (precum JA, JB şi JNZ în programul anterior), nu trebuie urmată
de două puncte. Este foarte important să înţelegem că, dacă etichetele Next şi LC
nu erau marcate ca locale prin adăugarea prefixului %%, în cadrul programului ar fi
existat mai multe instanţe de asemenea etichete (presupunând că
macroinstrucţiunea ar fi fost invocată de mai multe ori) şi asamblorul ar fi generat
o nouă eroare de etichetă duplicat începând cu a doua apariţie.
Modul în care asamblorul tratează macroinstrucţiunile şi etichetele locale
acestora se poate observa din fişierul majuscule.lst.
1 %line 31+1 majuscule.asm
2
3 [section .bss]
4 BUFLEN equ 1024
5 00000000 <gap> buffer resb BUFLEN
6 [section .text]
7 [global _start]
8 _start:
9 00000000 90 nop
10
11 citeste:
12 00000001 B803000000 mov eax,3
13 %line 41+0 majuscule.asm
14 00000006 BB00000000 mov ebx,0
15 0000000B B9[00000000] mov ecx,buffer
16 00000010 BA00040000 mov edx,BUFLEN
17 00000015 CD80 int 80
18 %line 42+1 majuscule.asm
19 00000017 89C6 mov esi,eax
20 00000019 83F800 cmp eax,0
21 0000001C 7430 je sfarsit
22
23 0000001E BA[00000000] mov edx,buffer
24 %line 46+0 majuscule.asm
25 00000023 89C1 mov ecx,eax
26 00000025 807C0AFF61 ..@4.LC: cmp byte [edx+ecx-1],'a'
27 0000002A 720A jb ..@4.Next
28 0000002C 807C0AFF7A cmp byte [edx+ecx-1],'z'
29 00000031 7703 ja ..@4.Next
30 00000033 806C0AFF20 sub byte [edx+ecx-1],20
31 00000038 49 ..@4.Next: dec ecx
32 00000039 75E8 jnz ..@4.LC
33 %line 47+1 majuscule.asm
34
35 0000003B B804000000 mov eax,4
36 %line 48+0 majuscule.asm
37 00000040 BB01000000 mov ebx,1
38 00000045 B9[00000000] mov ecx, buffer
39 0000004A 89F2 mov edx, esi
40 0000004C CD80 int 80
41 %line 49+1 majuscule.asm
42 0000004E EBAF jmp citeste
43 sfarsit:
44 00000050 B801000000 mov eax,1
45 %line 51+0 majuscule.asm
46 00000055 BB00000000 mov ebx,0
47 0000005A CD80 int 80
Deoarece etichetele trebuie să fie unice, YASM transformă eticheta locală
marcată cu %% într-o etichetă unică prin prefixarea acesteia cu ..@ plus un număr
de patru digiţi şi numele etichetei. De exemplu, etichetele locale %%LC şi %%Next
au devenit ..@4.LC, respectiv ..@4.Next. La o nouă invocare a
macroinstrucţiunii, YASM ar fi modificat numărul şi ar fi generat un nou sinonim
unic pentru fiecare etichetă locală.
La fel ca în cazul funcţiilor, macroinstrucţiunile pot fi grupate în biblioteci
externe programului. O bibliotecă de macroinstrucţiuni nu este altceva decât un
fişier text ce conţine codul sursă al macroinstrucţiunilor. Spre deosebire de funcţiile
adunate într-un modul, bibliotecile de macroinstrucţiuni sunt asamblate separat şi
trebuie parcurse la fiecare proces de asamblare a programului. Bibliotecile de
macroinstrucţiuni sunt introduse în program cu ajutorul directivei %include.

%include ”biblioteca.mac”

Teoretic, această propoziţie se poate afla oriunde în codul sursă, atât timp cât
definiţiile macroinstrucţiunilor se găsesc înaintea adresării lor. Dar tocmai din acest
motiv cel mai indicat este să includem directiva foarte aproape de începutul
fişierului sursă. Dacă biblioteca de macroinstrucţiuni nu se află în directorul curent,
în cadrul directivei putem specifica calea absolută sau relativă.

%include ”../libs/biblioteca.mac”

Dacă asamblorul nu poate localiza fişierul cu macroinstrucţiuni va genera un mesaj


de eroare.
Tot din listingul majuscule.lst am văzut că de fiecare dată când este
invocată macroinstrucţiunea toate instrucţiunile acesteia sunt reintroduse în
program. Dimensiunea programului va creşte simţitor pentru un număr
semnificativ de apeluri. Odată cu dimensiunea creşte şi memoria ocupată de
proces. În programul salutMacro.asm cele trei apeluri ale macroinstrucţiunii
Write generează un total de cincisprezece instrucţiuni. Dacă secvenţa de
instrucţiuni ar fi fost asamblată ca funcţie, ar fi fost necesare numai cinci
instrucţiuni, plus un RET şi trei instrucţiuni CALL. Dar atunci când programul
trebuie să fie cât mai rapid cu putinţă, macroinstrucţiunile au avantajul că elimină
încărcarea suplimentară generată de apelurile şi revenirile din funcţii. Viteza este
marele avantaj al macroinstrucţiunilor.
10.2.3. Funcţii de lucru cu transferuri de
intrare/ieşire

Macroinstrucţiunile şi funcţiile pot fi combinate. În acest moment deţinem


destule cunoştinţe pentru a putea scrie câteva funcţii care mijlocesc interacţiunea
noastră cu sistemul: citirea unui şir de caractere de la tastură, afişarea unui şir de
caractere la monitor, etc..
Scriem următoarele linii într-un fişier numit io.inc şi salvăm.

extern f_ReadStr f_WriteStr


extern f_nwln

%macro ReadStr 1-2 101


push ESI
push EDI
mov EDI,%1
mov ESI,%2
call f_ReadStr
pop EDI
pop ESI
%endmacro

%macro WriteStr 1
push ECX
mov ECX,%1
call f_WriteStr
pop ECX
%endmacro

%macro nwln 0
call f_nwln
%endmacro

%macro WriteChar 1
mov eax,4
mov ebx,1
mov ecx,%1
mov edx,1
int 80h
%endmacro

%macro ReadChar 1
mov eax,3
mov ebx,0
mov ecx,%1
mov edx,1
int 80h
%endmacro

%macro Exit 0
mov eax,1
mov ebx,0
int 80h
%endmacro

Eticheta 1-2 din linia macroinstrucţiunii ReadStr înseamnă că al doilea


parametru al acesteia poate lipsi. Dacă lipseşte (nu este specificat), valoarea
implicită este 101. ReadStr nu este decât o interfaţă pentru funcţia f_ReadStr.
Dar vom vedea că folosirea unui astfel de mecanism aduce avantaje. Funcţia
f_ReadStr citeşte un şir de caractere de la tastatură şi îl stochează într-un buffer.
Primeşte doi parametri: numele bufferului şi numărul de caractere ce trebuie citit.
Acesta din urmă poate lipsi, macroinstrucţiunea are grijă să îl seteze la o valoare
implicită.
;
;funcţia f_ReadStr
;
section .bss
temp_str resb 256
section .text
global f_ReadStr
f_ReadStr:

pushad
pushf
mov eax,3 ;funcţia de sistem READ
mov ebx,0 ;STDIN
mov ecx,temp_str ;buffer temporar
mov edx,0x100 ;count = 256 de caractere
int 80h
;;în ESI se află numărul de caractere - parametrul %2 din macro.
mov ecx,esi
dec ecx ;se scade un caracter (spaţiu pentru NULL)
;;în EDI se află adresa bufferului DESTINATIE dat în macro.
mov ebx,edi
mov esi,temp_str ;adresa de început pt. buffer-ul intern funcţiei
.bucla:
mov al,byte [esi] ;încarcă în AL primul caracter din şirul
introdus
cmp al,0xa ;verifica dacă utilizatorul a tastat ENTER
je .sfarsit ;ENTER înseamnă sfârşitul şirului
mov byte [ebx],al ;copiază caracterul în DESTINATIE
inc ebx ;următorul octet din DESTINATIE
inc esi ;urmatorul caracter din buffer-ul intern
loop .bucla ;repetă până când şirul s-a sfârşit
.sfarsit:
mov byte [ebx],0x0 ;adaugă caracterul NULL
popf
popad

ret

Funcţia f_ReadStr citeşte de la tastatură un şir de caracterele pe care îl încheie


cu un caracter NULL. Funcţia f_WriteStr efectuează operaţia inversă: scrie la
monitor un şir de caractere terminat în NULL. Primeşte un singur parametru:
adresa de la care începe şirul de afişat. Declararea funcţiei f_WriteStr se face
tot în fişierul io.inc.
;
;funcţia f_WriteStr
;
section .text
global f_WriteStr
f_WriteStr:

pushad
pushf
mov ebx,1 ;STDOUT
mov edx,1 ;count = 1 caracter
.repeta:
mov eax,4 ;funcţia de sistem WRITE
;;în ECX este adresa sirului sursă. Parametrul %1 din macro.
cmp byte [ecx],0x0 ;verifică dacă este caracterul NULL
je .sfarsit ;dacă este NULL, şirul s-a sfârşit
int 80h ;afişează caracterul
inc ecx ;următorul caracter
jmp .repeta ;se iese din buclă numai când ECX = 0
.sfarsit:
popf
popad

ret

Obsevaţi că etichetele din cadrul funcţiilor încep cu punct. Acest punct marchează
eticheta ca fiind locală (similar celor două simboluri procent din
macroinstrucţiuni). În programe, eticheta locală permite utilizarea aceluiaşi cuvânt
în zone de cod diferite. Etichetele cu punct sunt locale faţă de prima etichetă
globală (etichetele care nu încep cu punct) care le precede. Din acest motiv, o
etichetă locală este vizibilă numai după eticheta globală căreia îi aparţine. În
consecinţă, eticheta locală nu poate fi adresată de instrucţiuni aflate înaintea
etichetei globale corespunzătoare (şi care, repetăm, este prima etichetă globală
aflată înaintea etichetei locale). În cazul nostru, deoarece le folosim în corpul
funcţiilor, pare că etichetele locale nu sunt precedate de nicio etichetă globală. Dar,
într-un program, numele funcţiilor nu reprezintă altceva decât etichete globale.
Etichetele locale din cadrul funcţiilor sunt cel puţin locale funcţiilor în care sunt
definite. Fireşte, putem folosi etichete globale şi în corpul funcţiilor, lucru care
limitează şi mai mult vizibilitatea etichetei locale.
Funcţia f_nwln trimite la monitor caracterul \n.
;
;funcţia f_nwln
;
section .data
new_line db 0xa
section .text
global f_nwln
f_nwln:

pushad
mov eax,4 ;sys_write
mov ebx,1
mov ecx,new_line
mov edx,1
int 80h
popad

ret
Nume Operand Descriere
Afişează un şir terminat în NULL adresat de
WriteStr sursă m
eticheta sursă.
Citeşte Nr. de caractere de la tastatură şi le
ReadStr dest[,Nr.] m
stochează în dest ca şir terminat în NULL.
nwln - Afişează caracterul de linie nouă.

Următorul program citeşte un caracter de la tastatură şi afişează codul ASCII în


hexazecimal.
;
;hex.asm
;
%include "io.inc"
section .data
prompt db "Introduceti un caracter: ",0
mesaj1 db "Codul ASCII pentru '",0
mesaj2 db "'in hexa este ",0
mesaj3 db "Introduceti un nou caracter? (y/n)",0
hexTable db "0123456789ABCDEF"
section .bss
caracter resb 1
section .text
global _start
_start:
nop
citeste:
WriteStr prompt
ReadChar caracter
WriteStr mesaj1
WriteChar caracter
WriteStr mesaj2
mov al,[caracter]
mov ah,al
mov ebx,hexTable
shr al,4
xlatb
mov [caracter],al
;pushad
WriteChar caracter
;popad
mov al,ah
and al,0x0f
xlatb
mov [caracter],al
WriteChar caracter
nwln
Exit

Instrucţiunea XLATB înlocuieşte caracterul din registrul AL cu un octet din tabela


hexTable. Adresa de început a tabelei se încarcă, în prealabil, în EBX.
Conţinutul registrului AL este tratat ca index în această tabelă de conversie
(translaţie). Valoarea octetului corespunzător indexului înlocuieşte valoarea index
din AL. Instrucţiunea se utilizează îndeosebi pentru conversii de cod.

Observaţi că programul începe cu directiva %include “io.inc”. Aceasta


forţează asamblorul să includă în codul sursă conţinutul fişierului io.inc din
directorul curent. Efectul se poate observa în fişierul .lst. Aduceţi-vă aminte că
YASM diferenţiază caracterele mici de cele mari.
Pentru a genera executabilul trebuie să asamblăm pe rând funcţiile şi
programul principal, apoi să edităm legăturile:

yasm -f elf -g stabs f_ReadStr.asm


yasm -f elf -g stabs f_WriteStr.asm
yasm -f elf -g stabs f_nwln
yasm -f elf -g stabs hex.asm
ld -o hex hex.o f_WriteStr.o f_nwln.o f_ReadStr.o
–melf_i386

Programul nu funcţionează corect. Dacă depanaţi programul cu GDB devine


evident că problema este legată de rescrierea registrului AH în corpul celei de a
doua macroinstrucţiuni WriteChar. Pierdem caracterul introdus de la tastatură.
Macroinstrucţiunile sunt simple secvenţe de cod introduse în program de fiecare
dată când scriem numele acestora. Cu cât programele sunt mai complexe şi
folosesc mai multe registre, cu atât creşte posibilitatea să greşim.
În cazul acestui program putem salva conţinutul registrelor înainte de
intrarea în macroinstrucţiune cu PUSHAD. Avem şi alte posibilităţi, putem salva
numai conţinutul registrului EDX sau putem include instrucţiunile de salvare şi
recuperare a registrelor chiar în corpul macroinstrucţiunii. Dar, o soluţie mai
elegantă este să folosim funcţii. Modificăm înregistrările corespunzătoare
macroinstrucţiunilor ReadChar şi WriteChar din io.inc astfel:

%macro WriteChar 1
push EAX
mov AL,byte [%1]
call f_WriteChar
pop EAX
%endmacro

%macro ReadChar 1
push EAX
call f_ReadChar
mov byte [%1],AL
pop EAX
%endmacro

Creăm două fişiere f_ReadChar.asm şi f_WriteChar.asm în care


introducem instrucţiunile funcţiilor de citire, respectiv afişare, caracter.

section .bss section .bss


temp_char resb 256 temp_char resb 256
section .text section .text
global f_ReadChar global f_WriteChar
f_ReadChar: f_WriteChar:
pushf pushad
pushf
mov eax,3
mov ebx,0 mov byte [temp_char],al
mov ecx,temp_char mov eax,4
mov edx,1 mov ebx,1
int 80h mov ecx,temp_char
mov al,byte [temp_char] mov edx,1
int 80h
popf
ret popf
popad

ret

Nume Operand Descriere


WriteChar sursă m Afişează un caracter din sursă.
ReadChar dest m Citeşte un caracter în dest.

Reluaţi procesul de asamblare. Nu uitaţi să introduceţi în linia editorului de legături


fişierele obiect corespunzătoare acestor ultime două funcţii. Listingul programului
este tot hex.asm, varianta fără instrucţiuni PUSHAD şi POPAD.

10.2.4. Operaţii cu fişiere

Am văzut că funcţiile de sistem WRITE şi READ pot scrie informaţii la


monitor sau pot citi date de la tastatură pentru că aceste dispozitive sunt „fişiere”
deschise implicit pentru orice program care rulează. STDIN este un fişier asupra
căruia avem numai drepturi de citire (reprezintă tastatura), STDOUT este un fişier
asupra căruia avem numai drept de scriere (reprezintă monitorul). Înseamnă că
putem folosi aceste apeluri de sistem pentru orice tip de fişier, chiar şi pentru un
fişier înţeles în sensul clasic (date salvate pe disc). De exemplu, programul
majuscule.asm poate converti caracterele unor fişiere aflate pe disc prin
mecanismul de redirecţionare I/O.

./majuscule < fisier.txt > FISIER.txt

Numai că, spre deosebire de cele trei fişiere standard, deschise întodeauna de
sistemul de operare la începutul rulării unui program, fişierele aflate pe disc sunt
închise. Pentru a putea citi sau scrie date în ele, programul trebuie să le deschidă.
Evident, acest lucru se face printr-o funcţie de sistem. Funcţia OPEN primeşte ca
argumente: numele fişierului, un număr ce reprezintă modul de operare asupra
fişierului deschis (citire, scriere, citire şi scriere, dacă nu există trebuie creat, etc..)
şi un set de permisiuni (reamintim că permisiunile se referă la cine are dreptul să
acceseze respectivul fişier - utilizatorul, grupul din care face parte utilizatorul sau
acces universal-, şi pot fi verificate cu comanda ls).
În programele care urmează vom gestiona fişierele în modul următor:
• Specificăm sistemului de operare numele fişierului pe care dorim să îl
deschidem şi modul de operare asupra lui. Realizăm acest lucru cu
apelul de sistem OPEN,care are ca identificator (în registrul EAX)
numărul 5. Adresa primului caracter din fişier trebuie stocată în EBX.
Intenţia de citire/scriere, reprezentată ca număr, trebuie stocată în
registrul ECX. Deocamdată folosim 0 pentru fişierele din care dorim să
citim, alţi indicatori sunt prezentaţi în Tabelul 10.1 (dacă se doreşte
combinarea acestora se efectuează un SAU logic între valorile lor). În
final, setul de permisiuni trebuie trecut în registrul EDX.
• Sistemul de operare va returna în registrul EAX un descriptor de fişier.
Ne amintim că descriptorul de fişier este un număr folosit în program
ca referinţă pentru acel fişier, un identificator (ca şi când ne-am referi
la o persoană folosind CNP-ul şi nu numele).
• Citim şi/sau scriem fişierul, specificând de fiecare dată descriptorul
dorit.
• La finalul operaţiilor, fişierele sunt închise cu funcţia de sistem
CLOSE, apelul de sistem 6. Singurul parametru necesar este
descriptorul de fişier; plasat în registrul EBX. După CLOSE,
descriptorul de fişier nu mai este valid.

Programul următor ilustrează citirea unui fişier, stocarea conţinutului într-


un buffer şi afişarea acestuia pe ecran. Observaţi faptul că pentru a stoca datele
fişierului avem nevoie de un buffer. De asemenea, la citirea unui fişier trebuie
specificată cantitatea de informaţie pe care dorim să o citim, iar această cantitate nu
poate depăşi mărimea buffer-ului. Numărul de octeţi citit din fişier (returnat de
funcţia de sistem) este salvat într-o variabilă (nrCitit). Fişierul este creat înainte
de rularea programului cu comanda:

echo ”Acesta este un fisier text” > fisier.txt


;
;citireFisier.asm
;
section .data
numeFisier db ”fisier.txt”,0
descriptor dd 0
nrCitit dd 0
msgErr db ”Nu am putut deschide fişierul”,0xa
msgErr_lung equ $ - msgErr
section .bss
MAXBUF equ 100000
buffer resb MAXBUF
section .text
global _start
_start:
nop
mov eax,5 ;identificatorul open
mov ebx,numeFisier
mov ecx,0 ;O_RDONLY
mov edx,0644q
int 80h
;open returnează -1 în eax în caz de probleme
test eax,eax
jns readFile
;dacă sunt probleme, TEST EAX,EAX setează SF, afişăm mesaj de eroare şi
exit
mov eax,4
mov ebx,1
mov ecx,msgErr
mov edx,msgErr_lung
int 80h
mov eax,1
mov ebx,0
int 80h

readFile:
;salvăm descriptorul de fişier
mov [descriptor],eax
;citim fişierul
mov eax,3
mov ebx,[descriptor]
mov ecx,buffer
mov edx,MAXBUF
int 80h
;salvează numărul de octeţi citiţi
mov [nrCitit],eax
;afişăm buffer-ul
mov eax,4
mov ebx,1
mov ecx,buffer
mov edx,[nrCitit]
int 80h
;închidem fişierul
mov eax,6
mov ebx,descriptor
int 80h
;încheiem execuţia programului
mov eax,1
mov ebx,0
int 80h

Tabelul 10.2 Moduri de operare asupra fişierelor de către funcţia de sistem OPEN
Indicator Nr. în octal Descriere
O_RDONLY 00000 Deschide fişierul numai în modul citire.
O_WRONLY 00001 Deschide fişierul numai în modul scriere.
O_RDWR 00002 Deschide fişierul atât pentru citire cât şi pentru
scriere.
O_CREAT 00100 Dacă încă nu există, crează fişierul.
O_TRUNC 01000 Dacă fişierul există, şterge conţinutul acestuia.
O_APPEND 02000 Adaugă conţinut. Scrie la sfârşitul fişierului, nu la
început.

;
;creareFisier.asm
;
section .data
msgErr db ”Nu am putut crea fişierul”,0xa
msgErr_lung equ $ - msgErr
numeFisier db ”date.txt”
descriptor dd 0
buffer db ”What do you think about that?”,0xa
db ”I think you're alive...”,0xa
buffer_lung equ $ - buffer
section .text
global _start
_start:
nop
;crează fişierul
mov eax,8 ;identificatorul funcţiei de sistem create
mov ebx,numeFisier
mov ecx,0644q ;permisiunile fişierului creat
int 80h

test eax,eax
jns createFile
;afişează mesaj de eroare în caz că nu a putut fi creat
mov eax,4
mov ebx,1
mov ecx,msgErr
mov edx,msgErr_lung
int 80h
mov eax,1
mov ebx,0
int 80h
;în caz de succes salvează descriptorul întors de create în registrul EAX
createFile:
mov [descriptor],eax
;scrie în fişier
mov eax,4
mov ebx,[descriptor]
mov ecx,buffer
mov edx,buffer_lung
int 80h
;închide fişierul
mov eax,6
mov ebx,[descriptor]
int 80h
;ieşire din program
mov eax,1
mov ebx,0
int 80h

10.2.5. Argumente în linia de comandă

În capitolul anterior am studiat modul în care funcţiile primesc parametri


de intrare din partea programului principal. Dar şi programul principal poate primi
parametri de intrare de la utilizator. Am văzut că aproape toate comenzile Linux
primesc opţiuni în linie de comandă.
Modalitatea de transfer a parametrilor de intrare către programe diferă de la
un sistem de operare la altul. Înainte să explicăm metoda utilizată de Linux trebuie
să ne amintim cum este structurată memoria principală din perspectiva unui proces.
Când un program este lansat în execuţie, sistemul de operare crează pentru acesta o
zonă de memorie cu aspectul dat în Figura 10.1. Stiva programului, destinată
stocării variabilelor locale şi a parametrilor pentru funcţii, este reprezentată de
blocul superior de memorie. Deşi am putea crede că la începutul procesului stiva
este liberă (spaţiu gol), acest lucru nu este adevărat. Înainte să încarce programul în
memorie, sistemul de operare plasează în stivă următoarele informaţii:
• numărul de parametrii din linia de comandă;
• numele şi calea absolută a programului, aşa cum a fost el executat din
linia de comandă;
• fiecare parametru (argument) introdus de utilizator în linia de comandă
la execuţia programului;
• toate variabilele contextului de lucru la începutul rulării programului.

Toate sunt plasate în memorie conform planului prezentat în Figura 10.2.


Numele programului, argumentele din linia de comandă şi variabilele de mediu
sunt şiruri de lungime variabilă terminate în caracterul NULL (nume care denotă
32 de biţi de 0). Linux nu numai că încarcă şirurile în stivă, dar încarcă şi adresele
(pointeri) acestora.
4 GB 0FFFFFFFFH
Memoria virtuală a
KERNEL SPACE kernelului
(cod, date, heap, stivă)
3 GB 0BFFFFFFFH

Stivă program Bloc superior


ESP

Biblioteci partajate
040000000H

USER SPACE
Heap

section .bss Bloc inferior

section .data
Încărcate din
fişierul executabil
section .text
08048000H

00000000H
7 0
Figura 10.2 Structura unui process în Linux
La începutul execuţiei, registrul ESP indică vârful stivei. Deasupra adresei
din ESP se găsesc toate elementele introduse de sistemul de operare, urmând ca
variabilele locale şi parametrii funcţiilor (cadrele de stivă) să fie dispuse la adrese
mai mici decât cea din ESP. Începând cu indicatorul de stivă (ESP), urmează
numărul parametrilor din linia de comandă sub forma unui întreg fără semn de 4
octeţi, adresa locaţiei la care se află numele programului, adresele fiecărui
parametru din linia de comandă (fiecare câte 4 octeţi). Lista argumentelor se
termină cu un indicator NULL. De la acesta în sus începe o listă lungă de adrese pe
32 de biţi, fiecare indică un şir, terminat tot în NULL, către variabilele mediului de
lucru. Numărul acestora diferă de la sistem la sistem, dar poate fi aproape de 200.
La finalul listei de adrese pentru variabilele de mediu este încă un indicator. Acesta
marchează sfârşitul „directorului” de stivă.
La primele sisteme Linux adresa de început a stivei era întotdeauna
0BFFFFFFFH. În prezent, din considerente de securitate, sistemele Linux schimbă
puţin baza stivei odată cu fiecare rulare a programelor. De aceea, adresele de
început ale stivei pot diferi de la rulare la rulare, de obicei cu câteva milioane de
octeţi. În plus, între sfârşitul listei de adrese şi începutul şirurilor de elemente se
află o zonă variabilă de memorie neutilizată.
Structura stivei poate fi observată cu ajutorul depanatorului. Rulăm
programul prezentat în secţiunea anterioară: gdb program. Am văzut că
programul nu este lansat imediat în execuţie, ci este ţinut în memorie până când

0x00000000
Calea absolută a executabilului
Variabilele mediu propriu-zise
(fiecare reprezentată ca şir de caractere terminat cu
NULL)
Argumentele din linia de comandă propriu-zise
(fiecare reprezentat ca şir de caractere terminat cu
NULL)
Numele programului

Variabile de sistem şi spaţiu liber

0x00000000
Adresa variabilei de mediu 3
Adresa variabilei de mediu 2
Adresa variabilei de mediu 1
0x00000000
Adresa argumentului 3
Adresa argumentului 2
Adresa argumentului 1
Adresa numelui de progam
Număr de argumente ESP
0 31
Figura 10.3 Stiva programului la momentul lansării în execuţie

dăm comanda de rulare run. Acest lucru ne dă posibilitatea să specificăm punctele


de întrerupere. Totodată, ne dă posibilitatea să specificăm argumentele din linia de
comandă cu ajutorul comenzii set args.

(gdb) set args 10 20


(gdb) show args
Argument list to give program being debugged when it is started is "10 20".

În acest caz am introdus două argumente: numerele 10 şi 20. Chiar dacă programul
nu foloseşte argumente din linie de comandă, putem simula introducere lor în stivă.
Verificarea argumentelor introduse se face cu show args. O altă posibilitate este
să le specificăm imediat după şinia de rulare. Nu uitaţi ca în prealabil să setaţi
punctul de oprire.

(gdb) b *_start+1
Breakpoint 1 at 0x8048081: file main.asm, line 9.
(gdb) r 10 20
Starting program: /home/stefan/program 10 20
Breakpoint 1, _start () at main.asm:9
9 push 5
(gdb) p $esp
$1 = (void *) 0xffffd400

Observaţi că linia Starting program indică prezenţa argumentelor din linia de


comandă. Indicatorul de stivă ESP conţine adresa „vârfului” stivei, 0xffffd400.
Afişăm primele 20 de cuvinte începând cu această adresă, în format hexazecimal.

(gdb) x /20xw 0xffffd400


0xffffd400: 0x00000003 0xffffd580 0xffffd595 0xffffd598
0xffffd410: 0x00000000 0xffffd59b 0xffffd5bd 0xffffd5d0
0xffffd420: 0xffffd5f3 0xffffd603 0xffffd60e 0xffffd65e
0xffffd430: 0xffffd670 0xffffd69a 0xffffd6ba 0xffffd6c6
0xffffd440: 0xffffdbb6 0xffffdbdc 0xffffdc0e 0xffffdc1e

Prima valoare, 0x00000003, specifică numărul de argumente din linia de


comandă, incluzând şi numele programului. Următoarele două locaţii conţin
indicatori către numele programului şi şirurile de parametrii din linia de comandă.
Putem vedea valoarea acestor şiruri tot prin intermediul comenzii x, urmată de
adresele respective.

(gdb) x /s 0xffffd580
0xffffd580: "/home/stefan/program"
(gdb) x /s 0xffffd595
0xffffd595: "10"
(gdb) x /s 0xffffd598
0xffffd598: "20"
Atenţie, este important să reţinem că toate argumentele din linia de comandă sunt
specificate ca şiruri, chiar dacă arată ca numere.
Argumentele din linia de comandă sunt urmate de un indicator NULL care
separă adresele argumentelor de adresele variabilelor de mediu. Listăm câteva
variabile de mediu pe baza adreselor lor.

(gdb) x /s 0xffffd59b
0xffffd59b: "ORBIT_SOCKETDIR=/tmp/orbit-stefan"
(gdb) x /s 0xffffd5bd
0xffffd5bd: "SSH_AGENT_PID=1188"
(gdb) x /s 0xffffd5d0
0xffffd5d0: "GIO_LAUNCHED_DESKTOP_FILE_PID=4500"
(gdb) x /s 0xffffd5f3
0xffffd5f3: "SHELL=/bin/bash"
(gdb) x /s 0xffffd603
0xffffd603: "TERM=xterm"
(gdb) x /s 0xffffd65e
0xffffd65e: "WINDOWID=73400324"

La rândul lor, acestea se vor sfârşi cu un indicator NULL. Numărul lor depinde de
aplicaţiile prezente în sistem.

10.3. Operaţii aritmetice în reprezentări ASCII şi


BCD

Aşa cum reiese din paragrafele precedente, argumentele din linia de


comandă sunt dispuse în stivă sub forma unor şiruri de caractere. Dacă dorim să le
utilizăm ca numere trebuie să le convertim. Conversia caracterelor la valori întregi
sau în virgulă mobilă se poate face în mai multe moduri. Pentru operaţii simple (de
ex., o singură adunare), Intel a pus la dispoziţie instrucţiuni ce mijlocesc efectuarea
calculelor direct în format ASCII sau într-un format asemănător celui zecimal,
numit BCD (Binary Coded Decimal). Formatul BCD este destinat să simplifice
lucrul cu dispozitivele care utilizează numere zecimale (dispozitive care trebuie să
afişeze numere, precum ceasuri sau numărătoare). BCD permite procesorului să
efectueze calcule după un algoritm asemănător celui zecimal. În prezent,
instrucţiunile BCD sunt folosite rar, dar cunoaşterea lor şi a modului în care sunt
folosite de procesor poate fi utilă.
10.3.1. Formatul BCD

Formatul Zecimal Codat Binar realizează exact ce îi denotă numele:


codifică numerele zecimale în format binar. În funcţie de numărul de biţi necesar
reprezentării unei cifre zecimale, 4, respectiv 8 biţi, are două variante, compactat şi
necompactat. Fiecare valoare BCD este un întreg de 4 sau 8 biţi, cu o valoare între
0 şi 9. Valorile mai mari de 9 sunt considerate invalide. În esenţă, regulile sunt:
• fiecare cifră zecimală este înlocuită de 4 sau 8 biţi;
• se utilizează cifrele zecimale 0,1,2,3,4,5,6,7,8,9;
• valorile binare superioare lui 9 sunt interzise.

Tabelul 10.3 Formatul BCD


Zecimal Binar BCD compactat BCD necompactat
0 0 0000 00000000
1 1 0001 00000001
2 10 0010 00000010
3 11 0011 00000011
4 100 0100 00000100
5 101 0101 00000101
6 110 0110 00000110
7 111 0111 00000111
8 1000 1000 00001000
9 1001 1001 00001001

Regula prin care se formează un număr în BCD este regula zecimală, de


aici şi numele de zecimal codat binar – cifrele sunt reprezentate în binar, dar
regulile sunt zecimale. Pur şi simplu, ca să obţinem numărul 11 în BCD, alăturăm
reprezentările binare ale celor două cifre: 0001 0001.

Zecimal BCD compactat BCD necompactat


15 0001 0101 00000001 00000101
100 0001 0000 0000 00000001 00000000 00000000
99 1001 1001 00001001 00001001

10.3.2. Erori şi corecţii în ASCII

Presupunem că trebuie să scriem un mic program de calcul aritmetic.


Acesta primeşte ca argumente în linie de comandă două cifre şi un semn +, -, *, /.
Ordinea de introducere a argumentelor este cea convenţionlă: 5 + 3, 9 - 7, etc..
Pentru moment prezentăm numai modulul de adunare şi scădere (pentru un
listing mai scurt). Structura programului poate fi gandită astfel:
;
;calc.asm
;
section .data
nwln db 0ah
section .bss
rez resw 1
section .text
global _start
_start:
nop
mov esi,[esp+8] ;încărcăm în ESI adresa primei cifre
mov edi,[esp+16] ;încărcăm în EDI adresa celei de a doua cifre
mov ecx,[esp+12] ;adresa semnului aritmetic
mov dl,'+'
cmp dl,byte [ecx] ;comparam semnul aritmetic cu +
jne .L1
;; modulul de adunare
mov al,[esi]
add al,[edi]

jmp .end
.L1:
mov cl,'-' ;comparăm semnul aritmetic cu -
cmp cl,byte [esp+12]
jne .L2
;; modulul de scădere
jmp .end
.end:
;; afişare rezultat
mov eax,4
mov ebx,1
mov ecx,rez
mov edx,2
int 80h
;; new line
mov eax,4
mov ebx,1
mov ecx,nwln
mov edx,1
int 80h
;; exit
mov eax,1
mov ebx,0
int 80h

La final, din motive pur estetice, funcţia de afişare a rezultatului este urmată de
funcţia de trecere la linie nouă. Executabilul primeşte argumente din linie de
comandă astfel:

./calc 3 + 5

Fiecare argument este despărţit prin spaţiu. Când rulăm programul prin intermediul
depanatorului, pentru specificarea argumentelor folosim comanda set args 3
+ 5.
Primele trei instrucţiuni ale programului încarcă adresele argumentelor în
trei registre de uz general (în vederea adresării lor prin metoda indirectă).

(gdb) p /x $esi
$1 = 0xffffd598
(gdb) x /s $esi
0xffffd598: "3"
(gdb) x /s $edi
0xffffd59c: "5"
(gdb) x /s $ecx
0xffffd59a: "+"

Pe baza semnului se alege modulul aritmetic corespunzător. În consecinţă,


argumentul de semn este comparat pe rând cu fiecare semn în parte. Codul
semnului putea fi introdus în instrucţiune direct ca număr hexazecimal, dar am ales
formatul caracter ASCII, deoarece acesta este subiectul acestei secţiuni.

(gdb) p /x $dl
$5 = 0x2b
(gdb) p /c $dl
$6 = 43 '+'

Adunarea ASCII
Problemele încep imediat după instrucţiunea de adunare.
(gdb) n
17 mov al,[esi]
(gdb) n
18 adc al,[edi]
(gdb) n
19 jmp .end
(gdb) p /x $eax
$7 = 0x68

Aşa cum reiese din exemplul de mai jos, instrucţiunea adună corect caracterele
ASCII 3 (33H) şi 5 (35H).

33H = 0011 0011 +


35H = 0011 0101
68H = 0110 1000

Însă 68H reprezintă caracterul ASCII 'h', nu caracterul '8' (38H).


Pe de altă parte, valorile 33H şi 35H, privite din perspectiva formatului
BCD compactat, sunt valori valide pentru cifrele 3 şi 5. La fel şi 68H –
reprezentare validă pentru cifra 8. Digitul inferior reprezintă valoarea cifrei (3, 5,
respectiv 8), iar digitul superior este ignorat (îl putem considera 0, şi atunci avem
03 + 05 = 08). De fapt, deoarece cei 4 biţi inferiori conţin valoarea BCD, toţi digiţii
ASCII de la '0' la '9' pot fi consideraţi digiţi BCD valizi. În consecinţă, în BCD
compactat, adunarea 33H + 35H = 68H este corectă. Rezultatul poate fi afişat cu
uşurinţă dacă setăm digitul superior la valoarea 3. Totuşi, acest mecanism nu poate
fi aplicat în cazurile în care digitul rezultat este mai mare ca 9. Să considerăm
adunarea '7' + '5'.

37H = 0011 0111 +


35H = 0011 0101
6CH = 0110 1100

Chiar dacă ignorăm digitul superior (6), suma tot trebuia să fie de forma 01 02H.
Iată un prim caz de eroare în BCD. Adunarea a două cifre valide are ca rezultat o
combinaţie invalidă (necunoscută). În acest caz, 1100 (C). Principiul corecţiei în
BCD susţine ca, în astfel de cazuri, digitul cu combinaţie inexistentă trebuie adunat
cu 0110 (6 în zecimal). 6 este diferenţa între baza hexazecimală şi cea zecimală.

0000 1100 +
0000 0110
0001 0010
Aşadar, dacă am avea posibilitatea să adunăm în BCD, adunarea '7' + '5' ar da într-
adevăr '12', deoarece '7' şi '5' pot fi tratate ca valori BCD legale. Ei bine, Intel pune
la dispoziţie o astfel de instrucţiune: instrucţiunea AAA (ASCII Adjust after
Addition). Instrucţiunea AAA se foloseşte după o adunare efectuată cu instrucţiunea
ADD sau ADC. Suma rezultată în registrul AL este convertită în reprezentare BCD
în doi paşi:
• dacă digitul inferior din registrul AL este mai mare ca 9, sau dacă este
setat indicatorul de condiţii AF din registrul EFLAGS, adună 6 la AL
şi 1 la AH (în urma operaţiei, indicatorii de condiţii AF şi CF sunt
setaţi).
• în toate cazurile, digitul superior este trecut în zero.

În aceste condiţii, modulul de adunare din programul calc.asm poate fi


scris astfel:
;; modulul de adunare
mov al,[esi]
adc al,[edi]
aaa
or ax,3030h
cmp ah,30h
jne L0_1
mov [rez],al
jmp .end
L0_1:
xchg ah,al
mov [rez],ax
jmp .end

Pentru a putea afişa rezultatul, instrucţiunea OR AX,3030H setează


pentru fiecare cifră BCD digitul superior la 3. Se obţine un şir de două caractere
ASCII. În cazul adunării '7' + '5' şirul ASCII din AX este 3132H. Compararea lui
AH cu 30H are următoarele două scopuri:
• dacă rezultatul poate fi reprezentat cu un singur caracter, nu includem
în faţa acetuia caracterul 0 (pentru 4 + 4 afişăm 8, nu 08);
• dacă rezultatul este reprezentat pe două caractere, trebuie să ţinem cont
de ordinea corectă a octeţilor. Octetul superior (AH) este trecut în
memorie la adresă superioară, octetul inferior (AL) este trecut în
memorie la adresă inferioară. Primul afişat va fi AL.

Dacă dorim să adunăm un număr cu mai mulţi digiţi, trebuie să folosim o


buclă ce adună digit cu digit, începând cu primul din dreapta.

Scăderea ASCII
Corecţia rezultatului obţinut în urma unei operaţii de scădere se face cu
instrucţiunea AAS (ASCII Adjust after Substraction). Acţiunile sale sunt:
• dacă digitul inferior din AL este mai mare ca 9, sau indicatorul de
condiţii AF este setat, scade 6 din AL şi 1 din AH (în urma operaţiei,
indicatorii de condiţii AF şi CF sunt setaţi).
• în toate cazurile, digitul superior este iniţializat cu zero.

Este evident că ajustarea este necesară numai în cazul unui rezultat negativ.
Dacă rezultatul este pozitiv, conţinutul registrului AL nu este afectat de
instrucţiunea AAS.
Scădere ASCII cu rezultat pozitiv:

xor ah,ah ;şterge registrul AH


mov al,'8' ;AL = 38H
sub al,'3' ;AL = 38H – 33H = 05H
aas ;AX = 0005H
or al,30h ;AL = 35H

Scădere ASCII cu rezultat negativ:

xor ah,ah ;şterge registrul AH


mov al,'3' ;AL = 33H
sub al,'8' ;AL = 33H – 38H = FBH
aas ;AX = FF05H
or AL,30H ;AX = FF35H

În acest ultim caz, rezultatul din AL indică magnitudinea. Instrucţiunea


AAS setează indicatorul CF pentru a indica faptul că a fost generat un împrumut.
Dacă privim în contextul scăderii numerelor cu mai mulţi digiţi, rezultatul
final, FF35H, este util şi corect. De exemplu, dacă scădem 28 din 43 (de ex., 43 –
28), prima iteraţie a buclei scade 3 – 8; obţinem în AL rezultatul 5 şi avem CF
setat. Următoarea iteraţie, 4 – 2, foloseşte instrucţiunea SBB, care include în
operaţie şi împrumutul generat de scăderea precedentă; obţinem 1. Aşadar, după
operaţia SAU vom avea 31 35H, valoare care reprezintă răspunsul corect (35).
Modulul de scădere al programului nostru arată astfel:

;; modulul de scădere
mov al,[esi]
sub al,[edi]
aas
or al,30h
mov [rez],al
jmp .end

Atenţie, deoarece ar fi complicat programul, modulul de scădere nu ia în


considerare rezultatul negativ. Scopul nostru este să înţelegem instrucţiunea AAS.

Înmulţirea ASCII
Instrucţiunea de corecţie a rezultatului unei operaţii de înmulţire este AAM
(ASCII Adjust after Multiplication). Foarte important, spre deosebire de adunare şi
scădere, înmulţirea nu trebuie efectuată cu caractere ASCII, ci cu numere în format
BCD necompactat. Instrucţiunea AAM împarte valoarea din AL cu 10 şi păstrează
câtul în AH şi restul în AL.

mov al,3 ;operand în format BCD necompactat (8 biţi)


mov bl,8 ;operand în format BCD necompactat (8 biţi)
mul bl ;AX = 0018H (24 în zecimal)
aam ;AX = 0204H
or ax,3030H ;AX = 3234H

Observaţi că operaţia de înmulţire foloseşte numere în format BCD


necompactat (nu numere ASCII). Dacă digiţii din AL şi BL sunt în format ASCII,
trebuie să mascăm digitul superior. De aceea, codul modulului de înmulţire pentru
programul calc.asm arată astfel:

.L2:
mov dl,'*'
cmp dl,byte [ecx]
jne .L3
;; modulul de inmultire
mov al,[esi]
mov bl,[edi]
and al,0fh
and bl,0fh
mul bl
aam
or ax,3030h
cmp ah,30h
jne .L2_1
mov [rez],al
jmp .end
.L2_1:
xchg ah,al
mov [rez],ax
jmp .end

Deoarece pentru interpretorul de comenzi bash semnul steluţă este caracter


special, când efectuaţi o înmulţire, argumentul de semn din linia de comandă
trebuie precedat de caracterul \ (ESCAPE), asfel:

./calc 3 \* 5

Împărţirea ASCII
Instrucţiunea AAD (ASCII Adjust before Division) corectează valoarea
deîmpărţitului din acumulator înaintea împărţirii a două numere în format BCD
necompactat. Instrucţiunea AAD înmulţeşte AH cu 10, îl adună la AL (aceşti doi
paşi convertesc numărul din BCD necompactat în binar) şi apoi îl iniţializează cu
zero. De exemplu:

mov ax,0205H ;deîmpărţitul în format BCD necompactat


mov bl,08H ;împărţitorul în format BCD necompactat
aad ;AX = 0019H
div bl ;AX = 0103H

Instrucţiunea AAD converteşte numărul stocat AX din BCD necompactat în


format binar, astfel încât să poată fi folosită instrucţiunea DIV. DIV generează
câtul în registrul AL şi restul în registrul AH .
Modulul de împărţire prezentat mai jos exemplifică strictul necesar:
conversia din ASCII în BCD necompactat, şi efectul instrucţiunii AAD. Împarte o
cifră mai mare la una mai mică şi afişează rezultatul în formă brută (conţinutul
registrului AX).

.L3:
mov dl,'/'
cmp dl,byte [ecx]
jne .end
;; modulul de impartire
mov al,[esi]
sub al,30h
mov bl,[edi]
sub bl,30h
aad
div bl
or ax,3030h
mov [rez],ax
jmp .end

La fel ca la înmulţire, când specificăm argumentul de semn trebuie să


folosim caracterul ESCAPE.

10.3.3. Erori şi corecţii în BCD compactat

Numerele în format BCD compactat pot fi adunate sau scăzute cu ajutorul


a două instrucţiuni dedicate:
• DAA (Decimal Adjust after Addition)
• DAS (Decimal Adjust after Substraction)

Nu există suport pentru înmulţire sau împărţire. Pentru aceste operaţii,


numerele trebuie convertite în format BCD necompactat.
Instrucţiunile DAA şi DAS corectează rezultatul unei operaţii de adunare,
respectiv de scădere, în conformitate cu formatul BCD compactat. Pentru a înţelege
tipurile de corecţii necesare, studiem următoarele exemple:

29H = 0010 10001 + 17H = 0001 0111+ 42H = 0100 0010 +


49H = 0100 10001 24H = 0010 0100 71H = 0111 0001
72H = 0111 0010 3BH = 0011 1011 B3H = 1011 0011

În primul caz, rezultatul 72 nu este corect. Rezultatul corect, 78, se obţine


prin adunarea cu 6 a rezultatului obţinut. Eroarea constă în propagarea unui bit de
transport de la digitul inferior spre cel superior. Eroare semnalizată de indicatorul
AF.
În al doilea exemplu, rezultatul trebuia să fie 41. Deoarece digitul inferior
este mai mare ca 9, rezultatul corect se obţine prin adunare cu 6.
În ultimul exemplu, eroarea constă în depăşirea capacităţii digitului
superior. Soluţia este să adunăm la rezultat valoarea 60H. Împreună cu bitul de
transport generat (CF) rezultatul efectiv este 113.
Un exemplu legat de operaţiile de scădere este următorul:

52H = 0101 0010 -


24H = 0010 0100
2EH = 0010 1110

În concluzie, rezultatele operaţiilor de adunare sau scădere care folosesc


numere în format BCD compactat pot fi afectate de erori. Acestea sunt indicate de
prezenţa codurilor inexistente în BCD, de setarea indicatorului de transport (CF)
sau de setarea indicatorului de transport la jumătate (AF). În funcţie de caz,
corecţia se face prin adunarea sau scăderea rezultatului obţinut cu unul din
numerele 06H, 60H sau 66H.
Instrucţiunea DAA efectuează acest tip de corecţii asupra rezultatului
operaţiilor de adunare. Mai exact, DAA efectuează următoarele acţiuni:
• dacă digitul inferior din AL este mai mare ca 9, sau indicatorul de
transport la jumătate este setat, adună 6 la AL şi setează AF;
• dacă digitul superior din AL este mai mare ca 9, sau indicatorul de
transport este setat, adună 60H la AL şi setează CF.

Instrucţiunea DAS corectează rezultatul operaţiilor de scădere. Mai exact:


• dacă digitul inferior din AL este mai mare ca 9, sau indicatorul de
transport la jumătate este setat, scade 6 din AL şi setează AF;
• dacă digitul superior din AL este mai mare ca 9, sau indicatorul de
transport este setat, scade 60H din AL şi setează CF.

În acest moment cunoaştem trei reprezentări care pot fi folosite în


operaţiile aritmetice: binar, ASCII şi BCD. Utilizarea unei reprezentări în
detrimentul alteia depinde în mare măsură de tipul aplicaţiei. Procesarea numerică
în ASCII este indicată în cazul operaţiilor simple, efectuate de aplicaţii interactive,
care preiau date de la utilizator şi/sau afişează rezultate pe ecran. Aşa cum ştim,
sistemul de calcul schimbă informaţii cu utilizatorul numai în format ASCII.
Conversia datelor de intrare în format binar consumă resurse suplimentare şi creşte
timpul de execuţie. Procesarea numerică în BCD este mai lentă decât procesarea
numerică în ASCII, dar, în schimb, este mai flexibilă - aşa cum a reieşit şi din
paginile anterioare. În plus, conversia între ASCII şi BCD este foarte simplă.
Formatul BCD compactat este mai eficient decât ASCII din moment ce fiecare
octet poate stoca doi digiţi zecimali. Dar cam aici se încheie lista avantajelor.
În mod normal, datele numerice sunt reprezentate în binar. Este limbajul
nativ al calculatorului. Intern, acesta efectuează toate operaţiile aritmetice şi logice
numai cu date reprezentate în binar. Procesarea numerică în binar este mult mai
eficientă în comparaţie cu celelalte reprezentări. Când aplicaţiile folosesc prelucrări
numerice semnificative, versiunea binară e singura opţiune viabilă. Pe de altă parte,
tot ce introducem de la tastatură sau afişăm la monitor este în format text. Iar
formatul text înseamnă ASCII. De aceea se impune un proces de conversie. Datele
primite de la tastatură sunt convertite în binar, prelucrate intern, apoi reconvertite
în ASCII şi afişate la ecran. Aceste translatări necesită un efort suplimentar din
partea programatorului, dar numerele sunt procesate mult mai eficient în formă
binară şi, în final, se ajunge la un câştig de performanţă.

10.3.4. Conversia ASCII binar

Următorul program cere două numere de la tastatură, calculează suma şi


afişează rezultatul.
;
;suma.asm
;
%include ”io.inc”
section .data
msg_numar1 db "Introduceti primul numar: ",0
msg_numar2 db "Introduceti al doilea numar: ",0
msg_rez db "Suma este: ",0
msg_eroare db "A aparut transport!",0
section .bss
numar1 resd 1 ;stocheaza primul numar
numar2 resd 1 ;stocheaza al doilea numar
rez resd 1 ;stocheaza rezultatul
section .text
global _start
_start:
nop
WriteStr msg_numar1
ReadInt [numar1]

WriteStr msg_numar2
ReadInt [numar2]

;; calculeaza suma
mov eax,[numar1]
add eax,[numar2]
mov [rez],eax

;; verifica daca a aparut transport


jno fara_transport
WriteStr msg_eroare
nwln
jmp sfarsit
;; afiseaza rezultatul
fara_transport:
WriteStr msg_rez
WriteInt [rez]
nwln
sfarsit:
Exit

Pe lângă funcţiile declarate în io.inc, programul mai foloseşte două:


f_ReadInt şi f_WriteInt. Prima citeşte de la tastatură un şir de caractere, îl
converteşte în număr, şi întoarce rezultatul în registrul EAX. A doua preia un
număr din registrul EAX, îl converteşte în şir de caractere, şi afişează rezultatul la
monitor. Pentru citirea de la tastatură şi afişarea la monitor sunt folosite procedurile
cunoscute f_ReadStr şi f_WriteStr. Aşadar, în paragrafele următoare ne
vom concentra atenţia asupra conversiei caracter - valoare binară. Dar, mai întâi să
introducem în io.inc macroinstrucţiunile care le corespund:

extern f_ReadInt
extern f_WriteInt

%macro WriteInt 1
push EAX
mov EAX,%1
call f_WriteInt
pop EAX
%endmacro

%macro ReadInt 1
%ifnidni %1,EAX
push EAX
call f_ReadInt
mov %1,EAX
pop EAX
%else
call f_ReadLInt
%endif
%endmacro

%ifidn (if identity) este o directivă a asamblorului YASM care testează dacă
textul introdus este identic cu cel declarat. Construcţia %ifidn text1,text2
va determina asamblarea codului ce o urmează numai şi numai dacă text1 şi
text2, după expandarea liniei respective, sunt bucăţi identice de text. Spaţiile
albe nu sunt luate în considerare. %ifidni (if identity insensitive) este similară
lui %ifdni numai că nu este sensibilă la litere mari sau mici. %ifnidn (if not
identity) este forma negativă a lui %ifidn. Evident, există şi varianta %ifnidni.
Toate fac parte din familia directivelor de asamblare condiţionată (directive folosite
de preprocesorul YASM). Directivele de asamblare condiţionată permit asamblarea
unor secţiuni numai în cazurile în care sunt îndeplinite anumite condiţii. Sintaxa
generală este:

%if <condiţie1>
;intrucţiuni asamblate numai în cazul îndeplinirii <condiţie1>
%elif <condiţie2>
;instrucţiuni asamblate numai în cazul neîndeplinirii <condiţie1> şi
îndeplinirii <condiţie2>
%else
;instrucţiuni asamblate dacă nu este îndeplinită nicio condiţie anterioară
%endif

Aşa se explică şi apariţia %else şi %endif din macroinstrucţiunea noastră.


Similar, sunt posibile şi expresiile %elifidn, %elifnidn, %elifidni şi
%elifnidni. Preprocesorul YASM implementează o gamă largă de directive
foarte utile.21

10.3.5. Conversia caracterelor ASCII în binar

Funcţia f_ReadInt citeşte de la tastatură un şir de caractere ce reprezintă


un număr întreg cu semn şi returnează valoarea numerică în registrul EAX. Partea
esenţială a funcţiei converteşte o secvenţă de digiţi de intrare daţi în format ASCII
la echivalentul lor binar. Procesul de conversie implică înmulţirea repetată cu 10.
Presupunem că trebuie să convertim şirul de caractere '475' în echivalentul
său numeric 475. '475' înseamnă 343735H. Valoarea numerică a fiecărei cifre în
parte se obţine simplu, prin scădere cu 30H. În acest caz, algoritmul este:

Digit de intrare Valoare numerică Număr = număr × 10 + valoare numerică


Valoare iniţială - 0
'4' (34H) 4 0 × 10 + 4 = 4
'7' (37H) 7 4 × 10 + 7 = 47
'5' (35H) 5 47 × 10 + 5 = 475

21
http://www.nasm.us/doc/nasmdoc4.html
Deoarece numărul este reprezentat pe 32 de biţi, plaja de valori este
cuprinsă între –2.147.483.648 şi +2.147.483.647. Dacă introducem valorile
maxime, pe lângă caracterul de semn vom avea încă 10 coduri numerice. Un total
de 11. Deoarece funcţia f_ReadStr adaugă la sfârşit un caracter NULL, numărul
maxim de caractere valid este 12. Algoritmul prezentat, tradus în linii de
instrucţiuni, arată astfel:

.citeste_caracter:
;;furnizăm funcţiei f_ReadStr parametri de intrare: registrul ESI specifică
numărul de caractere presupus a fi citit de la tastatură, iar EDI adresa buffer-ului la
care vor fi stocate caracterele.
mov esi,0x10 ;buffer de 16 caractere. Acesta poate fi
oricât de mare, dar, din moment ce numărul maxim va avea 12 caractere, 16 este
acoperitor.
mov edi,buffer
call f_ReadStr
mov esi,buffer ;încarcă adresa bufferului în ESI
.converteste_caracter:
mov cl,byte [esi] ;încarcă caracterul în CL
cmp cl,0x0 ;NULL?
je .sfarsit_conversie ;dacă da, şirul de caractere s-a sfârşit
cmp al,0x30 ;comparam cu '0'
jb .caracter_invalid ;caracter < '0' => nu este caracter numeric
cmp al,0x39 ;comparam cu '9'
ja .caracter_invalid ;caracter > '9' => nu este caracter numeric
sub cl,0x30 ;convertim ASCII la numeric
mul ebx ;înmulţim EAX cu 10
jb .depasire_nr_caractere ;apariţia unui transport înseamnă
depăşirea domeniului de reprezentare. Se afişează mesaj de depăşire.
add eax,ecx ;adunăm digitul curent
jb .depasire_nr_caractere ;apariţia unui transport în urma
operaţiei de adunare înseamnă depăşire.
cmp eax,0x80000000 ;compară cu valoarea maximă
ja .depasire_nr_caractere ;dacă EAX > 2.147.483.648,
numărul depăşeşte plaja de valori
inc esi ;următorul caracter
;; reintră în bucla de conversie. Din buclă se iese atunci când se ajunge la caracterul
NULL.
jmp .converteste_caracter
.sfarsit_conversie:

Acesta este algoritmul de conversie propriu-zis. Observaţi că verificăm


validitatea caracterelor din buffer. Un lucru la care nu ne-am gândit înainte. Dar, la
o privire mai atentă, secvenţa are totuşi o problemă. De fapt, mai multe.
Utilizatorul poate introduce numere negative. În definitiv, putem aduna 30 cu -20.
În acest caz, şirul de caractere '20' va fi precedat de caracterul '-'. Sau, dintr-un
exces de precizie, utilizatorul poate specifica explicit semnul numerelor pozitive
(de ex, +30) sau poate introduce spaţii albe înaintea caracterelor numerice. Aceste
posibilităţi trebuie adresate. În plus, vă aduceţi aminte că numărul are maxim 12
caractere. Un număr cu mai mult de 12 caractere nu poate fi reprezentat, aşadar
este invalid. Secvenţa următoare de instrucţiuni trebuie să apară în cod imediat
după introducerea caracterelor în buffer.

.sarim_peste_spatii:
inc esi ;următorul caracter
cmp byte [esi],0x20 ;Spaţiu?
je .sarim_peste_spatii ;dacă da, ignoră-l şi treci la
următorul caracter

Bucla .sarim_peste_caractere este întreruptă de primul caracter


diferit de caracterul de spaţiu. Din acest moment ştim că şirul nu este precedat de
spaţii.

.sir_fara_spatii:
;;Numărul reprezentat pe 32 de biţi are maxim 12 caractere (caracter de semn, 10
caractere numerice, caracter NULL).
mov ecx,0xc ;contorizează nr. de caractere prelucrate
mov al,byte [esi] ;încarcă în AL primul caracter
cmp al,0x2b ;comparam cu semnul +
je .caracter_valid ;dacă este +, atunci caracter valid
cmp al,0x2d ;comparam cu semnul -
je .caracter_valid
.caracter_valid:
inc esi ;următorul caracter
dec ecx ;decrementăm ECX
jcxz .depasire_nr_caractere
.caracter_invalid:

Dacă ECX = 0 şi nu am terminat de parcurs şirul, înseamnă că numărul


este prea mare pentru a putea fi reprezentat pe 32 de biţi. Condiţia a fost testată cu
JCXZ. Deoarece instrucţiunea DEC nu setează indicatorii de stare, nu putem folosi
JZ.

.depasire_nr_caractere:
push ecx ; salvează ECX în stivă
;;încarcă în ECX adresa mesajului de depăşire, afişat cu f_WriteStr
mov ecx,mesaj_depasire
call f_WriteStr
pop ecx ;extrage ECX
jmp .citeste_caracter ;reia procesul de citire

Dacă primul caracter este invalid, semnalăm evenimentul şi reluăm


procesul de citire.

.niciun_caracter_valid:
push ecx
mov ecx,mesaj_lipsa_numar
call f_WriteStr
pop ecx
jmp .citeste_caracter ;reia procesul de citire

În acest moment putem înţelege listingul integral. În plus, parcurgem


procedura pentru cazul în care am introdus numărul -475 precedat de două
caractere de spaţiu.
;
;f_ReadInt.asm
;
section .data
mesaj_depasire db "Depasire domeniu de reprezentare",0
mesaj_lipsa_numar db "Nu ati introdus niciun numar!",0

section .bss
buffer resb 256

section .test
extern f_ReadStr
extern f_WriteStr
global f_ReadInt
f_ReadInt:
push ebx
push ecx
push edx
push esi
push edi
pushf
.citeste_caracter:
mov esi,0x10
mov edi,buffer
call f_ReadStr
;;deoarece am introdus două spaţii şi '-''4''7''5', bufferul conţine 7 caractere, ultimul
este caracterul NULL, introdus de funcţia de citire.
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
mov esi,buffer ;încarcă adresa bufferului în ESI
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;ESI
dec esi
.sarim_peste_spatii:
inc esi
cmp byte [esi],0x20
je .sarim_peste_spatii
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;; ESI
mov edi,esi ;salvam adresa de început în EDI
.sir_fara_spatii:
mov ecx,0xc
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;ECX = 12 ESI = EDI
mov al,byte [esi]
cmp al,'+'
je .caracter_valid
cmp al,'-'
je .caracter_valid
.testeaza_caracter:
cmp al,0x30
jb .caracter_invalid
cmp al,0x39
ja .caracter_invalid
.caracter_valid:
inc esi
dec ecx

;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL


;;
;;
;;ECX = 11 EDI ESI
jcxz .depasire_nr_caractere

mov al,byte [esi] ;încarcă în AL următorul caracter


jmp .testeaza_caracter
;;înapoi la eticheta .testează_caracter. Din această buclă se iese când
numărul de caractere depăşeşte 12 sau când se ajunge la caracterul NULL. Aşadar,
porţile de ieşire din buclă sunt reprezentate de etichetele .depasire_nr_
caractere sau .caracter_invalid.
.depasire_nr_caractere:
push ecx
mov ecx,mesaj_depasire
call f_WriteStr
pop ecx
jmp .citeste_caracter
.niciun_caracter_valid:
push ecx
mov ecx,mesaj_lipsa_numar
f_WriteStr
pop ecx
jmp .citeste_caracter ;sări la început
;;la primul caracter invalid se ajunge aici. Primul caracter invalid poate fi caracterul
NULL, aşa cum ne aşteptăm, sau poate fi orice alt caracter (de ex., utilizatorul
introduce 687a). De aceea, prima instrucţiune din buclă are grijă să introducă
caracterul NULL pe poziţia primului caracter invalid.
;;În cazul nostru, se iese din buclă când ajungem la caracterul NULL.
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;ECX = 8 EDI ESI
.caracter_invalid:
mov byte [esi],0x0 ;introduce caracterul NULL
cmp ecx,0xc ;dacă ECX = 12, înseamnă că este
vorba de primul caracter. Dacă primul caracter este invalid, afişăm mesaj.
je .niciun_caracter_valid
mov esi,edi ;dacă ECX != 12, avem un număr valid. În
acest caz, reintroducem adresa primului caracter în ESI şi pregătim registrele
pentru conversia ASCII - binar.
xor eax,eax ;EAX = 0
xor ecx,ecx ;ECX = 0
mov ebx,0xa ;EBX conţine multiplicatorul
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;ECX = 0 ESI = EDI
mov cl,byte [esi] ;încarcă în CL primul caracter
cmp cl,'-' ;semnul - ?
je .sari_semn ;da, sări peste caracterul de semn
cmp cl,'+' ;semnul + ?
jne .converteste_caracter ;nu este caracter de semn; sări la
bucla de conversie
.sari_semn:
inc esi ;sări peste semn
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;CL = 0x2d EDI ESI
;;conversia ASCII - binar
.converteste_caracter:
mov cl,byte [esi]
cmp cl,0x0
je .sfarsit_conversie
sub cl,0x30
mul ebx
jb .depasire_nr_caractere
add eax,ecx
jb .depasire_nr_caractere
cmp eax,0x80000000
ja .depasire_nr_caractere
inc esi ;următorul caracter
jmp .converteste_caracter
;;în acest moment numărul este în registrul EAX
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;CL = NULL EDI ESI
.sfarsit_conversie:
mov esi,edi ;încarcă în ESI adresa de început a şirului
mov cl,byte [esi] ;încarcă în CL primul caracter
;; 0x20 0x20 0x2d 0x34 0x37 0x35 NULL
;;
;;
;;CL = 0x2d ESI = EDI
;;verificăm dacă EDX este zero. Dacă este diferit de zero înseamnă că numărul a
depăşit domeniul de reprezentare.
cmp edx,0x0
jne .depasire_nr_caractere ;
cmp eax,0x80000000
jb .numar_OK ;nr. < 2.147.483.648, valid.
cmp cl,0x2d ;negativ?
jne .depasire_nr_caractere ;Pozitiv. Numărul pozitiv nu
trebuie să fie mai mare de 2.147.483.647. Cum aici s-a ajuns deoarece nr. >
2.147.483.648, înseamnă că a fost depăşită plaja de valori.
.numar_OK: ;număr < 2.147.483.648.
cmp cl,0x2d ;negativ?
jne .sfarsit ;pozitiv => utilizatorul a introdus un număr
pozitiv, fără semn explicit.
neg eax ;negativ => calculăm complementul faţă de doi.
.sfarsit:
popf
pop edi
pop esi
pop edx
pop ecx
pop ebx
ret

10.3.6. Conversia numerelor din binar în ASCII

Procedura f_WriteInt afişează pe ecran un întreg de 32 de biţi aflat în


registrul EAX. Funcţia separă digiţii individuali ai numărului şi îi converteşte în
reprezentări ASCII. Separarea digiţilor individuali se face prin împărţire la 10.
Etapele implicate în procesul de conversie sunt exemplificate prin conversia
numărului 475.
Cât Rest Valoare ASCII
475 / 10 47 5 + 30H 35H
47 / 10 4 7 + 30H 37H
4 / 10 0 4 + 30H 34H

Apar câteva probleme:


• Prima constă în faptul că obţinem digiţii în ordine inversă. În
consecinţă, aceştia trebuie introduşi în buffer în ordine inversă -
începând din partea dreaptă (sfârşitul bufferului). Dar, mai înainte,
trebuie să introducem caracterul NULL, astfel încât procedura de
afişare să recunoască finalul şirului. Bufferul are 12 caractere:
caracterul de semn, 10 caractere numerice, caracterul NULL.
• A doua problemă este legată de semn. Dacă numărul este negativ,
trebuie afişat semnul minus. Depistarea semnului se face prin
comparare cu zero. Dacă valoarea din EAX este mai mică decât zero,
înseamnă că numărul este negativ. În acest caz, trebuie să introducem
pe prima poziţie din buffer caracterul '-' şi să calculăm complementul
faţă de doi al numărului. Dacă numărul este pozitiv, folosim caracterul
spaţiu (afişarea obişnuită a numerelor pozitive), iar valoarea rămâne
neschimbată.

Prezentăm procedura. Presupunem că în registrul EAX se află valoarea


0xFFFFFF15 (numărul negativ -235).

;
;f_WriteInt.asm
;
section .bss
buffer resb 100
section .text
extern f_WriteStr
global f_WriteInt
f_WriteInt:
pusha
mov esi,buffer ;adresa de început a buffer-ului
mov byte [esi],0x20 ;introduce caracterul de spaţiu în primul
octet al buffer-ului
;;Buffer: 0x20 _ _ _ _ _ _ _ _ _ _ _ _
;;
;;
;; ESI
cmp eax,0x0 ;verificăm semnul
jge .numar_pozitiv ;pozitiv
mov byte [esi],0x2d ;în caz că numărul este negativ rescrie
primul octet al bufferului cu semnul '-'. In acest mod, la sfarsit stim daca numarul
este pozitiv sau negativ.
neg eax ;calculeaza complementul fata de 2
;;Buffer: 0x2d _ _ _ _ _ _ _ _ _ _ _ _
;;
;;
;; ESI, EAX = 0xEB (235)
.numar_pozitiv: ;numar pozitiv
mov ebx,0xa ;divizorul
add esi,0xb ;sărim la adresa ultimului caracter (ESI +
11)
mov byte [esi],0x0 ;introducem un caracter NULL
dec esi
;;Buffer: 0x2d _ _ _ _ _ _ _ _ _ _ _ NULL
;;
;;
;;EAX = 0xEB (235) ESI
mov ecx,0xa ;contorizăm numărul de caractere
procesate
.converteste_numar:
mov edx,0x0 ;EDX = 0
div ebx ;împărţim EDX:EAX la EBX
add dl,0x30 ;restul din DL se transformă în caracter
mov byte [esi],dl ;se introduce pe prima pozitie din dreapta a
numarului
;;Buffer: 0x2d _ _ _ _ _ _ _ _ _ _ 0x35 NULL
;;
;;
;;EAX = 23 ESI
dec esi ;se decremeanteaza ESI
dec ecx ;ECX este contorul, numărul nu trebuie să
depăşească 11 cifre
cmp eax,0x0 ;se compară EAX cu zero
;;Buffer: 0x2d _ _ _ _ _ _ _ _ _ _ 0x35 NULL
;;
;;
;;EAX = 23, ECX = 10 ESI
jne .converteste_numar ;dacă nu este egal reintrăm în bucla
de conversie

;;La ieşirea din bucla de conversie:


;;Buffer: 0x2d _ _ _ _ _ _ _ _ 0x32 0x33 0x35 NULL
;;
;;
;;EAX = 0, ECX = 7 ESI
jcxz .afiseaza_numar ;dacă EAX = 0 şi ECX = 0 numărul este
format din toate cele 10 caractere.
;;Dacă nu are 10 caractere, trebuie să copiem semnul. Acesta este şi cazul nostru.
mov bl,byte [buffer] ;compară BL cu primul octet din buffer.
Acesta poate fi 0x20 pentru număr pozitiv şi 0x2d pentru număr negativ
mov byte [esi],bl ;copiază semnul la poziţia curentă
;;Buffer: 0x2d _ _ _ _ _ _ _ 0x2d 0x32 0x33 0x35 NULL
;;
;;
;;EAX = 0, ECX = 7 ESI
cmp bl,0x20 ;dacă semnul este 0x20 atunci este număr
pozitiv şi nu trebuie să afişăm semnul.
jne .afiseaza_numar ;dacă este negativ înseamnă că putem afişa
semnul.
inc esi
.afiseaza_numar:
mov ecx,esi
call f_WriteStr
popa
ret

Asamblăm programul suma.asm şi generăm executabilul suma:

yasm -f elf -g stabs suma.asm


yasm -f elf -g stabs f_WriteStr.asm
yasm -f elf -g stabs f_ReadStr.asm
yasm -f elf -g stabs f_WriteInt.asm
yasm -f elf -g stabs f_ReadInt.asm
yasm -f elf -g stabs f_nwln.asm
ld -o suma suma.o f_WriteStr.o f_nwln.o f_ReadStr.o
f_ReadInt.o f_WriteInt.o –melf_i386
10.4. Biblioteci de funcţii

Modularizarea, ca metodă de rezolvare a problemelor legate de creşterea


dimensiunii programelor, introduce, la rândul ei, o nouă problemă, de data aceasta,
în procesul de generare a executabilului. Numărul mare de module complică
procesul de asamblare şi editare de legături, structura fişierului makefile, şi
aglomerează directorul ce conţine fişierele sursă şi obiect. În momente precum cele
de la sfârşitul secţiunii precedente, când a fost nevoie să executăm un număr
apreciabil de comenzi pentru generarea unui singur executabil, începem să ne
gândim cum putem scrie toate modulele într-un singur fişier. Una din posibilităţile
de rezolvare pe care le avem la dispoziţie constă în folosirea unor fişiere bibliotecă,
sau simplu, biblioteci. O bibliotecă este un fişier care conţine un număr oarecare de
module obiect. În etapa de editare de legături poate fi utilizată ca entitate de sine
stătătoare. În mod obişnuit, biblioteca este indexată, astfel încât simbolurile
conţinute (funcţiile, datele, etc.) să poată fi găsite cu uşurinţă; indexul indică
modulul care conţine definiţia unui anumit nume de simbol. Din acest motiv,
„legarea” unui program de module obiect aflate în biblioteci este mai rapidă decât
„legarea” programului de fişiere obiect aflate pe disc. De asemenea, utilizarea
bibliotecilor presupune deschiderea unui singur fişier. Acest lucru creşte şi mai
mult viteza procesului de editare a legăturilor. În Linux, la fel ca în toate sistemele
de operare moderne, dispunem de două posibilităţi prin care putem lega modulele
bibliotecilor de programele principale.
Prima metodă se numeşte legare statică (static linking). Legarea statică
introduce modulul obiect al unei biblioteci direct în fişierul executabil. Indiferent
dacă o funcţie definită în acel modul obiect este utilizată sau nu, codul său va face
parte din executabil. Acest lucru poate crea executabile de mari dimensiuni şi poate
duce la consum mare de memorie – atunci când rulăm în acelaşi timp mai multe
instanţe ale programului (fiecare instanţă are propria copie pentru aceeaşi funcţie).
A doua metodă se numeşte legare dinamică (dynamic linking). Legarea
dinamică permite programelor să adreseze funcţiile definite în biblioteci fără a
insera codul acestora în executabil. Bibliotecile legate dinamic pot fi partajate de
mai multe procese. Dacă un proces nou are nevoie de simboluri definite de o
bibliotecă aflată deja în memorie, poate folosi aceeaşi instanţă a bibliotecii.
Legarea dinamică permite consum de memorie redus şi face posibilă generarea
unor fişiere executabile de dimensiuni mai mici, salvând spaţiu pe disc.

10.4.1. Crearea bibliotecilor statice

Bibliotecile statice sunt simple colecţii de fişiere obiect obişnuite (deoarece


sunt simple pachete de fişiere obiect, se mai utilizează şi termenul de arhivă).
Această colecţie este creată cu ajutorul programului ar (archiver). Prin convenţie,
bibliotecile statice încep cu lib şi au extensia .a. Pentru a crea o bibliotecă statică
sau pentru a adăuga un fişier obiect la o bibliotecă statică existentă se foloseşte o
comandă de genul:

ar -crs libioapi.a f_nwln.o f_WriteStr.o f_ReadStr.o


f_WriteChar.o f_ReadChar.o f_WriteInt.o f_ReadInt.o

unde opţiunile:
-c crează o arhivă,
-r inserează în acea arhivă fişierele obiect specificate,
-s crează un index pentru arhivă.

În exemplul de mai sus a fost creată o bibliotecă statică numită libioapi.a,


care conţine copii ale fişierelor obiect prezente în comandă. Evident, fişierele
obiect trebuie generate înainte printr-un proces de asamblare. Comanda ar
permite, pe lângă alte opţiuni descrise în paginile de manual, şi afişarea modulelor
conţinute de bibliotecă:
ar t libioapi.a
f_nwln.o
f_WriteStr.o
f_ReadStr.o
f_WriteChar.o
f_ReadChar.o
f_WriteInt.o
f_ReadInt.o

Indexul poate fi listat cu ajutorul comenzii:

nm libioapi.a

Editarea legăturilor cu programul se face prin opţiunea -l. Aceasta


specifică numele bibliotecii utilizate de program. De asemenea, se poate utiliza
opţiunea –L, care specifică directorul în care se află biblioteca ('.', se referă la
directorul curent).

ld –o suma suma.o -L. -lioapi -melf_i386

Ordinea arhivelor în linia de comandă este importantă. Pentru fiecare


arhivă întâlnită în linia de comandă, asamblorul verifică dacă aceasta defineşte
vreun simbol cerut de fişierele obiect specificate înaintea ei pe linia de comandă.
Dacă defineşte vreun simbol necesar, modulul obiect respectiv este copiat în
executabil. De aceea, bibliotecile trebuie specificate la sfârşitul liniei de comandă22.

10.4.2. Crearea bibliotecilor partajate

Bibliotecile partajate se obţin printr-un proces asemănător celui descris


anterior: asamblarea unei colecţii de fişiere obiect şi înglobarea lor într-un fişier
partajat. Dar, spre deosebire de o bibliotecă statică - arhivă de fişiere obiect ce pot
fi copiate individual în executabil, în funcţie de simbolurile necesare programului
principal (fişierele obiect care nu conţin simboluri adresate de program nu sunt
copiate în corpul executabilului) -, o bibliotecă partajată nu este o arhivă. Codul
modulelor obiect este practic uniformizat într-un singur fişier obiect. În consecinţă,
încărcarea în memorie a bibliotecii partajate implică încărcarea în totalitate a
codului definit în corpul său. Din faptul că o bibliotecă partajată este un fişier
obiect rezultă altă consecinţă importantă. Când generăm fişiere obiect, nu
cunoaştem adresa de memorie la care vor fi încărcate. Pentru un fişier executabil,
segmentele de cod şi date încep la o adresă virtuală cunoscută dinainte. Codul
executabil nu este partajat şi fiecare executabil primeşte propriul spaţiu de adrese.
Acest lucru înseamnă că editorul de legături cunoaşte exact unde se va afla
secţiunea de date în memorie şi o va putea adresa direct. Bibliotecile nu cunosc
astfel de garanţii. Secţiunea lor de date se va afla la un anumit deplasament
specificat faţă de adresa de bază; dar exact unde se află această adresă nu se poate
cunoaşte decât la lansarea în execuţie. În concluzie, toate bibliotecile trebuie să
conţină cod capabil de a fi executat indiferent de poziţia sa în memorie, cunoscut
sub denumirea de cod independent de poziţie (PIC – Position Independent Code).
Reţineţi că secţiunea de date se află tot la un deplasament fix faţă de secţiunea de
cod, dar pentru a găsi adresa efectivă a datelor, deplasamentul trebuie adunat cu
adresa la care se încarcă biblioteca în memorie.
Din acest motiv, procesul de legare dinamică are loc în două etape:
• prima, la momentul editării legăturilor, editorul de legături verifică
faptul că toate simbolurile necesare programului se regăsesc în
program sau în una din bibliotecile sale partajate. Simbolurile din
bibliotecile partajate nu sunt incluse în fişierul executabil final, ci
numai etichetate. Aşadar, editorul de legături lasă în urmă referinţe
care trebuie completate cu informaţii disponibile numai la momentul
încărcării în memorie (de ex., adresa unei funcţii dintr-o bibliotecă).
Aceste referinţe se numesc relocări.
• În a doua etapă, la lansarea în execuţie a programului principal
(runtime), un alt program din sistem, numit editor de legături dinamic
22
Specificaţi fişierul suma.o la sfârşitul comenzii ld şi observaţi rezultatul.
(spre deosebire de cel anterior, denumit şi editor de legături static)
încarcă în memorie bibliotecile cu legare dinamică şi completează
adresele referinţelor (procesul de rezolvare a relocărilor).

Adresele la care editorul de legături dinamic încarcă în memorie


bibliotecile partajate nu sunt cunoscute dinainte. Pur şi simplu, pentru fiecare
bibliotecă necesară programului, editorul dinamic găseşte o zonă de memorie
convenabilă. Acest mecanism, împreună cu cel de memorie virtuală, face posibil ca
mai multe programe să folosească acelaşi cod. Avantajul major al bibliotecilor
partajate constă în faptul că dă posibilitatea mai multor programe să folosească
acelaşi cod (prin partajarea paginilor de memorie).

Tabelul deplasamentului global


Dar bibliotecile partajate pot conţine la rândul lor relocări. Dacă editorul de
legături dinamic modifică codul unei biblioteci partajate, conform unei relocări,
acel cod nu mai poate fi partajat, şi pierdem avantajul utilizării bibliotecilor
partajate. Imaginaţi-vă că editorul de legături dinamic trebuie să completeze în
secţiunea de cod adresa unui simbol de date. Folosind mecanismul relocărilor,
editorul de legături dinamic află adresa de memorie a acestuia şi rescrie codul
bibliotecii partajate. Din acest moment codul nu mai poate fi partajat. Pentru a
evita rescrierea codului, s-a stabilit ca editorul de legături dinamic să rezerve o
zonă din executabil special pentru memorarea adreselor de simboluri şi să scrie
adresa acolo. În acest mod, codul nu trebuie modificat. Zona rezervată adreselor de
simbol se numeşte tabelul deplasamentului global (GOT – Global Offset Table).
GOT este privat fiecărui proces şi conţine adresele absolute ale datelor private
(locale). Numai procesul care îl deţine are drepturi de scriere asupra lui.
Secţiunea de date a unei biblioteci partajate nu are aceste restricţii: din
moment ce secţiunea de date are drepturi de scriere, ea trebuie copiată oricum în
memorie (mai degrabă decât paginată) şi atât timp cât este copiată ea poate fi şi
relocată.

Tabelul de căutare al procedurii


Bibliotecile partajate conţin multe funcţii, iar programul poate include
numai o parte din ele. Chiar şi aşa, în funcţie de instrucţiunile de salt, programul
poate utiliza numai unele din cele incluse. Aşa cum am văzut, procesul de legare
dinamică foloseşte resurse consistente, implică încărcarea codului în memorie,
căutarea în mai multe tabele şi scrierea adreselor. Orice mecanism care optimizează
acest proces duce la o creştere de performanţă. Tabelul de căutare a procedurii
(PLT – Procedure Lookup Table) pune la dispoziţie legarea întârziată (lazy
binding). Legarea este sinonimă cu procesul de rezolvare a relocărilor pentru datele
locale din GOT. Când se completează o intrare în PLT se spune că funcţia a fost
legată de adresa reală.
Procesul de legare a funcţiilor consumă resurse la rândul său, de aceea,
acesta este întârziat până la momentul în care funcţia este apelată. Fiecare funcţie
din bibliotecă are o intrare în PLT care, iniţial, adresează un cod inactiv. Când
programul apelează funcţia, acesta apelează de fapt o intrare în PLT (în acelaşi
mod ca datele referite prin GOT). Codul inactiv va transfera editorului dinamic un
număr de parametri cu ajutorul cărora acesta află adresa reală a funcţiei. Adresa
reală înlocuieşte codul inactiv, astfel încât, la următorul apel, funcţia este încărcată
fără ajutorul editorului dinamic. Dacă o funcţie nu este utilizată, intrarea PLT nu va
fi niciodată modificată.

Suport YASM pentru codul independent de poziţie


Codul independent de poziţie presupune ca asamblorul să poată efectua
relocările prezentate în secţiunile anterioare. YASM le implementează prin
intermediul operatorului WRT. Totodată, pentru a obţine tipurile de relocări
necesare, YASM defineşte cinci simboluri speciale. Aceste simboluri sunt utilizate
la dreapta operatorului WRT. Aceştia sunt ..gotpc, ..gotoff, ..got, ..plt
şi ..sym.

Obţinerea adresei GOT

Pentru a putea fi găsit la încărcare, GOT este situat la o distanţă constantă


faţă de secţiunea de cod a bibliotecii partajate. Numele de simbol standard prin care
YASM se referă la GOT este _GLOBAL_OFFSET_TABLE_. Fiecare modul al
bibliotecii partajate trebuie să definească GOT ca simbol extern, astfel:

extern _GLOBAL_OFFSET_TABLE_

La începutul fiecărei funcţii care trebuie să acceseze secţiunile DATA sau


BSS, trebuie întâi să se calculeze adresa GOT. Acest lucru se face de obicei prin
scrierea funcţiei sub următoarea formă:

func: push ebp


mov ebp,esp
push ebx
call .get_GOT
.get_GOT:
pop ebx
add ebx,_GLOBAL_OFFSET_TABLE_+$$-.get_GOT wrt ..gotpc

;corpul funcţiei
mov ebx,[ebp-4]
mov esp,ebp
pop ebp
ret

Primele două linii ale funcţiei reprezintă prologul C standard, destinat


creării cadrului de stivă, iar ultimele trei linii formează epilogul C standard. A treia
şi a patra linie de la sfârşit salvează şi restaurează registrul EBX, deoarece
bibliotecile partajate scrise în cod independent de poziţie folosesc acest registru
pentru memorarea adresei GOT.
Partea importantă este formată din instrucţiunea CALL şi următoarele două
linii. Combinaţia CALL şi POP obţine adresa etichetei .get_GOT, fără a fi necesar
să cunoştem dinainte adresa la care va fi încărcat programul (instrucţiunea CALL
este codificată relativ la poziţia curentă). Instrucţiunea POP încarcă în registrul
EBX adresa etichetei .get_GOT (poziţia curentă). Instrucţiunea ADD utilizează
unul din tipurile speciale de relocare, GOTPC. Referirea la simbolul care marchează
începutul GOT prin wrt ..gotpc se finalizează cu obţinerea distanţei între
câmpul operand al instrucţiunii ADD şi adresa GOT. De aceea este necesar să
adaugăm la rezultat adresa de început a secţiunii curente, reprezentată prin două
simboluri $$, şi să scădem distanţa între adresa secţiunii curente şi adresa
.get_GOT, aflată deja în registrul EBX. La sfârşitul instrucţiunii ADD, EBX
conţine adresa de început a GOT.
Modificăm codul funcţiei f_nwln.asm astfel încât să devină independent
de poziţie.

;
;funcţia f_nwln , PIC
;
extern _GLOBAL_OFFSET_TABLE_
section .data
new_line: db 0xa
section .text
global f_nwln:function
f_nwln:
push ebp
mov ebp,esp
push ebx
call .get_GOT
.get_GOT:
pop ebx
add ebx,_GLOBAL_OFFSET_TABLE_+$$-.get_GOT wrt ..gotpc
lea esi,[ebx + new_line wrt ..gotoff]
mov eax,4
mov ebx,1
mov ecx,esi
mov edx,1
int 80h
mov ebx,[ebp-4]
mov esp,ebp
pop ebp
ret

Adresa GOT poate fi folosită la obţinerea adresei elementelor de date.


Majoritatea acestora se vor afla în secţiunile declarate local; ele pot fi accesate prin
intermediul tipului special ..gotoff. Instrucţiunea arată astfel:

lea esi,[ebx + new_line wrt ..gotoff]

Expresia new_line wrt ..gotoff este evaluată la momentul editării


legăturilor dinamice ca fiind distanţa de la începutul GOT la variabila locală
new_line. Adunarea cu adresa GOT din EBX va rezulta în ESI adresa locaţiei
new_line.
Dacă declarăm variabile de tip global fără să le asociem o dimensiune,
ele sunt partajate între modulele bibliotecii, dar nu sunt exportate către programul
care a încărcat biblioteca. Acestea se vor afla în secţiunile DATA şi BSS ca de
obicei, astfel încât să le putem accesa în acelaşi mod precum variabilele locale,
folosind mecanismul ..gotoff.

Adresarea elementelor de date externe şi comune

Dacă biblioteca trebuie să adreseze o variabilă externă (externă bibliotecii,


nu doar a unuia din modulele sale), folosim tipul special ..got. Tipul ..got, în
loc să furnizeze deplasamentul dintre adresa de bază GOT şi elementul de date,
returnează deplasamentul dintre adresa de bază GOT şi o înregistrare (intrare) GOT
care conţine adresa elementului de date. Editorul de legături static va configura
această intrare GOT la crearea bibliotecii, iar la momentul încărcării în memorie
editorul de legături dinamic va plasa în ea adresa corectă. Aşadar, pentru a obţine
în EAX adresa unui element de date extern new_line, trebuie să folosim
instrucţiunea:

mov eax,[ebx + new_line wrt ..got]


Aceasta încarcă adresa new_line într-o intrare din GOT. Editorul static,
atunci când crează biblioteca partajată, colectează împreună toate relocările de tip
..got şi construieşte GOT astfel încât să se asigure că acesta prezintă toate
intrările necesare.

Elementele de date de tip common sunt adresate în acelaşi mod. Directiva


common se foloseşte la declararea elementelor de date comune. Un element de
date comun este asemănător unuia global, numai că este declarat în secţiunea
datelor neiniţializate.

common data 4
este similar cu:
global data
section .bss
data resd 1

Diferenţa apare atunci când elementul de date comun este definit în mai
mult de un singur modul. Atunci, editorul de legături static îmbină aceste elemente
de date, iar referinţele către elementul de date respectiv, din toate modulele, vor fi
direcţionate către aceeaşi zonă de memorie. La fel ca directivele extern şi
global, directiva common poate primi extensii specifice formatului de
executabil:

common vector 100:4 ;aliniere la 4 octeţi

Exportul simbolurilor către utilizatorul bibliotecii

Dacă intenţionăm să exportăm simboluri către utilizatorul bibliotecii,


trebuie să declarăm dacă acestea sunt elemente de date sau funcţii, iar dacă sunt
date, trebuie specificată dimensiunea acestora. Acest lucru este necesar deoarece
editorul dinamic trebuie să creeze intrări PLT pentru orice funcţie exportată şi, de
asemenea, trebuie să mute datele exportate în afara secţiunii de date a bibliotecii
care le declară. Astfel, pentru exportul funcţiilor trebuie să folosim:

global func:function ;declară func ca funcţie


func: push ebp
;etc.

Iar pentru exportul elementelor de date, de exemplu, un vector:


global vector:data vector.end-vector ;se specifică mărimea
vector: resd 128
.end

Atenţie, dacă exportăm un element de date către utilizatorul bibliotecii prin


declararea acestuia ca global şi specificarea dimensiunii, elementul de date
respectiv va fi mutat în secţiunea de date a programului principal. Acesta nu se va
mai afla în secţiunea de date în care este declarat. Aşadar, trebuie să adresăm
propria variabilă globală cu mecanismul ..got, ca şi cum ar fi un element de date
extern (ceea ce efectiv a şi devenit), nu cu ..gotoff.
În mod asemănător, dacă trebuie să stocăm adresa unui element global,
exportat de către alt modul, în una din secţiunile de date curente, nu putem folosi
declaraţia standard:

data: dd element_de_date_global ;GREŞIT

YASM va interpreta acest cod ca fiind o relocare obişnuită, caz în care


elementele de date globale se regăsesc la un deplasament judecat în funcţie de
începutul secţiunii curente. Aşadar, această referinţă indică secţiunea de date
curentă, nu elementul global (acesta se află în altă parte). În loc de codul de mai
sus, scriem:

data: dd element_de_date_global wrt ..sym

..sym provoacă YASM să caute în tabela de simboluri un anumit element aflat la


adresa respectivă.
Metodele anterioare au aceeaşi semnificaţie şi pentru funcţii: adresarea
unei funcţii prin
funcptr: dd function

va furniza adresa codului scris în secţiunea curentă, în timp ce

funcptr: dd function wrt ..sym

va furniza adresa PLT pentru funcţia respectivă, acolo unde programul apelant
crede că se află aceasta.

Apelul funcţiilor din exteriorul bibliotecilor

Apelarea funcţiilor din afara bibliotecii partajate trebuie să se realizeze prin


intermediul unui PLT. Faţă de adresa la care este încărcată biblioteca în memorie
PLT este plasat la un deplasament cunoscut, astfel încât codul bibliotecii să poată
efectua apeluri PLT într-un mod independent de poziţie. PLT conţine instrucţiuni
de salt către deplasamentele conţinute de GOT, astfel încât apelurile de funcţii
către alte biblioteci partajate sau rutine din programul principal pot fi transmise
transparent către destinaţiile lor reale.
Pentru a apela o rutină externă trebuie să folosit un alt tip special de
relocare PIC, wrt ..plt. Aceasta este mai simplă decât variantele care lucrează
cu GOT: pur şi simplu se înlocuieşte CALL f_nwln cu versiunea relativă la PLT,
CALL f_nwln wrt ..plt.

10.4.3. Procesul de generare a bibliotecilor partajate

Următoarea comandă asamblează funcţia f_nwln.asm în format PIC


dată mai sus:
yasm –f elf –g stabs f_nwln.asm

Se obţine fişierul obiect f_nwln.o, transformat în bibliotecă partajată cu


comanda:

ld -shared -o libioapi.so f_nwln.o -melf_i386

Numele unei biblioteci partajate trebuie prefixat de cuvântul lib şi finalizat


cu .so. Opţiunea -shared indică editorului de legături faptul că trebuie să
genereze o bibliotecă partajată, nu un executabil. În acest moment, în directorul
curent se află biblioteca partajată libioapi.so, formată dintr-un singur modul obiect.
Studiem biblioteca partajată folosind trei unelte foarte utile, file, nm şi
objdump. Programul file specifică tipul fişierului şi ne permite să aflăm pentru
ce platformă a fost asamblat (compilat) acesta. Fişierul interogat poate fi un fişier
text, un executabil sau o bibliotecă.

file libioapi.so
libioapi.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV),
dynamically linked, not stripped

nm enumeră toate simbolurile care există într-un obiect (prin obiect înţelegem un
fişier obiect sau o bibliotecă). Afişează numele funcţiilor utilizate sau exportate de
către obiect, secţiuni, simboluri, etc.. Biblioteca noastră partajată produce
următorul listing:

nm libioapi.so
00000175 t .n_so
00001178 a _DYNAMIC
000011f0 a _GLOBAL_OFFSET_TABLE_
000011fd A __bss_start
000011fd A _edata
00001200 A _end
000011fc d new_line
00000140 T f_nwln
0000014e t f_nwln.get_GOT

Dezasamblorul objdump ne permite să studiem modul în care operatorul wrt a


acţionat asupra instrucţiunilor:

objdump -D -M intel libioapi.so

00000130 <f_nwln>:
130: 55 push ebp
131: 89 e5 mov ebp,esp
133: 53 push ebx
134: e8 00 00 00 00 call 139 <f_nwln.get_GOT>

00000139 <f_nwln.get_GOT>:
139: 5b pop ebx
13a: 81 c3 7f 10 00 00 add ebx,0x107f
140: 8d b3 0c 00 00 00 lea esi,[ebx+0xc]
146: b8 04 00 00 00 mov eax,0x4
14b: bb 01 00 00 00 mov ebx,0x1
150: 89 f1 mov ecx,esi
152: ba 01 00 00 00 mov edx,0x1
157: cd 80 int 0x80
159: 8b 5d fc mov ebx,DWORD PTR [ebp-0x4]
15c: 89 ec mov esp,ebp
15e: 5d pop ebp
15f: c3 ret

000011b8 <.got.plt>:
11b8: 60 pusha
11b9: 11 00 adc DWORD PTR [eax],eax
...

Disassembly of section .data:


000011c4 <new_line>:
11c4:0a .byte 0xa

În urma instrucţiunii LEA ESI,[EBX + 0xc], în registrul ESI se va


afla adresa new_line, 000011c4. Dacă scădem din aceasta 0xc rezultă adresa
000011b8, adică adresa de început a tabelei GOT.

10.4.4. Instalarea şi utilizarea bibliotecilor partajate

Următorul program foloseşte biblioteca partajată pentru a afişa la ecran un


caracter de linie nouă.

;
;prog.asm, PIC
;
section .text
extern f_nwln
global _start
_start:
nop
call f_nwln wrt ..plt
mov eax,1
mov ebx,0
int 80h

Asamblarea acestuia are loc în mod obişnuit:

yasm -f elf -g stabs prog.asm

În schimb, deoarece foloseşte o funcţie ce aparţine unei biblioteci partajate, aceasta


nu va fi inclusă în fişierul executabil final şi este nevoie ca editorul de legături
dinamic să încarce în memorie biblioteca partajată şi să rezolve relocările. În
sistemele Linux editorul de legături dinamic este ld-linux.so.2, prezent în directorul
/lib. Pentru a specifica programului principal că are nevoie de editorul dinamic
folosim parametrul -dynamic-linker.

ld -dynamic-linker /lib/ld-linux.so.2 -o prog prog.o -


L. -lioapi -melf_i386

Când rulăm un program care depinde de o bibliotecă partajată, sistemul va căuta


această bibliotecă în câteva locuri standard: /lib şi /usr/lib. Dacă fişierul
.so nu poate fi găsit în niciuna din aceste locaţii, executabilul nu va putea fi lansat
în execuţie. Comanda ldd [prog] poate depista bibliotecile partajate de care
depinde programul şi dacă ele sunt pot fi găsite de sistem sau nu. Avem două
opţiuni:
• copiem biblioteca partajată în unul din directoarele amintite, lucru care
nu este întotdeauna posibil, deoarece sunt necesare drepturi privilegiate
(de root),
• sau le folosim din cadrul unui alt director, posibil chiar directorul
curent. În acest caz, trebuie să configurăm corespunzător variabila de
mediu LD_LIBRARY_PATH. De exemplu, dacă biblioteca partajată se
află în acelaşi director cu fişierul executabil, executăm comanda:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

Acum putem instala biblioteca partajată. Bibliotecile partajate sunt


instalate cu ajutorul unui program special, numit ldconfig. În mod normal, dacă
bibliotecile se găsesc în directoarele standard, putem rula ldconfig pur şi
simplu. Dar, pentru că trebuie să instalăm o bibliotecă aflată în directorul curent,
rulăm:

ldconfig -v -n .

Programul se execută în mod obişnuit:

./prog

Încheiem aici subiectul bibliotecilor partajate. Procesul este mult mai


complex şi necesită divagaţii care nu intră în planul acestei cărţi. Observaţi că nu
am transcris funcţiile noastre în cod independent de poziţie. Metoda GOT este
destul de ineficientă din punct de vedere al performanţelor: codul trebuie să extragă
o adresă din GOT de fiecare dată când citeşte sau scrie în segmentul de date (risipă
de timp şi registre - EBX are rol dedicat). Deşi se poate folosi şi altă metodă mai
rapidă, am ales să prezintăm mecanismul standard. În plus, codul independet de
poziţie este mai uşor de implementat pe sistemele de 64 de biţi.
10.5. Apelul funcţiilor de sistem prin biblioteca
standard C

Orice sistem de operare similar ca funcţionalitate cu Unix conţine o


interfaţă de apelare a funcţiilor de sistem. Aceasta defineşte apelurile funcţiilor de
sistem. În Linux, biblioteca C se numeşte glibc şi conţine funcţii pentru toate
facilităţile puse la dispoziţie de sistemul de operare. În consecinţă, orice aplicaţie
creată sau instalată de utilizator poate accesa kernelul prin interfaţa de apelare a
funcţiilor de sistem sau prin intermediul glibc.

Aplicație Aplicație

Spațiul utilizator
Aplicație
Biblioteca C

Intefața de apelare a funcțiilor de sistem

Spațiul kernel

Kernel

Drivere de dispozitiv

Hardware

Figura 10.4 Relaţia între aplicaţii, kernel şi hardware


Această secţiune descrie modul în care putem utiliza funcţii C în
programele scrise în limbaj de asamblare. Bibliotecile C conţin toate funcţiile
standard utilizate în programele C, precum cea de afişare a şirurilor de caractere
(printf) sau funcţia de încheiere a proceselor (exit).
Următorul program afişează propriile argumente primite din linia de
comandă.
;
;args.asm
;
section .data
nrArgs db "Sunt %d argumente: ",10,0
Args db "%s",10,0
section .text
extern printf
extern exit
global _start
_start:
nop
mov ecx,[esp]
push ecx
push nrArgs
call printf
add esp,4
pop ecx
mov ebp,esp
add ebp,4
bucla: push ecx
push dword [ebp]
push Args
call printf
add esp,8
pop ecx
add ebp,4
loop bucla
push 0
call exit

Observaţi că funcţiile printf şi exit sunt declarate cu directiva


extern, la fel ca orice funcţie definită într-un fişier extern.
Dacă aţi lucrat în C, funcţia printf vă este cunoscută. Ne permite să
tipărim la ieşirea standard atât text simplu fără nicio formatare,

”Argumentele sunt următoarele: ”

cât şi propoziţii complicate, formate din cuvinte obişnuite, cifre sau caractere de
conversie a datelor numerice:
”Numărul de argumente este %d: ”

Sintaxa funcţiei printf este următoarea:

printf ”Text FORMATAT”, arg1, arg2 ...

unde textul formatat conţine caractere obişnuite, copiate explicit la ieşire,


dar şi coduri de control care determină modul de conversie şi tipărirea
argumentelor. Fiecare specificaţie de formatare este introdusă prin caracterul % şi
încheiată de un caracter de conversie. Câteva din codurile de conversie sunt listate
în Tabelul 10.3.
Tabelul 10.4 Coduri de conversie pentru funcţia printf
Cod de conversie Descriere
%d Argumentul este convertit în zecimal
%u Argumentul este convertit în zecimal fără semn
%x Argumentul este convertit în hexazecimal
%c Argumentul este considerat ca fiind un singur caracter
%s Argumentul este un şir de caractere
%% Afişează simbolul procent

Între % şi caracterul de conversie pot fi:


• un semn ‘–‘(minus), care semnifică alinierea la stânga a argumentului
convertit;
• un întreg, care specifică lungimea minimă a şirului. Argumentul va fi
aliniat la dreapta şi va fi completat cu spaţii albe până la lungimea
câmpului.

Caracterele din cadrul argumentului corespunzător formatului %s sunt


preluate până la întâlnirea caracterului NULL. Acesta este şi motivul pentru care în
program am ataşat un zero la şirurile destinate mesajelor:

"Sunt %d argumente: ",10,0

Numărul 10 reprezintă caracterul ASCII \n.


Mecanismul de formatare este simplu, problema reală constă în
transmiterea argumentelor. Funcţia printf nu permite specificarea numărului de
argumente. Numărul acestora variază în funcţie de necesităţile programatorului. În
schimb, toţi parametrii primiţi de funcţiile bibliotecii C trebuie transmişi prin
intermediul stivei, în concordanţă cu convenţia C. Acest lucru se face direct, prin
introducerea valorii, sau indirect, prin referinţă (prin introducerea adresei de 32 de
biţi a argumentului). Pentru elemente de date cu valori de 32 sau 64 de biţi,
introducem valorile direct. Pentru elemente de date mai mari, precum şiruri sau
vectori, introducem în stivă adresa de început (în jargonul C, transmiterea unei
adrese către ceva se numeşte „transmiterea unui pointer” către acel ceva).
Când funcţia printf primeşte mai mulţi parametrii, aceştia trebuie
introduşi în stivă într-o ordine foarte exactă: de la dreapta la stânga, aşa cum ar fi
apărut în corpul instrucţiunii printf într-un program scris în C. Un exemplu
simplu din C este următorul:

printf(”Întotdeauna %d + %d = %d”, 2,3,5);

Aceasta este o propoziţie în C care apelează funcţia printf. Şirul de bază este
închis între ghilimele şi reprezintă primul argument. Şirul este urmat de câteva
argumente numerice. Trebuie să fie o valoarea numerică pentru fiecare cod de
formatare %d găsit în şir. Ordinea în care aceste elemente trebuie introduse în stivă
începe cu elementul din dreapta şi continuă către stânga, şirul de bază fiind ultimul.
În asamblare, secvenţa arată astfel:

push 5
push 3
push 2
push mesaj
call printf
add esp,16

Identificatorul mesaj este adresa şirului de bază, aşa cum este acesta
definit în segmentul de date.

Mesaj db ”Întotdeauna %d + %d = %d”,0

Instrucţiunea de adunare de la final eliberează stiva. Ne amintim că, de


fiecare dată când introducem ceva în stivă cu instrucţiunea PUSH, indicatorul de
stivă ESP se deplasează către adrese mai mici cu un număr de octeţi egal cu
dimensiunea elementelor. Patru argumente reprezintă 16 octeţi. La sfârşitul
apelului indicatorul de stivă trebuie să conţină adresa dinaintea acestuia, aşadar,
adunăm 16.
10.5.1. Editarea legăturilor cu funcţiile C

Când folosim în programele de asamblare funcţii din biblioteca C trebuie


să „legăm” fişierele bibliotecii de fişierul obiect al programului. Dacă funcţiile
bibliotecii C nu sunt disponibile, editorul de legături va afişa mesaj de eroare.

ld -o args args.o
args.o: In function `_start':
args.asm:13: undefined reference to `printf'
args.o: In function `bucla':
args.asm:20: undefined reference to `printf'
args.asm:26: undefined reference to `exit'

Pentru a putea edita legăturile cu funcţiile C, acestea trebuie să fie


disponibile în sistem. În sistemele Linux, biblioteca C standard dinamică se află în
fişierul libc.so.x, unde x este o valoarea care reprezintă versiunea bibliotecii.
Acest fişier bibliotecă conţine funcţiile standard C, incluzând printf şi exit.
Fişierul libc.so trebuie legat explicit de codul obiect, folosind parametrul -l al
editorului de legături GNU, ld. Când utilizăm parametrul -l, nu trebuie să
specificăm numele complet al bibliotecii. Editorul de legături presupune că
biblioteca se va afla într-un fişier numit /lib/libx.so, unde x este numele
bibliotecii specificat ca parametru în linia de comandă – în acest caz, litera c. Pe de
altă parte, funcţiile nu vor fi incluse în fişierul executabil final şi biblioteca
dinamică trebuie încărcată în memorie la momentul lansării în execuţie a
programului, de un alt program. Pentru sistemele Linux, acest program este ld-
linux.so.2, regăsit în directorul /lib. Pentru a specifica programului nostru
că are nevoie de editorul de legături dinamic, folosim parametrul -dynamic-
linker.

ld -dynamic-linker /lib/ld-linux.so.2 -o args -lc


args.o -verbose

Asamblarea se face în mod obişnuit, cu YASM. La rulare, pentru a găsi biblioteca


libc.so programul foloseşte încărcătorul dinamic ld-linux.so.2.
Putem testa programul args cu orice număr de parametrii.

./args test 10 20 30
Sunt 5 argumente:
./args
test
10
20
30

Amintiţi-vă că primul parametru este considerat numele programului, aşadar,


primul argument din linia de comandă este al doilea parametru. O greşeală întâlnită
frecvent constă în compararea cu zero a numărului de argumente din linia de
comandă. Din moment ce numele programului se va regăsi întotdeauna pe linia de
comandă, numărul de parametri nu va fi niciodată zero.
Următorul program afişează variabilele de mediu.
;
;env.asm
;
section .data
Args db "%s",10,0
section .text
extern printf
extern exit
global _start
_start:
nop
mov ebp,esp
add ebp,12
bucla:
cmp dword [ebp],0
je sfarsit
push dword [ebp]
push Args
call printf
add esp,12
add ebp,4
loop bucla
sfarsit:
push 0
call exit

./env
USERNAME=stefan
DEFAULTS_PATH=/usr/share/gconf/gnome.default.path
GIO_LAUNCHED_DESKTOP_FILE=/usr/share/applications/terminator.desktop
XDG_CONFIG_DIRS=/etc/xdg/xdg-gnome:/etc/xdg
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
DESKTOP_SESSION=gnome
PWD=/home/stefan
GDM_KEYBOARD_LAYOUT=us
LANG=en_US.UTF-8
GDM_LANG=en_US
MANDATORY_PATH=/usr/share/gconf/gnome.mandatory.path
UBUNTU_MENUPROXY=libappmenu.so
COMPIZ_CONFIG_PROFILE=ubuntu
GDMSESSION=gnome
SHLVL=1
HOME=/home/stefan
LANGUAGE=en_US:en

Când în linia de comandă nu sunt specificaţi parametrii, secţiunea variabilelor de


mediu începe la un deplasament de 12 octeţi faţă de adresa stocată în registrul ESP.
Sfârşitul secţiunii este marcat cu un caracter NULL. Compararea valorii din stivă
cu zero verifică faptul că am ajuns la sfârşitul secţiunii. Dacă adresa nu este zero,
se afişează şirul indicat de aceasta. Variabilele de mediu prezente în sistem depind
de aplicaţiile active şi de setările locale. Putem verifica acest lucru prin crearea
unei noi variabile.

export TEST=/home/test
./env | grep TEST
TEST=/home/test
11. INTERFAŢA CU LIMBAJELE DE NIVEL
ÎNALT

În capitolul III am prezentat avantajele şi dezavantajele limbajelor de nivel


înalt şi de asamblare şi, încă din prefaţă, am atenţionat cititorul că scrierea
completă a programelor în asamblare trebuie evitată cât de mult posibil.
Programele în asamblare sunt mai lungi, mai dificil de actualizat şi, mai ales, nu
sunt portabile. În schimb, oferă acces direct la hardware şi pot reduce timpul de
execuţie. Ca urmare, dacă este necesar, programele de nivel înalt pot beneficia de
performanţa ridicată a unor funcţii scrise în asamblare. În astfel de programe este
posibil ca o funcţie de nivel înalt să apeleze o funcţie de nivel scăzut, sau invers. În
acest capitol prezentăm diferite modalităţi prin care putem introduce funcţii scrise
în asamblare în corpul programelor de nivel înalt, în acest caz, limbajul C. Scopul
nostru este acela de a ilustra principiile implicate. Odată ce acestea sunt înţelese,
discuţia poate fi generalizată la orice alt limbaj de nivel înalt.

Cele două metode prin care putem introduce secvenţe de instrucţiuni în


asamblare în codul C sunt:
• scrierea instrucţiunilor de asamblare direct în codul C, sau
• „legarea” de programul C a unor module scrise în asamblare, la
momentul compilării. Proces asemănător celui de legare a programelor
C de funcţiile aflate în bibliotecile C standard.

Prima metodă se numeşte inline assembly şi este utilă numai atunci când
trebuie să includem în codul C o secvenţă relativ scurtă de cod în asamblare. În caz
contrar, este preferată metoda modulelor separate. În acest ultim caz, modulul scris
în asamblare devine o funcţie pe care programul C o tratatează la fel ca pe orice
altă funcţie C. Dacă sunt utilizate module separate, C şi asamblare, fiecare modul
este translatat în fişierul obiect corespunzător: modulul C cu ajutorul unui
compilator, modulul în asamblare cu ajutorul unui asamblor. În procesul de
generare a fişierului executabil editorul de legături utilizează ambele fişiere obiect.
Să presupunem că programul nostru hibrid conţine două module: un modul C,
fişierul main.c, şi un modul în asamblare, fişierul proc.asm. Procesul prin care se
obţine fişierul executabil conţine două etape. În prima etapă generăm fişierul obiect
pentru modulul în asamblare:

yasm -f elf -g stabs proc.asm


Acest pas crează fişierul proc.o. A doua etapă compilează modulul C şi editează
legăturile între cele două module obiect:

gcc -g stabs -o program main.c proc.o –m32

Editorul de legături este invocat automat de către GCC.

11.1. Apelarea modulelor în asamblare din C

În capitolul intitulat Funcţii am demonstrat cum trebuie să arate o funcţie în


limbaj de asamblare astfel încât să poată fi utilizată de un program în asamblare.
Aceeaşi tehnică poate fi utilizată şi pentru funcţiile în asamblare care vor fi
încorporate în programe C. Tot în acel capitol am văzut că funcţiile pot primi în
diferite moduri parametrii de intrare, unul dintre ele fiind caracteristic programelor
C. Programele C folosesc o metodă şi un format specific, şi anume, transferul
parametrilor prin intermediul stivei. În plus, unele registre au rol prestabilit, rol
care trebuie respectat cu stricteţe.
Tabelul 11.1 Rolul registrelor în convenţia de apel C
Registru Descriere
Destinat să păstreze valoarea returnată de funcţie; poate fi modificat
EAX
până la revenirea din funcţie.
EBX Destinat să indice tabelul global de deplasamente; trebuie păstrat
ECX Disponibil pentru uzul intern al funcţiei
EDX Disponibil pentru uzul intern al funcţiei
EBP Indică adresa de bază a stivei; trebuie păstrat
ESP Indică noua locaţie a stivei în cadrul funcţiei; trebuie păstrat
EDI Utilizat ca registru local de către programul C; trebuie păstrat
ESI Utilizat ca registru local de către programul C; trebuie păstrat
Destinat să păstreze valoarea în virgulă mobilă returnată de funcţie;
ST(0)
poate fi modificat până la revenirea din funcţie
ST(1) - ST(7) Disponibile pentru uzul intern al funcţiei

Regulile care trebuie respectate de orice funcţie pentru a putea fi apelată dintr-un
program C formează aşa numitele convenţii de apel C. Toate funcţiile
implementate în bibliotecile C respectă aceste reguli. Aceleaşi convenţii trebuie
respectate cu stricteţe şi de funcţiile scrise în asamblare. Pe scurt, acestea sunt:
• Funcţia trebuie să păstreze valorile iniţiale ale registrelor EBX,
ESP, ESI, EDI. Funcţia poate folosi aceste registre în corpul ei dar,
la revenirea din funcţie, registrele menţionate trebuie să conţină
valorile dinainte de apelarea acesteia. Conţinutul tuturor celorlalte
registre de uz general poate fi modificat după dorinţă.
• Rezultatul unei funcţii este returnat în registrul EAX - în cazul unei
valori de 32 de biţi sau mai mici -, în EDX:EAX - pentru o valoare
de 64 de biţi -, sau în registrul ST(0) – pentru valori în virgulă
mobilă. Şirurile, structurile sau alte elemente de date mai mari de
32 de biţi sunt returnate prin referinţă (funcţia returnează în EAX
adresa de început a acestora).
• Parametrii de intrare ai funcţiilor sunt introduşi în stivă în ordine
inversă faţă de cea a apariţiei lor în declaraţia funcţiei.
Presupunând că avem funcţia Proc(alpha, beta, gamma),
primul parametru introdus în stivă trebuie să fie gamma, urmat de
beta şi, ultimul, alpha.
• Funcţiile nu extrag parametrii din stivă. Programul apelant
efectuează acest lucru după revenirea din funcţie, fie prin
instrucţiunea POP, fie prin adunarea unui deplasament la
indicatorul de stivă ESP (metodă întâlnită mai des, deoarece este
mai rapidă).
• Deşi nu este o convenţie explicită, următoarea cerinţă este
importantă: eticheta punctului de intrare în program pentru funcţia
în asamblare trebuie să fie declarată global.

Respectarea convenţiilor de apel C începe cu salvarea şi refacerea


conţinutului registrelor: introducerea lor în stivă, la începutul funcţiei, şi extragerea
lor din stivă, la revenirea în programul principal. Procesul are loc în secvenţele
standard, prolog şi epilog (la fel ca la funcţiile utilizate de programele în asamblare
studiate în capitolele precedente).

11.1.1. Structura funcţiei în asamblare

Structura şablon a funcţiilor în asamblare destinate apelării din interiorul


programelor C arată astfel:
;
;FuncTemplate.asm
;
section .data

section .bss
section .text
global proc ;punct de intrare necesar editorului de legături

proc:
push ebp ;setează cadrul de stivă
mov ebp,esp
sub esp,12
push ebx ;funcţia trebuie să păstreze EBP, EBX, ESI, & EDI
push esi
push edi

< codul funcţiei >

pop edi ;restaurează registrele EBP, EBX, ESI & EDI


pop esi
pop ebx
mov esp,ebp ;elimină cadrul de stivă înainte de revenire
pop ebp
ret ;revenire în program

Acest şablon trebuie respectat de toate funcţiile în asamblare ce vor fi


apelate din cadrul programelor C sau C++. Desigur, dacă o funcţie nu alterează
conţinutul registrelor EBX, ESI sau EDI, putem omite instrucţiunile PUSH şi POP
echivalente. O mică remarcă: din faptul că este necesară salvarea registrele EBX,
ESP, EBP, ESI şi EDI reiese că toate celelalte registre de uz general pot fi rescrise.
Şi aici, mare atenţie: pot fi rescrise de oricine, nu numai de noi. Dacă din interiorul
funcţiei scrise de noi sunt apelate alte funcţii (din bibliotecile standard, de
exemplu), acele funcţii pot altera valorile din EAX, ECX şi EDX. Acest lucru
înseamnă că nu putem presupune faptul că o valoare contor aflată în ECX va
rămâne nemodificată după un apel al funcţiei C printf. Dacă funcţia scrisă de
noi utilizează ECX ca registru contor şi totodată include apeluri către funcţii
bibliotecă – sau către orice altă funcţie care nu e scrisă de noi – trebuie să salvăm
explicit valoarea acestuia în stivă, înainte de apel, şi să refacem explicit valoarea
acestuia, la revenirea din funcţie. Acelaşi lucru este valabil şi pentru EAX şi EDX.
În cazul celui dintâi, ştim că funcţiile standard returnează rezultatul în registrul
EAX, aşadar, când apelăm astfel de funcţii, este evident că acesta va fi rescris.
Observaţi instrucţiunea SUB inclusă în prolog. Aceasta rezervă în stivă
spaţiu destinat stocării variabilelor locale (utilizate în cadrul funcţiei noastre).
Instrucţiunea din şablon rezervă în stivă un număr de 12 octeţi de memorie. Aceştia
pot stoca trei elemente de date a câte 4 octeţi fiecare. Dacă este necesar mai mult
spaţiu, se scade din ESP numărul de octeţi corespunzător. Variabilele locale sunt
adresate din interiorul funcţiei, relativ la registrul EBP. De exemplu, dacă prima
variabilă locală ocupă 4 octeţi, adresa locaţiei va fi [EBP-4]. A doua variabilă
locală poate fi adresată cu [EBP-8], iar a treia cu [EBP-12].
Totuşi, funcţiile în asamblare pot declara propriile segmente .data şi
.bss. În acest caz, la momentul compilării, aceste zone de memorie vor fi
combinate cu zonele .data şi .bss ale programului C.

11.1.2. Cadrul de stivă

Pentru programele în asamblare, stiva este extrem de importantă. Pentru


programele C sau cele care interacţionează cu C, stiva este esenţială. În C, stiva are
un rol central. Motivul este simplu: compilatoarele sunt roboţi care traduc codul C
în limbaj în asamblare. Acest lucru înseamnă că folosesc metode mecanice, iar
majoritatea acestora depind enorm de existenţa stivei. În sistemele Linux,
mecanismele folosite de compilator se bazează pe noţiunea de cadru de stivă.
Compilatoarele folosesc cadrele de stivă în procesul de alocare a spaţiului pentru
variabile locale.
Un cadru de stivă reprezintă o regiune (din stivă) marcată ca aparţinând
unei anumite funcţii. În esenţă, aceasta este regiunea cuprinsă între adresele aflate
în două registre: indicatorul de bază (EBP) şi indicatorul de stivă (ESP).
Stiva

EBP+16:
EBP+12: Datele din această zonă
aparțin următorului cadru
EBP+8: de stivă
Direcția de creștere a adreselor de memorie

EBP+4: EBP rămâne fix până când


cadrul de stivă este
EBP+0: EBP apelant EBP eliminat.
EBP-4: EBX apelant EPB conține valoarea ESP
a funcției apelante.
EBP-8: ESI apelant
EBP-12: EDI apelant
EBP-16: (locație temporară) Această regiune reprezintă
cadrul de stivă curent
EBP-20: (locație temporară)
EBP-24: (locație temporară)
EBP-28: (locație temporară)
EBP-32: (locație temporară)
EBP-36: (locație temporară) ESP

Memorie neutilizată

ESP se deplasează
în sus și jos

Figura 11.1 Un cadru de stivă


Cadrul de stivă este creat prin salvarea în stivă a registrului EBP (registrul
EBP al funcţiei apelante) şi copierea indicatorului de stivă al funcţiei apelante în
registrul EBP. Odată ce EBP este ancorat la acel capăt al cadrului de stivă,
indicatorul de stivă, ESP, se poate deplasa liber, în funcţie de necesităţile funcţiei.
Primele două instrucţiuni din prolog:

push ebp
mov ebp,esp

În urma acestor două instrucţiuni, registrul EBP este considerat ancora noului cadru
de stivă (sau indicator de cadru). Toate elementele aflate deasupra lui în stivă
(deasupra cadrului de stivă al funcţiei curente) nu pot fi adresate decât prin
intermediul acestuia. Acolo sunt şi parametrii de intrare ai funcţiei, dacă aceasta
necesită aşa ceva. Un alt motiv pentru care EBP nu trebuie modificat este că
valoarea indicatorului de stivă al apelantului, ESP, se află în EBP. Revenirea din
funcţie cu un ESP modificat înseamnă funcţionarea defectuoasă a programului
apelant.
Înainte de finalul funcţiei, cadrul de stivă trebuie eliminat. Acesta este rolul
ultimelor două instrucţiuni din epilog:

mov esp,ebp
pop ebp

În acest moment, cadrul de stivă nu mai există şi putem executa în


siguranţă instrucţiunea RET. Instrucţiunea RET redă controlul programului C.

11.1.3. Compilarea modulelor

Compilatorul GCC poate obţine fişiere obiect din funcţii scrise în limbaj
de asamblare şi le poate adăuga la corpul programului C. Fişierul obiect al funcţiei
în asamblare este generat cu ajutorul asamblorului YASM.
Următoarea funcţie în asamblare afişează un mesaj prin intermediul
funcţiei standard printf.
;
;proc.asm
;
section .data
Msg db "Mesaj din interiorul functiei in asamblare.",10,0
section .text
global proc
extern printf
proc:
push ebp
mov ebp,esp
sub esp,12
push ebx
push esi
push edi

push Msg
call printf
add esp,4

pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret

Funcţia proc.asm foloseşte secvenţele prolog şi epilog specifice convenţiei


de apel C. Observaţi cum numele său este declarat global şi faptul că textul
mesajului este declarat în secţiunea datelor iniţializate. Pentru afişarea acestuia se
foloseşte funcţia standard printf, declarată ca extern. Asamblăm funcţia în
asamblare:

yasm -f elf -g stabs proc.asm –melf_i386

Odată creat fişierul obiect, acesta poate fi specificat în linia de comandă a


compilatorului, alături de fişierul C sursă. Fişierul programului principal este numit
main.c.

/* programul principal apelează funcţia în asamblare */


#include <stdio.h>
int main()
{
extern void proc();
proc();
printf("Mesaj din interiorul programului principal.\n");

return 0;
}

Programul principal apelează funcţia în asamblare pe baza numelui. Parantezele


rotunde indică faptul că numele respectiv reprezintă o funcţie. Foarte important,
funcţia trebuie declarată ca funcţie externă. În acest caz, funcţia nu necesită
parametri de intrare şi nici nu returnează vreo valoare. Următoarele comenzi crează
executabilul şi îl rulează.

gcc -g stabs -o program main.c proc.o –m32

./program
Mesaj din interiorul functiei in asamblare.
Mesaj din interiorul programului principal.

Codul compilat poate fi dezasamblat cu programul objdump:

objdump -d program -M intel -j .text


Veţi observa că sunt prezente mai multe secţiuni. Cele care ne interesează sunt
main şi proc. Secţiunea main conţine codul în asamblare generat de compilator
pentru programul C.

080483f4 <main>:
80483f4: 55 push ebp
80483f5: 89 e5 mov ebp,esp
80483f7: 83 e4 f0 and esp,0xfffffff0
80483fa: 83 ec 10 sub esp,0x10
80483fd: e8 1e 00 00 00 call 8048420 <proc>
8048402: c7 04 24 00 85 04 08 mov DWORD PTR [esp],0x8048500
8048409: e8 16 ff ff ff call 8048324 <puts@plt>
804840e: b8 00 00 00 00 mov eax,0x0
8048413: c9 leave
8048414: c3 ret

Secţiunea proc conţine codul funcţiei în asamblare:

08048420 <proc>:
8048420: 55 push ebp
8048421: 89 e5 mov ebp,esp
8048423: 83 ec 0c sub esp,0xc
8048426: 53 push ebx
8048427: 56 push esi
8048428: 57 push edi
8048429: 68 18 a0 04 08 push 0x804a018
804842e: e8 e1 fe ff ff call 8048314 <printf@plt>
8048433: 83 c4 04 add esp,0x4
8048436: 5f pop edi
8048437: 5e pop esi
8048438: 5b pop ebx
8048439: 89 ec mov esp,ebp
804843b: 5d pop ebp
804843c: c3 ret

Funcţia proc este apelată din programul principal în mod obişnuit, cu


instrucţiunea CALL. În acelaşi mod în care funcţia proc, la rândul său, apelează
funcţia printf.
11.2. Linii de asamblare în codul sursă C

11.2.1. Sintaxa AT&T

Încă din capitolul II am văzut că instrucţiunile maşină ale procesoarelor


x86 sunt codificate în limbaj de asamblare prin două seturi de instrucţiuni
mnemonice. Totodată, ne amintim că instrucţiunea mnemonică reprezintă numai o
modalitate prin care fiinţele umane deduc ce înseamnă pentru procesor o anumită
secvenţă de cifre binare. În loc să scriem secvenţa de zero şi unu 1000100
111000011, scriem MOV BX,AX. La fel de bine am putea scrie COPIAZĂ BX
în AX, dar, din nefericire, creatorul limbajului de asamblare nu a fost român.
Folosim MOV BX,AX numai pentru că aşa a sugerat Intel. Setul alternativ de
instrucţiuni mnemonice pentru procesoare x86 a apărut din dorinţa de a face
sistemul de operare Unix cât mai uşor de implementat pe diferite arhitecturi
hardware. Aceste instrucţiuni se numesc mnemonici AT&T. Cum toate
instrumentele de dezvoltare software cu sursă deschisă (compilatorul GCC,
depanatorul GDB, asamblorul AS) sunt destinate unui sistem de operare înrudit cu
Unix, şi anume Linux, toate folosesc nativ sintaxa AT&T. În procesul de
compilare, GCC nu face altceva decât să traducă codul sursă C în cod sursă scris în
limbajul de asamblare conform cu sintaxa AT&T. Ne amintim din capitolul III cum
compilatorul CC primeşte ca intrare un fişier sursă .c şi generează un fişier sursă
.s, pe care îl trimite apoi asamblorului GAS. Utilitarele GNU lucrează în acelaşi
fel pe toate platformele. Într-un sens, limbajul de asamblare este un limbaj
intermediar folosit numai în beneficiul compilatorului. De cele mai multe ori,
programatorii nu se vor întâlni cu el. Cu toate acestea, dacă trebuie să lucraţi cu
GCC şi biblioteca standard C sau cu o mulţime de alte biblioteci scrise în C şi
pentru C, trebuie cel puţin să vă familiarizaţi cu mnemonicele AT&T. Mai ales
dacă lucraţi pe platforme Unix sau înrudite. Să ne amintim cu arată un program în
această sintaxă:

gcc -S prog.c -o - -m32

.file "prog.c"
.text
.globl main
.type main, @function
main:
pushl %ebp push ebp
movl %esp, %ebp mov ebp, esp
subl $16, %esp sub esp, 16
movl $40, -4(%ebp) mov DWORD PTR [ebp-4], 40
movl $50, -8(%ebp) mov DWORD PTR [ebp-8], 50
movl -4(%ebp), %eax mov eax, DWORD PTR [ebp-4]
movl %eax, -12(%ebp) mov DWORD PTR [ebp-12], eax
movl -8(%ebp), %eax mov eax, DWORD PTR [ebp-8]
movl %eax, -4(%ebp) mov DWORD PTR [ebp-4], eax
movl -12(%ebp), %eax mov eax, DWORD PTR [ebp-12]
movl %eax, -8(%ebp) mov DWORD PTR [ebp-8], eax
movl $0, %eax mov eax, 0
leave leave
ret ret
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits

În dreapta listingului apare echivalentul instrucţiunilor AT&T în sintaxa Intel.


Sintaxa Intel a fost obţinută cu:

gcc -S prog.c -o - -masm=intel

Reguli generale impuse de sintaxa AT&T:


• În sintaxa AT&T, comentariile sunt delimitate cu semnul # (diez). În
sintaxa Intel se foloseşte ; (punct şi virgulă).
• Mnemonicele AT&T şi numele registrelor sunt scrise întotdeauna cu
caractere mici. Acest lucru respectă faptul că Unix diferenţiază literele
mari de cele mici. În timp ce Intel sugerează utilizarea majusculelor, dar
acceptă literele mici, AT&T „cere” litere mici.
• Numele de registre sunt întotdeauna precedate de simbolul % (procent).
Acest lucru permite asamblorului să recunoască numele de registre.

Intel: EAX
AT&T: %eax

• În sintaxa AT&T, operandul destinaţie este întotdeauna în dreapta,


operandul sursă este în stânga. Aşadar, operanzii sunt în ordine inversă
faţă de sintaxa Intel.
• Întotdeauna, la mnemonica instrucţiunilor maşină cu operanzi se adaugă un
sufix de un caracter, sufix care indică dimensiunea operazilor. Caracterele
sufix sunt b (byte), w (word) şi l (long), indicând un octet, doi octeţi sau
patru octeţi. În caz contrar, GAS va încerca să ghicească singur
dimensiunea operanzilor. Este indicat ca asamblorul să nu fie lăsat să
ghicească, deoarece o poate face greşit.

Intel: mov bx,ax


AT&T: movw %ax,%bx

• În sintaxa AT&T, operanzii imediaţi şi valorile constante sunt întotdeauna


precedate de semnul $ (dolar). Acest lucru permite asamblorului să
recunoască operanzi de tip imediat.

Intel: mov eax, _const


AT&T: movl $_const, %eax

Introducem imediatul 0A00BH în registrul EBX:

Intel: mov ebx, 0A00BH


AT&T: movl $0xa00b, %ebx

Introducem imediatul 255 în registrul AL:

Intel: mov al, 255


AT&T: movb $255, %al

Încarcă în registrul ECX adresa variabilei globale C, total:

Intel: mov ecx, total


AT&T: movl $total, %ecx

• În sintaxa AT&T, deplasamentul din metodele de adresare a memoriei sunt


valori cu semn plasate în afara parantezelor rotunde care conţin baza,
indexul şi factorul de scală.

Intel: [bază + index × scală ) ± deplasament]


AT&T: ± deplasament (bază, index, scală)

Simbolul ± semnifică faptul că deplasamentul este cu semn; poate fi


pozitiv sau negativ, lucru care indică dacă această valoare se adună sau se scade
din restul adresei efective. În mod obişnuit, vom întâlni explicit semnul minus
numai atunci când deplasamentul este negativ, lipsa semnului denotă deplasament
pozitiv. Deplasamentul şi factorul de scală sunt opţionale.

Pentru adresarea indirectă şi directă, sintaxa AT&T foloseşte paranteze


rotunde, spre deosebire de sintaxa Intel, care foloseşte paranteze pătrate. De
exemplu,

mov eax, [ebx]


mov eax,[d]

este scrisă în AT&T ca

movl (%ebx), %eax


movl d,%eax

În acest ultim caz, observaţi diferenţa dintre:

Intel: mov eax,[d] AT&T: movl $d, %eax


Intel: mov eax, d AT&T: movl d, %eax

Prima instrucţiune încarcă în registrul EAX conţinutul adresat de eticheta d, a doua


instrucţiune încarcă chiar adresa.
În sintaxa Intel, dacă dorim să extragem un element de o anumită
dimensiune, folosim operatorii byte ptr, word ptr sau dword ptr, unde
ptr poate să lipsească. De exemplu:

Intel: mov ax, word [ebp]


AT&T: movw (%ebp), %ax
sau
Intel: mov byte al, [ebx]
AT&T: movb (%ebx), %al

Dacă există deplasament, acesta trebuie poziţionat în faţa parantezelor:

Intel: mov dword eax, [ebx-4]


AT&T: movl -4(%ebx), %eax

Intel: mov al, byte [ebx + edi + 28]


AT&T: movb 28(%ebx,%edi), %al

Intel: mov ebx, [eax + _variabila]


AT&T: movl _variabila(%eax), %ebx

Când sunt folosite toate câmpurile modului de adresare a memoriei,


întâlnim expresii de genul:
Intel: mov edx, [ebx + edi * 8 + _vector]
AT&T: movl _vector(ebx,edi,8), %edx

Câteva expresii mai puţin intuitive:

Intel: mov ebx, [eax * 4 + vector] ;adresare indexată


AT&T: movl vector(,%eax,4), %ebx
şi
Intel: mov ecx, [eax + 1] ;adresare bazată
AT&T: movl 1(%eax), %ecx

Ne oprim aici. Ca notă, amintiţi-vă că puteţi folosi registrul ESP în interiorul


expresiei de calcul a adresei efective, dar numai ca registru de bază.

11.2.2. Formatul de bază

Structura necesară unei funcţii în asamblare pentru a putea fi introdusă


direct în programele C (introducerea directă a instrucţiunilor de asamblare în
corpul C) nu diferă foarte mult faţă de structura funcţiilor în limbaj de asamblare
studiate până acum. Singura diferenţă este că, de această dată, secvenţa de
instrucţiuni se află chiar în fişierele sursă C. Compilatorul GNU C este atenţionat
că urmează o secţiune scrisă în asamblare prin cuvântul cheie asm. Formatul de
bază al unei secţiuni asm este următorul:

asm(”cod în asamblare” );

Codul în asamblare aflat între paranteze rotunde trebuie să respecte un format


specific:
• Instrucţiunile trebuie introduse între ghilimele;
• Dacă sunt prezente mai multe instrucţiuni, acestea trebuie separate printr-
un caracter de linie nouă, \n. Adesea, pentru a face liniile mai uşor de citit,
este introdus şi un caracter \t.

A doua regulă este necesară deoarece compilatorul tratează codul din secţiunea
asm textual, plasându-l în codul generat exact aşa cum este scris. Următorul
exemplu reprezintă funcţia de ieşire din program:

asm(“movl $1, %eax\n\tmovl $0, %ebx\n\tint $0x80”);

Deoarece formatul nu este foarte lizibil, majoritatea programatorilor scriu


instrucţiunile pe linii separate. În acest caz, fiecare instrucţiune trebuie închisă între
ghilimele.

asm ( “movl $1, %eax\n\t”


“movl $0, %ebx\n\t”
“int $0x80”);

Acest format este mai uşor de citit şi înţeles. Secţiunea asm poate fi inclusă
oriunde în codul sursă. Următorul program demonstrează cum arată o secţiune asm
într-un program.

/* global.c – AT&T – Exemplu care utilizează variabile C */


#include <stdio.h>
int a = 50;
int b = 60;
int rez;
int main() {

asm ( “pusha\n\t”
“movl a, %eax\n\t”
“movl b, %ebx\n\t”
“imull %ebx, %eax\n\t”
“movl %eax, rez\n\t”
“popa”);

printf(“the answer is %d\n”, rez);


return 0;
}

Variabilele a, b şi rez sunt definite în programul C ca variabile globale şi sunt


folosite în secţiunea asm. Observaţi că, în interiorul secţiunii asm, valorile sunt
utilizate ca locaţii de memorie şi nu ca elemente de date imediate. Variabilele pot fi
de asemenea folosite în altă parte a programului.

Variabilele de date trebuie declarate ca globale. În interiorul secţiunii asm nu


putem folosi variabile locale.

Codul în asamblare generat de compilator arată astfel:

gcc -S global.c -o - -m32

.file "global.c"
.globl a
.data
.align 4
.type a, @object
.size a, 4
a:
.long 50
.globl b
.align 4
.type b, @object
.size b, 4
b:
.long 60
.comm rez,4,4
.section .rodata
.LC0:
.string "the answer is %d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
#APP
# 8 "global.c" 1
pusha
movl a, %eax
movl b, %ebx
imull %ebx, %eax
movl %eax, rez
popa
# 0 "" 2
#NO_APP
movl rez, %edx
movl $.LC0, %eax
movl %edx, 4(%esp)
movl %eax, (%esp)
call printf
movl $0, %eax
leave
ret
.size main, .-main
.ident"GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
.section .note.GNU-stack,"",@progbits

Observaţi că a şi b au fost definite în segmentul datelor iniţializate şi au primit


valoare, iar rez a fost definit în segmentul datelor neiniţializate. Sintaxa AT&T
defineşte două directive prin care putem declara date neiniţializate, .comm este
una dintre ele. Nu intrăm în detalii.
Codul generat foloseşte prologul şi epilogul C standard, ultimul
implementat prin instrucţiunea leave. În cadrul codului generat se află o regiune
cuprinsă între simbolurile #APP şi #NO_APP. Aceasta este secţiunea care conţine
codul specificat cu asm. Observaţi efectul caracterelor \n şi \t asupra amplasării
codului.
Din exemplu reiese încă o caracteristică importantă: secvenţa „utilă” de
instrucţiuni în asamblare trebuie precedată de instrucţiunea PUSHA şi încheiată cu
instrucţiunea POPA. Este important de reţinut că înainte de execuţia codului în
asamblare trebuie să salvaţi valorile iniţiale ale registrelor, iar la final trebuie să
restauraţi conţinutul acestora deoarece este posibil ca GCC să folosească acele
registre în altă parte a codului sursă (dacă sunt modificate în interiorul secţiunii
asm, pot apărea efecte nedorite).

Sintaxa Intel
Până acum codul inserat în program a respectat sintaxa AT&T. A fost mai
uşor pentru noi să folosim sintaxa AT&T pe o platformă Unix, cu un compilator ce
recunoaşte nativ sintaxa AT&T. Să vedem cum se modifică programul anterior
dacă specificăm secţiunea asm în sintaxa Intel.

/* global.c – Intel - Exemplu care utilizează variabile C */


#include <stdio.h>
int a = 50;
int b = 60;
int rez;
int main()
{
asm (".intel_syntax noprefix\n\t"
"pusha\n\t"
"mov eax,a\n\t"
"mov ebx,b\n\t"
"imul eax,ebx\n\t"
"mov rez,eax\n\t"
"popa\n\t"
".att_syntax prefix");
printf("the answer is %d\n", rez);
return 0;
}

Observaţi că la începutul secvenţei în asamblare am inserat ".intel_syntax


noprefix\n\t". Aceasta indică asamblorului GAS că următoarea secvenţă de
instrucţiuni în asamblare foloseşte sintaxa Intel. noprefix înseamnă că registrele
nu au nevoie de simbolul % ca prefix. Ultima linie, ".att_syntax prefix",
indică asamblorului GAS că instrucţiunile următoare vor fi asamblate în sintaxa
implicită, AT&T. În acest caz, compilarea se face în mod obişnuit:

gcc -g stabs -o global global.c –m32

Dacă lipsea ultima linie din secţiunea asm, ".att_syntax noprefix",


puteam obţine executabilul forţând compilatorul GCC să tranducă tot codul în
sintaxa Intel:

gcc -g stabs -o global global.c -masm=intel –m32

Modificatorul volatile
Când introducem instrucţiuni în asamblare în programe C trebuie să ne
gândim ce efect ar putea avea procesul de compilare asupra acestora. În etapele de
transformare a codului sursă în cod de asamblare, compilatorul poate încerca să
optimizeze codul scris în asamblare în vederea creşterii performanţei. De obicei,
acest lucru are loc prin eliminarea funcţiilor neutilizate, partajarea registrelor între
variabile care nu sunt folosite în acelaşi timp sau rearanjarea codului pentru o
parcurgere mai uşoară.
Însă este posibil ca optimizarea să producă efecte nedorite, în special
asupra funcţiilor din secţiunea asm. Modificatorul volatile, plasat imediat
după cuvântul cheie asm, indică compilatorului faptul că nu este dorită optimizarea
acelei secţiuni de cod. Formatul este următorul:

asm volatile (”cod în asamblare”);

Utilizarea __asm__
În unele cazuri, cuvântul asm, utilizat la identificarea unei secţiuni de cod
în asamblare, trebuie modificat. Specificaţiile ANSI C utilizează cuvântul asm în
alte scopuri, lucru care împiedică utilizarea sa ca identificator de secţiune în
asamblare. Dacă scrieţi cod utilizând convenţiile ANSI C, trebuie să utilizaţi
cuvântul cheie __asm__ (două caractere underscore). Codul în asamblare din
interiorul secţiunii nu se modifică. Dacă avem nevoie de modificatorul
volatile, acesta trebuie scris __volatile__.

__asm__ __volatile__ (".intel_syntax noprefix\n\t"


"pusha\n\t"
"mov eax,a\n\t"
"mov ebx,b\n\t"
"imul eax,ebx\n\t"
"mov rez,eax\n\t"
"popa\n\t"
".att_syntax prefix");

11.2.3. Formatul extins

Formatul de bază are câteva limitări. În primul rând toate valorile de intrare
şi de ieşire trebuie să utilizeze variabile globale definite în program. În plus, trebuie
să fim extrem de atenţi să nu modificăm valorile vreunui registru. Formatul extins
pune la dispoziţie câteva opţiuni adiţionale ce ne permit să controlăm exact modul
în care este interpretat codul în asamblare. Formatul versiunii extinse a secţiunii
asm arată astfel:

asm (”cod în asamblare” : locaţii de ieşire : operanzi


de intrare : lista de modificări);

Acest format are patru secţiuni, fiecare separată prin două puncte:
• cod în asamblare - propoziţiile în asamblare propriu zise;
• locaţii de ieşire - registre şi/sau locaţii de memorie ce vor conţine valorile
de ieşire la finalul codului în asamblare;
• operanzi de intrare - registre şi/sau locaţii de memorie ce conţin valorile de
intrare necesare codului din secţiunea scrisă în asamblare;
• lista de modificări - registre ce vor fi modificate de către codul în
asamblare.

Prezenţa tuturor secţiunilor nu este obligatorie. Dacă codul în asamblare nu


generează valori de ieşire, secţiunea locaţiilor de ieşire poate lipsi. În schimb,
separarea secţiunilor prin două puncte se păstrează. Doar dacă secţiunea codului în
asamblare nu modifică niciun registru ultimele două puncte pot fi omise.
În formatul asm de bază valorile de intrare şi ieşire erau definite ca
variabile globale în programul C şi încorporate în secţiunea în asamblare pe baza
numelui acestora. În formatul extins putem atribui valori de intrare şi ieşire atât
registrelor cât şi locaţiilor de memorie. Formatul listei de valori de intrare şi ieşire
este:

”caracter_de_control” (variabilă)

unde variabila este o variabilă C declarată în program. În formatul asm extins pot
fi folosite atât variabile globale cât şi locale. Caracterul de control (constraint)
specifică unde trebuie plasată variabila (pentru valori de intrare) sau unde trebuie
depozitată (pentru valori de ieşire). Acesta defineşte locaţia finală a unei variabile:
registru sau locaţie de memorie.
Tabelul 11.2 Caractere de control
Caracter de control Descriere
a Foloseşte registrele %eax, %ax sau %al
b Foloseşte registrele %ebx, %bx sau %bl
c Foloseşte registrele %ecx, %cx sau %cl
d Foloseşte registrele %edx, %dx sau %dl
S Foloseşte registrele %esi sau %si
D Foloseşte registrele %edi sau %di
r Foloseşte orice registru de uz general
q Foloseşte registrul %eax, %ebx, %ecx sau %edx
A Combină registrele %eax şi %edx pentru valori de 64 de biţi
f Foloseşte un registrul pentru variabile în virgulă mobilă
m Foloseşte locaţia de memorie a variabilei
V Foloseşte numai o locaţie de memorie directă
i Foloseşte un întreg imediat
n Foloseşte un întreg imediat de valoare cunoscută
g Foloseşte orice registru sau locaţie de memorie disponibilă

Pe lângă caracterele de control, valorile de ieşire pot include modificatori de


control. Modificatorii de control indică modul în care compilatorul trebuie să
trateze operanzii.
Tabelul 11.3 Modificatori de control
Modificator de ieşire Descriere
+ Operandul poate fi atât citit cât şi scris (read-write)
= Operandul poate fi numai scris (write-only). Valoarea
anterioară este înlocuită cu valoarea de ieşire curentă.
% Operandul poate fi schimbat cu următorul dacă este necesar
& Operandul poate fi şters şi reutilizat înainte de finalizarea
codului în asamblare.
Următorul exemplu

asm volatile (".intel_syntax noprefix\n\t"


"cld\n\t"
"rep\n\t"
"stosl\n\t"
".att_syntax prefix\n\t"
: /* nicio locaţie de ieşire */
: "c" (contor), "a" (valoare), "D" (dest)
: "%ecx", "%edi"
);

încarcă valoarea aflată în registrul EAX în locaţia de memorie indicată de


adresa dest, de un număr de ori contor. Linia operanzilor de intrare

: "c" (contor), "a" (valoare), "D" (dest)

încarcă contor în ECX, valoare în EAX şi dest în EDI. Aceste informaţii


pot ajuta compilatorul să optimizeze codul. De exemplu, în timpul operaţiunilor de
alocare a registrelor compilatorul ar putea aranja ca valoarea să se afle deja în
registrul EAX sau, dacă codul în asamblare s-ar afla într-o buclă, ar putea păstra
conţinutul EAX de-a lungul execuţiei acesteia.
Observaţi că nu am specificat nimic în secţiunea locaţiilor de ieşire. Nu
este necesar ca valorile de ieşire să fie întotdeauna precizate. Valorile de intrare ale
unor instrucţiuni indică şi valorile de ieşire. În cazul nostru, valoarea de ieşire este
deja definită ca fiind una din valorile de intrare (adresa indicată de registrul EDI),
aşadar aceasta nu mai trebuie specificată în câmpul locaţiilor de ieşire. Dar,
deoarece nu am definit explicit nicio valoare de ieşire, este important să folosim
cuvântul cheie volatile. În caz contrar, din moment ce secţiunea asm nu
produce niciun rezultat, compilatorul poate considera toată secţiunea inutilă şi
renunţă la ea (nu o introduce în fişierul executabil).
Lista de modificări
: "%ecx", "%edi"

indică compilatorului faptul că registrele EAX şi EDI au fost rescrise


(valorile acestora la finalul secvenţei în asamblare diferă de cele avute la intrarea în
secţiune). În lista de modificări registrele sunt scrise cu prefixul %. Dacă scriem
într-o locaţie de memorie trebuie să includem în listă cuvântul ”memory”. Dacă
sunt modificaţi indicatorii de condiţii din registrul EFLAGS este indicat să
introducem în lista de modificări operatorul ”cc” (conditional codes), care anunţă
compilatorul de aceste modificări.

Din nefericire, directivele extinse folosesc sintaxa AT&T, din acest motiv
am inserat în faţa acestora opţiunea ".att_syntax prefix". Sintaxa GNU
Intel defineşte numai sintaxa pentru codurile de instrucţiune, nu pentru directive,
funcţii, macroinstrucţiuni, etc.. Directivele folosesc încă sintaxa AT&T. Se poate
întampla să fie nevoie să comutăm de la o sintaxă la alta chiar şi în interiorul
secvenţei în asamblare. În special atunci când lucrăm cu operanzi aflaţi în
memorie, deoarece mecanismul de substituţie al operanzilor foloseşte sintaxa
AT&T. Din acest motiv, în exemplele următoare vom folosi numai sintaxa AT&T.

Definirea registrelor
Următoarea secvenţă demonstrează cum putem declara registre în formatul
asm extins.

asm (“imull %%edx, %%ecx\n\t”


“movl %%ecx, %%eax”
: “=a”(rez)
: “d”(val1), “c”(val2));

Registrul de ieşire este modificat cu semnul egal; indicăm faptul că asupra lui pot fi
efectuate numai operaţii de scriere. Compilatorul încarcă valorile variabilelor
val1 şi val2 în registrele EDX şi ECX. Val1 şi val2 pot fi variabile globale
sau locale. În ultimul caz, ele se găsesc în zona de stivă a programului. Rezultatul
generat în registrul EAX este apoi transferat în variabila rez. Observaţi că
registrele au ca prefix două semne %% în loc de unul singur. Acest lucru este
necesar deoarece, în secvenţa în asamblare, fiecare operand este adresat de
compilator pe baza unui număr (placeholder) precedat de semnul %. Compilatorul
atribuie fiecărei valori de intrare sau ieşire prezente în câmpurile formatului extins
un număr, pe baza poziţiei sale în listă, începând cu zero. De exemplu:

asm (”cod în asamblare”


: ”=r” (rez)
: ”r” (val1), ”r” (val2)
);

va produce următoarele coduri:


• %0, pentru registrul care conţine variabila rez.
• %1, pentru registrul care conţine variabila val1.
• %2, pentru registrul care conţine variabila val2.
Metoda permite utilizarea în secvenţa de asamblare atât a registrelor cât şi a
locaţiilor de memorie. Numărul total de operanzi este limitat la zece sau la numărul
maxim de operanzi pe care îl poate lua o instrucţiune (care este mai mare). Codul
în asamblare arată astfel:

imull %1, %2
movl %2, %0

Deoarece GCC identifică operanzii pe baza %0, %1, ş.a.m.d, %edx ar fi interpretat
ca fiind parametrul %e, care nu există şi, în consecinţă, ar fi ignorat. Apoi ar
încerca să găsească simbolul dx, simbol invalid, deoarece nu are prefixul %, dar
care oricum nu era ce se intenţionase.
Aşadar, pentru o secvenţă în asamblare, de exemplu una care înmulţeşte o
valoare cu 5, putem declara registrele explicit

asm ("leal (%%ebx,%%ebx,4), %%ebx"


: "=b" (x)
: "b" (x) );

caz în care trebuie să avem grijă să folosim două simboluri procent, %%, sau putem
permite compilatorului să aleagă registrele. Cu excepţia cazului în care avem
nevoie explicit de un anumit registru, cel mai bine este să permitem compilatorului
să aleagă. Numărul registrelor este limitat şi se poate întampla ca GCC să nu poată
folosi registrele specificate fără să „ascundă” valorile anterioare.

asm ("leal (%1,%1,4), %0"


: "=r" (x)
: "r" (x) );

Mai mult, dacă dorim ca variabila să folosească acelaşi registru, atât pentru intrare
cât şi pentru ieşire, putem specifica registrul atribuit pe baza codului %0.

asm ("leal (%0,%0,4), %0"


: "=r" (x)
: "0" (x) );

Dacă se lucrează cu un număr mare de valori de intrare şi ieşire, metoda numerică


poate deveni problematică. De aceea, compilatorul GNU pune la dispoziţie o
variantă alternativă, şi anume, declararea unor denumiri. Numele este declarat în
secţiunea care defineşte valorile de intrare sau ieşire şi respectă următorul format:

%[nume] ”caracter_de_control” (variabilă)


În acest caz, valoarea nume devine identificatorul variabilei.

asm (“imull %[valoare1], %[valoare2]”


: [valoare2] “=r”(val2)
: [valoare1] “r”(val1), “0”(val2));
Bibliografie

1. Intel Corporation, „Software Developer’s Manual – Basic


Architecture”, vol. 1, Order Number 243190, 1997.
2. Intel Corporation, „Software Developer’s Manual – Instruction Set
Reference”, vol. 2, Order Number 243191, 1997.
3. Intel Corporation, „Software Developer’s Manual – System
Programming Guide”, vol. 3, Order Number 243192, 1997.
4. Intel Corporation, „Intel 80386 Programmer’s Reference Manual”,
1986.
5. Iulian Bădescu, „Microprocesoare”, Printech, Bucureşti, 2002,
ISBN: 973-652-547-3
6. Ubuntu documentation team, „Ubuntu Server Guide”, 2012,
https://help.ubuntu.com/12.04/serverguide/serverguide.pdf.
7. Vasile Lungu, „Procesoare INTEL, Programarea în limbaj de
asamblare”, Ediţia a II-a, Editura Teora, 2004, ISBN: 973-20-
0099-6.
8. Jeff Dunteman, „Assembly Language Step-by-Step”, Wiley
Publishing, Inc., 2009, ISBN: 978-0-470-49702-9.
9. Sivarama Dandamudi, „Guide to Assembly Language
Programming in Linux”, Springer Science+Bussiness Media, Inc.,
2005, ISBN-10: 0-387-25897-3.
10. Richard Stallman, Roland Pesch, Stan Shebs, et al., „Debugging
with GDB – The GNU Source-Level Debugger”, Ninth Edition,
ver. 20040217, Free Software Foundation, ISBN: 1-882114-77-9.
11. Peter Jay Salzman, „Using GNU’s GDB Debugger”, Tutorial,
http://www.cs.cmu.edu/~gilpin/tutorial/
12. Richard Blum, „Professional Assembly Language”, Wiley
Publishing, Inc., 2005, ISBN: 0-7645-7901-0.
13. Bob Nevelin, „Linux Assembly Language Programming”, First
Edition, Prentice Hall, Inc., 2000, ISBN: 0-13-087940-1.
14. Paul Carter, „PC Assembly Language”, 2006, http://www.dr
paulcarter.com/pcasm/
15. Robert Platz, „Introduction to Computer Organization with x86-64
Assembly Language & GNU/Linux”, 2011, http://bob.cs.sonoma.
edu/IntroCompOrg_Jan_2011.pdf
16. Yariv Kaplan, „Introduction to Protected-Mode”, 1997, Articol,
Internals.com, http://www.internals.com/articles/protmode/introd
uction.htm
17. Randal Bryant, David O’Hallaron, „Computer Systems – A
Programmer’s Perspective”, 2nd Edition, Pearson Education, Inc,
2011, ISBN: 0-13-610804-0.
18. Dragoş Acostăchioaie, “Programare C şi C++ pentru Linux”,
Editura Polirom, 2002, ISBN: 973-681-112-3.
19. blog.interlinked.org, „Vim Introduction and Tutorial”, Tutorial,
http://blog.interlinked.org/tutorials/vim_tutorial.html
20. Free Software Foundation, „GCC 4.6.3 Manual”, 2010, http://gcc.
gnu.org/onlinedocs/gcc-4.6.3/gcc/
21. NASM Development Team, „NASM – The Netwide Assembler”,
ver. 2.10.01, 2012, http://www.nasm.us/xdoc/2.10.01/nasmdoc.pdf
22. Peter Johnson, „Yasm User Manual”, 2012, http://www.tortall.net
/projects/yasm/manual/manual.pdf
23. Tool Interface Standards, „Executable and Linkable Format
(ELF)”, Portable Formats Specification, ver. 1.1, http://www.sky
free.org/linux/references/ELF_Format.pdf
24. Phillip, „Using Assembly Language in Linux”, Articol, 2001,
http://asm.sourceforge.net//articles/linasm.html
25. Bharata Rao, „Inline assembly for x86 in Linux”, Articol, 2001,
http://www.ibm.com/developerworks/linux/library/l-ia/index.html
26. Brennan Underwood, „Brennan’s Guide to Inline Assembly”, ver.
1.1.2.2, Articol, http://www.delorie.com/djgpp/doc/brennan/bren
nan_att_inline_djgpp.html
27. Ram Narayan, „Linux assemblers: A comparison of GAS and
NASM”, Articol, 2007, http://www.ibm.com/developerworks/linux
/library/l-gas-nasm/index.html
28. iecc.com, „Dynamic Linking and Loading”, rev. 2.3, Articol, 1999,
http://www.iecc.com/linker/linker10.html
29. Ashish Bansal, „Shared objects for the object disoriented!”,
Articol, 2001, http://www.ibm.com/developerworks/library/l-
shobj/
30. Baris Simsek, „Libraries”, EnderUNIX Software Development
Team, 2004, http://www.enderunix.org/simsek/articles/
libraries.pdf

S-ar putea să vă placă și