Differences

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

Link to this comparison view

cpl:labs:07 [2016/10/04 07:53]
bogdan.nitulescu
cpl:labs:07 [2016/11/15 02:35] (current)
bogdan.nitulescu [Prototipuri de funcții]
Line 1: Line 1:
-====== ​06Generarea de codApeluri de funcții și cadre de stivă ​======+====== ​07Code generationFunction calls and stack frames ​======
  
-** TO BE PUBLISHED SOON **+====== Apelul de funcţii ====== 
 +===== Transmiterea parametrilor ===== 
 +În limbajele de nivel înalt, exista câteva modalități de transmitere a parametrilor și de întoarcere a rezultatelor,​ dintre care amintim:  
 +  ​apelul prin valoare 
 +  ​apelul prin referinta  
 +  ​apelul prin rezultat 
 +  ​apelul prin valoare-rezultat 
 +  * apelul prin nume. 
 +<​note>​ Folosim termenul de argument sau **argument actual** pentru a ne referi la expresia care este trimisă unei rutine și termenul de parametru sau **parametru formal** pentru a referi variabila căreia această expresie îi este asociată în rutina apelată </​note>​ 
 +==== Apelul prin valoare ==== 
 +Fiecare argument este evaluat și valoarea lui este pusă la dispoziție procedurii apelate în parametrul formal corespunzător.  
 + 
 +În timpul execuției procedurii, nu există nicio interacțiune cu variabilele apelantului. Excepția apare în cazul în care argumentul este un pointer și procedura apelată ar putea folosi valoarea pointerului pentru a modifica valorile către care indică acesta.  
 + 
 +Apelul prin valoare se implementează de obicei prin copierea valorii fiecărui argument în parametrul corespunzător la intrarea în procedura apelată. Aceasta modalitate poate fi foarte eficientă în cazul argumentelor care pot fi ținute în regiștri, dar foarte ineficientă pentru tipuri de date compuse - vectori, structuri, obiecte - deoarece presupune un transfer de date semnificativ în memorie. 
 + 
 +Apelul prin valoare este singurul mecanism de pasare a parametrilor în limbajul C. Un parametru poate fi însă și adresa unui obiect; se obține astfel efectul apelului prin referință. 
 + 
 +În C++, la transferul unui obiect prin valoare se va crea o copie a acelui obiect, si se va apela constructorul de copiere, daca acesta exista. 
 + 
 +<​code>​class C; 
 +void f(C a);            // parametrul a se transmite prin valoare 
 +void g() { C x; f(x); } // se apeleaza constructorul C::C(const C&) ce copiaza x in a 
 +</​code>​ 
 + 
 +==== Apelul prin referință ==== 
 +Realizează o asociere între argumentul actual și parametrul corespunzator. Procedura apelată va avea acces total la argument pe toată perioada execuției sale, putând să schimbe parametrul actual sau să îl transmită mai departe altor proceduri.  
 + 
 +Ca implementare tipică, la intrarea în procedura apelată se determină adresa argumentului și aceasta se pune la dispoziția procedurii ca mijloc de accesare a argumentului. Acest mecanism este foarte eficient când vectori sunt transmiși ca parametri, dar poate fi ineficient în cazul parametrilor de mărime mică care ar putea fi transmiși prin regiștri.  
 + 
 +Apelul prin referință poate cauza un //alias// - o situație în care aceeași locație de memorie poate fi accesată folosind două nume simbolice diferite. Un exemplu (C#) : 
 +<code c">​void f(ref int i, ref int j) { i++; j++; } 
 +void g() { int x = 1; f(x,x); /* x == 3 */ }</​code>​ 
 + 
 +Pot apărea probleme în cazul în care o constantă este transmisă prin referință;​ procedura apelată ar putea modifica acea constantă, ceea ce este ilegal. Soluția uzuală este copierea constantelor în noi locații de memorie și transmiterea acestor adrese când se face apelul prin referință.  
 + 
 +Apelul prin referința este implementat implicit în Fortran, sau explicit via o sintaxă specială în C++, C# sau PHP. Același efect se poate obține în C și C++ prin transmiterea adresei ca valoare. 
 + 
 +<​note>​ 
 +În Java, JavaScript sau Python, parametrii sunt transmiși prin valoare, la fel ca și în C. Acest lucru este valabil atât pentru primitive cât și pentru obiecte. 
 + 
 +Afirmația **obiectele se transmit prin referință** este greșită. Deoarece variabilele din Java conțin referințe către obiecte și nu obiectele în sine, corect este **referința obiectelor este transmisă prin valoare**. 
 +</​note>​ 
 + 
 +Codul urmator C++ folosește apelul prin referință pentru a interschimba valorile variabilelor a și b. Codul similar Java, însă, folosește apelul prin valoare și lasă variabilele neschimbate. 
 + 
 +**C++** 
 +<code c++>void swap(Obj &a, Obj &b) { Obj t=a; a=b; b=t; } 
 +void f() { Obj a,b; swap(a,b); } </​code>​ 
 + 
 +**Java** 
 +<code java>​void swap(Obj a, Obj b) { Obj t=a; a=b; b=t; } 
 +void f() { Obj a,b; swap(a,b); } </​code>​ 
 + 
 +==== Apelul prin rezultat ==== 
 +Este similar celui prin valoare, cu deosebirea că aici se întorc valorile de la apelat la apelant. La intrarea în procedura apelată nu se întamplă nimic, iar la întoarcere valoarea unui parametru apelat prin rezultat este pusă la dispoziția apelantului prin copierea valorii lui în argumentul asociat. Acest tip de apel apare în cazul parametrilor de tip out din Ada sau C#: 
 +<code c">​ 
 +void a(out int result) 
 +{ result = 7; /* daca lipseste atribuirea in result, da eroare de compilare */ }  
 +[...] 
 +// exemplu de folosire: 
 +int rez; 
 +a(out rez); // acum rez a primit valoarea din a. 
 +</​code>​ 
 + 
 +==== Apelul prin valoare-rezultat ==== 
 +Este caracterizat de reuniunea proprietăților celor două tipuri de apel: prin valoare și prin rezultat. La intrarea în procedura apelată valoarea argumentului se copiază în parametru, iar la ieșire valoarea parametrului se copiază înapoi în argument. Este implementat în Fortran sau Ada pentru parametri de tip inout, și apare și în cazul apelurilor de procedură pe un alt computer (//RPC - remote procedure calls//). Diferenta față de apelul prin referință este că nu mai apar situații de alias între argumente. 
 + 
 +==== Apelul prin nume ==== 
 +Este un mecanism de evaluare întârziată a argumentelor unei funcții (//lazy evaluation//​). Este similar cu transmiterea prin referință,​ însă argumentele actuale sunt evaluate de fiecare dată când parametrul formal este folosit și nu doar o singură dată la intrarea în procedură. 
 + 
 +Astfel, dacă argumentul este a[i], și valoarea lui i se schimbă între două folosiri ale argumentului,​ la cele două folosiri se vor accesa de fapt două elemente diferite ale vectorului. 
 + 
 +Mecanismul este rar în limbajele moderne, dar a fost folosit istoric (ALGOL 60). Un exemplu: 
 +<code pascal>​ 
 +begin 
 +  integer array a[1..2]; integer i; 
 + 
 +  procedure f(x, j); 
 +    integer x, j; 
 +    begin 
 +      integer k; 
 +      k := x; 
 +      j := j+1; 
 +      x := j; 
 +      f := k; 
 +    end; 
 + 
 +  i := 1; 
 +  a[1] := 5; 
 +  a[2] := 8;  
 +  outinteger(a[1],​ f(a[i],i), a[2]); 
 +end 
 +</​code>​ 
 +În exemplul de mai sus în care i și a[i] sunt transmiși de către programul principal procedurii f, prima folosire a parametrului x aduce valoarea lui a[1], pe cand cea de-a doua folosire seteaza valoarea a[2]. Apelul outinteger() va avea ca rezultat afișarea valorilor „5 5 2”. Dacă s-ar fi folosit apelul prin referința, atunci s-ar fi afișat „5 5 8”. 
 + 
 +O variantă a apelului prin nume este folosită în unele limbaje funcționale,​ în care evaluarea unui argument se face o singură dată, dar la prima folosire a acestuia, și nu la intrarea în funcție. 
 + 
 + 
 +===== Convenții de apel ===== 
 +Apelarea unei proceduri dintr-o altă procedură presupune existența unui '​protocol'​ prin care se trece controlul de la apelant la apelat, prin care parametrii sunt pasați în aceeași direcție iar valoarea rezultatului este pasata de la apelat la apelant. În cazul unui model simplu de runtime, execuția unei proceduri constă în cinci faze, fiecare dintre acestea având mai multe subfaze: 
 +  * Asamblarea argumentelor ce trebuie transferate procedurii și pasarea controlului. 
 +    * Fiecare argument este evaluat și pus în registrul sau locația de pe stivă corespunzatoare;​ evaluare poate înseamna calcularea adresei lui (pentru cei pasați prin referință),​ valoarea lui (pentru cei pasați prin valoare), etc. 
 +    * Se stabilește adresa codului procedurii (pentru cele mai multe limbaje însă, a fost stabilită la compilare sau linkare) 
 +    * Regiștrii care au fost folosiți și vor mai fi și după întoarcerea din procedura apelată, se stochează în memorie dacă protocolul specifică că este datoria apelantului sa facă acest lucru 
 +    * Se salvează adresa de întoarcere și se execută un salt la adresa codului procedurii (de obicei o instrucțiune ''​call''​ face aceste lucruri) 
 +  * Prologul procedurii, executat la întrarea în procedură, stabilește mediul necesar adresării și poate salva regiștrii folosiți de procedură în scopuri proprii 
 +  * Se executa procedura, care la rândul ei poate apela alte proceduri 
 +  * Epilogul procedurii restaurează valorile regiștrilor și mediul de adresare al apelantului,​ asamblează valoarea pe care trebuie să o întoarcă și îi redă controlul apelantului 
 +    * Regiștrii salvați de procedura apelată sunt restaurați din memorie 
 +    * Valoarea care trebuie întoarsă se pune în locul corespunzator (dacă procedura întoarce o valoare) 
 +    * Se incarcă adresa de revenire și se executa un salt la această adresă (de obicei, o instrucțiune ''​ret''​ face acest lucru) 
 +  * Codul din procedura apelantă care se afla după apel își termină restaurarea mediului sau de execuție și primește valoarea intoarsă 
 +    * Regiștrii salvați de către procedura apelantă sunt restaurați din memorie 
 +    * Se folosește valoarea întoarsă de procedura apelată. 
 + 
 +===== Cadre de stiva (stack frames) ===== 
 + 
 +Deși ar fi ideal ca toți operanzii să fie ținuți în regiștri, majoritatea procedurilor necesită spatiu de memorie pentru: 
 +  * variabilele care fie nu au primit regiștri, fie nu pot fi ținute în regiștri pentru că le sunt încărcate adresele (explicit sau implicit, ca in cazul parametrilor transferati prin referință) sau pentru că trebuie să fie indexabile 
 +  * a oferi un loc "​standard"​ pentru a salva valorile din regiștri atunci când se execută un apel de procedură 
 +  * a oferi unui debugger o modalitate de a cunoaște lanțul de proceduri active la un moment dat (call stack) 
 + 
 +Deoarece aceste spații de memorie devin necesare la intrarea într-o procedură și devin inutile la ieșirea din aceasta, ele sunt grupate în cadre de stivă(activation records sau stack frames), care la randul lor sunt organizate sub formă de stiva. Un cadru de stivă poate conține: 
 +  * valorile parametrilor trimiși rutinei curente care nu încap în regiștrii destinați acestui scop,  
 +  * toate sau o parte din variabilele locale,  
 +  * o zona de salvare a regiștrilor temporari alocați de compilator,​ 
 +  * adresa de întoarcere din procedura etc. 
 + 
 +Pentru a putea accesa locatiile din cadrul de stivă curent în timpul execuției, acestora li se asociază distante în unitati de memorie relative la un pointer stocat într-un registru. Pointerul poate fi frame pointer-ul ''​fp''​ care indică prima locație a cadrului curent, sau stack pointer-ul ''​sp''​ care indică vârful stivei, adica imediat dupa ultima locație a cadrului curent. Numeroase compilatoare aranjează în memorie cadrele de stivă astfel încat începutul acestora se află la adrese mai mari de memorie decât sfârșitul acestora. Această alegere face ca deplasamentul față de sp-ul curent în cadrul curent să fie pozitiv, așa cum se vede în figura de mai jos. 
 + 
 +{{ :​cpl:​labs:​laborator-05-stack-sp.png?​480 |}} 
 + 
 +Unele compilatoare folosesc amândoi pointerii, ''​sp''​ și fp, având variabile relative la amândoi regiștrii. 
 + 
 +{{ :​cpl:​labs:​laborator-05-stack-fp.png?​480 |}} 
 + 
 +Caracteristicile limbajului și cele ale hardware-lui determină alegerea unuia dintre acești registri sau chiar folosirea ambilor pentru accesarea locațiilor de pe stivă. Problemele sunt urmatoarele:​ 
 +  * folosirea a doi regiștri pentru accesul la stivă înseamnă a consuma un registru care ar putea folosi altor scopuri; 
 +  * dacă deplasamentul scurt față de un singur registru oferit de instrucțiunile load (încărcare din memorie) și store (salvare în memorie) este suficient pentru a acoperi spatiul de memorie al majorității înregistrărilor de activare (ceea ce se întâmplă pentru cele mai multe  arhitecturi). 
 +  * dacă dimensiunea cadrului de stivă este sau nu cunoscută la momentul compilării,​ și dacă se modifică pe parcursul execuției procedurii. 
 + 
 +Dimensiunea unui cadru de stivă nu este cunoscută, de exemplu, dacă trebuie să fie suportate funcții de alocare a memoriei de tipul ''​alloca()''​ din biblioteca C prin care se alocă spațiu în mod dinamic în cadrul de stivă curent, și se întoarce un pointer la acel spațiu. 
 + 
 +Prin urmare, este suficient și de preferat să se folosească doar sp-ul, în cazul în care nu trebuie să se ofere suport pentru funcții de tipul ''​alloca()''​. 
 + 
 +Efectul unui apel ''​alloca()''​ este să extindă cadrul de stivă făcând sp-ul să indice la o locație nouă. Prin urmare, deplasamentele relative la sp vor pointa către alte locații. Deoarece în C se poate calcula adresa unei variabile locale și apoi folosi, se impune ca acele cantități accesate relativ la sp să nu poată fi direct adresabile de catre utilizator și se preferă ca acestea să fie valori necesare la apelul unei alte proceduri.  
 + 
 +Astfel, în momentul când există fp, adresarea relativa la sp se foloseste pentru: 
 +  * transmiterea argumentelor unei alte proceduri,  
 +  * salvarea regiștrilor peste un apel de procedură 
 +  * întoarcerea rezultatelor dintr-o altă procedură.  
 + 
 +Pentru a oferi suport pentru ''​alloca()''​ avem nevoie atât de un registru sp cât și de unul fp. Chiar dacă aceasta necesita înca un registru în plus, exista avantajul execuției rapide în cazul apelurilor de proceduri. Astfel, la intrarea în procedură:​ 
 +  * se salvează vechiul fp pe stivă, în cadrul noului cadru, 
 +  * se setează valoarea noului fp la valoarea vechiului sp și 
 +  * adaugă lungimea cadrului curent la vechiul sp pentru a obține noua valoare a lui sp. 
 + 
 +La întoarcerea din procedură, procesul invers este: 
 +  * se setează valoarea noului sp la valoarea vechiului fp 
 +  * se încarcă noul fp de pe stivă 
 + 
 +{{ :​cpl:​labs:​laborator-05-stack-full.png?​480 |}} 
 + 
 +==== Exemplu: funcție minimală ==== 
 +<code c>int add(int a, int b) 
 +
 +      return a + b; 
 +}</​code>​ 
 + 
 +== gcc - x86 (CISC) == 
 + 
 +<code asm> 
 +_add:                     ; Identificatorii C sunt prefixati de caracterul _ 
 + 
 +                          ; La intrarea in functie, stiva contine: 
 +                          ;     | b 
 +                          ;     | a 
 +                          ; esp | Adresa de intoarcere 
 +                           
 +  pushl   ​%ebp ​           ; Prin conventie, registrul ebp este frame pointer 
 +  movl    %esp, %ebp      ; catre inceputul cadrului de stiva al unei functii. 
 +                          ; Stiva acum: 
 +                          ; ebp + 12 | b 
 +                          ; ebp +  8 | a 
 +                          ; ebp +  4 | adresa de intoarcere 
 +                          ; ebp      | ebp din functia anterioara 
 +                           
 +                          ; Deoarece stiva creste in jos, parametrii se gasesc la  
 +                          ; deplasamente pozitive (ebp+8) in timp ce variabilele locale  
 +                          ; se gasesc la deplasamente negative (ebp-8). 
 +   
 +  movl    12(%ebp), %eax  ; eax = b [ebp + 12] 
 +  movl    8(%ebp), %edx   ; edx = a [ebp + 8 ] 
 +                          ; Registrii eax si edx sunt volatili, deci pot fi folositi de  
 +                          ; catre functie fara restrictii. 
 +                           
 +  addl    %edx, %eax      ; eax += edx 
 +                          ; Valoarea intoarsa de functie este plasata in registrul eax. 
 +                           
 +  popl    %ebp 
 +  ret 
 +</​code>​ 
 + 
 +== gcc – ARM (RISC) == 
 +<code asm> 
 +add: 
 +   ​add ​    r0, r0, r1 
 +   ​bx ​     lr 
 +</​code>​ 
 + 
 +Functia nu foloseste stiva. Atat parametrii, cat si adresa de intoarcere sunt transmisi prin registrii procesorului:​ 
 + 
 +| R0 | a | 
 +| R1 | b | 
 +| LR | Adresa de intoarcere | 
 + 
 +  
 +Valoarea intoarsa de functie trebuie plasata in R0. Codul generat este astfel echivalent cu: 
 + 
 +  * R0 = R0 + R1 
 +  * Salt la LR. 
 + 
 +== JVM (masina virtuala bazata pe stiva) == 
 + 
 +<​code>​ 
 +public static int add(int, int); 
 +   ​Stack=2,​ Locals=2, Args_size=2 
 +   ​0: ​  ​iload_0 
 +   ​1: ​  ​iload_1 
 +   ​2: ​  ​iadd 
 +   ​3: ​  ​ireturn  
 +</​code>​ 
 + 
 +Masina virtuala Java nu are registri si foloseste stiva pentru toate operatiile. 
 + 
 +Functia are 2 argumente, ce sunt copiate in zona de variabile locale, si foloseste 2 locatii de stiva. 
 + 
 +Variabilele locale sunt copiate pe stiva: 
 +<​code>​ 
 +  push local[0]; --> a 
 +  push local[1]; --> b 
 +</​code>​ 
 +  
 +Instructiunea de adunare inlocuieste doua valori din varful stivei cu suma lor. Codul echivalent intr-o masina cu registri: 
 + 
 +<​code>​ 
 +  pop a 
 +  pop b 
 +  c = a + b 
 +  push c 
 +</​code>​ 
 + 
 +Functia intoarce valoarea ramasa în vârful stivei. 
 +==== Exemplu: evaluarea unei expresii ==== 
 +<code c>int add_mul(int a, int b, int c, int d) 
 +
 +      return a * b + c * d; 
 +}</​code>​ 
 + 
 +== gcc - x86 == 
 +<code asm> 
 +_add_mul: 
 +  pushl   ​%ebp 
 +  movl    %esp, %ebp 
 + 
 +; Stiva, dupa executia codului de initializare:​ 
 +;    +20  d 
 +;    +16  c 
 +;    +12  b 
 +;    +8   a 
 +;    +4   ​Adresa de intoarcere 
 +; ebp+0   ebp din functia anterioara 
 + 
 +; Compilatorul descompune calculul expresiei in instructiuni simple 
 +; Apoi mapeaza variabilele temporare pe registrii hardware ai procesorului 
 + 
 +  movl    8(%ebp), %edx  ;   T1 = a       ; edx = a 
 +  movl    12(%ebp), %eax ;   T2 = b       ; eax = b 
 +  imull   %edx, %eax     ; ​  T3 = T1 * T2 ; eax = edx * eax 
 +  movl    16(%ebp), %ecx ;   T4 = c       ; ecx = c 
 +  movl    20(%ebp), %edx ;   T5 = d       ; edx = d 
 +  imull   %ecx, %edx     ; ​  T6 = T4 * T5 ; edx = ecx * edx 
 +  addl    %edx, %eax     ; ​  T7 = T3 + T6 ; eax = eax + edx 
 +  
 +  popl    %ebp 
 +  ret 
 +</​code>​ 
 + 
 +== JVM == 
 + 
 +^ public static int add_mul(int,​ int, int, int); ^  Stiva  ^ 
 +| Code: | | 
 +| Stack=3, Locals=4, Args_size=4 | ::: | 
 +| 0:   ​iload_0 |  a  | 
 +| 1:   ​iload_1 |  a  | 
 +| ::: |  b  | 
 +| 2:   imul |  a * b  | 
 +| 3:   ​iload_2 |  a * b  | 
 +| ::: |  c  | 
 +| 4:   ​iload_3 |  a * b  | 
 +| ::: |  c  | 
 +| ::: |  d  | 
 +| 5:   imul |  a * b  | 
 +| ::: |  c * d  | 
 +| 6:   iadd |  a * b + c * d  | 
 +| 7:   ​ireturn | | 
 +  
 +Functia intoarce valoarea ramasa pe stiva. 
 + 
 +==== Exemplu: apeluri de funcții ==== 
 +<code c>int add_mul2(int a, int b, int c, int d) 
 +
 +      return mul(a,b) + mul(c,d); 
 +}</​code>​ 
 + 
 +== gcc - x86 == 
 +<code asm> 
 +_add_mul2:​ 
 +        pushl   ​%ebp 
 +        movl    %esp, %ebp 
 +        pushl   ​%ebx 
 +        subl    $20, %esp 
 +; Conform conventiei de apel, registrul ebx nu trebuie modificat de catre functii.  
 +; Pentru ca add_mul2() il foloseste, el este salvat pe stiva la intrarea in functie 
 +; si restaurat la iesire. 
 +
 +; Stiva, dupa executia codului de initializare:​ 
 +
 +; ebp+20 ​ d 
 +; ebp+16 ​ c 
 +; ebp+12 ​ b 
 +; ebp+8   a 
 +; ebp+4   ​Adresa de intoarcere 
 +; ebp+0   ebp din functia anterioara 
 +;         ebx din functia anterioara 
 +;         ​Spatiu pentru variabilele locale. 
 +; esp+4   Al doilea parametru pentru mul(int,​int) 
 +; esp+0   ​Primul parametru pentru mul(int,​int) 
 + 
 +        movl    12(%ebp), %eax 
 +        movl    %eax, 4(%esp) 
 +        movl    8(%ebp), %eax 
 +        movl    %eax, (%esp) 
 +        call    _mul 
 +; Codul de nivel inalt: ebx = mul ( a, b ) 
 +; Parametrii pentru mul ( a, b ) sunt plasati in spatiul prealocat. 
 + 
 +        movl    %eax, %ebx 
 +; Rezultatul functiei este salvat in ebx. Se foloseste ebx, deoarece, conform 
 +; conventiei, este un registru nonvolatil, prin urmare nu va fi distrus de urmatorul 
 +; apel al functiei mul(). 
 + 
 +        movl    20(%ebp), %eax 
 +        movl    %eax, 4(%esp) 
 +        movl    16(%ebp), %eax 
 +        movl    %eax, (%esp) 
 +        call    _mul 
 +; eax = mul ( c,d ) 
 +; De remarcat ca parametrii unei functii apartin cadrului de stiva al functiei  
 +; anterioare, care o apeleaza.  
 +; In cazul nostru, stiva la apelul lui mul(c,d) : 
 +
 +;                           d 
 +;                           c 
 +;                           b 
 +;                           a 
 +; Cadrul add_mul2() --> ​    ​Adresa de intoarcere din add_mul2() 
 +;                           ebp din functia anterioara 
 +;                           ebx din functia anterioara 
 +;                           ​Spatiu pentru variabilele locale. 
 +;                           d 
 +;                           c 
 +; Cadrul mul()      --> ​    ​Adresa de intoarcere din mul() 
 +;                           ... 
 + 
 +        addl    %eax, %ebx 
 +        movl    %ebx, %eax 
 +; Calculul valorii intoarse de add_mul2():​ 
 +;  ebx += eax 
 +;  eax = ebx 
 + 
 +        addl    $20, %esp 
 +        popl    %ebx 
 +        popl    %ebp 
 +        ret 
 +</​code>​ 
 + 
 +===== Prototipuri de funcții ===== 
 +Un prototip de funcție reprezintă o declarare a funcției care omite corpul funcției, dar specifică numele, tipul parametrilor și tipul întors. În timp ce definiția unei funcții specifică implementarea unei funcții, prototipul poate fi descris ca fiind interfața funcției. Acest lucru este pus în evidență în IDE-urile care au opțiuni diferite “Go to declaration” și “Go to definition”. 
 + 
 +Prototipurile sunt folosite de compilator pentru a determina caracteristicile unei funcții ce urmează a fi apelata, pentru a i se transmite parametrii și a se primi valoarea întoarsă, fără a avea acces la codul sursă al funcției. 
 + 
 +Dacă prototipul funcției vizibil în locul în care funcția este apelata diferă de prototipul functiei din locul în care funcția este definita, atunci se pot ajunge la apeluri de funcție eronate, datorate incompatibilității între numărul și tipul parametrilor din locul apelării funcției și din funcția propriu-zisă. 
 + 
 +În limbajul C, dacă o funcție nu este declarată, ea va avea automat atribuit prototipul implicit al funcțiilor:​ 
 + 
 +<code c> 
 +// pentru apeluri de tipul 
 +func(); x = func(); 
 +// semnătura implicită este 
 +int func(void);​ 
 +</​code>​  
 + 
 +Pentru funcții care sunt apelate cu argumente semnătura implicită este  
 +<code c> 
 +func(x, y, z); 
 +// 
 +int func(typeof_x,​ typeof_y, typeof_z);​ 
 +</​code>​ 
 + 
 +Un exemplu de functionare eronată a unui apel de funcție din motive de incompatibilitate între prototipurile unei funcții: 
 +<code c> 
 +//        fisier1.c 
 +/* func() nedeclarata ​                  ​prototip implicit 
 +                                        int func(void) */ 
 +int main() 
 +
 +    /* ... */ 
 +    value += func(); 
 +    /* ... */ 
 +
 +</​code>​ 
 +<code c> 
 +// fisier2.c 
 +/* prototip int func(int, int) */ 
 +long long func(int param1, int param2) 
 +
 +    return (long long) param1 + param2; 
 +
 +</​code>​ 
 +În acest caz, în locul de unde se face apelul (din funcția main) se foloseşte prototipul implicit, care împreună cu un apel greşit de funcție, duce la cod eronat: 
 +  * în locul de unde se face apelul, nu se transmite niciun parametru, și se aşteaptă să se întoarcă o valoare de tip int, 
 +  * în funcția apelată, se așteapta 2 parametri care vor fi citiți eronat din locațiile corespunzătoare,​ iar valoarea întoarsă va fi un long long.  
 + 
 +Este important de observat, totuși, ca majoritatea limbajelor moderne (inclusiv C++, versus C) nu acceptă tipuri si prototipuri implicite. 
 + 
 +====== Excepții ====== 
 + 
 + 
 +Există anumite situații în care se dorește/​preferă să se arunce o excepție. Controlul se transferă către blocul de cod desemnat să se execute în momentul în care se produce excepția. Acest cod se numește handler. Când există un handler pentru excepția aruncată, se spune că excepția a fost prinsă. 
 +Din punctul de vedere al utilizatorului,​ mecanismul de tratare a excepțiilor este format din urmatoarele elemente: blocuri de try, blocuri de catch și expresii de throw. 
 + 
 +<code C> 
 +#include <​iostream>​ 
 +using namespace std; 
 + 
 +int a; 
 + 
 +int main () { 
 +  try 
 +  { 
 +    if (a != 0) 
 +       throw 10; 
 +  } 
 +  catch (int e) 
 +  { 
 +    cout << "A fost aruncata exceptia numarul " << e << '​\n';​ 
 +  } 
 +  return 0; 
 +
 +</​code>​ 
 + 
 +Când mai multe blocuri de try sunt imbricate și se produce un throw, într-o functie apelată de un bloc try interior, controlul se transferă spre exterior din blocurile try imbricate, până când se gasește primul bloc de catch al cărui argument se potrivește cu argumentul aruncat de excepție. 
 + 
 +<code C> 
 +try 
 +
 +      func1(); 
 +      try 
 +      { 
 +             ​func2();​ 
 +      } 
 +      catch (type1_err) { /* ... */ } 
 +      func3(); 
 +
 +catch (type2_err) { /* ... */ } 
 +// daca nu se arunca nicio exceptie, executia ajunge in acest punct. 
 +</​code>​ 
 + 
 +În exemplul de mai sus, dacă //​type1_err//​ este aruncat din blocul try interior (din func2()), excepția este prinsă de blocul de catch interior, și, presupunand că acest bloc nu transferă controlul, este apelată func3(). Daca //​type1_err//​ este aruncată după blocul de try interior, de exemplu de către func3(), din cauză că nu există niciun bloc catch care să prindă această excepție, se va apela funcția **terminate()**. 
 + 
 +Blocurile de catch conțin rutina de tratare a excepției. Obiectele permise, pe care rutina le poate prinde, sunt declarate în paranteză, după cuvântul cheie catch.  
 +===== Excepțiile la compile-time ===== 
 + 
 +Aruncarea unei excepții este de obicei implementată sub forma unui apel la o funcție _throw_ din biblioteca runtime. Numele difera de la o implementare la alta. Pentru ca _throw_ să funcționeze corect, are nevoie de o tabela ce listeaza toate blocurile catch ale unei funcții. Tabela este generată de catre compilator. Pentru a fi mai usor de urmarit, mai jos este adaugat si codul echivalent, cu etichete.  
 + 
 +^ Cod cu excepții ^ Cod echivalent ^ 
 +| <code C>  
 +processFile(...) { 
 +  try { 
 +    f = openFile(...);​ 
 +  } 
 +  catch (FileError e) { 
 +    log(e); 
 +  } 
 +
 +</​code>​ | <code C>  
 +processFile(...) { 
 +try_start:​ 
 +  f = openFile(...);​ 
 +try_end: 
 +  goto try_next; 
 +catch_FileError:​ 
 +  e=*(FileError*)_exception_;​ 
 +  log(e); 
 +try_next: 
 +}  
 +</​code>​ | 
 +| <code C> 
 +openFile(...) { 
 +  x = open(...);​ 
 +  if (x < 0) 
 +    throw FileError(x);​ 
 +}              
 +</​code>​ | <code C> 
 +openFile(...) { 
 +  x = open(...);​ 
 +  if (x < 0) { 
 +     ​FileError tmp(x); 
 +     ​_exception_ = &tmp; 
 +     ​_throw_(“FileError”);​ 
 +  } 
 +
 +</​code>​ | 
 + 
 +Intrarea in tabela generata pentru intervalul try din exemplul de mai sus: 
 + 
 +^ Interval de aplicabilitate ^ Început catch ^ Tipul excepției ^ 
 +| try_start … try_end | catch_FileError ​ | FileError | 
 + 
 +GCC-ul genereaza tabela de exceptii (exception handler framework) in sectiunile .eh_frame / .eh_frame_hdr):​ 
 + 
 +===== Excepțiile la runtime ===== 
 + 
 +Să presupunem că se executa funcția func_ex, care aruncă o excepție. În vârful stivei se află cadrul de stivă al funcției func_ex. În cazul în care excepția a fost aruncată dintr-un bloc de try, se caută un blocul de catch care sa trateze tipul erorii aruncate. În cazul în care acesta se găsește, se începe procesul de stack-unwinding,​ dacă nu se găsește, se va apela funcția terminate(). 
 +Vom discuta în continuare despre **stack unwinding**,​ pornind de la următorul exemplu: 
 +<code C> 
 +#include <​iostream>​ 
 +#include <​string>​ 
 + 
 + void func2() 
 + {  
 +  throw std::​exception();​ 
 + } 
 +  
 + void func1() 
 + { 
 +   ​std::​string str = "Ana are ...";  
 +   ​func2();​ 
 + } 
 +  
 + int main() 
 + { 
 +   try 
 +   { 
 +      func1(); 
 +   }  
 +   ​catch(...)  
 +   { } 
 + } 
 +</​code>​ 
 + 
 +La runtime, în momentul în care func2 aruncă excepția, pe stivă există următoarele cadre: main, func1, func2. 
 +Funcția func2 nu prinde aceasta excepție, dar există in program un bloc de catch care sa o prinda, așadar, incepe **stack unwinding**.  
 + 
 +Stack unwinding parcurge lista de cadre de stiva pornind din vârful stivei, și: 
 +  - apelează destructorii pentru toate obiectele locale funcției al cărei cadrul se află în vârful stivei; 
 +  - face restore la regiștrii callee-saved 
 +  - se scoate cadrul de pe stiva 
 +  - se revine la pasul 1 (acum, în vârful stivei este funcția care a apelat funcția ce tocmai am scos-o de pe stivă); 
 + 
 +Revenind la exemplul nostru, func2() nu prinde exceptia, nu are nicio variabilă locală, deci singurul pas va fi scoaterea cadrului de pe stivă. Funția func1() are acum cadrul în vârful stivei, dar nici func1() nu contine blocul de catch, deci, se va apela destructorul lui **str**, apoi cadrul funcției func1() este scos de pe stiva. Se ajunge astfel la funcția main() care prinde excepția. ​  
 + 
 +<​note>​În procesul de unwinding, destructorii sunt apelați în ordine inversă constructorilor.</​note>​ 
 + 
 +   
 +<​note>​Stack unwinding este folosit și de ''​debugger''​ pentru a afișa stiva de apeluri și pentru a putea reveni cu execuția dintr-un punct anterior.  
 +</​note> ​  
 + 
 +Puteți să vă gandiți și la altă situație în care s-ar putea folosi acelați mecanism?  
 + 
 + 
 +====== Exerciții ====== 
 +{{:​cpl:​labs:​laborator-07-arhiva.zip|Arhiva}} laboratorului. 
 +  - Scrieți un program ''​C''​ care să determine sensul stivei (este mărită sau micșorată valoarea adresei vârfului stivei la operații de ''​push''?​). 
 +  - În directorul ''​return_struct''​ din arhiva laboratorului aveți implementate două funcții: una care întoarce o structură și alta care primește o structură ca parametru. Rulați ''​make asm''​ pentru a genera un fișier "​.s"​ și explicați:​ 
 +    * modul în care se trimite un argument de tip structură,​ 
 +    * modul în care se întoarce o structură. 
 +  - Modificați ''​doar''​ funcția **ask** din programul **ovr** pentru a afișa nota corectă. 
 +  - Scrieți o funcție ''​fast_multiply_3''​ în asm care să înmulțească trei numere întregi primite ca argumente. Declarați funcția ca fiind de tip ''​fastcall''​ (Pe arhitecturi Intel x86, folosirea conveției de apel fastcall, presupune că: 
 +    * primul argument se află în ''​%ecx''​ 
 +    * al doilea argument se află în ''​%edx''​ 
 +    * toți ceilalți parametri se află pe stivă ca la convenția de apel 
 +    * Următoarea linie declară o funcție specificându-i convenție de apel de tip ''​fastcall''​ 
 +    * <code c> __attribute__((fastcall)) int fast_multiply_3(int a, int b, int c); </​code>​ 
 +    * Apelați funcția definită la pasul anterior dintr-un fișier ''​C''​. Observați și explicați ce se întâmplă dacă în cadrul fișierului ''​C''​ nu apare declarația funcției ''​fast_multiply_3''​ sau dacă acestei declarații îi lipsește decoratorul ''​fastcall''​. 
 +    * Pentru operația de înmulțire folosiți instrucțiunea 
 +    * <code asm> imull %eax, %ecx # echivalent cu %ecx := %ecx * %eax </​code>​ 
 +  - Comentați performanța codului ''​perf_exceptii.cpp'',​ din directorul exceptii. 
 +  - Executati exemplul ''​exceptii.cpp''​ din directorul exceptii, explicati ce se intampla la runtime si corectati-l.
  
cpl/labs/07.1475556803.txt.gz · Last modified: 2016/10/04 07:53 by bogdan.nitulescu
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