====== Laborator 11: Interactiunea C-assembly ====== Având în vedere că limbajul de asamblare prezintă dificultăți atât în citirea cât și în dezvoltarea codului, tendința 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 vedea cum se pot integra module de assembly în programe C și viceversa. ===== Utilizarea procedurilor assembly în funcții C ===== 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ă codul 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ă. ==== Declararea procedurii ==== Pentru a ne asigura că procedura assembly și modulul C se vor combina cum trebuie și vor fi compatibile, următorii pași trebuie urmați: * declararea labelului procedurii ca fiind global, folosing directiva GLOBAL. Pe lângă asta, orice date care vor fi folosite de către procedură trebuie declarate ca fiind globale. * folosirea directivei EXTERN pentru a declara procedurile și datele globale ca fiind externe. * folosirea convenției de numire din C -- i.e. precedarea tuturor numelor (date și proceduri) cu underscore. ==== Setarea stivei ==== 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: push ebp mov ebp, esp EBP-ul ne oferă posibilitatea să îl folosim ca un index în cadrul stivei și nu ar trebui alterat pe parcursul procedurii. ==== Conservarea registrelor ==== 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. ==== Transmiterea parametrilor din C către procedura assembly ==== Programele C trimit parametrii către procedurile assembly folosind stiva. Să considerăm următoarea secvență de program C: extern int Sum(); ... int a1, a2, x; ... x = Sum(a1, a2); 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ă. Astfel, la intrarea în corpul procedurii, stiva va fi intactă. 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: _Sum push ebp ; creeaza stack frame pointer 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 ; refa base pointerul ret 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ă. ===== Apelarea de funcții C din proceduri assembly ===== Î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: global _main extern _printf section .data text db "291 is the best!", 10, 0 strformat db "%s", 0 section .code _main push dword text push dword strformat call _printf add esp, 8 ret Remarcați faptul că procedura este declarată ca fiind globală și se numește _main, care este punctul de pornire al oricărui program C. Din moment ce în C parametrii sunt puși pe stivă în ordine inversă, offsetul stringului este pus prima oară, urmat de offsetului șirului de formatare. Funcția C poate fi apelată după aceea, însa stiva trebuie restaurată la ieșirea din funcție. 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). ===== Inline assembly ===== În primul rând, ce este "inline"? 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ției. Avantajul principal al funcțiilor inline este acela că se pierde overheadul rezultat din apelul unei funcții. Pe de altă parte, dimensiunea binarului va fi mai mare. Nu are sens să declarăm ca fiind inline funcțiile recursive. De ce? Acum este ușor să ghicim la ce se referă expresia "inline assembly": un set de instrucțiuni assembly scrise ca funcții inline. Inline assembly este folosit ca o metoda de optimizare și este foarte des întâlnit în system programming. În programele C/C++ se pot insera instrucțiuni în limbaje de asamblare folosing cuvântul cheie "asm". Pentru mai multe detalii, consultați [[http://www.codeproject.com/Articles/15971/Using-Inline-Assembly-in-C-C|linkul]]. ===== Exerciții ===== ==== Pregătire infrastructură ==== Pentru acest laborator vom folosi {{http://elf.cs.pub.ro/asm/res/laboratoare/lab-11-tasks.zip|această arhivă de resurse}}. Descărcați arhiva și accesați conținutul acesteia. Pentru desfășurarea acestui laborator vom folosi interfața în linia de comandă. Pentru că folosim atât cod C cât și cod în limbaj de asamblare, va trebui să deschideți consola de Visual Studio, care are mediul deja configurat pentru compilarea de programe folosind comanda ''cl''. Pentru a deschide consola de Visual Studio urmați pașii: * Apăsați butonul ''Start''. * Accesați opțiunea ''All apps''. * Mergeți la litera ''V''. * Deschideți meniul **de tip director** ''Visual Studio 2015''. * Selectați opțiunea ''VS2015 x86 Native Tools Command Prompt''. ==== [1p] 1. Tutorial: Buclă for în inline assembly ==== În subdirectorul ''inline-for/'' din arhiva de sarcini a laboratorului aveți o implementare a unei bucle for folosind inline assembly. Urmăriți codul și compilați-l și rulați-l în consola Visual Studio. Pentru a-l compila rulați comanda build.bat În urma rulării comenzii rezultă executabilul ''inline_for.exe'' pe care îl putem executa folosind comanda .\inline_for.exe 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. Observați că dacă dorim să folosim valorile variabilelor din programul C, le trecem doar cu același nume în codul inline assembly. ==== [1.5p] 2. Rotație în inline assembly ==== Î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. În subdirectorul ''inline-rotate/'' găsiți un schelet de cod pe care să îl folosiți pentru a implementa, folosind mnemonicile respectiv ''rol'' și ''ror'' rotație 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]]. Pentru compilare folosiți scriptul ''build.bat''. ==== [1.5p] 3. CPUID în inline assembly ==== La nivelul procesoarelor moderne există o instrucțiune simplă, accesibilă doar din limbaj de asamblare, care oferă informații despre procesor numită ''cpuid''. În subdirectorul ''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 compilare folosiți scriptul ''build.bat''. 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 ==== [1p] 4. Tutorial: Calcul maxim în assembly cu apel din C ==== În subdirectorul ''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. Urmăriți codul din cele două fișiere și modul în care se transmit argumentele funcției și valoarea de retur. Compilați și rulați programul. Acordați atenție înțelegerii codului înainte de a trece la exercițiul următor. ==== [2p] 5. Extindere calcul maxim în assembly cu apel din C ==== 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. La afișare se va afișa și poziția din vector pe care se găsește maximul. 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 unsigned int pos; iar apelul funcției ''get_max'' îl veți face în forma: max = get_max(arr, 10, &pos); ==== [1p] 6. Tutorial: Calcul maxim în C cu apel din assembly ==== În subdirectorul ''max-assembly-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 limbaj de asamblare de unde se apelează funcția ''get_max()'' definită în C. Urmăriți codul din cele două fișiere și modul în care se transmit argumentele funcției și valoarea de retur. Compilați și rulați programul. Acordați atenție înțelegerii codului înainte de a trece la exercițiul următor. ==== [2p] 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 *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. La afișare se va afișa și poziția din vector pe care se găsește maximul. 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 pos: dd 0 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. ==== [2p] Bonus: Calcul maxim în assembly cu apel din C pe 64 de biți ==== Actualizați 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. Pentru aceasta, va trebui să asamblați programul în limbaj de asamblare pentru un executabil pe 64 de biți și să folosiți consola Visual Studio pe 64 de biți. [[https://msdn.microsoft.com/en-us/library/windows/hardware/ff561499%28v=vs.85%29.aspx|Calling convention in Windows x64 binaries]]. Pe arhitectura x64 parametri nu se mai trimit stivă, ci se pun registre. Primii 3 parametri se pun în: RCX, RDX, R8. Aceasta nu este o convenţie adoptată uniform. Această conveţie este este doar pe Windows, pe Linux având alte registre care sunt folosite pentru a transmite parametri unei funcţii. Trebuie să aveți în vedere următorii pași: * Să folosiți pentru dezvoltare consola ''VS2015 x64 Native Tools Command Prompt''. * Să folosiți opțiunea ''-f win64'' la ''nasm''. * Să folosiți [[https://msdn.microsoft.com/en-us/library/windows/hardware/ff561499%28v=vs.85%29.aspx|convenția de apel Windows x64]]. * Să înlocuiți numele ''_get_max'' cu ''get_max'' (fără undescore-ul de la început) în fișierul ''max.asm''. ==== [2p] Bonus: Calcul maxim în C cu apel din assembly pe 64 de biți ==== Actualizați 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. Pentru aceasta, va trebui să asamblați programul în limbaj de asamblare pentru un executabil pe 64 de biți și să folosiți consola Visual Studio pe 64 de biți. Să folosiți binarul ''gcc'' din calea cu MinGW64, adică ''%%C:\"Program Files (x86)"\SASM\MinGW64\bin\gcc%%''. E suficient să obțineți executabilul ''main.exe''. Programul nu va funcționa din cauza unor probleme neelucidate de linking. Vom depana problema în următoarea perioadă :-) ===== Soluții ===== [[http://elf.cs.pub.ro/asm/res/laboratoare/lab-11-sol.zip|Soluții de referință pentru exercițiile de laborator]]