Documente Academic
Documente Profesional
Documente Cultură
în limbaj de asamblare
Ștefan Gabriel Șoriga Iulian Bădescu
Organizarea capitolelor
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
Bibliografie .................................................................................................379!
1. REPREZENTAREA INFORMAŢIEI ÎN
SISTEMELE DE CALCUL
0101001101101001011011010111000001101100011101010010000
0011000110110000100100000010000100111010101101110011000
01001000000111101001101001011101010110000100101110
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!!
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.
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.
135!" = d! d! d! d! d! d! d! d! = 10000111! .
Verificăm:
Verificăm:
53 69 6d 70 6c 75 20 63 61 20
42 75 6e 61 20 7a 69 75 61 2e
Împărţit în dublu cuvinte (unirea două câte două a cuvintelor de mai sus):
Aşadar,
0,57!" = 0, d! d! d! d! d! d! d! d! d! d! d!" d!! = 0,100100011110! = 0,91E!"
1.4. Exerciții
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
a) 32.45 e) 15.32
b) 147.83 f) 7.8
c) 3.0125 g) 63.25
d) 255.255 h) 18.5
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
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.
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ă
CPU (ALU,
Intrare și
Registre și Memorie
Ieșire (I/O)
Control)
Magistrala de
Magistrala de date
sistem
Magistrala de adrese
Magistrala de control
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.
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.3. Memoria
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.
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
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ă.
Memorie L1 Cache
Registrul de instrucțiune
Unitatea de control
Registre
Unitatea
Aritmetică și
Logică
Interfața cu
Registrul indicatorilor de magistralele
stare
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.
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.
Ciclu de execuţie
Extrage Decodifică Execută Extrage Decodifică Execută
timp
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).
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
2.3. Exerciţii
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.
Nivel 5
Aplicație
Nivel 3
Limbaj de asamblare
Dependent de
Nivel 2
sistem
Limbaj mașină
Nivel 1
Apeluri de sistem
Nivel 0
Hardware
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.
B0
89
C2
89
D1
89
C8
7 0
Figura 3.4 Reprezentarea instrucţiunilor maşină în hexazecimal
mov eax,ecx
mov ecx,edx
mov edx,eax
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
Editor de
legături
Alt$fișier$obiect Fișier$executabil
Biblioteci$de$fișiere$obiect
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.
Î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.
gcc -E
gcc -S
gcc -c
gcc
/* 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
3.2.2. Compilarea
.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
3.2.3. Asamblarea
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.
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
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ă.
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.4. Exerciţii
3.2. De ce este considerat limbajul de asamblare limbaj de nivel scăzut iar C limbaj
de nivel înalt?
...
section .rodata
date read-only
section .data
date iniţializate
section .bss
date neiniţializate
section .text
cod
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).
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.
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.
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
segment .data
segment .text
08048000H
00000000H
7 0
Figura 4.2 Aspectul memoriei pentru un proces în linux
• 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
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 '$'
_init dw 10*25
este echivalent cu
_init dw 250
Ordinea octeţilor
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
Constante
Î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
%assign i j+10
%define i [EBX+2]
Câteva exemple:
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:
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):
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:
:[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
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
4.5.2. Asamblarea
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ă.
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.
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:
Î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
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ă.
ld -o prog prog.o
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:
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.
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.
prog: prog.o
ld -o prog prog.o
prog.o: prog.asm
yasm -f elf -g stabs prog.asm
make -k
Odată fişierul binar obţinut, îl putem executa prin apelarea sa din linie de
comandă:
./firstProg
Segmentation Fault
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
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)
stefan@laptop:~$ gdb -q
(gdb) file prog
Reading symbols from /home/stefan/prog...done.
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.
Î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
(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
(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.
(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
(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
(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
(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) info b
No breakpoints or watchpoints.
(gdb) b *_start+1
Breakpoint 1 at 0x8048081: file prog.asm, line 14.
(gdb) r
Starting program: /home/stefan/prog
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.
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).
4.4. Conform cărui criteriu aranjează un procesor Intel x86 octeţii de date în
memorie?
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 '-'
ls -l testMarime
-rwxr-xr-x 1 ubuntu users 992 2011-02-19 21:59 testMarime
ls -l testMarime
-rwxr-xr-x 1 ubuntu users 1075 2011-02-19 22:05 testMarime
ls -l testMarime
-rwxr-xr-x 1 ubuntu users 9271 2011-02-19 22:05 testMarime
mov destinaţie,sursă
mov eax, 1
mov ebx, 0
int 80h
mov al, 13
mov [b], al
mov bx, 65535
mov [w], bx
mov ecx, 0aabbccddh
mov [d], ecx
;mov ebp, 0
;mov [b], ebp
valori: dw 10,20,30,40,50,60
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.
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
mov eax,1
mov ebx,0
int 080h
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 [b], al
mov [w], bx
În acelaşi mod,
mov eax,1
mov ebx,0
int 080h
Să considerăm instrucţiunea:
mov [d], 1 ;validă, adresare directă prin deplasament
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 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
mov eax,1
mov ebx,0
int 080h
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
se poate folosi
Echivalentul
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
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.
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.
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
mov r, r
mov r, m
mov m, i
mov r, m
mov m, r
mov r,rmi
mov m,ri
mov bx,al
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
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:
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.
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.
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
[reg]
[imediat]
[reg+reg]
[reg+imediat]
[reg+reg+imediat]
[reg+scală×reg]
[scală×reg+imediat]
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).
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ă]
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ă.
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
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.
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
add al,constantă
add eax,constantă
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
1111 1101 +
1
----------
1111 1110
-1 + 1 = 0 1111 1111 +
0000 0001
------------
CF = 1 0000 0000
Î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).
mov eax,1
mov ebx,0
int 080h
(gdb) i r al
al 0x81 -127
(gdb) i r eax
eax 0x81 129
(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
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.
;movzx eax, al
mov eax,1
mov ebx,0
int 080h
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.
;
;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ă
mov eax,0
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ă):
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.
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.
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.
1 ← transport 1 → împrumut
0000 1000 + 0010 1011 -
1000 1000 0101 1100
1001 0000 1100 1111
7.3.6. Indicatorul de paritate
;
;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
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
add destinaţie,sursă
mov eax,1
mov ebx,0
int 080h
section .data
val db 132
mov eax,1
mov ebx,0
int 080h
mov bl,2
mov al,127
add bl,al
EAX EBX
401d0219 d18d50e1
ECX EDX
4016edec e09c1528
adc destinaţie,sursă
add ebx,edx
adc eax,ecx
mov eax,1
mov ebx,0
int 080h
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
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
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
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.
jc sfarsit
mov eax,1
int 080h
sfarsit:
mov eax,1
mov ebx,0
int 080h
CF = 1 fără semn CF = 1
0 255
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.
sub ebx,edx
sbb eax,ecx
mov eax,1
mov ebx,0
int 080h
inc destinaţie
dec destinaţie
mul sursă
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
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
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
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).
mov ecx,eax
mov eax,ebx
mov ebx,ecx
xchg operand1,operand2
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
mov eax,'ABCD'
(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
(gdb) i r eax
eax 0x41424344 1094861636
mov eax,'ABCD'
bswap eax
mov eax,1
mov ebx,0
int 80h
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ă
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
cmpxchg8b destinaţie
mov eax,1
mov ebx,0
int 80h
7.7. Instrucțiuni de prelucrare la nivel de bit
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:
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ă
Î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.
test al,00000100b
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
shl destinaţie
shl destinaţie,CL
shl destinaţie,imm8
shr destinaţie
shr destinaţie,CL
shr destinaţie,imm8
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
mov al,-4
shr al,1
movsx ecx,al
mov eax,1
mov ebx,0
int 080h
shld destinație,sursă,contor
shrd destinație,sursă,contor
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
rol destinaţie,1
rol destinaţie,CL
rol destinaţie,imm8
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țiunile din acest grup modifică numai indicatorul de transport. Ceilalți cinci
indicatori de stare rămân nemodificați.
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.
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
jmp adresă
inc edi
mov ecx,[val+edi*4]
cmovnbe ebx,ecx
sfarsit:
mov eax,1
mov ebx,0
int 080h
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
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
mov eax,1
mov ebx,0
int 080h
loop adresă
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
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).
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
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:
mov ecx,8
bucla:
movsb
loop bucla
rep instrucţiune
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.
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.
mov eax,1
mov ebx,0
int 80h
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.
./cmpStr
echo $?
push sursă
pop destinaţie
pop eax
pop eax
pop eax
pop ax
pop eax
mov eax,1
mov ebx,0
int 080h
ESP = 0xbfc7b4f0
00 0xbfc7b4ef
03 0xbfc7b4ee
ba 0xbfc7b4ed
c4 ESP = 0xbfc7b4ec
7 0
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.
pusha
pushf
popf
popa
pushad
pop dword [val]
pushfd
popad
popfd
mov eax,1
mov ebx,0
int 080h
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
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Ă
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
S E M
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.
0 0 0 1 1 0 1 0 0 0 0 0 0 0
0 1 0 1 1 0 1 0 0 0 0 0 0 0
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
0 1 0 1 0 1 0 0 0 0 0 0 0 0
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
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
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.
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ă):
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:
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!!"# .
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.
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ă
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).
Î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.
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).
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.
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.
Metoda de rotunjire
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).
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
mov eax,1
mov ebx,0
int 80h
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.
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)
(gdb) p $st0
$6 = 1
(gdb) p $st1
$7 = 12.340000152587890625
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
mov eax,1
mov ebx,0
int 80h
Adunarea
mov eax,1
mov ebx,0
int 80h
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
fsubr sursă
execută operaţia
ST0 = sursă – 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
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
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
./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.
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.
finit
fld dword [fs3]
fld dword [fs4]
fldenv [buffer]
mov eax,1
mov ebx,0
int 80h
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]
mov eax,1
mov ebx,0
int 80h
8.4. Exerciţii
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.
!! ! + !" + ! = 0.
−! + ! ! − 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
_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.
mov eax,1
mov ebx,0
int 080h
_functie:
mov [temp],ebx
mov ebx,eax
mov eax,[temp]
ret
9.4. Transferul controlului
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 eax,1
mov ebx,0
int 080h
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
mov eax,1
mov ebx,0
int 080h
Paramentrul funcţiei
Adresa de revenire ESP
31 0
Stivă
Paramentru 3 funcţie
Parametru 2 funcţie
Paramentru 1 funcţie
Adresa de revenire ESP
31 0
Stivă
functie:
mov eax,[esp+4]
neg eax
ret
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:
functie:
mov ebp,esp
push ebx
mov eax,[ebp+4]
neg eax
pop ebx
mov esp,ebp
ret
Problemă II:
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>
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
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ă
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:
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
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ă
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
push ebp
mov ebp,esp
sub esp, <octeţi>
mov esp,ebp
pop ebp
Eliberarea stivei
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ă.
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
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
(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
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
(gdb) i r esp
esp 0xffffd7d0 0xffffd7d0
(gdb) n
9 push dword val2
(gdb)
10 call multiply
(gdb) x /2wx $esp
0xffffd7c8: 0x080490b4 0x080490b0
(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.
push ebx
section .text
global _functie
_functie:
mov eax,1
mov ebx,0
int 080h
mov eax,[ebp+8]
mov [mem],eax
add eax,1
push eax
neg dword [mem]
mov eax,[mem]
pop eax
leave
ret
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
9.9. Exerciţii
/* f.c */
int f(void) {
return 0;
}
#include <stdio.h>
int main(){
int x = 1;
double y,z;
y = 1.23;
z = (double)x +y;
return 0;
}
void swap( int num1 , int num2 ) void swap( int *num1 , int *num2 )
{ {
int temp ; int temp ;
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.
Dispecer de apel
Tabela vectorilor de
întrerupere
Vector 80h
man 2 exit
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
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
(gdb) b *end
Breakpoint 1 at 0x80480a5: file return.asm, line 19.
(gdb) r
Starting program: /home/stefan/return
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
buffer resb 40
10.2.2. Macroinstrucţiuni
%macro Exit
mov eax,1
mov ebx,0
int 80h
%endmacro
%macro mxchg 2
xchg eax, %1
xchg eax, %2
xchg %1, eax
%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
%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”
%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
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
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ă.
%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
ret
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.
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
Biblioteci partajate
040000000H
USER SPACE
Heap
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
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
Î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
(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.
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: "+"
(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).
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.
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:
;; modulul de scădere
mov al,[esi]
sub al,[edi]
aas
or al,30h
mov [rez],al
jmp .end
Î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.
.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
./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:
.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
WriteStr msg_numar2
ReadInt [numar2]
;; calculeaza suma
mov eax,[numar1]
add eax,[numar2]
mov [rez],eax
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
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:
.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
.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:
.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
.niciun_caracter_valid:
push ecx
mov ecx,mesaj_lipsa_numar
call f_WriteStr
pop ecx
jmp .citeste_caracter ;reia procesul de citire
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
;
;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
unde opţiunile:
-c crează o arhivă,
-r inserează în acea arhivă fişierele obiect specificate,
-s crează un index pentru arhivă.
nm libioapi.a
extern _GLOBAL_OFFSET_TABLE_
;corpul funcţiei
mov ebx,[ebp-4]
mov esp,ebp
pop ebp
ret
;
;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
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:
va furniza adresa PLT pentru funcţia respectivă, acolo unde programul apelant
crede că se află aceasta.
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
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
...
;
;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
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
ldconfig -v -n .
./prog
Aplicație Aplicație
Spațiul utilizator
Aplicație
Biblioteca C
Spațiul kernel
Kernel
Drivere de dispozitiv
Hardware
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: ”
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.
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'
./args test 10 20 30
Sunt 5 argumente:
./args
test
10
20
30
./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
export TEST=/home/test
./env | grep TEST
TEST=/home/test
11. INTERFAŢA CU LIMBAJELE DE NIVEL
ÎNALT
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:
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.
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
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
Memorie neutilizată
ESP se deplasează
în sus și jos
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
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
return 0;
}
./program
Mesaj din interiorul functiei in asamblare.
Mesaj din interiorul programului principal.
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
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
.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
Intel: EAX
AT&T: %eax
asm(”cod în asamblare” );
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:
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.
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”);
.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
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.
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:
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__.
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:
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.
”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ă
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.
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:
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
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.
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.