Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Studio di funzioni e procedure in delphi e assembler

Studio di funzioni e procedure in delphi e assembler

E-mail Stampa PDF
Indice
Studio di funzioni e procedure in delphi e assembler
Come rendere più veloce una funzione
Tutte le pagine

In questo articolo verrà approfondito il codice macchina relativo a diverse funzioni compilate in delphi. Questo ci permetterà di capire in quale modo una funzione o una procedura verrà implementa, e quindi cosa dovremo aspettarci di trovare in memoria durante l'esecuzione di un applicazione. Verrà anche spiegato cosa cambia nella implementazione di una funzione nel caso questa venisse dichiarata come stdcall, cdecl etc..

I vari esempi avranno in comune la stessa funzionalità, ma avremo diverse implementazioni di essa.
Si tratta di un semplice programma che somma due variabili, e che nel primo esempio non utilizzerà ne funzioni ne procedure.

Riporto per intero il codice dell'applicazione delphi:

program Test1;
{$APPTYPE CONSOLE}
uses
SysUtils;

var
a,b,c:integer;
begin
a:=3;
b:=5;
c:=a+b;
writeln(c);
readln;
end.

dopo averlo compilato e mandato in esecuzione in memoria avremo il seguente codice:

Indirizzo   Codice           Istruzione         Delphi 
--------- ---------- ----------------- -----------
0040914D B803000000 mov eax,$00000003 //a:=3;
00409152 BA05000000 mov edx,$00000005 //b:=5;
00409157 8D1C02 lea ebx,[edx+eax] //c:=a+b;
....

In seguito il risultato in EBX verrà manipolato per la conversione in stringa
e la stampa a schermo.

Uso di una funzione

Nel successivo esempio useremo la funzione "somma" al posto della somma esplicita.

function somma(x,y:integer):integer;
begin
result:=x+y;
end;

var
a,b,c:integer;
begin
a:=3;
b:=5;
c:=somma(a,b);
writeln(c);
readln;
end.

Il codice in memoria si presenterà così:

0040914D   B803000000       mov eax,$00000003   //a:=3;
00409152 BA05000000 mov edx,$00000005 //b:=5;
00409157 E8A8FAFFFF call somma //c:=somma(a,b);
0040915C 8BD8 mov ebx,eax

analizziamolo meglio:

Preparazione dei parametri per la chiamata alla funzione
0040914D B803000000 mov eax,$00000003 //a:=3;
00409152 BA05000000 mov edx,$00000005 //b:=5;
Chiamata della funzione
00409157 E8A8FAFFFF call somma //c:=somma(a,b);

dopo aver sistemato i valori nei due registri, viene memorizzato nello stack (cioè viene spinto dentro nella pila) l'indirizzo dell'istruzione successiva 0040915C, così che essa venga eseguita al ritorno.
Dopo di che c'è il salto all'indirizzo dove risiede il codice della funzione.

   funzione somma
00408C04 03D0 add edx,eax //result:=x+y;
00408C06 8BC2 mov eax,edx //->notare che il risultato viene messo in EAX
00408C08 C3 ret //end;

nella esecuzione della istruzione di ritorno viene prelevato il valore in cima alla pila (0040915C) e questo valore viene usato come indirizzo di destinazione. Dato che in cima avevamo messo l'indirizzo della istruzione successiva alla chiamata di funzione, ci ritroveremo a proseguire l'esecuzione del codice iniziale.

0040915C 8BD8             mov ebx,eax

Al ritorno dalla somma il risultato viene spostato in EBX e successivamente usato per la conversione e la stampa come avveniva nel test1.

 

Funzione con molti parametri

Negli esempi precedenti le funzioni agivano direttamente su delle variabili memorizzate precedentemente nei registri.
Il problema e che molte volte i parametri di una funzione sono molti di più del numero di registri, vediamo quindi cosa accade in questo caso alla nostra funzione:

function somma(x,y,z,w,k:integer):integer;
begin
result:=x+y+z+w+k;
end;

var
c:integer
begin
c:=somma(1,2,3,4,5);
writeln(c);
readln;
end.

