调试优化的代码和内联函数
对于Windows 8,调试器和 Windows 编译器已得到增强,以便可以调试优化的代码和调试内联函数。 调试器显示参数和局部变量,无论它们是存储在寄存器中还是存储在堆栈中。 调试器还会在调用堆栈中显示内联函数。 对于内联函数,调试器显示局部变量,但不显示参数。
优化代码后,将转换代码以更快地运行并使用更少的内存。 有时,由于删除死代码、合并代码或将函数置于内联中,函数会被删除。 也可以删除局部变量和参数。 许多代码优化会删除不需要或使用的局部变量;其他优化删除循环中的感应变量。 公共子表达式消除将局部变量合并在一起。
Windows 的零售版本已优化。 因此,如果你运行的是 Windows 的零售版本,则拥有一个旨在与优化代码很好地配合使用的调试器尤其有用。 若要使优化代码的调试有效,需要两个主要功能:1) 本地变量的准确显示,2) 调用堆栈上内联函数的显示。
准确显示局部变量和参数
为了便于准确显示局部变量和参数,编译器将有关局部变量和参数的位置的信息记录在符号 (PDB) 文件中。 这些位置记录跟踪变量的存储位置和这些位置有效的特定代码范围。 这些记录不仅有助于跟踪变量) 寄存器或堆栈槽中 (的位置,还有助于跟踪变量的移动。 例如,参数可能首先位于 register 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 命令示例。 还可以操作单个断点。 请参阅下面的 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 命令启用内联函数调试。 标准调试方法