Ca tot m-am apucat sa cotrobai prin assembler si sa incerc sa inteleg instructiunile de baza , am decis sa fac un tutorial . Nu am sa prezint chestii foarte complicate sau alte trasnai , cat lucruri simple care cred ca valoareaza foarte mult daca sunt intelese pe deplin . Hai sa incepem :
Cred ca prima data ar fi bine sa discutam despre registri . Cum assemblerul are rolul sa mareasca viteza de executie si sa optimizeze programul acesta are acces si al registri . Ce fac acesti registri ? Va ajuta sa stocati/transferati date mai repede . Acum intrebarea normala ? La ce m-ar ajuta chestia asta ? Procesoarele din ziua de azi sunt destul de rapide si nu prea se simte diferenta . Eh .. o sa se simta . Sa presupunem ca avem un for care incrementeaza un contor [ o variabila i ] de un milion de ori . De fiecare data cand se intra in bucla variabila i este incrementata . Ce inseamna asta ? Acces la memorie .. timp pierdut ! Am codat un exemplu banal de bucla for in C++ . Incrementez din 1 in 1 [ contorul i ] si la fiecare iteratie incrementez o variabila a [ la fel ca si contorul ] . Asta o fac de 1.000.000.000 ori . Cronometrat dureaza in jur de 3-4 secunde . Aceeasi bucla for [ cu acelasi rezultat ] dar scrisa in assembler dureaza in jur de 1.5-2 secunde . Nu-i mare branza , dar ganditi-va ce-ar fi sa facem bucla aceea for la randul ei de un miliard de ori . Ati prins ideea .
Exemple de registri - registri pe 8 biti [ un octet ] : AL,AH,BL,BH,CL,CH,DL,DH. Ideea e ca seturile de doi cate doi sunt analoage , doar ca incep cu alta litera . Ce-i interesant este L-ul si H-ul care se repeta la fiecare set in parte . Poate ti-ai dat seama ca vine de la LOW si HIGH . Putin ciudat nu ? Daca punem cap la cap un octet AH cu un AL obtinem , normal , doi octeti [ unul langa altul ] sau un cuvant . Chestia interesanta este ca acesti doi octeti pusi cap la cap AH AL formeaza tocmai un registru de 16 biti [ un cuvant / doi octeti ] , defapt sunt practic in componenta lui . Discutam imediat .
- registri pe 16 biti [ doi octeti ] : AX,BX,CX,DX . Sa luam exemplul AX . AX-ul practic este setul AL,AH . Da , asta este . Nu e bine sa gandim registri ca pe niste variabile . Ci ca niste portiuni de memorie . Un registru mare [ o sa discutam imediat ] are mai multe bucati . Primul octet este AL , al doilea AH [ primii biti LOW , urmatorii AH ] , dar acestia doi la un loc pot insemna in acelasi timp si AX si tot asa . Ce vreau sa spun prin asta ? Daca tu introduci o anumita valoare [ sa spunem 2 ] in AL si apoi introduci valoarea 15 in AX risti ca valoarea ta 2 din AL sa fie suprascrisa de valoarea 15 . De ce ? Te-ai prins tu .
- registri pe 32 de biti [ doua cuvinte / patru octeti ] : EAX,EBX,ECX,EDX . Ideea e cam la fel , cum explicam mai sus . Daca introduci sa spunem in AX valoarea 15 [ poti sa introduci si o valoare mai mare , ca na , suntem pe doi octeti ] si apoi in EAX valoarea 32 , risti ca valoarea ta din AX sa fie suprascrisa . Normal , EAX-ul incepe sa scrie de pe bitii de pe pozitia cea mai de jos [ cei din AL ] si tot asa ..
Nici eu nu vreau sa risc sa fac prea mare tam-tam pe baza acestor registri , sa nu spun vreo prostie . Hai deci sa dsicutam despre niste instructiuni care ne pot ajuta sa transferam date . Defapt sa ne jucam cu datele din registri . Mentionez ca folosesc Visual C++ pentru compilare . Am vazut ca prin asta merge sa introduci inline ASM ceva de genu _asm { } . Nu stiu daca e cea mai buna alegere , dar in fine , merge .
Instructiuni * mov
destinatie,
sursa Ce face comanda de mai sus ? Move [ muta / transfera ] date de la sursa la destinatie . Atentie , datele trebuie sa fie de acelasi tip . Nu putem transfera intr-un octet doi octeti , ca n-avem loc , normal . Voi prezenta un mic exemplu mai jos [ de mentionat ca nu este un program intreg C++ ci doar o fasie de cod ] :
char a;
_asm
{
mov al,0;
mov a,al;
}
cout << int(a) << endl;
Ce face codul de mai sus ? In primul rand combina C++ cu ASM . In al doilea rand transfera in variabila a valoarea 0 . In al treilea rand ai sa spui : " optimizare de unde ? nu era mai usor sa scriem in C++ a=0 ? " . Ai dreptate , dar aici e doar un exemplu cum poti sa te joci cu instructiunea mov si cum sa ai grija ca sursa si destinatia sa fie de acelasi tip [ octet - octet , cuvant - cuvant , dublu cuvant - dublu cuvant ] . Stim ca tipul char este pe un octet si registrul al are o bucata de un octet , deci toate lucrurile sunt bune . Prima data transferam in al valoarea 0 , apoi in a valoarea lui al , adica 0 . Apoi afisam valoarea intreaga a lui a [ nu caracterul cu codul ASCII 0 ] . Acum ca stii cam ce sunt aia registri poti sa modifici codul de mai sus cand a este un int [ pe patru octeti ] .
Ce spuneam mai sus ? Nu trebuie sa vedeti registri ca pe niste variabile [ si odata pusa valoarea acolo , acolo ramane ] . Trebuie sa-i vedeti ca pe niste bucati dintr-un spatiu de memorie mai mare si odata modificat pe registri de dimensiuni mai mare se modifica pe toti cei de dedesupt . Exemplu :
char a;
short b;
int c;
_asm
{
mov al,15;
mov a,al;
mov ax,31;
mov a,al;
}
cout << int(a) << endl;
- Ce se intampla in cod ? In registrul de 8 biti [ primii 8 biti din locatia mare de memorie pentru registri , LOW ] punem valoarea 15 . Dar punem si in AX valoarea 31 . Atentie : AX incepe sa puna biti de la inceputul lui AL [ pana il umple ] si apoi continua pe AH . Deci modificat AX , modificam implicit si AL-ul , deci valoarea lui AL va deveni 31.
* add
destinatie,
sursa Adauga in destinatie , sursa . Deci calculeaza destinatie+sursa . In exemplul de mai jos se adauga la 0 [ valoarea initiala a lui a , valoarea 2 ] .
int a=0;
_asm
{
add a,2;
}
cout << a << endl;
* sub
destinatie,
sursa Scade din valoarea destinatiei , valoarea sursei .
char a=5;
_asm
{
sub a,2
}
cout << int(a) << endl;
* inc
destinatie Mareste cu o unitate valoarea destinatiei . In exemplul de mai jos , la afisare va aparea 3 . N-are rost sa mai dau exemplul si de instructiunea
dec care face exact inversul lui inc . Scade o unitate din valoarea data .
int a=2;
_asm
{
inc a
}
cout << a << endl;
Ca veni si partea frumoasa a lucrurilor si ca tot ne-am plictisit de banalele operatii de adunare si scadere haideti sa trecem la
inmultire* mul
sursa ( pentru numerele
fara semn )
Ciudat ! Pe cine cu cine si unde stocheaza rezultatul ? Buna intrebare . O inmultire e ceva de genu a*b si se poate stoca in c=a*b . Noi avem doar
sursa . Sursa se inmulteste cu un registru [ implicit ] de acelasi numar de octeti cu ea . Practic daca sursa este pe un octet se va inmulti cu AL [ pe un octet ] . Unde stocam rezultatul ? Daca inmultim doi octeti , normal ca rezultatul o sa fie mai mare de un octet . El va fi repartizat in AX . Un mic exemplu mai jos va clarifica lucrurile :
char a=3;
short b;
_asm
{
mov al,2
imul a;
mov b,ax
}
cout << b << endl;
Variabila a ocupa un octet . Daca execut mul a [ cum a este pe un octet ] se va inmulti variabila a cu registrul echivalent dimensiunii sale al . Cum in al am mutat 2 si in variabila a avem 3 , in ax vom avea 6 . Destul de interesant nu ?
Sa presupunem ca vrem sa inmultim doua cuvinte [ un cuvant are doi octeti ] . Mutam in ax valoarea 2 si in a valoarea 4 [ a este acum de tipul short , un cuvant si ax este de doi octeti = un cuvant ] . Practic rezultatul ar trebui stocat intr-o variabila de doua cuvinte cum ar fi eax sa zicem . Dar , nu este asa . Rezultatul va fi stocat in perechea de registre DX , AX . Primii biti in AX si ceilalti in DX . O sa explic imediat de ce .
Sa presupunem ca vrem sa inmultim patru octeti cu patru octeti . Unde va fi stocat rezultatul ? Nu avem/nu putem folosi registri de 64 de biti [ pe care ar incapea rezultatul inmultirii ] . Rezultatul este stocat in perechea de registri EDX,EAX analog cu mai sus . De ce insa mai sus rezultatul nu e stocat direct intr-un registru de 4 octeti ? Din motive de compatibilitate intre procesoare vechi/noi . Cand nu erau registri de 32 de biti nu se putea pune rezultatul inmultirii a doi registri de 16 biti , intr-un registru de 32 de biti [ ca nu exista ] . Deci a fost pus in DX , AX . Acum , la procesoarele mai noi unde au aparut registri de 32 de biti , nu se putea modifica ce a fost inainte din motive de compatibilitate . Ar fi trebuit compilat totul dupa aia ca sa mai functioneze . Asta e toata chestia .
Pentru inmultire de numere cu semn se foloseste
imul sursa la fel ca
mul de mai sus . Cam asta e ideea , nu intru in detalii .
Ultima [ dar nu cea din urma , nici ultima pana la urma , dar ultima din acest tutorial ] este
inmultirea . Inmultire , impartire , se leaga , deci teoretic ar trebui sa se lege si modul in care se fac aceastea in limbaj de asamblare . Da si practic se leaga .
* div
sursa Iar ai intrat in ceata . Impartire formata doar dintr-o singura chestie ? Impartirea e ceva de genu a=b/c . Dar observi ca acum , rezultatul impartirii nu ar trebui sa mai ocupe un spatiu dublu ca numar de octeti , ci teoretic unul de doua ori mai mic , ca impartim doar . Operandul care se da este c-ul . La ce impart . Daca impart la operand pe un octet [ sa zicem ca ar fi un short sau un registru pe un octet gen cl ] deimpartitul este AX , adica registrul de doua ori mai mare .
Catul impartirii se transfera in AL si
restul se transfera in AH .
Cam ciudat , dar la restul ? Daca vreau sa impart la un operand pe doi octeti ? Nu cumva deimpartitorul nu o sa fie EAX ci perechea DX,AX ? Ai dreptate . Sa fi atent in cazul in care deimpartitul intra pe doi octeti sa setezi DX-ul pe 0 si in AX sa pui rezultatul . Iti dai tu seama de ce [ daca vrei sa-ti mearga impartirea ] . Catul va fi pus in AX si restul in DX .
Analog daca vrem sa impartim la operandul pe 4 octeti gen ecx , deimpartitul va fi EDX, EAX catul fiind pus in EAX si restul in EDX .
Mare branza , nimic interesant [ NOT ] . Ca sa nu termin tutorialul fara pic de interactivitate am sa discut un pic si despre salturi . Cum sa facem bucla for despre care discutam mai devreme . Dar de data asta in asm .
N-am sa intru prea tare in instructiunile de salt/etichete sau alte chestii ca este destul de mult de discutat despre ele . Am sa prezint doar la general cateva chestii necesare pentru a intelege exemplu de mai jos [ da , am sa dau prima data exemplul ca sa te fac curios ] :
_asm
{
mov al,0;
mov eax,1;
e1: cmp eax,5
jae e2
inc eax
jmp e1
e2:
mov a1,eax;
}
cout << "val a " <
Ce face codul de mai sus ? O bucla ! Initial avem in registrul al valoarea 0 [ registrul al va fi folosit drept suma, la fiecare iteratie se va incrementa ] . Registrul eax e initializat cu valoarea 1 [ gandeste-te la eax ca la un contor i ] . e1 si e2 sunt etichete . O sa discutam mai tarziu putin ce inseamna si cu ce ne ajuta o eticheta.
Ca intr-un while/for comparam contorul , la noi eax , cu valoarea pana la care vrem sa ajungem . Aici cmp face aceasta comparatie . Dar ce face jae ? Jae compara doua numere fara semn daca primul numar este mai mare sau egal decat al doilea . Ce nevoie am eu ca al meu contor sa fie mai mare sau egal cu 5 . Am nevoie , in cazul in care eax [ contorul ] este mai mare sau egal cu 5 atunci va sari la afisare . Am aflat si rolul etichetei . Practic jae-ul sare executia codului la eticheta pe care i-am dat-o . Cand nu mai avem nevoie sa facem iteratii mergem la e2 si afisam / calculam / transferam , tot ce avem chef .
In cazul in care nu s-a sarit la sfarsit [ nu s-a terminat bucla ] trece peste jae e2 la urmatoarea instructiune care incrementeaza pe eax . Asta inseamna ca eax inca este mai mic decat 5 [ sau probabil egal , daca in urma incrementarii trebuie sa iesim din bucla ] , dar trebuie sa sarim [ jmp asta face , sare la o eticheta , neconditionat , adica fara conditia cmp ] sa verificam daca mai continuam sau sarim la afisare . Si tot asa .
Oricum , ideea de baza nu a fost sa prezint cine stie ce sofisticarie ci doar niste elemente de baza despre ASM . In cazul in care aveti ceva de completat sau semnalati vreo / mai multe erori nu ezitati .
Ne mai auzim !