我使用 DbgEng 为 WinDbg 创建了一个扩展,它记录了包含用户模式和内核模式下每条执行指令的寄存器和堆栈状态的跟踪。
对于步进跟踪,我使用断点(我还尝试将 TRAP 标志设置为记录跟踪的线程上下文,但它给出了相同的结果)。在每一步之后并遇到断点 IDebugEventCallbacks::Breakpoint() 被调用,它返回 DEBUG_STATUS_BREAK。这会导致调试器在引擎循环中完成 IDebugControl::WaitForEvent() 函数,并记录当前指令的状态。
跟踪记录在多核系统(4 个物理核、8 个虚拟核)上的测试多线程应用程序中。测试应用程序在主线程之外创建了 6 个线程。4个线程异步记录同一个函数func0的trace。其他 3 个创建的线程模拟计算。
测试程序代码:
void func3();
void func2();
void func1();
DWORD WINAPI func0(LPVOID lpArg);
DWORD WINAPI LoopProcessing(LPVOID lpArg);
void func3()
{
int a = 1;
a += 2;
a *= 60;
a += 31;
printf(u8"func3\n");
a /= 2;
a *= 777;
a -= 31;
}
void func2()
{
int a = 1;
a += 5;
a *= 41;
a += 55;
printf(u8"func2\n");
a /= 3;
a *= 414;
a -= 41;
}
void func1()
{
int a = 1;
a += 95;
a *= 14;
a += 72;
printf(u8"func1\n");
a /= 1;
a *= 42;
a -= 37;
}
DWORD WINAPI func0(LPVOID lpArg)
{
func1();
func2();
func3();
return 0;
}
DWORD WINAPI LoopProcessing(LPVOID lpArg)
{
int a = 1,
b = 2,
c = 3;
b = 2 * c + 1;
c = a + b * c;
a = b * c + a;
SwitchToThread();
return 0;
}
int main(int argc, char* argv[])
{
const size_t szThreadsCount = 6;
HANDLE hThreads[szThreadsCount];
for (int iIndexIteration = 0; iIndexIteration < 15; iIndexIteration++)
{
printf("====================== Iteration %i start ======================\n", iIndexIteration);
hThreads[0] = CreateThread(NULL, NULL, func0, NULL, NULL, NULL);
hThreads[1] = CreateThread(NULL, NULL, LoopProcessing, NULL, NULL, NULL);
hThreads[2] = CreateThread(NULL, NULL, func0, NULL, NULL, NULL);
hThreads[3] = CreateThread(NULL, NULL, LoopProcessing, NULL, NULL, NULL);
hThreads[4] = CreateThread(NULL, NULL, func0, NULL, NULL, NULL);
hThreads[5] = CreateThread(NULL, NULL, LoopProcessing, NULL, NULL, NULL);
for (int iIndex = 0; iIndex < 3; iIndex++)
{
func0(0);
}
WaitForMultipleObjects(szThreadsCount, hThreads, TRUE, INFINITE);
for (int iIndex = 0; iIndex < szThreadsCount; iIndex++)
{
CloseHandle(hThreads[iIndex]);
hThreads[iIndex] = nullptr;
}
}
return 0;
}
在大多数情况下,一切都按预期工作,我得到了一个完整记录的跟踪,如下所示:
…
036|00007ff6`ad314215 call Saek0!ILT+550(__CheckForDebuggerJustMyCode) (00007ff6`ad30122b)
Registers:
rax: 0x00000000CCCCCCCC rbx: 0x0000000000000000 rcx: 0x00007FF6AD33208C
rdx: 0x00007FF6AD30155F rsi: 0x0000000000000000 rdi: 0x0000000000A2FE18
rip: 0x00007FF6AD314215 rsp: 0x0000000000A2FD10 rbp: 0x0000000000A2FD30
r8: 0x0000000000000001 r9: 0x00007FF6AD30155F r10: 0x0000000000000000
r11: 0x0000000000000000 r12: 0x0000000000000000 r13: 0x0000000000000000
r14: 0x0000000000000000 r15: 0x0000000000000000
EFlags: 0xCCCCCCCC00000302
037|00007ff6`ad31421a mov dword ptr [rbp+4],1 ss:00000000`00000004=????????
Registers:
rax: 0x0000000000000001 rbx: 0x0000000000000000 rcx: 0x00007FF6AD33208C
rdx: 0x00007FF6AD30155F rsi: 0x0000000000000000 rdi: 0x0000000000A2FE18
rip: 0x00007FF6AD31421A rsp: 0x0000000000A2FD10 rbp: 0x0000000000A2FD30
r8: 0x0000000000000001 r9: 0x00007FF6AD30155F r10: 0x0000000000000000
r11: 0x0000000000000000 r12: 0x0000000000000000 r13: 0x0000000000000000
r14: 0x0000000000000000 r15: 0x0000000000000000
EFlags: 0xCCCCCCCC00000202
038|00007ff6`ad314221 mov eax,dword ptr [rbp+4] ss:00000000`00000004=????????
Registers:
rax: 0x0000000000000001 rbx: 0x0000000000000000 rcx: 0x00007FF6AD33208C
rdx: 0x00007FF6AD30155F rsi: 0x0000000000000000 rdi: 0x0000000000A2FE18
rip: 0x00007FF6AD314221 rsp: 0x0000000000A2FD10 rbp: 0x0000000000A2FD30
r8: 0x0000000000000001 r9: 0x00007FF6AD30155F r10: 0x0000000000000000
r11: 0x0000000000000000 r12: 0x0000000000000000 r13: 0x0000000000000000
r14: 0x0000000000000000 r15: 0x0000000000000000
EFlags: 0xCCCCCCCC00000302
…
但有时调试器会跳过步骤并且不会触发断点。
例如:
00007ff6`ad314215 call Saek0!ILT+550(__CheckForDebuggerJustMyCode) (00007ff6`ad30122b)
00007ff6`ad31421a mov dword ptr [rbp+4],1
00007ff6`ad314221 mov eax,dword ptr [rbp+4]
假设已记录状态rip = 00007ff6'ad314215,并在以下地址00007ff6'ad31421a处设置断点。之后,我继续执行程序并期望触发地址 00007ff6'ad31421a 处的断点。但是,我当前的 rip = 00007ff6'ad314221 并没有预期在 00007ff6'ad31421a 处到达断点并调用 IDebugEventCallbacks::Breakpoint(),而是使用以下参数调用 IDebugEventCallbacks::Exception():
Exception code: (0x80000003) == EXCEPTION_BREAKPOINT
Exception address: 0x7FF6AD314221 - address of the next instruction after the breakpoint.
threadContext.Rip: 0x7FF6AD314221 - instruction address of current thread context.
因此系统已经执行了 00007ff6'ad31421a 处的指令,但由于某种原因,调试器没有命中设置的断点,并在未设置任何断点的下一条指令(00007ff6`ad314221)上引发异常。
我还尝试在 IDebugEventCallbacks::ChangeEngineState() 中检查程序计数器 (threadContext.rip) 以查找正在记录其执行的线程,但我从来没有得到一个计数器等于被跳过的指令的地址 (00007ff6'ad31421a) .
因此,我的跟踪中没有地址 00007ff6'ad31421a 的状态:
…
036|00007ff6`ad314215 call Saek0!ILT+550(__CheckForDebuggerJustMyCode) (00007ff6`ad30122b)
Registers:
rax: 0x00000000CCCCCCCC rbx: 0x0000000000000000 rcx: 0x00007FF6AD33208C
rdx: 0x00007FF6AD30155F rsi: 0x0000000000000000 rdi: 0x0000000000A2FE18
rip: 0x00007FF6AD314215 rsp: 0x0000000000A2FD10 rbp: 0x0000000000A2FD30
r8: 0x0000000000000001 r9: 0x00007FF6AD30155F r10: 0x0000000000000000
r11: 0x0000000000000000 r12: 0x0000000000000000 r13: 0x0000000000000000
r14: 0x0000000000000000 r15: 0x0000000000000000
EFlags: 0xCCCCCCCC00000302
037|00007ff6`ad314221 mov eax,dword ptr [rbp+4] ss:00000000`00000004=????????
Registers:
rax: 0x0000000000000001 rbx: 0x0000000000000000 rcx: 0x00007FF6AD33208C
rdx: 0x00007FF6AD30155F rsi: 0x0000000000000000 rdi: 0x0000000000A2FE18
rip: 0x00007FF6AD314221 rsp: 0x0000000000A2FD10 rbp: 0x0000000000A2FD30
r8: 0x0000000000000001 r9: 0x00007FF6AD30155F r10: 0x0000000000000000
r11: 0x0000000000000000 r12: 0x0000000000000000 r13: 0x0000000000000000
r14: 0x0000000000000000 r15: 0x0000000000000000
EFlags: 0xCCCCCCCC00000302
…
问题:
- 这种行为的原因是什么?
- 我遇到了这样的观点,即这种行为可能是由在多核系统上执行引起的,但是这个判断的作者没有提到任何有理由证明这一点的来源。如果确实是这种情况,有人可以提供有关基本原理的链接吗?
- 有什么解决问题的建议吗?
- 是否有工具可以让您在 Windows 中记录与用户模式和内核模式类似的状态跟踪?