Freigeben über


Debuggen von leistungsoptimiertem Code

Microsoft verfügt über bestimmte Techniken, mit denen kompilierter und verknüpfter Code neu angeordnet und damit effizienter ausgeführt wird. Diese Techniken optimieren die Komponente für Speicherhierarchien und basieren auf Trainingsszenarien.

Durch die resultierende Optimierung wird das Paging (und Seitenfehler) reduziert und die räumliche Lokalität zwischen Code und Daten erhöht. Es behebt einen wichtigen Leistungsengpass, der durch eine schlechte Positionierung des Originalcodes entstehen würde. Bei einer Komponente, die diese Optimierung durchlaufen hat, kann es sein, dass der Code oder Datenblock innerhalb einer Funktion an andere Stellen der Binärdatei verschoben wurde.

In Modulen, die mit diesen Techniken optimiert wurden, befinden sich die Code- und Datenblöcke häufig an anderen Speicheradressen als den Speicherorten, an denen sie nach der normalen Kompilierung und Verknüpfung liegen würden. Darüber hinaus wurden Funktionen möglicherweise in viele nicht zusammenhängende Blöcke aufgeteilt, damit die am häufigsten verwendeten Codepfade nahe beieinander auf denselben Seiten liegen können.

Daher hat eine Funktion (oder ein beliebiges Symbol) plus ein Offset nicht unbedingt die gleiche Bedeutung wie in nicht optimiertem Code.

Debuggen von leistungsoptimiertem Code

Beim Debuggen können Sie sehen, ob ein Modul leistungsoptimiert wurde, indem Sie den !lmi-Erweiterungsbefehl in jedem Modul verwenden, für das Symbole geladen wurden:

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

Beachten Sie in dieser Ausgabe den Begriff perf in der Zeile „Merkmale“. Dies weist darauf hin, dass diese Leistungsoptimierung auf ntdll.dll angewendet wurde.

Der Debugger kann eine Funktion oder ein anderes Symbol ohne Offset verstehen. Dadurch können Sie problemlos Haltepunkte auf Funktionen oder anderen Beschriftungen setzen. Die Ausgabe eines Disassemblierungsvorgangs kann jedoch verwirrend sein, da diese Disassemblierung die vom Optimierer vorgenommenen Änderungen widerspiegelt.

Da der Debugger versucht, nahe am Originalcode zu bleiben, werden Sie möglicherweise einige interessante Ergebnisse sehen. Die Faustregel beim Arbeiten mit leistungsoptimierten Codes lautet einfach, dass Sie bei optimiertem Code keine zuverlässige Adressarithmetik durchführen können.

Hier ist ein Beispiel:

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

Aus der Haltepunktliste können Sie ersehen, dass die Adresse von IPTransmit 0xF8640CA6 ist.

Wenn Sie einen Codeabschnitt innerhalb dieser Funktion bei 0xF864B4CB disassemblieren, zeigt die Ausgabe an, dass dies 0xE48 Bytes nach dem Anfang der Funktion liegt. Wenn Sie jedoch die Basis der Funktion von dieser Adresse subtrahieren, scheint der tatsächliche Offset 0xA825 zu sein.

Folgendes passiert: Der Debugger zeigt tatsächlich eine Disassemblierung der Binäranweisungen, beginnend bei 0xF864B4CB. Doch anstatt den Offset durch einfache Subtraktion zu berechnen, zeigt der Debugger – so gut er kann – den Offset zum Funktionseintrag an, wie er im Originalcode vor der Durchführung der Optimierungen vorhanden war. Dieser Wert ist 0xE48.

Wenn Sie sich andererseits IPTransmit+0xE48 ansehen, sehen Sie Folgendes:

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

Was hier geschieht, ist, dass der Debugger das Symbol IPTransmit als gleichwertig mit der Adresse 0xF8640CA6 erkennt und der Befehlsparser eine einfache Addition durchführt, um herauszufinden, dass 0xF8640CA6 + 0xE48 = 0xF8641AEE ist. Diese Adresse wird dann als Argument für den Befehl u (Unassemble) verwendet. Aber sobald dieser Ort analysiert wird, stellt der Debugger fest, dass dies nicht IPTransmit plus einen Offset von 0xE48 ist. In der Tat ist es überhaupt nicht Teil dieser Funktion. Vielmehr entspricht es der Funktion ARPTransmit plus einem Offset von 0xD8.

Der Grund dafür ist, dass die Leistungsoptimierung nicht durch Adressarithmetik umkehrbar ist. Der Debugger kann zwar eine Adresse übernehmen und deren ursprüngliches Symbol und Offset ableiten, verfügt jedoch nicht über genügend Informationen, um ein Symbol und einen Offset zu übernehmen und in die richtige Adresse zu übersetzen. Daher ist die Disassemblierung in diesen Fällen nicht hilfreich.