Differences

This shows you the differences between two versions of the page.

Link to this comparison view

iocla:laboratoare:laborator-09 [2020/10/11 16:24]
ssamoilescu [Exerciții]
iocla:laboratoare:laborator-09 [2022/05/06 15:02] (current)
radu.nichita
Line 1: Line 1:
-====== Laborator 09: Interactiunea C-assembly ​======+====== Laborator 09: Apeluri de funcții ​======
  
-Având ​în vedere că limbajul ​de asamblare prezintă dificultăți atât în citirea cât și în dezvoltarea codului, tendințgenerală este aceea de a se migra către limbaje de nivel înalt (care sunt mult mai ușor de citit și oferă un API mult mai ușor de utilizat). Cu toate acestea, tot există situații în care, din rațiuni de optimizare, se folosesc mici rutine assembly care sunt integrate în modulul limbajului de nivel inalt.+În acest laborator vom prezenta modul în care se realizează apeluri ​de funcții. Vom vedea cum putem folosi instrucțiunile ''​%%call%%'' ​și ''​%%ret%%''​ pentru ​realiza apeluri ​de funcții ​și cum folosim stiva pentru a transmite parametrii unei funcții.
  
-În acest laborator vom vedea cum se pot integra module ​de assembly în programe C și viceversa.+Laboratorul este de forma //learn by doing//, partea practică alternând între secțiuni de tip tutorial, cu parcurgere pas cu pas și prezentarea soluției, ​și exerciții care trebuie să fie rezolvate.
  
-===== Utilizarea procedurilor assembly în funcții C =====+===== Cunoștințe și abilități ce vor fi dobândite ​=====
  
-Pentru ca un program C să ajungă să fie executat, este necesar ca acesta să fie tradus în codul mașina al procesorului;​ aceasta este sarcina unui compilator. Având în vedere că acest cod rezultat în urma compilării nu este întotdeauna optim, ​în anumite cazuri se preferă înlocuirea unor porțiuni ​de cod scris în C cu porțiuni de cod assembly care să facă același lucru, însă cu o performanță mai bună.+  * Traducerea apelului ​și implementării unei funcții din limbajul C în limbaj ​de asamblare 
 +  * Transmiterea parametrilor in diferite arhitecturi. 
 +  * Folosirea instrucțiunilor ''​%%call%%'' ​și ''​%%ret%%''​ pentru a realiza un apel de funcție 
 +  * Implementarea unei funcții în limbaj de asamblare 
 +  * Folosirea stivei pentru a transmite parametrii unei funcții 
 +  * Apelarea unei funcții externe (aflată în biblioteca standard C) din limbaj de asamblare
  
-==== Declararea procedurii ​====+===== Transmiterea Parametrilor =====
  
-Pentru ​ne asigura că procedura assembly și modulul C se vor combina cum trebuie și vor fi compatibile,​ următorii pași trebuie urmați:+Cand vine vorba de chema o functie cu parametri exista doua mari optiuni de plasare a acestora:
  
-  ​declararea labelului procedurii ca fiind globalfolosing directiva GLOBAL. Pe lângă astaorice date care vor fi folosite de către procedură trebuie declarate ca fiind globale. +1. **Plasarea in registre** - aceasta metodain mod intuitivpresupune transmiterea parametrilor cu ajutorul registrelor.
-  * folosirea directivei EXTERN pentru a declara procedurile și datele globale ca fiind externe.+
  