Prima di proseguire con l'analisi del codice vorrei ricordare che :

  • ESP è lo stack pointer, cioè il registro che contiene sempre l'indirizzo di memoria della cima della pila (stack)
  • EBP è  il base pointer, cioè l'indirizzo della base della pila ed quindi un riferimento per tutti i parametri delle funzioni o delle loro variabili locali, a volte anche chiamato frame pointer.
  • EIP punta sempre alla istruzione di codice che dovrà essere eseguita dal processore, questo valore quindi è quell'indirizzo che al momento della chiamata ad una funzione viene infilato nella pila. Le istruzioni di jump modificano questo valore.

ora vediamo un po' il codice in memoria

c:=somma(1,2,3,4,5);
0040914D 6A04 push $04
0040914F 6A05 push $05

00409151 B903000000 mov ecx,$00000003
00409156 BA02000000 mov edx,$00000002
0040915B B801000000 mov eax,$00000001
00409160 E89FFAFFFF call somma
00409165 8BD8 mov ebx,eax

come possiamo notare prima della chiamata della funzione il codice cerca di memorizzare i suoi parametri infilandoli nei vari registri EAX=1,EDX=2,ECX=3, questo metodo è la così detta FASTCALL.
Terminati i registri infila i restanti 2 parametri nella pila.
Una volta sistemati i valori dei parametri chiama la funzione somma, naturalmente, come in visto in precedenza, viene infilato nella pila il valore presente nell' EIP cio "00409165".
Se volessimo rappresentare la pila di memoria questa sarebbe così composta:

Indirizzo Valore
0012FFA0 00409165 <-- ESP
0012FFA4 00000005 k
0012FFA8 00000004 w
0012FFAC ................
................ ................

vediamo cosa accade passando alla funzione somma

begin
00408C04 55 push ebp //memorizzo il valore di EBP nella pila
00408C05 8BEC mov ebp,esp //metto il valore ESP in EBP per poterlo usare
result:=x+y+z+w+k;
00408C07 03D0 add edx,eax
00408C09 03CA add ecx,edx
00408C0B 034D0C add ecx,[ebp+$0c] // k = [EBP+12] (valore presente all'indirizzo EBP+12)
00408C0E 034D08 add ecx,[ebp+$08] // w = [EBP+8]
00408C11 8BC1 mov eax,ecx
end;
00408C13 5D pop ebp //ripristino il vecchio valore di EBP
00408C14 C20800 ret $0008

Questa volta la funzione un po' diversa, perché deve prendersi i valori dei parametri dallo stack, per fare questo gli occorre un riferimento fisso nello stack che in questo caso sarà EBP.
L'EBP è il puntatore alla base dello stack, mentre l'ESP è ul puntatore alla cima dello stack. Quindi quando lo stack si riempie o si svuota il registro ESP, che contiene l'indirizzo di memoria della cima della pila, verrà modificato.
Sinceramente in questo esempio non è veramente indispensabile, ma il compilatore usa uno standard generale, per cui le variabili nello stack saranno sempre raggiungibili tramite un puntatore base e un numero, cioè il parametro w si troverà sempre sommando la cima dello stack ad un offset (ESP + 12) e il parametro k si troverà nello stack all'indirizzo ESP + 8.
Diamo una occhiata a come lo stack ora:

Indirizzo Valore
0012FF9C 0012FFC0 <-- ESP
0012FFA0 00409165
0012FFA4 00000005 k
0012FFA8 00000004 w
0012FFAC ................
................ ................

L'incremento per ogni indirizzo di memoria è di 4 byte quindi se aggiungiamo 4 all'ESP (0012FF9C) troviamo l'indirizzo di ritorno (0012FFA0), aggiungendo 8 troviamo l'indirizzo di memoria del parametro k (0012FFA4) e aggiungendo 12 quello del parametro w (0012FFA8).
Il problema che all'interno della funzione ci potrebbero essere istruzioni che modificano lo stack e quindi l'ESP potrebbe variare, per questo motivo non possiamo prenderlo come riferimento, ma dobbiamo conservarne il valore iniziale in un registro a parte, in questo caso EBP.
Da questo momento in poi nella funzione potremo sempre accedere ai parametri sommando a EBP una costante:

[ebp+$0c]=w
[ebp+$08]=k

Prima di terminare la funzione andremo a ripristinare il vecchio valore di EBP dalla pila e infine chiamare il return sempre ricavandolo dalla pila.

 

