risorse | instrumentation

instrumentation

Introduzione

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 funzioni di controllo _penter e _pexit

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
  }
}

Identificazione della funzione chiamata

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

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:

Uso della libreria instrumentation

Per attivare l'analisi con la libreria instrumentation, procedere come segue:

  1. compilare i moduli da analizzare con le opzioni /Gh e /GH;
  2. includere il file di libreria instrumentation.lib;
  3. richiedere la generazione del file *.map in fase di linking (opzione /MAP[:filename]);
  4. avviare il programma;
  5. effettuare le attività da analizzare;
  6. terminare il programma.

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

Note

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