Debugowanie zoptymalizowanego kodu i funkcji wbudowanych

W przypadku systemu Windows 8 debuger i kompilator systemu Windows zostały ulepszone, dzięki czemu można debugować zoptymalizowany kod i debugować funkcje wbudowane. Debuger wyświetla parametry i zmienne lokalne niezależnie od tego, czy są przechowywane w rejestrach, czy na stosie. Debuger wyświetla również funkcje wbudowane w stosie wywołań. W przypadku funkcji wbudowanych debuger wyświetla zmienne lokalne, ale nie parametry.

Gdy kod zostanie zoptymalizowany, zostanie przekształcony w celu szybszego uruchamiania i używania mniejszej ilości pamięci. Czasami funkcje są usuwane w wyniku usunięcia martwego kodu, scalania kodu lub umieszczania wbudowanych funkcji. Można również usunąć zmienne lokalne i parametry. Wiele optymalizacji kodu usuwa zmienne lokalne, które nie są potrzebne lub używane; inne optymalizacje usuwają zmienne indukcji w pętlach. Eliminacja wspólnych podwyrażeń łączy zmienne lokalne.

Kompilacje detaliczne systemu Windows są zoptymalizowane. Dlatego jeśli korzystasz z kompilacji detalicznej systemu Windows, szczególnie przydatne jest posiadanie debugera, który jest przeznaczony do pracy z zoptymalizowanym kodem. Aby umożliwić efektywne debugowanie zoptymalizowanego kodu, wymagane są dwie podstawowe funkcje: 1) dokładne wyświetlanie zmiennych lokalnych i 2) wyświetlanie funkcji wbudowanych na stosie wywołań.

Dokładne wyświetlanie zmiennych lokalnych i parametrów

Aby ułatwić dokładne wyświetlanie zmiennych lokalnych i parametrów, kompilator rejestruje informacje o lokalizacjach zmiennych lokalnych i parametrów w plikach symboli (PDB). Te rekordy lokalizacji śledzą lokalizacje przechowywania zmiennych i określone zakresy kodu, w których te lokalizacje są prawidłowe. Te rekordy pomagają nie tylko śledzić lokalizacje (w rejestrach lub w gniazdach stosu) zmiennych, ale także ruch zmiennych. Na przykład parametr może najpierw znajdować się w rejestrze RCX, ale zostaje przeniesiony do miejsca w stosie, aby zwolnić RCX. Następnie jest przeniesiony do rejestru R8, gdy jest intensywnie używany w pętli, a później przenoszony do innego miejsca w stosie, gdy kod jest poza pętlą. Debuger systemu Windows używa bogatych rekordów lokalizacji w plikach PDB i używa bieżącego wskaźnika instrukcji, aby wybrać odpowiednie rekordy lokalizacji dla zmiennych lokalnych i parametrów.

Ten zrzut ekranu okna Ustawienia lokalne w programie Visual Studio przedstawia parametry i zmienne lokalne dla funkcji w zoptymalizowanej aplikacji 64-bitowej. Funkcja nie jest wbudowana, więc widzimy zarówno parametry, jak i zmienne lokalne.

Zrzut ekranu przedstawiający okno Ustawienia lokalne w programie Visual Studio z wyświetlonymi parametrami i zmiennymi lokalnymi dla funkcji w zoptymalizowanej aplikacji 64-bitowej.

Za pomocą polecenia dv -v można wyświetlić lokalizacje parametrów i zmiennych lokalnych.

Zrzut ekranu przedstawiający dane wyjściowe polecenia z lokalizacjami parametrów i zmiennych lokalnych przy użyciu polecenia dv -v.

Zwróć uwagę, że w oknie Ustawienia lokalne są wyświetlane parametry poprawnie, mimo że są one przechowywane w rejestrach.

Oprócz śledzenia zmiennych z typami pierwotnymi, rekordy lokalizacji śledzą elementy członkowskie danych lokalnych struktur i klas. Następujące dane wyjściowe debugera zawierają struktury lokalne.

0:000> dt My1
Local var Type _LocalStruct
   +0x000 i1               : 0n0 (edi)
   +0x004 i2               : 0n1 (rsp+0x94)
   +0x008 i3               : 0n2 (rsp+0x90)
   +0x00c i4               : 0n3 (rsp+0x208)
   +0x010 i5               : 0n4 (r10d)
   +0x014 i6               : 0n7 (rsp+0x200)

0:000> dt My2
Local var @ 0xefa60 Type _IntSum
   +0x000 sum1             : 0n4760 (edx)
   +0x004 sum2             : 0n30772 (ecx)
   +0x008 sum3             : 0n2 (r12d)
   +0x00c sum4             : 0n0

