wenmo8 发布的文章

我们希望在WinDbg中自动显示、搜索和过滤std::map对象。std::vectors的脚本相对简单,因为vectors中数据的平面结构;map是更复杂的野兽。
具体地说,Visual C++ STL中的映射是作为红黑树实现的。每个树节点都有三个重要的指针:左指针、右指针和父指针。此外,每个节点都有一个Myval字段,其中包含std::对以及节点表示的键和值。
迭代树结构需要递归,WinDbg脚本没有任何语法来定义函数。但是,我们可以递归地调用脚本——允许脚本包含$$>a<命令,该命令使用不同的参数集再次调用脚本。脚本的路径也可以在${$arg0}中找到。
当递归调用脚本时,伪寄存器的值(如$t0)将被递归调用破坏。当我偶然发现.push和.pop命令时,我正处于动态分配内存或调用shell进程来存储和加载变量的边缘,这两个命令分别存储和加载寄存器上下文。这些是递归WinDbg脚本必须的。
好,假设您想显示std::map<int,point>中键小于或等于2的值。

0:000> $$>a< traverse_map.script my_map -c ".block { .if (@@(@$t9.first) <= 2) { .echo —-; ?? @$t9.second } }"

size = 10
—-
struct point
   +0x000 x                : 0n1
   +0x004 y                : 0n2
   +0x008 data             : extra_data
—-
struct point
   +0x000 x                : 0n0
   +0x004 y                : 0n1
   +0x008 data             : extra_data
—-
struct point
   +0x000 x                : 0n2
   +0x004 y                : 0n3
   +0x008 data             : extra_data

对于每个pair(存储在$t9伪寄存器中),块检查第一个组件是否小于或等于2,如果小于或等于2,则输出第二个组件。
接下来是剧本。注意,它比我们对向量的处理要复杂得多,因为它本质上是用一组不同的参数调用自己,然后递归地重复。

.if ($sicmp("${$arg1}", "-n") == 0) {
    .if (@@(@$t0->_Isnil) == 0) {
        .if (@$t2 == 1) {
            .printf /D "<exec cmd=\"db %p L10\">%p</exec>\n", @$t0, @$t0
            .printf "key = "
            ?? @$t0->_Myval.first
            .printf "value = "
            ?? @$t0->_Myval.second
        } .else {
            r? $t9 = @$t0->_Myval
            command
        }
    }

    $$ Recurse into _Left, _Right unless they point to the root of the tree
    .if (@@(@$t0->_Left) != @@(@$t1)) {
        .push /r /q
        r? $t0 = @$t0->_Left
        $$>a< ${$arg0} -n
        .pop /r /q
    }
    .if (@@(@$t0->_Right) != @@(@$t1)) {
        .push /r /q
        r? $t0 = @$t0->_Right
        $$>a< ${$arg0} -n
        .pop /r /q
    }
} .else {
    r? $t0 = ${$arg1}

    .if (${/d:$arg2}) {
        .if ($sicmp("${$arg2}", "-c") == 0) {
            r $t2 = 0
            aS ${/v:command} "${$arg3}"
        }
    } .else {
        r $t2 = 1
        aS ${/v:command} " "
    }

    .printf "size = %d\n", @@(@$t0._Mysize) 
    
    r? $t0 = @$t0._Myhead->_Parent
    r? $t1 = @$t0->_Parent

    $$>a< ${$arg0} -n

    ad command
}

特别值得注意的是,as命令配置了一个别名,然后递归调用使用该别名为映射的每个元素调用一个命令块;比较字符串的$sicmp函数;以及输出DML块的.printf/D函数。最后,当_Left或_Right等于树的根时,递归终止(在本例中就是这样实现树的)。

从前,非托管代码开发人员必须非常努力地将代码偏移量与源文件名和行号关联起来。一种方法涉及为每个模块生成.cod文件(程序集列表),然后费力地将指令偏移量与.cod文件的内容进行比较。
例如,如果从具有客户机接收到错误BatteryMeter!TemperatureAndBatteryUpdaterThread+0xd0,可以返回BatteryMeter.exe的.cod文件,查找TemperatureAndBatteryUpdaterThread的代码列表,然后查找位于(或靠近)偏移量0xd0处的源行。
这个过程可以自动化。前几天有人问我是否还需要使用.cod文件,答案是否定的。如果你启动WinDbg,你只需“文件>打开转储文件”,输入你的.exe或.dll作为转储文件名,然后发出ln命令,如下所示:

0:000> ln BatteryMeter!TemperatureAndBatteryUpdaterThread+0xd0
d:\dev\batterymeter\batterymeterdlg.cpp(58)

如果您手头没有WinDbg又希望自动执行此操作(可能是通过脚本),可以使用DbgHelp API加载相应模块的符号,然后查找符号名称和源信息。所涉及的函数是SymLoadModule64、SymFromName和SymGetLineFromAddr64,生成的程序不超过100行代码:

 

DWORD displacement;
IMAGEHLP_LINE64 line;
RtlZeroMemory(&line, sizeof(line));
line.SizeOfStruct = sizeof(line);
if (!SymGetLineFromAddr64(hProcess, symbolAddress, &displacement, &line))
{
    printf("*** Error retrieving source line for %s: 0x%x\n",
        argv[1], GetLastError());
    return 1;
}
printf("%s [0x%I64x] = %s line %d (+0x%x)\n", argv[1], symbolAddress,
    line.FileName, line.LineNumber, displacement);

 

下面是应用程序崩溃转储的调用堆栈。报告的崩溃是名为“HelperLibrary”的模块内的访问冲突,我们没有该模块的符号或源代码。调用堆栈看起来不太可能:

0:000> kv
ChildEBP RetAddr  Args to Child             
WARNING: Stack unwind information not available. Following frames may be wrong.
0028fcec 74ba339a 7efde000 0028fd38 77479ed2
  HelperLibrary+0x1014
0028fcf8 77479ed2 7efde000 776a5346 00000000
  kernel32!BaseThreadInitThunk+0xe (FPO: [1,0,0])
0028fd38 77479ea5 011212b2 7efde000 00000000
  ntdll!__RtlUserThreadStart+0x70 (FPO: [SEH])
0028fd50 00000000 011212b2 7efde000 00000000
  ntdll!_RtlUserThreadStart+0x1b (FPO: [2,2,0])

除了HelperLibrary+0x1014之外,这里没有其他真正的帧,但是我们非常确定堆栈上应该还有其他代码,比如应用程序的主函数
要从这个堆栈重建某些内容,您需要了解谁调用了HelperLibrary+0x1014,即使您没有准确的符号。通常,这是一个遍历EBP引用的问题,但是如果它那么简单,调试器就已经做到了!

好吧,那么EBP怎么样了?

0:000> r ebp
ebp=0034fbfc
0:000> ln ebp
0:000> u ebp
0034fbfc 08fc            or      ah,bh
0034fbfe 3400            xor     al,0
0034fc00 9a33ba7400e0fd  call    FDE0:0074BA33
0034fc07 7e48            jle     0034fc51
0034fc09 fc              cld
0034fc0a 3400            xor     al,0
0034fc0c d29e477700e0    rcr     byte ptr [
  esi-1FFF88B9h],cl
0034fc12 fd              std

如果你没有注意到,这不是实际的代码,而是一堆被解释为指令的数据。EBP完全有可能被损坏,指向完全不相关的位置,但也有另一种可能:当前代码正在使用FPO。
什么是FPO?FPO(帧指针省略)是一种优化技术,而编译器使用EBP寄存器作为暂存值来存储杂项数据,就像任何其他通用寄存器一样。如何处理局部变量和函数参数?直接通过ESP。
换言之,当使用FPO(您可以使用/Oy编译开关启用它)时,编译器可以自由地避免使用以前的EBP值创建“真实”堆栈帧。没有从当前EBP值开始的堆栈帧的链接列表。如果没有FPO信息(出现在符号文件中,而我们没有这些信息),调试器将无法执行任何操作。
这使得我们需要反汇编HelperLibrary+0x1014并尝试手动找出它返回的位置。让我们看看HelperLibrary+0x1014(粗体的违规指令)附近:

0:000> u HelperLibrary+0x1014-0x14 L8
HelperLibrary+0x1000:
66951000 56              push    esi
66951001 ff157c209566    call    dword ptr [6695207c]
66951007 50              push    eax
66951008 c60061          mov     byte ptr [eax],61h
6695100b ff1578209566    call    dword ptr [66952078]
66951011 83c408          add     esp,8
66951014 c60661          mov     byte ptr [esi],61h
66951017 c3              ret

这看起来,确实,像一个FPO的栈帧-没有EBP被看到。但是,随后的ret指令会转到某个地方,因此我们可以查看ESP并在那里找到返回地址:

0:000> dps esp L1
0034fb9c  66951040 HelperLibrary+0x1040

好的,那么HelperLibrary+0x1040是什么?

0:000> u HelperLibrary+0x1040-0x20 LC
HelperLibrary+0x1020:
66951020 56              push    esi
66951021 8bf0            mov     esi,eax
66951023 56              push    esi
66951024 ff157c209566    call    dword ptr [6695207c]
6695102a 50              push    eax
6695102b c60061          mov     byte ptr [eax],61h
6695102e ff1578209566    call    dword ptr [66952078]
66951034 03742410        add     esi,dword ptr [esp+10h]
66951038 83c408          add     esp,8
6695103b e8c0ffffff      call    HelperLibrary+0x1000
66951040 5e              pop     esi
66951041 c3              ret

有点意思。此帧也不使用EBP,因此我们可以预期返回值为ESP+4(因为返回之前的pop指令)。但在这个函数中,我们如何计算ESP的值呢?好吧,假设上一个函数返回。它已经从堆栈中删除了四个字节(返回地址)。ESP的下一个值是ESP+4,我们需要再添加4个字节来解释“pop esi”指令。

0:000> dps esp+8 L1
0034fba4  6695106b HelperLibrary!ImportantFunction+0x1b

好吧!我们正在取得一些真正的进展,我们有一个非常小的偏移量,即使没有符号,ImportantFunction可能是一个导出函数,因此我们在DLL中有它的位置:

0:000> u HelperLibrary!ImportantFunction LD
HelperLibrary!ImportantFunction:
66951050 8b442404        mov     eax,dword ptr [esp+4]
66951054 85c0            test    eax,eax
66951056 7501            jne     HelperLibrary!ImportantFunction+0x9
66951058 c3              ret
66951059 837c240c00      cmp     dword ptr [esp+0Ch],0
6695105e 7e0e            jle     HelperLibrary!ImportantFunction+0x1e
66951060 8b4c2408        mov     ecx,dword ptr [esp+8]
66951064 49              dec     ecx
66951065 51              push    ecx
66951066 e8b5ffffff      call    HelperLibrary+0x1020
6695106b 83c404          add     esp,4
6695106e b801000000      mov     eax,1
66951073 c3              ret

这是另一个没有EBP使用痕迹的函数。注意,它有参数,并且它使用来自ESP的直接偏移量来访问这些参数,ESP是FPO的信号。这个函数返回到哪里?好吧,在上一个函数返回后,ESP已经在当前值的ESP+0xC处。ImportantFunction再添加四个字节,然后返回,因此我们需要查看ESP+0x10:

0:000> dps esp+0x10 L1
0034fbac  00ed100f MainApp!wmain+0xf

对!我们离开了动态链接库,回到符号区!因此,重建的堆栈如下所示:

HelperLibrary!…somefunction…
HelperLibrary!…someotherfunction…
HelperLibrary!ImportantFunction
MainApp!wmain

这些都不是调试器提供的。作为参考,这里是相同的调用堆栈与符号(其中包含FPO信息)

 

0:000> kv
ChildEBP RetAddr  Args to Child             
003dfee4 668e1040 00000001 668e106b 0000000f
 
HelperLibrary!AnotherHelperFunction+0x14 (FPO: [0,0,0])
003dfeec 668e106b 0000000f 010c100f 010c20f4
  HelperLibrary!HelperFunction+0x20 (FPO: [1,0,4])
003dfef4 010c100f 010c20f4 00000010 00000001
  HelperLibrary!ImportantFunction+0x1b (FPO: [3,0,0])