Parametri come riferimento

Vediamo ora un esempio di procedura con un parametro passato come riferimento.

procedure somma(x,y:integer;var z:integer);
begin
z:=x+y;
end;

var
c:integer;
begin
somma(1,2,c);
writeln(c);
readln;
end.

La funzionalità del codice la stessa di tutti gli altri esempi, cambia solo l'implementazione, quindi anche il codice in memoria sarà diverso.

somma(1,2,c);
0040914C B908F94000 mov ecx,$0040F908
00409151 BA02000000 mov edx,$00000002
00409156 B801000000 mov eax,$00000001
0040915B E8A4FAFFFF call somma

a differenza di prima il valore dell'ultimo parametro non viene conservato nei registri, ma viene conservato uno spazio in memoria all'indirizzo "0040F908", quindi alla mia procedura gli passo proprio questo indirizzo conservato in ECX.
Vediamo un po' il codice della procedura:

z:=x+y;
00408C04 03D0 add edx,eax
00408C06 8911 mov [ecx],edx
end;
00408C08 C3 ret

Il risultato della somma viene spostato nell'indirizzo di memoria conservato in ECX e una volta terminata la procedura, a quell'indirizzo potrò sempre trovare il valore della mia variabile.

Le funzioni trattate fino ad ora seguono la convenzione di chiamata FastCall che corrisponde a dichiarare la funzione con la procedura come register. In delphi questo solitamente è omesso perché è implicito.

Direttiva
Ordine parametri
Clean-up
Usa i registri per i parametri?
register
Left-to-right
Routine
Yes
pascal
Left-to-right
Routine
No
cdecl
Right-to-left
Caller
No
stdcall
Right-to-left
Routine
No
safecall
Right-to-left
Routine
No

Nei paragrafi seguenti studieremo nel dettaglio i vari tipi di convenzione di chiamata.

 

Dichiarazione di funzione con StdCall

Per il quinto esercizio riprendiamo in considerazione l'esempio con una funzione semplice aggiungendo la convenzione di chiamata "StdCall".

function somma(x,y:integer):integer; stdcall;
begin
result:=x+y;
end;

var
c:integer;
begin
c:=somma(1,2);
writeln(c);
readln;
end.

La stdcall modifica il tipo di chiamata e si oppone alla FastCall che il compilatore applica di default, infatti prima abbiamo visto che, se i registri sono liberi al momento della chiamata, essi sono utilizzati per i parametri della funzione, ora invece con la stdcall saranno memorizzati forzatamente nello stack:

c:=somma(1,2);
0040914D 6A02 push $02
0040914F 6A01 push $01
00409151 E8AEFAFFFF call somma
00409156 8BD8 mov ebx,eax

e la funzione somma:

begin
00408C04 55 push ebp
00408C05 8BEC mov ebp,esp
result:=x+y;
00408C07 8B4508 mov eax,[ebp+$08]
00408C0A 03450C add eax,[ebp+$0c]
end;
00408C0D 5D pop ebp
00408C0E C20800 ret $0008

Come si può osservare viene usato il classico EBP come riferimento fisso e i parametri x e y saranno a EBP+12 e a EBP+8.
Ho ritenuto importante mostrare questo tipo di chiamata perché è d'obbligo usarla quando si usano le API di windows o una funzione di callback.

Da notare che nei vari esempi riportati fino ad ora ci siamo ritrovati sempre in 2 casi ben precisi di ritorno di funzione:

  1. ret
  2. ret $0008

Nel primo caso si ha un ritorno pulito, nel senso che la funzione non ha "sporcato" lo stack inserendovi i parametri e quindi l'unica cosa che la funzione deve fare un pop sulla pila di memoria (prelevare l'ultimo elemento della pila) e saltare all'indirizzo di memoria in esso contenuto.
Nel secondo caso prima di fare il pop e il salto al procedimento di partenza, deve ripulire lo stack quindi deve spostare il puntatore tante volte quanti sono i parametri memorizzati nella pila moltiplicati per 4, che la dimensione di una cella di memoria.
Essendo solo 2 i parametri infatti l'istruzione ret viene appunto accompagnata dal numero esadecimale $0008.

 

Dichiarazione di funzione con cdecl

Per completare questo articolo vediamo l'ultimo caso, ovvero la convenzione di chiamata "cdecl".

