Отладка оптимизированного кода и встроенных функций
Для Windows 8 отладчик и компилятор Windows были усовершенствованы, чтобы можно было отлаживать оптимизированный код и отлаживать встроенные функции. Отладчик отображает параметры и локальные переменные независимо от того, хранятся ли они в регистрах или в стеке. Отладчик также отображает встроенные функции в стеке вызовов. Для встроенных функций отладчик отображает локальные переменные, но не параметры.
При оптимизации кода он преобразуется для более быстрого выполнения и использования меньшего объема памяти. Иногда функции удаляются в результате удаления неработающего кода, объединения кода или размещения встроенных функций. Локальные переменные и параметры также можно удалить. Многие оптимизации кода удаляют локальные переменные, которые не нужны или не используются; другие оптимизации удаляют индукционные переменные в циклах. Исключение общих подвыъявов объединяет локальные переменные.
Розничные сборки Windows оптимизированы. Таким образом, если вы используете розничную сборку Windows, особенно полезно иметь отладчик, который хорошо работает с оптимизированным кодом. Для эффективной отладки оптимизированного кода требуются две основные функции: 1) точное отображение локальных переменных и 2) отображение встроенных функций в стеке вызовов.
Точное отображение локальных переменных и параметров
Чтобы упростить точное отображение локальных переменных и параметров, компилятор записывает сведения о расположении локальных переменных и параметров в файлы символов (PDB). Эти записи о расположении отслеживают расположения хранения переменных и конкретные диапазоны кода, в которых эти расположения являются допустимыми. Эти записи помогают не только отслеживать расположения переменных (в регистрах или слотах стека), но и их перемещение. Например, параметр может сначала находиться в регистре RCX, но перемещается в слот стека для освобождения RCX, затем перемещается на регистрацию R8, когда он активно используется в цикле, а затем перемещается в другой слот стека, когда код выходит из цикла. Отладчик Windows использует полнофункционированные записи расположения в PDB-файлах и использует текущий указатель инструкции для выбора соответствующих записей расположения для локальных переменных и параметров.
На этом снимке экрана окна Локальные в Visual Studio показаны параметры и локальные переменные для функции в оптимизированном 64-разрядном приложении. Функция не является встроенной, поэтому мы видим как параметры, так и локальные переменные.
Для просмотра расположений параметров и локальных переменных можно использовать команду dv -v .
Обратите внимание, что в окне Локальные параметры отображаются правильно, даже если они хранятся в регистрах.
Помимо отслеживания переменных с примитивными типами, записи расположения отслеживают элементы данных локальных структур и классов. В следующих выходных данных отладчика отображаются локальные структуры.
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
Ниже приведены некоторые замечания о предыдущих выходных данных отладчика.
- Локальная структура My1 показывает, что компилятор может распространять элементы данных локальной структуры в регистры и несмежные слоты стека.
- Выходные данные команды dt My2 будут отличаться от выходных данных команды dt _IntSum 0xefa60. Нельзя предположить, что локальная структура будет занимать непрерывный блок памяти стека. В случае с My2 остается только
sum4
в исходном блоке стека; остальные три элемента данных перемещаются в регистры. - Некоторые элементы данных могут иметь несколько расположений. Например, My2.sum2 имеет два расположения: одно из них регистрирует ECX (выбирает отладчик Windows), а другое — 0xefa60+0x4 (исходный слот стека). Это может произойти и для локальных переменных примитивного типа, и отладчик Windows применяет эвристические прецеденты, чтобы определить, какое расположение использовать. Например, регистрировать расположения всегда следует за расположениями стека.
Отображение встроенных функций в стеке вызовов
Во время оптимизации кода некоторые функции размещаются в строке. То есть тело функции помещается непосредственно в код, как расширение макроса. Вызов функции и возврат к вызывающей объекту не выполняется. Чтобы упростить отображение встроенных функций, компилятор сохраняет данные в PDB-файлах, которые помогают декодировать фрагменты кода для встроенных функций (т. е. последовательности блоков кода в вызывающих функциях, принадлежащих к вызываемой функции, размещаемой в них), а также локальные переменные (локальные переменные в этих блоках кода). Эти данные помогают отладчику включать встроенные функции в рамках очистки стека.
Предположим, что вы скомпилируете приложение и принудительно встраивать функцию с именем func1
.
__forceinline int func1(int p1, int p2, int p3)
{
int num1 = 0;
int num2 = 0;
int num3 = 0;
...
}
С помощью команды bm можно задать точку останова в 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)]
После выполнения одного шага можно func1
использовать команду k для просмотра func1
в стеке вызовов. С помощью команды dv можно просмотреть локальные переменные для func1
. Обратите внимание, что локальная переменная num3
отображается как недоступная. Локальная переменная может быть недоступна в оптимизированном коде по ряду причин. Возможно, переменная не существует в оптимизированном коде. Возможно, переменная еще не инициализирована или переменная больше не используется.
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>
Если взглянуть на кадр 1 в трассировке стека, можно увидеть локальные main
переменные для функции. Обратите внимание, что две переменные хранятся в регистрах.
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
Отладчик Windows объединяет данные из PDB-файлов, чтобы найти все места, в которых была размещена определенная функция. Вы можете использовать команду x для вывода списка всех сайтов вызывающих объектов встроенной функции.
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
Так как отладчик Windows может перечислять все вызывающие сайты встроенной функции, он может задать точки останова внутри встроенной функции, вычисляя смещения от сайтов вызывающих объектов. Для задания точек останова для встроенных функций можно использовать команду bm (которая используется для задания точек останова, соответствующих шаблонам регулярных выражений).
Отладчик Windows группирует все точки останова, заданные для конкретной встроенной функции, в контейнер точек останова. Вы можете управлять контейнером точек останова в целом с помощью таких команд, как be, bd, bc. См. следующие примеры команд bd 3 и bc 3 . Вы также можете управлять отдельными точками останова. См. следующий пример команды 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
Так как для встроенных функций нет явных инструкций по вызову или возврату, пошаговое выполнение на исходном уровне особенно сложно для отладчика. Например, можно непреднамеренно войти в встроенную функцию (если следующая инструкция является частью встроенной функции) или выполнить шаг и выйти из одной встроенной функции несколько раз (так как блоки кода для встроенной функции были разделены и перемещены компилятором). Чтобы сохранить привычный интерфейс пошагового выполнения, отладчик Windows поддерживает небольшой концептуальный стек вызовов для каждого адреса инструкции кода и создает внутренний конечный автомат для выполнения операций пошагового выполнения, операций пошагового перехода и выхода. Это обеспечивает достаточно точное приближение к пошаговому интерфейсу для невстраиваемых функций.
Дополнительные сведения
Примечание Чтобы отключить отладку встроенных функций, можно использовать команду .inline 0 . Команда .inline 1 включает отладку встроенных функций. Стандартные методы отладки