003dff04 010c1191 00000001 00771b78 00771c10
  MainApp!wmain+0xf (FPO: [2,0,0])
003dff48 74ba339a 7efde000 003dff94 77479ed2
  MainApp!__tmainCRTStartup+0x122 (FPO: [Non-Fpo])
003dff54 77479ed2 7efde000 777db3e9 00000000
  kernel32!BaseThreadInitThunk+0xe (FPO: [1,0,0])
003dff94 77479ea5 010c12b2 7efde000 00000000
  ntdll!__RtlUserThreadStart+0x70 (FPO: [SEH])
003dffac 00000000 010c12b2 7efde000 00000000
  ntdll!_RtlUserThreadStart+0x1b (FPO: [2,2,0])

简介

这个!uniqstack扩展扩展显示的所有线程的堆栈的所有当前进程,不包括显示为具有重复项的堆栈中。

使用形式

!uniqstack [ -b | -v | -p ] [ -n ]

参数

  • -b
    将导致显示以包括前三个参数传递给每个函数。
  • -v
    将导致显示以包括帧指针省略 (FPO) 信息。 在基于 x86 的处理器中,还会显示的调用约定信息。
  • -p
    将导致显示堆栈跟踪中包含每个函数的完整参数。 此列表将包括每个参数的数据类型、 名称和值。 这要求的完整符号信息。
  • -n
    导致要显示的帧号码。

环境支持

Windows 2000

Uext.dll

Windows XP 及更高版本

Uext.dll

说明

此扩展是类似于 k、 kb、 kc、 kd、 kp、 kP,kv (显示堆栈回溯) 命令,只不过它不会显示重复的堆栈。

这个!uniqstack命令枚举了所有的线程调用堆栈并消除了重复,这样您就可以一眼就知道这几百个线程在做什么。

0:021> !uniqstack
Processing 22 threads, please wait

.  0  Id: 464.ca8 Suspend: 1 Teb: 7f9dd000 Unfrozen
      Start: SillyThreadPool!ILT+120(_wmainCRTStartup) (00ed107d)
      Priority: 0  Priority class: 32  Affinity: ff
ChildEBP RetAddr 
00a6f510 76e0cfb2 ntdll!NtReadFile+0xc
00a6f578 5285d9de KERNELBASE!ReadFile+0x10e
00a6f62c 5285d19c MSVCR110D!_read_nolock+0x7be
00a6f684 527a4246 MSVCR110D!_read+0x24c
00a6f6b4 527a26b3 MSVCR110D!_filbuf+0x126
00a6f714 527a2708 MSVCR110D!getc+0x223
00a6f720 527a2718 MSVCR110D!_fgetchar+0x18
00a6f728 00ed14ca MSVCR110D!getchar+0x8
00a6f808 00ed1a49 SillyThreadPool!wmain+0x7a
00a6f858 00ed1c3d SillyThreadPool!__tmainCRTStartup+0x199
00a6f860 7755850d SillyThreadPool!wmainCRTStartup+0xd
00a6f86c 77d1bf39 KERNEL32!BaseThreadInitThunk+0xe
00a6f8b0 77d1bf0c ntdll!__RtlUserThreadStart+0x72
00a6f8c8 00000000 ntdll!_RtlUserThreadStart+0x1b

.  1  Id: 464.13d4 Suspend: 1 Teb: 7f9da000 Unfrozen
      Start: SillyThreadPool!ILT+265(?MyThreadPoolWorkerYGKPAXZ) (00ed110e)
      Priority: 0  Priority class: 32  Affinity: ff
ChildEBP RetAddr 
00d3f7b8 76e01129 ntdll!NtWaitForSingleObject+0xc
00d3f824 76e010b4 KERNELBASE!WaitForSingleObjectEx+0x8f
00d3f838 00ed141e KERNELBASE!WaitForSingleObject+0x12
00d3f914 7755850d SillyThreadPool!MyThreadPoolWorker+0x2e
00d3f920 77d1bf39 KERNEL32!BaseThreadInitThunk+0xe
00d3f964 77d1bf0c ntdll!__RtlUserThreadStart+0x72
00d3f97c 00000000 ntdll!_RtlUserThreadStart+0x1b