Poniżej przedstawiono kilka obserwacji dotyczących powyższych danych wyjściowych debugera.

  • Lokalna struktura My1 ilustruje, że kompilator może rozdzielać lokalne elementy danych struktury do rejestrów i nieciągłych miejsc w stosie.
  • Dane wyjściowe polecenia dt My2 różnią się od danych wyjściowych polecenia dt _IntSum 0xefa60. Nie można założyć, że struktura lokalna zajmie ciągły i nieprzerwany blok pamięci stosu. W przypadku My2 tylko sum4 pozostaje w oryginalnym bloku stosu; pozostałe trzy pola danych są przenoszone do rejestrów.
  • Niektóre elementy członkowskie danych mogą mieć wiele lokalizacji. Na przykład my2.sum2 ma dwie lokalizacje: jedna to rejestr ECX (wybierany przez debuger systemu Windows), a druga to 0xefa60+0x4 (oryginalny slot stosu). Może się to zdarzyć również w przypadku zmiennych lokalnych typu pierwotnego, a debuger systemu Windows nakłada precedensowe heurystyki w celu określenia lokalizacji do użycia. Na przykład, lokalizacje w rejestrach zawsze mają pierwszeństwo przed lokalizacjami w stosie.

Wyświetlanie wbudowanych funkcji w stosie wywołań

Podczas optymalizacji kodu niektóre funkcje są umieszczane w wierszu. Oznacza to, że treść funkcji jest umieszczana bezpośrednio w kodzie, podobnie do rozszerzenia makra. Nie ma ani wywołania funkcji, ani powrotu do wywołującego. Aby ułatwić wyświetlanie funkcji wbudowanych, kompilator przechowuje dane w plikach PDB, które pomagają w dekodowaniu fragmentów kodu dla funkcji wbudowanych (czyli sekwencji bloków kodu w funkcjach wywołujących, które należą do funkcji wywoływanych, umieszczanych w trybie inline), a także zmiennych lokalnych (zmiennych o określonym zakresie w tych blokach kodu). Te dane pomagają debugerowi uwzględniać funkcje wbudowane jako część procesu odwijania stosu.

Załóżmy, że skompilujesz aplikację i wymuszasz, aby funkcja o nazwie func1 była inline.

__forceinline int func1(int p1, int p2, int p3)
{
   int num1 = 0;
   int num2 = 0;
   int num3 = 0;
   ...
}

Możesz użyć polecenia bm, aby ustawić punkt przerwania na .func1

0:000> bm MyApp!func1
  1: 000007f6`8d621088 @!"MyApp!func1" (MyApp!func1 inlined in MyApp!main+0x88)
0:000> g

Breakpoint 1 hit
MyApp!main+0x88:
000007f6`8d621088 488d0d21110000  lea     rcx,[MyApp!`string' (000007f6`8d6221b0)]

Po wykonaniu jednego kroku w programie func1, możesz użyć polecenia k, aby zobaczyć func1 na stosie wywołań. Możesz użyć polecenia dv , aby wyświetlić zmienne lokalne dla func1. Zwróć uwagę, że zmienna num3 lokalna jest wyświetlana jako niedostępna. Zmienna lokalna może być niedostępna w zoptymalizowanym kodzie z wielu powodów. Może się okazać, że zmienna nie istnieje w zoptymalizowanym kodzie. Może się okazać, że zmienna nie została jeszcze zainicjowana lub że zmienna nie jest już używana.

0:000> p
MyApp!func1+0x7:
000007f6`8d62108f 8d3c33          lea     edi,[rbx+rsi]

0:000> knL
# Child-SP          RetAddr           Call Site
00 (Inline Function) --------`-------- MyApp!func1+0x7
01 00000000`0050fc90 000007f6`8d6213f3 MyApp!main+0x8f
02 00000000`0050fcf0 000007ff`c6af0f7d MyApp!__tmainCRTStartup+0x10f
03 00000000`0050fd20 000007ff`c7063d6d KERNEL32!BaseThreadInitThunk+0xd
04 00000000`0050fd50 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

0:000> dv -v
00000000`0050fcb0            num1 = 0n0
00000000`0050fcb4            num2 = 0n0
<unavailable>                num3 = <value unavailable>

Jeśli przyjrzysz się ramce 1 w śladzie stosu main , możesz zobaczyć zmienne lokalne dla funkcji. Zwróć uwagę, że dwie zmienne są przechowywane w rejestrach.

0:000> .frame 1
01 00000000`0050fc90 000007f6`8d6213f3 MyApp!main+0x8f

0:000> dv -v
00000000`0050fd08               c = 0n7
@ebx                            b = 0n13
@esi                            a = 0n6

