Debug di codice ottimizzato per le prestazioni

Microsoft dispone di determinate tecniche usate per riordinare il codice compilato e collegato in modo che venga eseguito con maggiore efficienza. Queste tecniche ottimizzano il componente per le gerarchie di memoria e si basano su scenari di training.

L'ottimizzazione risultante riduce il paging (e gli errori di pagina) e aumenta la località spaziale tra codice e dati. Risolve un collo di bottiglia delle prestazioni chiave che verrebbe introdotto da un posizionamento non ottimale del codice originale. Un componente sottoposto a questa ottimizzazione può avere il codice o il blocco di dati all'interno di una funzione spostata in posizioni diverse del file binario.

Nei moduli ottimizzati da queste tecniche, le posizioni del codice e dei blocchi di dati vengono spesso trovate in indirizzi di memoria diversi da quelli in cui si trovano dopo la normale compilazione e il collegamento. Inoltre, le funzioni possono essere state suddivise in molti blocchi non contigui, in modo che i percorsi di codice più usati possano trovarsi vicini tra loro nelle stesse pagine.

Pertanto, una funzione (o qualsiasi simbolo) più un offset non avrà necessariamente lo stesso significato che avrebbe nel codice non ottimizzato.

Debug di codice ottimizzato per le prestazioni

Durante il debug, è possibile verificare se un modulo è stato ottimizzato per le prestazioni usando il comando di estensione !lmi in qualsiasi modulo per cui sono stati caricati i simboli:

0:000> !lmi ntdll
Loaded Module Info: [ntdll]
         Module: ntdll
   Base Address: 77f80000
     Image Name: ntdll.dll
   Machine Type: 332 (I386)
     Time Stamp: 394193d2 Fri Jun 09 18:03:14 2000
       CheckSum: 861b1
Characteristics: 230e stripped perf
Debug Data Dirs: Type Size     VA  Pointer
                 MISC  110,     0,   76c00 [Data not mapped]
     Image Type: DBG      - Image read successfully from symbol server.
                 c:\symbols\dll\ntdll.dbg
    Symbol Type: DIA PDB  - Symbols loaded successfully from symbol server.
                 c:\symbols\dll\ntdll.pdb

In questo output si noti il termine perf nella riga "Caratteristiche". Ciò indica che questa ottimizzazione delle prestazioni è stata applicata a ntdll.dll.

Il debugger è in grado di comprendere una funzione o un altro simbolo senza offset; in questo modo è possibile impostare punti di interruzione su funzioni o altre etichette senza problemi. Tuttavia, l'output di un'operazione disassembly potrebbe generare confusione, perché questo disassembly rifletterà le modifiche apportate dall'ottimizzatore.

Poiché il debugger tenterà di rimanere vicino al codice originale, è possibile che vengano visualizzati alcuni risultati divertenti. La regola generale quando si utilizzano codici ottimizzati per le prestazioni è semplicemente che non è possibile eseguire un'aritmetica degli indirizzi affidabile sul codice ottimizzato.

Ecco un esempio:

kd> bl
 0 e f8640ca6     0001 (0001) tcpip!IPTransmit
 1 e f8672660     0001 (0001) tcpip!IPFragment

kd> u f864b4cb
tcpip!IPTransmit+e48:
f864b4cb f3a4             rep     movsb
f864b4cd 8b75cc           mov     esi,[ebp-0x34]
f864b4d0 8b4d10           mov     ecx,[ebp+0x10]
f864b4d3 8b7da4           mov     edi,[ebp-0x5c]
f864b4d6 8bc6             mov     eax,esi
f864b4d8 6a10             push    0x10
f864b4da 034114           add     eax,[ecx+0x14]
f864b4dd 57               push    edi

È possibile visualizzare dall'elenco dei punti di interruzione che l'indirizzo di IPTransmit è 0xF8640CA6.

Quando si unassembla una sezione di codice all'interno di questa funzione in 0xF864B4CB, l'output indica che si tratta di 0xE48 byte oltre l'inizio della funzione. Tuttavia, se si sottrae la base della funzione da questo indirizzo, l'offset effettivo sembra essere 0xA825.

Ciò che accade è questo: il debugger mostra effettivamente un disassembly delle istruzioni binarie a partire da 0xF864B4CB. Ma invece di calcolare l'offset in base a una semplice sottrazione, il debugger visualizza , come meglio può, l'offset alla voce della funzione presente nel codice originale prima dell'esecuzione delle ottimizzazioni. Tale valore è 0xE48.

D'altra parte, se si tenta di esaminare IPTransmit+0xE48, si noterà quanto segue:

kd> u tcpip!iptransmit+e48
tcpip!ARPTransmit+d8:
f8641aee 0856ff           or      [esi-0x1],dl
f8641af1 75fc             jnz     tcpip!ARPTransmit+0xd9 (f8641aef)
f8641af3 57               push    edi
f8641af4 e828eeffff       call    tcpip!ARPSendData (f8640921)
f8641af9 5f               pop     edi
f8641afa 5e               pop     esi
f8641afb 5b               pop     ebx
f8641afc c9               leave

Ciò che accade qui è che il debugger riconosce il simbolo IPTransmit come equivalente all'indirizzo 0xF8640CA6 e il parser dei comandi esegue un'aggiunta semplice per trovare che 0xF8640CA6 + 0xE48 = 0xF8641A edizione Enterprise. Questo indirizzo viene quindi usato come argomento per il comando u (Unassemble). Tuttavia, dopo l'analisi di questo percorso, il debugger rileva che non si tratta di IPTransmit più un offset di 0xE48. Infatti, non fa parte di questa funzione. Corrisponde invece alla funzione ARPTransmit più un offset di 0xD8.

Il motivo è che l'ottimizzazione delle prestazioni non è reversibile tramite l'aritmetica degli indirizzi. Anche se il debugger può accettare un indirizzo e dedurre il simbolo e l'offset originali, non dispone di informazioni sufficienti per accettare un simbolo e un offset e convertirlo nell'indirizzo corretto. Di conseguenza, il disassembly non è utile in questi casi.