risorse | instrumentation
instrumentation è una libreria che realizza l'MCI. MCI sta per Method Call Interception, una tecnica che consente di intercettare le chiamate a funzione effettuate durante l'esecuzione di un programma. Le ragioni che giustificano il ricorso a tale tecnica sono riconducibili a due tipi di analisi:
Le recenti versioni del Microsoft Visual Studio offrono un'estensione proprietaria che, se attivata, inducono il compilatore ad inserire una chiamata a funzione all'inizio e al termine di ogni metodo o funzione del programma. Le due funzioni si chiamano rispettivamente _penter e _pexit, e sono attivate dalle opzioni di compilazione /Gh e /GH. Le firme delle due funzioni sono rispettivamente:
Secondo quanto riportato dalla documentazione ufficiale, entrambe le funzioni devono salvare sullo stack i contenuti di tutti i registri in ingresso e li devono ripristinare all'uscita.
La direttiva naked richiede al compilatore di non generare prologo/epilogo per le due funzioni.
Non serve dichiarare le due funzioni, a meno che queste non vengano chiamate esplicitamente; di norma dunque è sufficiente definirle:
extern "C" void __declspec(naked) _cdecl _penter( void ) { _asm { push eax push ebx push ecx push edx push ebp push edi push esi } // entering function _asm { pop esi pop edi pop ebp pop edx pop ecx pop ebx pop eax ret } }
extern "C" void __declspec(naked) _cdecl _pexit( void ) { _asm { push eax push ebx push ecx push edx push ebp push edi push esi } // exiting function _asm { pop esi pop edi pop ebp pop edx pop ecx pop ebx pop eax ret } }
Una volta definite le funzioni di controllo, nasce l'esigenza di stabilire l'identità della funzione agganciata, dato che le funzioni _penter e _pexit non ricevono alcun parametro che consenta di stabilire qual'è la funzione oggetto della chiamata. Tra le tecniche disponibili, le più frequentemente adottate sono due: la prima ricorre alle informazioni di debug, mentre la seconda fa uso dell'indirizzo di ritorno disponibile nello stack. Il primo metodo, che richiede la presenza delle informazioni di debug nei file oggetto, recupera automaticamente i nomi simbolici delle funzioni (al costo di un maggiore sforzo computazionale), mentre il secondo lo fa a posteriori, purché si disponga del file *.map generato dal linker.
Nel seguito viene descritta una possibile implementazione della seconda delle due. La soluzione proposta consiste nel definire le funzioni di controllo in modo tale che richiamino due funzioni ausiliarie, dette funzioni di notifica, passando loro l'indirizzo della funzione originale.
Si consideri il frammento di codice (le etichette racchiuse tra parentesi quadre rappresentano indirizzi in memoria):
// some code fun(p0, p1, ..., pN) [ret-addr] // some other code
La chiamata alla funzione fun ha l'effetto di caricare sullo stack il valore dei parametri (da destra a sinistra, che la convenzione sia __cdecl oppure __stdcall), quindi l'indirizzo di ritorno:
pN | |||
... | |||
p1 | |||
p0 | |||
ret-addr | ← esp |
Successivamente viene effettuata la chiamata vera e propria:
call fun
il controllo è dunque trasferito alla funzione fun, che, essendo stata decorata dal compilatore delle due chiamate _penter e _pexit, risulta essere:
[fun] call _penter [fun-body] // f body call _pexit
Dunque, non appena giunti in fun, viene subito effettuata una nuova chiamata a funzione, e conseguentemente anche l'indirizzo di ritorno di questa nuova chiamata viene inserito nello stack:
pN | |||
... | |||
p1 | |||
p0 | |||
ret-addr | |||
fun-body | ← esp |
Si giunge infine all'interno della funzione _penter, che provvede innanzitutto a salvare la configurazione dei registri generici:
[_penter] pushad
Il contenuto dello stack ora è:
pN | |||
... | |||
p1 | |||
p0 | |||
ret-addr | |||
fun-body | |||
<REGS> | ← esp |
_penter prosegue ricavando il puntatore allo stack frame della chiamata:
mov eax, esp add eax, 32
pN | |||
... | |||
p1 | |||
p0 | |||
ret-addr | |||
fun-body | ← eax (esp + 32) | ||
<REGS> | ← esp |
_penter pone sullo stack questo valore per passarlo come parametro alla funzione di notifica di chiamata:
push eax
pN | |||
... | |||
p1 | |||
p0 | |||
ret-addr | |||
fun-body | [frame] | ||
<REGS> | |||
frame | ← points to the stack's cell containing fun-body |
A questo punto viene effettuata la chiamata alla funzione di notifica:
call NotifyEnter
La funzione di notifica riceve come parametro l'indirizzo [fun-body] che può essere utilizzato come identificativo univoco per distinguere tutte le funzioni chiamate durante l'esecuzione del programma.
Un ragionamento analogo si compie per la chiamata _pexit all'uscita della funzione fun.
La libreria instrumentation costituisce un esempio d'uso della tecnica appena descritta, per scopi di profiling. Definendo delle opportune funzioni di notifica la libreria registra, per ogni funzione, gli istanti di inizio e termine dell'esecuzione. Al termine dell'esecuzione, i risultati sono salvati su file di testo per una immediata consultazione.
L'oggetto principale della libreria è Hook, che espone due funzioni statiche che rappresentano le funzioni di notifica richiamate dalle funzioni di controllo _penter e _pexit. La sua responsabilità principale è quella di inoltrare le notifiche ad un generico oggetto di tipo Hook::Observer.
La raccolta dei dati è curata dall'oggetto Tracer, che utilizza la struttura FnCall per memorizzare i dati caratteristici di ogni chiamata effettuata.
L'oggetto Logger si occupa infine del salvataggio dei dati raccolti; viene chiamato in causa all'uscita del programma dall'oggetto Tracer.
La liberia consiste di due file:
Per attivare l'analisi con la libreria instrumentation, procedere come segue:
Al termine della prova, le statistiche relative alle chiamate effettuate verranno registrate nel file instrumentation.log, salvato nella stessa cartella che ospita il programma analizzato.
Le statistiche raccolte sono:
A partire dalle statistiche contenute nel file instrumentation.log si possono ricavare altri dati di interesse. Un esempio è costituito dallo script Python instrumentation-stats.py, che determina i seguenti parametri:
L'associazione indirizzo/nome (decorato) della funzione avviene tramite il file .map, associando il valore della colonna addr dei file di log con la colonna Rva+Base del file .map.
Analizzando a titolo d'esempio il programma di prova instrumentation-test.cpp, la natura delle statistiche raccolte sarà del tipo:
call parent addr elapsed 0 N/A 0041b0b0 1.206 1 0 0041af50 0.082 2 1 0041aef0 0.049 3 2 0041ae70 0.022 4 0 0041af50 0.705 5 4 0041aef0 N/A 6 5 0041ae70 N/A 7 0 0041b010 0.087 8 7 0041b010 0.075 9 8 0041b010 0.063 10 9 0041b010 0.031 11 10 0041b010 0.018 12 11 0041b010 0.006 13 0 0041b010 N/A 14 13 0041b010 N/A 15 14 0041b010 N/A 16 15 0041b010 N/A 17 16 0041b010 N/A 18 17 0041b010 N/A
La rielaborazione delle stesse per mezzo dello script Python produce come risultato:
addr name hits exceptions min max total 0041aef0 inner 2 1 0.049 0.049 0.049 0041af50 outer 2 0 0.082 0.705 0.787 0041b010 recurse 12 6 0.006 0.087 0.280 0041b0b0 main 1 0 1.206 1.206 1.206 0041ae70 bomb 2 1 0.022 0.022 0.022
La libreria instrumentation è stata sviluppata in ambiente Win32 su Microsoft Windows XP Professional con Microsoft Visual Studio 2005/2008 Professional Edition. Non funziona in ambiente x64 per due motivi: in primo luogo i frammenti assembly sono specifici della piattaforma Intel a 32 bit, inoltre i compilatori Microsoft a 64 bit non consentono di intercalare codice C/C++ con codice assembly.
Attenzione: i log prodotti superano facilmente le centinaia di MB. Conviene valutare di volta in volta i moduli per i quali è opportuno attivare l'analisi.
Pagina modificata il 15/11/2011