Debuger systemu Windows agreguje dane z plików PDB, aby znaleźć wszystkie miejsca, w których została umieszczona określona funkcja. Za pomocą polecenia x można wyświetlić listę wszystkich witryn wywołujących funkcji wbudowanej.

0:000> x simple!MoreCalculate
00000000`ff6e1455 simple!MoreCalculate =  (inline caller) simple!wmain+8d
00000000`ff6e1528 simple!MoreCalculate =  (inline caller) simple!wmain+160

0:000> x simple!Calculate
00000000`ff6e141b simple!Calculate =  (inline caller) simple!wmain+53

Ponieważ debuger systemu Windows może wyliczyć wszystkie miejsca wywołania funkcji wbudowanej, może ustawić punkty przerwania wewnątrz tej funkcji, obliczając przesunięcia od miejsc wywołania. Możesz użyć polecenia bm (używanego do ustawiania punktów przerwania, które pasują do wzorców wyrażeń regularnych), aby ustawić punkty przerwania dla funkcji wbudowanych.

Debuger systemu Windows grupuje wszystkie punkty przerwania ustawione dla określonej funkcji wbudowanej w kontener punktu przerwania. Kontenerem punktu przerwania można manipulować jako całością, używając poleceń takich jak be, bd, bc. Zobacz następujące przykłady poleceń bd 3 i bc 3 . Można również manipulować poszczególnymi punktami przerwania. Zobacz poniższy przykład polecenia be 2.

0:000> bm simple!MoreCalculate
  2: 00000000`ff6e1455 @!"simple!MoreCalculate" (simple!MoreCalculate inlined in simple!wmain+0x8d)
  4: 00000000`ff6e1528 @!"simple!MoreCalculate" (simple!MoreCalculate inlined in simple!wmain+0x160)

0:000> bl
 0 e 00000000`ff6e13c8 [n:\win7\simple\simple.cpp @ 52]    0001 (0001)  0:**** simple!wmain
 3 e <inline function>     0001 (0001)  0:**** {simple!MoreCalculate}
     2 e 00000000`ff6e1455 [n:\win7\simple\simple.cpp @ 58]    0001 (0001)  0:**** simple!wmain+0x8d (inline function simple!MoreCalculate)
     4 e 00000000`ff6e1528 [n:\win7\simple\simple.cpp @ 72]    0001 (0001)  0:**** simple!wmain+0x160 (inline function simple!MoreCalculate)

0:000> bd 3
0:000> be 2

0:000> bl
 0 e 00000000`ff6e13c8 [n:\win7\simple\simple.cpp @ 52]    0001 (0001)  0:**** simple!wmain
 3 d <inline function>     0001 (0001)  0:**** {simple!MoreCalculate}
     2 e 00000000`ff6e1455 [n:\win7\simple\simple.cpp @ 58]    0001 (0001)  0:**** simple!wmain+0x8d (inline function simple!MoreCalculate)
     4 d 00000000`ff6e1528 [n:\win7\simple\simple.cpp @ 72]    0001 (0001)  0:**** simple!wmain+0x160 (inline function simple!MoreCalculate)

0:000> bc 3

0:000> bl
 0 e 00000000`ff6e13c8 [n:\win7\simple\simple.cpp @ 52]    0001 (0001)  0:**** simple!wmain

Ponieważ nie ma jawnych instrukcji wywołania ani zwracania dla funkcji wbudowanych, kroki na poziomie źródła są szczególnie trudne dla debugera. Na przykład można przypadkowo wykonać krok do funkcji wbudowanej (jeśli następna instrukcja jest częścią funkcji wbudowanej) lub możesz wielokrotnie przechodzić i wychodzić z tej samej funkcji wbudowanej (ponieważ bloki kodu funkcji wbudowanej zostały podzielone i przeniesione przez kompilator). Aby zachować znane środowisko kroków, debuger systemu Windows obsługuje mały stos wywołań koncepcyjnych dla każdego adresu instrukcji kodu i tworzy maszynę stanu wewnętrznego do wykonywania kroków, kroków i operacji wyjścia. To zapewnia dość dokładne przybliżenie doświadczenia krokowego dla funkcji nie będących funkcjami wbudowanymi.

Dodatkowe informacje

Uwaga Możesz użyć polecenia .inline 0, aby wyłączyć debugowanie funkcji wbudowanych. Polecenie .inline 1 umożliwia debugowanie funkcji wbudowanych. Standardowe techniki debugowania

Zobacz również

Standardowe techniki debugowania