function somma(x,y:integer):integer; cdecl;
begin
result:=x+y;
end;

var
c:integer;
begin
c:=somma(1,2);
writeln(c);
readln;
end.

La chiamata cdecl è tipica dei compilatori C e l'unica differenza dalla chiamata sdtcall è che il "cleanup" dello stack, ovvero la pulizia della pila di memoria non la fa la funzione prima di ritornare al procedimento chiamante, ma la fa proprio quest'ultimo:

c:=somma(1,2);
0040914D 6A02 push $02
0040914F 6A01 push $01
00409151 E8AEFAFFFF call somma
00409156 83C408 add esp,$08 //modifica il puntatore alla cima della pila
00409159 8BD8 mov ebx,eax

con la funzione somma che invece è così:

begin
00408C04 55 push ebp
00408C05 8BEC mov ebp,esp
result:=x+y;
00408C07 8B4508 mov eax,[ebp+$08]
00408C0A 03450C add eax,[ebp+$0c]
end;
00408C0D 5D pop ebp
00408C0E C3 ret

Leggendo le istruzioni assembler vediamo che nonostante la funzione usi 2 parametri dallo stack, il "ret" è semplice e lascia nella pila le 2 variabili.
Queste verranno invece eliminate spostando il puntatore della cima dello stack ad opera del procedimento principale, non appena la funzione è rientrata, con l'istruzione "ADD  ESP,$08".

Graficamente se alla fine della chiamata abbiamo uno stack fatto così:

Indirizzo Valore
0012FFA4 00000001 k <-- ESP
0012FFA8 00000002 w
0012FFAC ................
................ ................

dopo l'istruzione "ADD  ESP,$08" avremo:

Indirizzo Valore
0012FFA4 00000001 k
0012FFA8 00000002 w
0012FFAC ................ <-- ESP
................ ................

questo equivale implicitamente ad una cancellazione delle 2 celle di memoria della pila, perché il programma a questo punto con dei successivi "push" potrà sovrascriverle.

Oltre a questi tipi di chiamata ce ne possono essere molti altri come ad esempio il safecall, usato dagli oggetti COM, e che abbastanza più complesso delle altre chiamate perché include dei controlli e la gestione per le eccezioni.

 

Un ulteriore dettaglio riguarda l'interpretazione del codice macchina, infatti molti potrebbero chiedersi come si passa dal codice "E8AEFAFFFF" all'istruzione "CALL somma", dove somma sta per "00408C04", che è appunto l'indirizzo dove si trova l'inizio della funzione somma.

Indirizzo  istruzione     istruzione in chiaro 
--------- ---------- --------------------
00409151 E8AEFAFFFF CALL somma

allora iniziamo a spiegare che E8 rappresenta l'istruzione x86 CALL mentre i byte successivi (AEFAFFFF) sono l'indirizzo di memoria a cui andare per iniziare esecuzione della funzione somma.
Il problema e che AEFAFFFF non è un indirizzo, almeno non esplicitamente. Prima di tutto lo dobbiamo riordinarlo:

AE|FA|FF|FF => FF|FF|FA|AE 

otteniamo quindi FFFFFAAE, ora questo numero non è proprio un indirizzo ma un offset, per cui per trovare l'indirizzo dovremo sommare l'indirizzo attuale a questo offset e più 5, che la lunghezza in byte della attuale istruzione.
Ricapitolando l'indirizzo a cui saltare per iniziare la funzione somma è dato da:

FFFFFAAE + 00409151 + 5 = 1 00408C04*
(Offset) + (EIP) + 5 = (indirizzo funzione)

Il primo 1 nel risultato, essendo fuori dal range degli 8 byte usati per indirizzare la memoria, non viene considerato e l'indirizzo finale 00408C04 è quello esattamente in cui troveremo la funzione somma, come si può anche constatare dall'ultimo esempio di codice.

*Il fatto che si esca dal range degli otto bit ci permette con una stessa operazione, ovvero l'addizione, di spostarci avanti e indietro rispetto all'indirizzo di partenza.

In allegato troverete i sorgenti di tutti gli esempi trattati in questo articolo.



Ultimo aggiornamento ( Giovedì 25 Agosto 2011 20:00 )  
Loading

Login