. 21  Id: 464.1574 Suspend: 1 Teb: 7f879000 Unfrozen
      Start: ntdll!DbgUiRemoteBreakin (77d5dbeb)
      Priority: 0  Priority class: 32  Affinity: ff
ChildEBP RetAddr 
0279faa4 77d5dc24 ntdll!DbgBreakPoint
0279fad4 7755850d ntdll!DbgUiRemoteBreakin+0x39
0279fae0 77d1bf39 KERNEL32!BaseThreadInitThunk+0xe
0279fb24 77d1bf0c ntdll!__RtlUserThreadStart+0x72
0279fb3c 00000000 ntdll!_RtlUserThreadStart+0x1b

Total threads: 22
Duplicate callstacks: 19 (windbg thread #s follow):
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20

简介

!findstack扩展查找所有包含指定的符号或模块的堆栈。此命令搜索线程调用堆栈中的特定符号,并显示匹配的线程。

使用形式

!findstackSymbol[DisplayLevel]

!findstack -?

参数

  • Symbol

指定符号或模块。

  • DisplayLevel

指定显示内容。这可以是以下任何一个值。默认值为1

0
显示仅包含每个线程的线程 ID符号

1
显示线程 ID 和包含每个线程的帧符号

2
将显示包含每个线程的整个线程堆栈符号

  • -?

在调试器命令窗口中显示此扩展的一些简要帮助文本。

说明

内核模式此扩展还显示有关堆栈,包括每个线程的状态的简短摘要信息。

此扩展的输出的一些示例如下:

0:023> !uext.findstack wininet
Thread 009, 2 frame(s) match
        * 06 03eaffac 771d9263 wininet!ICAsyncThread::SelectThread+0x22a
        * 07 03eaffb4 7c80b50b wininet!ICAsyncThread::SelectThreadWrapper+0xd

Thread 011, 2 frame(s) match
        * 04 03f6ffb0 771cda1d wininet!AUTO_PROXY_DLLS::DoThreadProcessing+0xa1
        * 05 03f6ffb4 7c80b50b wininet!AutoProxyThreadFunc+0xb

Thread 020, 6 frame(s) match
        * 18 090dfde8 771db73a wininet!CheckForNoNetOverride+0x9c
        * 19 090dfe18 771c5e4d wininet!InternetAutodialIfNotLocalHost+0x220
        * 20 090dfe8c 771c5d6a wininet!ParseUrlForHttp_Fsm+0x135
        * 21 090dfe98 771bcb2c wininet!CFsm_ParseUrlForHttp::RunSM+0x2b
        * 22 090dfeb0 771d734a wininet!CFsm::Run+0x39
        * 23 090dfee0 77f6ad84 wininet!CFsm::RunWorkItem+0x79

Thread 023, 9 frame(s) match
        * 16 0bd4fe00 771bd256 wininet!ICSocket::Connect_Start+0x17e
        * 17 0bd4fe0c 771bcb2c wininet!CFsm_SocketConnect::RunSM+0x42
        * 18 0bd4fe24 771bcada wininet!CFsm::Run+0x39
        * 19 0bd4fe3c 771bd22b wininet!DoFsm+0x25
        * 20 0bd4fe4c 771bd706 wininet!ICSocket::Connect+0x32
        * 21 0bd4fe8c 771bd4cb wininet!HTTP_REQUEST_HANDLE_OBJECT::OpenConnection_Fsm+0x391
        * 22 0bd4fe98 771bcb2c wininet!CFsm_OpenConnection::RunSM+0x33
        * 23 0bd4feb0 771d734a wininet!CFsm::Run+0x39
        * 24 0bd4fee0 77f6ad84 wininet!CFsm::RunWorkItem+0x79

0:023> !uext.findstack wininet!CFsm::Run 0
Thread 020, 2 frame(s) match
Thread 023, 3 frame(s) match

0:023> !uext.findstack wininet!CFsm 0
Thread 020, 3 frame(s) match
Thread 023, 5 frame(s) match