-==== Setarea ​stivei ====+**Avantaje** 
 +  * Este foarte usor de folosit atunci cand numarul parametrilor este mic. 
 +  * Este foarte rapida, intrucat parametrii sunt imediat accesibili din registre. 
 + 
 +**Dezavantaje** 
 +  * Din cauza faptului ca exista un numar limitat de registre, numarul de parametri ai unei functii ajunge sa fie limitat. 
 +  * E foarte probabil ca unele registre sa fie folosite in interiorul functiei apelate si devine necesara salvarea temporara a registrelor pe stiva inaintea apeluluide functie. Astfel, cel de-al doilea avantaj enumerat dispare, deoarece accesul la stiva presupune lucru cu memoria, adica latenta crescuta. 
 + 
 +2. **Plasarea pe stivă** - aceasta metoda presupune push-uirea pe stiva a tuturor parametrilor. 
 + 
 +**Avantaje** 
 +  * Poate fi transmis un numar mare de parametri. 
 +**Dezavantaje** 
 +  * Este lenta intrucat se face acces la memorie. 
 +  * Mai complicata din punct de vedere al accesului la parametri. 
 + 
 +<​note>​ Pentru arhitecturiile **32-bit** se foloseste metoda plasarii pe stiva, iar pentru cele **64-bit** se foloseste metoda plasarii in registre. Noi vom folosi conventia de la 32-bit architecture. </​note>​ 
 + 
 +===== Apelul unei funcții ===== 
 + 
 +Atunci când apelăm o funcție, pașii sunt următorii:​ 
 + 
 +  * Punem argumentele pe stivă, apelul de tip push fiind în ordinea inversă în care sunt trimiși ca argumente funcției. 
 +  * Apelăm ''​%%call%%''​. 
 +  * Restaurăm stiva la sfârșitul apelului. 
 + 
 +==== Funcționarea ​stivei ==== 
 + 
 +După cum știm, operațiile pe stivă sunt de două tipuri: 
 + 
 +  * ''​%%push val%%''​ în care valoarea ''​%%val%%''​ este plasată pe stivă 
 +  * ''​%%pop reg/​mem%%''​ în care ce se găsește în vârful stivei se plasează în registru sau într-o zonă de memorie 
 + 
 +În momentul în care se face ''​%%push%%''​ spunem că stiva **crește** (se adaugă elemente). Din motive ce vor fi explicate mai bine la SO, pointerul de stivă (indicat de registrul ''​%%esp%%''​ pe 32 de biți) scade in valoare atunci cand **stiva creste** (la ''​%%push%%''​). Putem spune totusi ca aceasta contrazicere in numire vine de la faptul ca stiva este deregula reprezentata pe verticala si valorile mici se afla sus, iar valorile mari se afla jos. 
 + 
 +La fel, în momentul în care facem ''​%%pop%%''​ spunem că stiva **scade** (se scot elemente). Acum pointer-ul de stivă (indicat de registrul ''​%%esp%%''​ pe 32 de biți) crește valoric. 
 + 
 +Un sumar al acestui lucru este explicat foarte bine la acest [[https://​en.wikibooks.org/​wiki/​X86_Disassembly/​The_Stack|link]]. 
 + 
 +Spre exemplu, daca avem functia foo cu urmatoarea semnatura (in limbaj C):
  
-Atunci când se intră intr-o procedură, este necesar să se seteze un stack frame către care să se trimită parametrii. Desigur, dacă procedura nu primește parametri, acest pas nu este necesar. Așadar, pentru a seta stiva, trebuie inclus următorul cod: 
 <​code>​ <​code>​
-push ebp +int foo(int aint b, int c); 
-mov ebpesp+
 </​code>​ </​code>​
 +Apelul acestei functii va arata astfel:
  
-EBP-ul ne oferă posibilitatea să îl folosim ca un index în cadrul stivei și nu ar trebui alterat pe parcursul procedurii.+<​code>​ 
 +mov ecx, [c]     ; luam valoarea parametrului c dintr-o zona de memorie 
 +mov ebx, [b] 
 +mov eax, [a]
  
-==== Conservarea registrelor ====+push ecx         ; punem parametrii in ordine inversa, incepand cu c 
 +push ebx         ; apoi b 
 +push eax         ; apoi a 
 +call foo         ; apelam functia 
 +add esp, 12      ; restauram stiva
  
-Este necesar ca procedura să conserve valoarea registrelor ESI, EDI, EBP și a registrelor segment. În cazul în care aceste registre sunt corupte, este posibil ca programul să producă erori la întoarcerea din procedura assembly.+</​code>​ 
 +===== Apelantul si Apelatul =====
  
-==== Transmiterea parametrilor din C tre procedura assembly ====+Atunci când apelăm o funcție spunem ​că funcția care apelează (contextul care apelează) se cheamă **apelant** (sau **caller**),​ iar funcția apelată se cheamă **apelat** (sau **callee**). In paragraful anterior am discutat despre cum arată lucrurile la nivelul apelantului (cum construim stiva). 
 + 
 +Haideți să urmărim ce se întâmplă si la la nivelul apelatului. Până în momentul instrucțiunii ''​%%call%%''​ stiva conține parametrii funcției. Apelul ''​%%call%%''​ poate fi echivalat grosier următoarei secvențe:
  
-Programele C trimit parametrii către procedurile assembly folosind stiva. Să considerăm următoarea secvență de program C: 
 <​code>​ <​code>​
-extern int Sum(); +push eip 
-   ... +jmp function_name 
-int a1, a2, x; +
-   ... +
-x = Sum(a1, a2);+
 </​code>​ </​code>​
 +Adică și apelul ''​%%call%%''​ folosește în continuare stiva și salvează adresa următoarei instrucțiuni,​ cea de după ''​%%call%%''​ numită și instrucțiunea de retur sau adresa de retur (return address). Aceasta este necesară pentru a ști, în apelat, unde să revenim în apelant.
  
-Când C-ul execută apelul către Sum, mai întâi face push la argumente ​pe stivă, în ordine inversă, apoi face efectiv call către procedură. Astfella intrarea ​în corpul proceduriistiva va fi intactă.+În apelat, la începutul său (numit preambul, preamble) se salvează frame pointer-ul (în arhitectura i386 este vorba de registrul ''​%%ebp%%''​) urmând ca frame pointer-ul să refere adresa curentă de pe stivă ​(adică tocmai fostul frame pointer). Deși nu este obligatoriesalvarea frame pointer-ului ajută la debugging și este în cele mai multe cazuri folosită. Din aceste motiveorice apel de funcție va avea în generalpreambulul:
  
-Cum variabilele ''​a1''​ și ''​a2''​ sunt declarate ca fiind valori ''​int'',​ vor folosi fiecare câte un cuvânt pe stivă. Metoda aceasta de pasare a parametrilor se numește pasare prin valoare. Codul procedurii Sum ar putea arăta în felul următor: 
 <​code>​ <​code>​
-Sum: +push ebp 
-        ​push    ebp             ; creează stack frame pointer +mov ebp, esp 
-        mov     ​ebp, esp +
-        ​mov ​    eax, [ebp+8] ​   ; ia primul argument +
-        mov     ecx, [ebp+12] ​  ; ia al doilea argument +
-        add     eax, ecx        ; suma celor 2 +
-        pop     ​ebp ​            ; reface base pointerul +
-        ret+
 </​code>​ </​code>​
 +Aceste modificări au loc în apelat. De aceea este responsabilitatea acestuia să restaureze stiva la vechea sa valoare. De aceea este uzuală existența unui epilog care să readucă stiva la starea sa inițială; acest epilog este:
  
-Este interesant de remarcat o serie de lucruri. În primul rând, codul assembly pune în mod implicit valoarea de retur a procedurii în registrul ''​eax''​. În al doilea rând, comanda ''​ret''​ este suficientă pentru a ieși din procedură, datorită faptului că compilatorul de C se ocupă de restul lucrurilor, cum ar fi îndepărtarea parametrilor de pe stivă.+<​code>​ 
 +leave
  
-===== Apelarea de funcții C din proceduri assembly =====+</​code>​ 
 +Dupa aceasta instructiune,​ stiva este ca la începutul ​funcției (adică imediat după call). 
 + 
 +Pentru incheierea functiei, este necesar ca executia codului sa se intoarca (return) si sa continue sa execute de la instructiunea de dupa ''​%%call%%''​-ul care a pornit functia. Acest lucru presupune sa influentam registrul ''​%%eip%%''​ si sa punem valoarea care a fost salvata pe stiva initial de apelul ''​%%call%%''​. Acest lucru este indeplinit folosind instructiunea:​
  
-În majoritatea cazurilor, apelarea de rutine sau funcții din biblioteca standard C dintr-un program în limbaj de asamblare este o operație mult mai complexă decât viceversa. Să luăm exemplul apelării funcției ''​printf''​ dintr-un program în limbaj de asamblare: 
 <​code>​ <​code>​
-global ​ main+ret
  
-extern ​ printf+</​code>​ 
 +care este grosier echivalentul instrucțiunii:​
  
-section .data+<​code>​ 
 +pop eip
  
-text    db      "291 is the best!"10+</​code>​ 
-strformat db    "​%s"​0+Spre exempludefinitia si corpul functiei foocare realizeaza suma a 3 numerevor arata astfel:
  
-section .code+<code
 +foo: 
 +    push ebp 
 +    mov ebp, esp
  
-main +    mov eax, [ebp + 8] 
-        ​push ​   dword text +    ​mov ebx, [ebp + 12] 
-        ​push ​   dword strformat +    ​mov ecx[ebp + 16]
-        call    printf +
-        add     esp+
-        ret +
-</​code>​+
  
-Remarcați faptul că procedura este declarată ca fiind globală și se numește ''​main''​ - punctul de pornire al oricărui program C. +    add eax, ebx 
-Din moment ce în C parametrii sunt puși pe stivă în ordine inversă, offsetul stringului este pus prima oară, urmat de offsetul șirului de formatare. Funcția C poate fi apelată după aceeaînsa stiva trebuie restaurată la ieșirea din funcție.+    add eaxecx
  
-Când se face linkarea codului assembly trebuie inclusă și biblioteca standard C (sau biblioteca care conține funcțiile pe care le folosiți).+    leave 
 +    ret
  
-===== Inline assembly =====+</​code>​ 
 +==== Remarcati: ​====
  
-În primul rândce este "​inline"?​+1. O functie se defineste printr-un label. 
 +2. Dupa preambulul functieistiva arata in felul urmator:
  
-Termenul ''​inline''​ este un cuvânt cheie în limbajul C și este folosit în declararea funcțiilorÎn momentul în care compilatorul găsește o funcție declarată ca fiind inline, acesta va înlocui toate apelurile către funcția respectivă cu corpul funcțieiAvantajul principal al funcțiilor inline este acela că se pierde overheadul rezultat din apelul unei funcțiiPe de altă parte, dimensiunea binarului va fi mai mare.+{{https://​raw.githubusercontent.com/​systems-cs-pub-ro/​iocla/​master/​laborator/​content/​apel-functii/​images/​function_stack1.jpg?​width=650|function_stack1.jpg}}
  
-<​note>​ +3. De observat că pe parcursul execuției funcției, ceea ce nu se schimbă este poziția frame pointer-ul. Acesta este și motivul denumirii sale: pointează la frame-ul curent al funcției. De aceea este comun ca accesarea parametrilor unei funcții să se realizeze prin intermediul frame pointer-ului. Presupunând un sistem pe 32 de biți și parametri de dimensiunea cuvântului procesorului (32 de biți, 4 octeți) vom avea: 
-Nu are sens să declarăm ca fiind inline ​funcțiile recursive. De ce? +        * primul argument se găsește la adresa ''​%%ebp+8%%''​ 
-</note>+        * al doilea argument se găsește la adresa ''​%%ebp+12%%''​ 
 +        * al treilea argument se găsește la adresa ''​%%ebp+16%%''​ 
 +        * etc. 
 + 
 +Acesta este motivul pentru care, pentru a obține parametrii funcției ''​%%foo%%''​ în registrele ''​%%eax%%'',​ ''​%%ebx%%'',​ ''​%%ecx%%'',​ folosim construcțiile:​ 
 + 
 +<​code>​ 
 +    mov eax, dword [ebp+8] ​  ; primul argument in eax 
 +    mov ebx, dword [ebp+12] ​ ; al doilea argument in ebx 
 +    mov ecx, dword [ebp+16] ​ ; al treilea argument in ecx 
 +</code>
  
-Acum este ușor să ghicim la ce se referă expresia "​inline assembly":​ un set de instrucțiuni assembly scrise ca funcții inlineInline assembly ​este folosit ca o metoda ​de optimizare și este foarte des întâlnit în system programming.+4. Valoare de retur a unei functii se plaseaza in registre (in general in eax). 
 +   * Daca valoarea de retur este pe **8 biti** rezultatul functiei ​se plaseaza in ''​%%al%%''​. 
 +   * Daca valoarea ​de retur este pe **16 biti** rezultatul functiei se plaseaza in ''​%%ax%%''​. 
 +   * Daca valoarea de retur este pe **32 biti** rezultatul functiei se plaseaza in ''​%%eax%%''​. 
 +   * Daca valoarea ​de retur este pe **64 biti** rezultatul se plaseaza in registrele ''​%%edx%%''​ si ''​%%eax%%''​. Cei mai semnificativi 32 de biti se plaseaza in ''​%%edx%%'',​ iar restul in registrul ''​%%eax%%''​. 
 +   //De asemnea, in unele cazuri, se poate returna o adresa de memorie catre stiva/heap, sau alte zone de memorie, care refera obiectul dorit in urma apelului functiei.//
  
-În programele C/C++ se pot insera instrucțiuni în limbaje de asamblare folosing cuvântul cheie "​asm"​.+5. O functie foloseste aceleasi registre hardware, asadar, la iesirea din functie valorile registrelor nu mai sunt aceleasi. Pentru a evita aceasta situatie, ​se pot salva unele/toate registrele pe stiva.
  
-Pentru ​mai multe detaliiconsultați ​[[http://www.codeproject.com/Articles/15971/​Using-Inline-Assembly-in-C-C|linkul]] pentru gcc sau [[https://msdn.microsoft.com/en-us/​library/​4ks26t93.aspx|linkul]] pentru ​cl.+<​note>​ Deoarece limbajele de asamblare ofera mai multe oportunitatiexista necesitatea de a avea conventii de apelare a functiilor in x86. Diferenta dintre acestea poate consta in ordinea parametrilor,​ modul cum parametrii sunt pasati functiei, ce registre trebuiesc conservate de apelat sau daca apelantul ori apelatul se ocupa de pregatirea stivei. Mai multe detalii puteti gasi [[https://en.wikipedia.org/wiki/X86_calling_conventions|aici]] sau [[https://levelup.gitconnected.com/x86-calling-conventions-a34812afe097|aici]] daca wikipedia e prea mainstream ​pentru ​voi</​note>​
  
 ===== Exerciții ===== ===== Exerciții =====
-==== Pregătire infrastructură ==== 
  
-Pentru acest laborator ​vom folosi [[http://elf.cs.pub.ro/asm/​res/​laboratoare/​lab-08-tasks.zip|această arhivă de resurse]]Descărcați arhiva șaccesați conținutul acesteia.+<note important>​ În cadrul laboratoarelor ​vom folosi ​repository-ul de git al materiei ​[[https://github.com/​systems-cs-pub-ro/iocla|IOCLA]]. Repository-ul este clonat pe desktop-ul mașinii virtualePentru a îl actualiza, folosiți comanda ''​%%git pull origin master%%''​ din interiorul directorului în care se află repository-ul (''​%%~/​Desktop/​iocla%%''​). Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă.Dacă doriți să descărcațrepository-ul în altă locație, folosiți comanda ''​%%git clone https://​github.com/​systems-cs-pub-ro/​iocla ${target}%%''​. Pentru mai multe informații despre folosirea utilitarului git, urmați ghidul de la [[https://​gitimmersion.com/|Git Immersion]] </​note>​
  
-Pentru desfășurarea acestui laborator vom folosi linia de comandă.+==== 0Recapitulare:​ Șirul lui Fibonacci ====
  
-==== 1Tutorial: Buclă for în inline assembly ====+Completați fișierul ''​%%fibo.asm%%''​ din arhivă pentru a realiza un program care afișează primele N numere din șirul lui Fibonacci.
  
-În subdirectorul ''​1-inline-for/''​ din arhiva de sarcini a laboratorului aveți o implementare a unei bucle for folosind inline assembly.+Aveți voie să folosiți doar memorie alocată pe stivă.
  
-Urmăriți codul și compilați-l și rulați-l într-un terminalPentru a-l compila rulați comanda<​code>​ +==== 1Hello, world! ====
-make +
-</​code>​ +
-În urma rulării comenzii rezultă executabilul ''​inline_for''​ pe care îl putem executa folosind comanda<​code>​ +
-./​inline_for +
-</​code>​+
  
-Urmăriți în cod partea de inline assembly ​din blockul ce începe ​cu ''​__asm__''​. ​Înțelegeți modul în care funcționează inline assembly înainte ​de a trece la exercițiul următor.+Deschideți fișierul ''​%%hello-world.asm%%'',​ asamblați-l și rulați-l. Observați afișarea mesajului //Hello, world!// 
 + 
 +Remarcați că
 + 
 +  * Programul ''​%%hello-world.asm%%''​ folosește apelul funcției ''​%%puts%%''​ (funcție externă modulului curent) pentru a efectua afișarea. Pentru aceasta pune argumentul pe stivă șapelează funcția. 
 +  * Variabila ''​%%msg%%'' ​din programul ''​%%hello-world.asm%%''​ conține octetul ''​%%10%%''​. Acesta simbolizează caracterul //​line-feed//​ (''​%%\n%%''​),​ folosit pentru a adăuga o linie nouă pe Linux. 
 + 
 +Încheierea ​cu ''​%%\n%%'' ​este, în general, utilă pentru afișarea șirurilorFuncția ''​%%puts%%''​ pune automat o linie nouă după șirul afișat, însă aceasta trebuie adăugată explicit în cazul folosirii funcției ''​%%printf%%''​. 
 + 
 +==== 2. Dezasamblarea unui program scris în C ==== 
 + 
 +După cum spuneam, în final, totul ajunge în limbaj de asamblare (ca să fim 100% corecți, totul ajunge cod mașină care are o corespondență destul de bună cu codul asamblare). Adesea ajungem să avem acces doar la codul obiect al unor programe și vrem să inspectăm ​modul în care arată. 
 + 
 +Pentru a observa acest lucru, haideți să compilăm până la codul obiect un program scris în C și apoi să-l dezasamblăm. Este vorba de programul ''​%%test.c%%''​ din arhiva de laborator. 
 + 
 +<​note>​ Pentru ​compila un fișier cod sursă C/C++ în linia de comandă, urmați pașii: 
 + 
 +  - Deschideți un terminal. (shortcut ''​%%Ctrl+Alt+T%%''​) 
 +  - Accesați directorul în care aveți codul sursă. 
 +  - Folosiți comanda
  
-<note tip> 
-Structura generală a unei directive de inline assembly este următoarea:​ 
 <​code>​ <​code>​
-__asm__ ( AssemblerTemplate ​ +gcc -m32 -o <​executabil>​ <​nume-fisier>​ 
-          : OutputOperands  +
-          [ : InputOperands +
-          [ : Clobbers ]] +
-         )+
 </​code>​ </​code>​
 +unde ''​%%<​nume-fisier>​%%''​ este numele fișierului iar ''​%%<​executabil>​%%''​ este executabilul rezultat.
  
-''​AssemblerTemplate''​ este un string care constituie instrucțiunile in limbaj de asamblare executate de programul vostru. ''​gcc''​ nu va ține cont de spațiile albe din acest string, din cauza aceasta trebuie sa marcați fiecare instrucțiune cu ''​\n'' ​(opțional ''​\t''​ pentru indentare).+  - Dacă doriți **doar** să compilați fișierul ​(**fără** să-l link-ați), atunci folosiți comanda
  
-''​OutputOperands''​ și ''​InputOperands''​ reprezintă variabilele de ieșire, respectiv intrare ale rutinei voastre. Convențional,​ variabilele de ieșire sunt transferate prin referință,​ iar cele de intrare prin valoare. Pentru variabilele de ieșire, folosiți declarații de forma ''​%%"​=r"​%% (<variabila_din_codul_C>)'',​ iar pentru variabile de intrare, ''​%%"​r"​%% (<variabila_din_codul_C>)''​.+<code> 
 +gcc -m32 -c -o <fisier-obiect>​ <​nume-fisier>
  
-Prin ''​Clobbers'' ​menționați registrele pe care îi folosiți în rutina voastră de asamblare, ​și în felul acesta instruiți compilatorul ​să nu se atingă de eiAltfelsunt șanse să îi folosească în alte scopuri.+</​code>​ 
 +unde ''​%%<​nume-fisier>​%%'' ​este numele fișierului iar ''​%%<​fisier-obiect>​%%''​ este fișierul obiect rezultat. 
 +</​note>​ 
 +În cazul nostru, întrucât dorim doar să compilăm fișierul ''​%%test.c%%''​ la modulul obiectvom accesa din terminal directorul în care se găsește fișierul și apoi vom rula comanda 
 + 
 +<​code>​ 
 +gcc -m32 -c -o test.o test.c 
 + 
 +</​code>​ 
 +În urma rulării comenzii de mai sus în directorul curent vom avea fișierul obiect ''​%%test.o%%''​.
  
-În instrucțiuni,​ trebuie să înlocuiți aparițiile variabilelor din program (e.g. ''​sum''​),​ cu registrul aferent (e.g. ''​%0''​). Registrele de tip general (''​%%"​r"​%%''​) sunt numerotați crescător începând cu 0, în ordinea declarațiilor. Există și posibilitatea ​de a mapa explicit o variabilă la un anumit registru (e.g. ''​%%"​=a"​%% (var)''​ va mapa variabila ''​var''​ la registrul ''​eax''​),​ însă, pentru simplitate, în laborator folosim doar mapări la registrele generale.+Putem obține șforma în limbaj ​de asamblare ​acestuia folosind comanda
  
-Pentru debugging, puteți inspecta fișierul de assembly generat de ''​gcc''​ - acesta se generază executând comanda: 
 <​code>​ <​code>​
-make asm+gcc -m32 -masm=intel -S -o test.asm test.c 
 </​code>​ </​code>​
-</​note>​+În urma rulării comenzii de mai sus obținem fișierul ''​%%test.asm%%''​ pe care îl putem vizualiza folosind comanda
  
-==== 2Rotație în inline assembly ====+<​code>​ 
 +cat test.asm
  
-În limbajul C avem suport pentru operații de shiftare pe biți dar nu avem suport pentru operații de rotație pe biți. Acest lucru în ciuda prezenței operațiilor de rotație pe biți la nivelul procesorului.+</​code>​ 
 +Pentru a dezasambla codul unui modul obiect vom folosi un utilitar frecvent întâlnit ​în lumea Unix: ''​%%objdump%%''​Pentru dezasamblare,​ vom rula comanda
  
-În subdirectorul ''​2-inline-rotate/''​ găsiți un schelet de cod pe care să îl folosiți pentru a implementa, folosind mnemonicile ''​rol''​ și respectiv ''​ror'',​ rotații pe biți. O descriere scurtă a acestor instrucțiuni găsiți [[https://​en.wikibooks.org/​wiki/​X86_Assembly/​Shift_and_Rotate#​Rotate_Instructions|aici]].+<​code>​ 
 +objdump ​-M intel -d <​path-to-obj-file>​
  
-Pentru compilare folosiți comanda ​''​make''​.+</​code>​ 
 +unde ''​%%<​path-to-obj-file>​%%'' ​este calea către fișierul obiect ''​%%test.o%%''​. 
 + 
 +Veți obține un output similar celui de mai jos
  
-<note tip> 
-La o implementare corectă a rotației cu 8 biți la stânga și dreapta, în urma rulării executabilului ''​./​inline_rotate'',​ veți obține un rezultat de forma: 
 <​code>​ <​code>​
-./​inline_rotate +$ objdump -M intel -d test.o 
-init0x12345678rot_left0x34567812rot_right0x78123456+ 
 +test.o    file format elf32-i386 
 + 
 +Disassembly of section .text: 
 + 
 +0000054d <​second_func>:​ 
 + ​54d: ​  ​55 ​                     push   ebp 
 + ​54e: ​  89 e5                   ​mov ​   ebp,esp 
 + 550  e8 a6 00 00 00          call   5fb <​__x86.get_pc_thunk.ax>​ 
 + ​555: ​  05 ab 1a 00 00          add    eax,0x1aab 
 + 55a  8b 45 08                mov    eax,DWORD PTR [ebp+0x8] 
 + ​55d: ​  8b 10                   ​mov ​   edx,DWORD PTR [eax] 
 + ​55f: ​  8b 45 0c                mov    eax,DWORD PTR [ebp+0xc] 
 + ​562: ​  01 c2                   ​add ​   edx,eax 
 + ​564: ​  8b 45 08                mov    eax,DWORD PTR [ebp+0x8] 
 + ​567: ​  89 10                   ​mov ​   DWORD PTR [eax],edx 
 + ​569: ​  ​90 ​                     nop 
 + ​56a: ​  ​5d ​                     pop    ebp 
 + ​56b: ​  ​c3 ​                     ret 
 + 
 +0000056c <​first_func>:​ 
 + ​56c: ​  ​55 ​                     push   ebp 
 + ​56d: ​  89 e5                   ​mov ​   ebp,esp 
 + ​56f: ​  ​53 ​                     push   ebx 
 + ​570: ​  83 ec 14                sub    esp,0x14 
 + ​573: ​  e8 83 00 00 00          call   5fb <​__x86.get_pc_thunk.ax>​ 
 + ​578: ​  05 88 1a 00 00          add    eax,​0x1a88 
 + ​57d: ​  c7 45 f4 03 00 00 00    mov    DWORD PTR [ebp-0xc],​0x3 
 + ​584: ​  83 ec 0c                sub    esp,0xc 
 + ​587: ​  8d 90 80 e6 ff ff       ​lea ​   edx,​[eax-0x1980] 
 + ​58d: ​  ​52 ​                     push   edx 
 + ​58e: ​  89 c3                   ​mov ​   ebx,eax 
 + ​590: ​  e8 4b fe ff ff          call   3e0 <​puts@plt>​ 
 + ​595: ​  83 c4 10                add    esp,0x10 
 + ​598: ​  83 ec 08                sub    esp,0x8 
 + ​59b: ​  ff 75 f4                push   DWORD PTR [ebp-0xc] 
 + ​59e: ​  8d 45 08                lea    eax,​[ebp+0x8] 
 + ​5a1: ​  ​50 ​                     push   eax 
 + ​5a2: ​  e8 a6 ff ff ff          call   54d <​second_func>​ 
 + ​5a7: ​  83 c4 10                add    esp,0x10 
 + ​5aa: ​  8b 45 08                mov    eax,DWORD PTR [ebp+0x8] 
 + ​5ad: ​  8b 5d fc                mov    ebx,DWORD PTR [ebp-0x4] 
 + ​5b0: ​  ​c9 ​                     leave 
 + ​5b1: ​  ​c3 ​                     ret 
 + 
 +000005b2 <​main>:​ 
 + ​5b2: ​  8d 4c 24 04             ​lea ​   ecx,​[esp+0x4] 
 + ​5b6: ​  83 e4 f0                and    esp,​0xfffffff0 
 + ​5b9: ​  ff 71 fc                push   DWORD PTR [ecx-0x4] 
 + ​5bc: ​  ​55 ​                     push   ebp 
 + ​5bd: ​  89 e5                   ​mov ​   ebp,esp 
 + ​5bf: ​  ​53 ​                     push   ebx 
 + ​5c0: ​  ​51 ​                     push   ecx 
 + ​5c1: ​  e8 8a fe ff ff          call   450 <​__x86.get_pc_thunk.bx>​ 
 + ​5c6: ​  81 c3 3a 1a 00 00       ​add ​   ebx,​0x1a3a 
 + ​5cc: ​  83 ec 0c                sub    esp,0xc 
 + ​5cf: ​  6a 0f                   ​push ​  0xf 
 + ​5d1: ​  e8 96 ff ff ff          call   56c <​first_func>​ 
 + ​5d6: ​  83 c4 10                add    esp,0x10 
 + ​5d9: ​  83 ec 08                sub    esp,0x8 
 + ​5dc: ​  ​50 ​                     push   eax 
 + ​5dd: ​  8d 83 8e e6 ff ff       ​lea ​   eax,​[ebx-0x1972] 
 + ​5e3: ​  ​50 ​                     push   eax 
 + ​5e4: ​  e8 e7 fd ff ff          call   3d0 <​printf@plt>​ 
 + ​5e9: ​  83 c4 10                add    esp,0x10 
 + ​5ec: ​  b8 00 00 00 00          mov    eax,0x0 
 + ​5f1: ​  8d 65 f8                lea    esp,​[ebp-0x8] 
 + ​5f4: ​  ​59 ​                     pop    ecx 
 + ​5f5: ​  ​5b ​                     pop    ebx 
 + ​5f6: ​  ​5d ​                     pop    ebp 
 + ​5f7: ​  8d 61 fc                lea    esp,​[ecx-0x4] 
 + ​5fa: ​  ​c3 ​                     ret 
 </​code>​ </​code>​
-</​note>​+Există multe alte utilitare care permit dezasamblare de module obiect, majoritatea cu interfața grafică și oferind și suport pentru debugging. ''​%%objdump%%''​ este un utilitar simplu care poate fi rapid folosit în linia de comandă.
  
-==== 3CPUID în inline assembly ====+Este interesant de urmărit, atât în fișierul ''​%%test.asm%%''​ cât și în dezasamblarea sa, modul în care se face un apel de funcție, lucru despre care vom discuta în continuare.
  
-La nivelul procesoarelor moderne există o instrucțiune simplă, accesibilă doar din limbaj de asamblare, care oferă informații despre procesor numită ''​cpuid''​.+==== 3Afișarea unui șir ====
  
-În subdirectorul ​''​3-inline-cpuid/​'' ​găsiți un schelet de cod pe care să îl folosiți pentru obținerea vendor ID string-ului procesorului folosind instrucțiunea ​''​cpuid''​. ​Completați scheletul ​și faceți programul să afișeze informațiile dorite.+Pentru afișarea unui string putem folosi macro-ul intern ​''​%%PRINTF32%%''​. Sau putem folosi ​o funcție precum ​''​%%puts%%''​. ​În fișierul ''​%%print-string.asm%%''​ este implementată afișarea unui string folosind macro-ul ''​%%PRINTF32%%''​.
  
-Pentru compilare folosiți comanda ​''​make''​.+Urmărind fișierul ''​%%hello-world.asm%%''​ ca exemplu, implementați afișarea șirului folosind și ''​%%puts%%''​.
  
-<​note ​tip> +<​note> ​Urmăriți și indicațiile din secțiunea ​//"​Apelul unei funcții"​//. </note>
-Pentru informații despre instrucțiunea ''​cpuid''​ consultați și aceste link-uri: +
-  * http://wiki.osdev.org/CPUID +
-  * https://​en.wikipedia.org/wiki/​CPUID#​EAX.3D0:​_Get_vendor_ID+
  
-</​note>​+==== 4. Afișarea lungimii unui șir ====
  
 +Programul ''​%%print-string-len.asm%%''​ afișează lungimea unui șir folosind macro-ul ''​%%PRINTF32%%''​. Calculul lungimii șirului ''​%%mystring%%''​ are loc în cadrul programului (este deja implementat).
  
-==== 4Tutorial: Calcul maxim în assembly cu apel din C ====+Implementați programul pentru a face afișarea lungimii șirului folosind funcția ''​%%printf%%''​.
  
-În subdirectorul ''​4-5-max-c-calls/''​ din arhiva de sarcini a laboratorului găsiți o implementare ​de calcul a maximului unui număr în care funcția ''​main()'' ​este definită în C de unde se apelează ​funcția ''​get_max()'' ​definită în limbaj de asamblare.+La sfârșit veți avea afișată ​de două ori lungimea șirului: inițial cu apelul macro-ului ​''​%%PRINTF32%%'' ​și apoi cu apelul ​funcției externe ​''​%%printf%%''​.
  
-Urmăriți codul din cele două fișiere și modul în care se transmit argumentele ​funcției șvaloarea ​de retur.+<​note>​ Gândiți-vă că apelul ''​%%printf%%''​ este de forma ''​%%printf("​String length is %u\n", len);​%%''​. Trebuie să construiți stiva pentru acest apel. 
 + 
 +Pașii de urmat sunt: 
 + 
 +  - Marcarea simbolului ''​%%printf%%''​ ca simbol extern. 
 +  - Definirea șirului de formatare ''​%%"​String length is %u", 10, 0%%''​. 
 +  - Realizarea apelului funcției ''​%%printf%%'',​ adică: 
 +    - Punerea celor două argumente pe stivă: ​șirul de formatarea ​și lungimea. 
 +    - Apelul ''​%%printf%%''​ folosind ''​%%call%%''​. 
 +    - Restaurarea stivei. 
 + 
 +Lungimea șirului ​se găsește în registrul ''​%%ecx%%''​. 
 +</​note>​ 
 +==== 5. Afișarea șirului inversat ==== 
 + 
 +În soluția de mai sus adăugați ​funcția ''​%%reverse_string%%''​ astfel încât să avețun listing similar celui de mai jos:
  
-Compilați și rulați programul. Pentru a-l compila rulați comanda: 
 <​code>​ <​code>​
-make+[...] 
 +section .text 
 +global main 
 + 
 +reverse_string:​ 
 +    push ebp 
 +    mov ebp, esp 
 + 
 +    mov eax, dword [ebp+8] 
 +    mov ecx, dword [ebp+12] 
 +    add eax, ecx 
 +    dec eax 
 +    mov edx, dword [ebp+16] 
 + 
 +copy_one_byte:​ 
 +    mov bl, byte [eax] 
 +    mov byte [edx], bl 
 +    dec eax 
 +    inc edx 
 +    loopnz copy_one_byte 
 + 
 +    inc edx 
 +    mov byte [edx], 0 
 + 
 +    leave 
 +    ret 
 + 
 +main: 
 +    push ebp 
 +    mov ebp, esp 
 +[...] 
 </​code>​ </​code>​
 +<note important>​ Când copiați funcția ''​%%reverse_string%%''​ în programul vostru, rețineți că fucția începe la eticheta ''​%%reverse_string%%''​ și se oprește la eticheta ''​%%main%%''​. Eticheta ''​%%copy_one_byte%%''​ este parte a funcției ''​%%reverse_string%%''​. </​note>​
 +
 +Funcția ''​%%reverse_string%%''​ inversează un șir și are următoarea signatură: ''​%%void reverse_string(const char *src, size_t len, char *dst);​%%''​. Astfel ca primele ''​%%len%%''​ caractere și șirul ''​%%src%%''​ sunt inversate în șirul ''​%%dst%%''​.
 +
 +Realizați inversarea șirului ''​%%mystring%%''​ într-un nou șir și afișați acel nou șir.
 +
 +<​note>​
 +Pentru a defini un nou șir, recomandăm ca, în secțiunea de date să folosiți construcția
  
-În urma rulării comenzii rezultă executabilul ''​mainmax''​ pe care îl putem executa folosind comanda: 
 <​code>​ <​code>​
-./mainmax+store_string times 64 db 0 
 </​code>​ </​code>​
 +Construcția creează un șir de 64 de octeți de zero, suficient pentru a stoca inversul șirului.
 +Apelul echivalent în C al funcției este ''​%%reverse_string(mystring,​ ecx, store_string);​%%''​. În registrul ''​%%ecx%%''​ am presupus că este calculată lungimea șirului.
  
-<note important>​ +Nu puteți folosi direct valoarea ''​%%ecx%%'' ​în forma ei curentă. După apelul funcției ''​%%printf%%''​ pentru afișare numărului valoarea ''​%%ecx%%''​ se pierdeCa să o păstrați, aveți două opțiuni:
-Acordați atenție ​înțelegerii codului înainte de a trece la exercițiul următor. +
-</​note>​+
  
-<note important>​ +  - Stocați valoarea registrului ''​%%ecx%%''​ în prealabil pe stivă (folosind ''​%%push ecx%%''​ înaintea apelului ''​%%printf%%''​) și apoi să o restaurați după apelul ''​%%printf%%''​ (folosind ''​%%pop ecx%%''​). 
-Valoarea de retur a unei funcții este plasată în registrul ​''​eax''​.+  - Stocați valoarea registrului ''​%%ecx%%''​ într-o variabilă globală, pe care o definiți ​în secțiunea ''​%%.data%%''​. 
 + 
 +Nu puteți folosi un alt registru pentru că sunt șanse foarte mari ca și acel registru să fie modificat de apelul ​''​%%printf%%'' ​pentru afișarea lungimii șirului.
 </​note>​ </​note>​
-==== 5Extindere calcul maxim în assembly cu apel din C ====+==== 6Implementarea funcției toupper ​====
  
-Extindeți programul ​de la exercițiul anterior (în limbaj de asamblare ​și C) astfel încât ​funcția ''​get_max()'' ​să aibă acum signatura ''​unsigned int get_max(unsigned int *arr, unsigned int len, unsigned int *pos)''​. Al treilea argument al funcției este adresa în care se va reține poziția din vector pe care se găsește maximul.+Ne propunem implementarea funcției ''​%%toupper%%''​ care traduce literele mici în litere mari. Pentru aceasta, porniți de la fișierul ''​%%toupper.asm%%''​ din arhiva de exerciții a laboratorului ​și completați corpul ​funcției ''​%%toupper%%''​.
  
-La afișare se va afișși poziția din vector pe care se găsește maximul.+Șirul folosit este ''​%%mystring%%'' ​și presupunem că este un șir valid. Acest șir este transmis ca argument funcției ''​%%toupper%%''​ în momentul apelului.
  
-<note tip> +Faceți înlocuirea ​//in place//, nu este nevoie de un alt șir.
-Pentru reținerea poziției, cel mai bine este definiți o variabilă locală ''​pos''​ în funcția ''​main''​ din fișierul C (''​main.c''​) în forma<​code>​ +
-    unsigned int pos; +
-</code> +
-iar apelul funcției ''​get_max''​ îl veți face în forma:<​code>​ +
-    max = get_max(arr,​ 10, &​pos);​ +
-</code> +
-</note> +
-==== 6Tutorial: Calcul maxim în C cu apel din assembly ====+
  
-În subdirectorul ​''​6-7-max-assembly-calls/​''​ din arhiva de sarcini a laboratorului găsiți o implementare ​de calcul ​maximului unui număr în care funcția ​''​main()''​ este definită în limbaj de asamblare de unde se apelează funcția ​''​get_max()'' ​definită ​în C.+<​note>​ Ca să traduceți o litera mică în literă mare, trebuie să **scădeți** ​''​%%0x20%%''​ din valoare. Aceasta este diferența între litere mici șmari; de exemplu ''​%%a%%'' ​este ''​%%0x61%%''​ iar ''​%%A%%''​ este ''​%%0x41%%''​. Puteți vedea în [[http://​man7.org/​linux/​man-pages/​man7/​ascii.7.html|pagina de manual ascii]].
  
-Urmăriți codul din cele două fișiere și modul în care se transmit argumentele funcției ​și valoarea ​de retur.+a să citiți sau să scrieți octet cu octet folosiți construcția ''​%%byte [reg]%%''​ așa cum apare și în implementarea determinării lungimii unui șir în fișierul ''​%%print-string-len.asm%%'',​ unde ''​%%[reg]%%''​ este registrul ​de tip pointer în care este stocată adresa șirului în acel punct.
  
-Compilați șrulați programul.+Vă opriți atunci când ațajuns la valoarea ''​%%0%%''​ (''​%%NULL%%''​ byte). Pentru verificare puteți folosi ''​%%test%%''​ așa cum se întâmplă și în implementarea determinării lungimii unui șir în fișierul ''​%%print-string-len.asm%%''​</​note>​
  
-<note important>​ +==== Bonus: toupper doar pentru litere mici ====
-Acordați atenție înțelegerii codului înainte de a trece la exercițiul următor. +
-</​note>​ +
-==== 7. Extindere calcul maxim în C cu apel din assembly ​====+
  
-Extindeți programul de la exercițiul anterior (în limbaj de asamblare și C) astfel încât ​funcția ''​get_max()''​ să aibă acum signatura ''​unsigned int get_max(unsigned int *arrunsigned int len, unsigned int *pos)''​. Al treilea argument al funcției este adresa în care se va reține poziția din vector pe care se găsește maximul.+Implementați funcția ''​%%toupper%%'' ​astfel încât translatarea ​să aibă loc doar pentru caractare reprezentând litere micinu litere mari sau alte tipuri de caractere.
  
-La afișare se va afișa și poziția din vector pe care se găsește maximul.+==== Bonus: rot13 ====
  
-<note tip> +Realizați și folosiți o funcție care face translatarea [[https://​rot13.com/​|rot13]] a unui șir.
-Pentru a reține poziția, cel mai bine este să definiți o variabilă globală în fișierul assembly (''​main.asm''​) în secțiunea ''​.data'',​ în forma<​code>​ +
-    posdd 0 +
-</code> +
-Această variabilă o veți transmite (prin adresă) către apelul ''​get_max''​ și prin valoare pentru apelul ''​printf''​ pentru afișare.+
  
-Pentru afișare modificați șirul ''​print_format''​ și apelul ''​printf''​ în fișierul assembly (''​main.asm''​) ca să permită afișare a două valori: maximul și poziția. +==== Bonus: ​rot13++ ​====
-</​note>​ +
-==== 8. Bonus: ​Calcul maxim în assembly cu apel din C pe 64 de biți ====+
  
-Intrați în subdirectorul ​''​8-max-c-calls-x64/​'' ​și faceți implementarea calculului maximului în limbaj de asamblare ​pe un sistem pe 64 de biți. Porniți de la programul de la exercițiile 4 și 5 în așa fel încât să îl rulați folosind ​un sistem pe 64 de biți.+Implementați ''​%%rot13%%''​ pe un array de șiruri: șirurile sunt continue ​în memorie separate prin terminatorul de șir (''​%%NULL%%''​-byte,​ ''​%%0%%''​). De exemplu: ''​%%ana\0are\0mere\0%%''​ este un array de trei șiruri.
  
-<note tip> +Aplicați ''​%%rot13%%''​ pe caracterele alfabetice și înlocuiți terminatorul de șir cu spațiu (''​%%'​ '​%%'',​ blank, caracterul ''​%%32%%''​ sau ''​%%0x20%%''​)Astfel, șirul inițial ''​%%ana\0are\0mere\0%%''​ se va traduce în ''​%%nan ner zrer%%''​.
-[[https://​en.wikipedia.org/​wiki/​X86_calling_conventions]].+
  
-Primul lucru pe care trebuie ​-l aveți în vedere este că pe arhitectura x64 registrele au o dimensiune ​de 8 octeți ​și au nume +<​note>​ Pentru a defini array-ul de șiruri ​care să conțină terminatorul ​de șir, folosiți o construcție de forma:
-diferite decât cele pe 32 de biți (pe lângă extinderea celor tradiționale''​eax''​ devine ''​rax'',​ ''​ebx''​ devine ''​rbx'',​ etc., mai există altele noi: ''​R10''​-''​R15'':​ pentru mai multe informații vedeți [[http://​stackoverflow.com/​questions/​20637569/​assembly-registers-in-64-bit-architecture|aici]]).+
  
-De asemeneape arhitectura x64 parametrii nu se mai trimit pe stivăci se pun în registre. Primii 3 parametri se pun în: ''​RDI''​''​RSI''​ și ''​RDX''​. Aceasta nu este o convenţie adoptată uniform. Această convenţie este valabilă doar pe Linuxpe Windows având alte registre care sunt folosite pentru a transmite parametrii unei funcţii.+<​code>​ 
 +mystring db "​ana"​0"​are"​0"​mere",​ 0
  
-Convenția de apel necesită ca, pentru funcțiile cu număr variabil ​de argumente, ''​RAX'' ​să fie setat la numărul de registre vector folosiți pentru a pasa argumentele. ​''​printf'' ​este o funcție cu număr variabil ​de argumente, și dacă nu folosiți alte registre decât cele menționate ​în paragraful anterior pentru trimiterea argumentelor,​ trebuie ​să setați ''​RAX = 0'' ​înainte ​de apel. Citiți mai multe [[https://​stackoverflow.com/​questions/​38335212/​calling-printf-in-x86-64-using-gnu-assembler|aici]].+</​code></​note>​ 
 +<​note>​ Va trebui să știți când sa vă opriți din parcurgerea array-ului ​de șiruri. Cel mai simplu este să definiți o variabilă de lungime în secțiunea ''​%%.data%%''​de forma 
 + 
 +<​code>​ 
 +len dd 10 
 + 
 +</​code>​ 
 +în care să rețineți fie lungimea totală a șirului (de la începutul până la ultimul ​''​%%NULL%%''​-byte), fie numărul ​de șiruri din array.
 </​note>​ </​note>​
-==== 9. Bonus: Calcul maxim în C cu apel din assembly pe 64 de biți ====+===== Alte resurse ​===== 
 + 
 +  * [[http://​www.nasm.us/​|nasm]]
  
-Intrați în subdirectorul ''​9-max-assembly-calls''​ și faceți implementarea calculului maximului în C cu apel din limbaj de asamblare pe un sistem pe 64 de biți. Porniți de la programul de la exercițiile 6 și 7 în așa fel încât să îl rulați folosind un sistem pe 64 de biți. Urmați indicațiile de la exercițiul anterior și aveți grijă la ordinea parametrilor. ​+===== Soluții =====
  
 +  * Soluțiile pentru exerciții sunt disponibile [[https://​elf.cs.pub.ro/​asm/​res/​laboratoare/​lab-09-sol.zip|aici]].
iocla/laboratoare/laborator-09.1602422692.txt.gz · Last modified: 2020/10/11 16:24 by ssamoilescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0