Dump Analyze 101


DUMP 分析基础知识

本篇文章主要包含分析 DUMP 时我们需要掌握的基础知识以及常见场景下的 DUMP 分析方法。

调试符号 / Symbol

调试符号 / Symbol 是编译器编译时生成的调试信息数据库。通常在 DUMP 分析的最早执行的命令就是设置调试符号路径。调试符号一般分成私有调试符 (Private Symbol) 和公有调试符 (Public Symbol) 。

  • Private Symbol

    一般而言 Private Symbol 包含了方法名地址映射、数据类型和结构定义、变量的名称类型地址及作用域(包含本地变量和全局变量)、每个指令对应的源代码行数。

  • Public Symbol

    Public Symbol 一般包含非静态方法名地址映射、标记为 extern 的全局变量名(通常不包含变量类型)。

    VS 默认会生成全量的符号文件 (Full Symbol Files),我们通常称之为 “Private Symbol”。 但实际 Full Symbols Files 中包含了 Private Symbol 和 Public Symbol。 我们可以通过 PDBCopy 工具从 Full Symbol Files 中移除 Private Symbol,生成 Public Symbol。

在大部分场景下使用 Public Symbol 就可以分析 DUMP。以下是几个常用的 Public Symbol Server 地址:

在使用 Windbg 时常用的与 Symbol 相关的命令如下:

  • 默认设置 C:\symbols 为 Symbols 缓存

    .symfix C:\symbols
    
  • 添加 symbol Server 或者缓存

    /*添加本地路径 D:\project\debug\*/
    .sympath+ D:\project\debug\
    
    /* 添加 UNC Symbol Cache 路径 \\SymCache\Symbols */
    .sympath+ \\SymCache\symbols
    
    /* 添加 Chromium Symbol Server 并缓存至 C:\symbols */
    .sympath+ C:\symbols*https://www.chromium.org/developers/how-tos/debugging-on-windows
    
  • 如果加载 Symbol 失败,可以开启调试模式,在加载 Symbol 时会输出相应的日志:

/* 开启调试模式 */
!sym noisy
或者
.symopt+0x80000000

/*关闭调试模式*/
!sym quiet
或者
.symopt-0x80000000

对于经常会使用的 Symbol Server 或者缓存路径,可以通过设置环境变量 _NT_SYMBOL_PATH 来指定,这样启动调试器时会自动加载对应的 Symbol 路径。

引用文档

Symbol path for Windows debuggers

Public Symbols and Private Symbols

常用 Windbg 命令

常用的 Windbg 内置命令可以参考 Common Windbg Commands

Windbg Extension

Windbg Extension 是 Windbg 的扩展插件,用户可以根据自己的需求来实现相关功能,简化调试过程。根据实现方式不同可以分成以下

如何使用 Windbg Extension

  • 配置 Extension 路径
    • 配置环境变量 _NT_DEBUGGER_EXTENSION_PATH
    • 使用.extpath+ 命令
  • 加载 Extension

    • 如果插件位于默认的路径或者配置的插件路径,则可以直接使用 !ExtensionName.Command 的方式自动加载;

    • 如果插件为与其他路径,则可以使用如下方式加载:

      .load path\ExtensionName.dll
      
  • 查看插件顺序

    .chain
    
  • 查看命令匹配的插件,如果多个插件实现了相同的命令,则可以通过该方法查看其匹配的命令,如下例,mex 和 exts 均实现了 gflag 命令,当前匹配的顺序则按照插件的排列顺序:

    0:019> .extmatch gflag
    !mex.gflag
    !exts.gflag
    

常用的 Windbg Extension

  • Mex 微软出品的插件,包含了很多非常有用的功能;
  • SOS Dotnet/Dotnet Core 内置的调试插件,功能强大;
  • Netext 开源的 .NET 调试插件,Rodney Viana 开发,目前就职于微软;
  • SOSEX Steve Johnson开发的 .NET 调试插件,是对 SOS 的很强大的补充,在他的博客 STEVE’S TECHSPOT中有详细的使用介绍。
  • pykd PYKD 为 windbg 提供了 python 接口,可以使用 python 开发自动化分析脚本;
  • psscor2/psscor4 现已废弃
  • 内置的Extensions:
    • JSprovider: 为 Windbg 提供了 js 接口,方便使用 js 脚本开发自动化分析脚本;
    • TTDExt: 用于 WindbgX 中新增功能 TTD 分析;
    • exts/uext/ntsdexts/kdexts/ext/dbghelp/Kernel Mode/User Mode/Spechialized Extension 该部分可以参考 Gerneral Extensions

汇编指令 / Assymbly Instructions

在分析 Native 应用时,如果没有源码,我们通常都需要接触汇编指令,因此我们需要熟悉常见的汇编指令。

常见的汇编优化指令

当前Windows 10 支持 ARM64/AArch64, 参考 Windows 10 on ARM。在调试 ARM64 的应用时使用 ARM64 版本的 Windbg,而调试 ARM64 处理器上运行的 X86 应用,则需要使用 X86 版本的 Windbg。指令集也需要跟随应用切换。

.effmach x86: Switch to and see x86 context, simulating the effect of using x86 WinDbg.

.effmach arm64: Switch to and see ARM64 context

.effmach chpe: Switch to and see CHPE context.

调用约定 / Calling Convention

在调试 Native 应用时,如果没有 Private Symbol, 则我们需要依赖调用约定来获取参数值,因此我们需要熟悉不同处理器下的调用约定。

  • X86 的调用约定可以参考 X86 Calling Convention;

    函数必须保存除 eax, ecx, and edxesp 以外的所有寄存器, 其中 eax, ecx, and edx 可以在函数间改变, esp 则需要根据调用约定来更新;

    eax 用于保存函数返回值,如果函数返回值为 64 位,则使用 edx:eax 来保存。

    • Win32 (__stdcall)

      函数参数通过栈来传递,压栈顺序为从右往左,由函数体( callee)清栈

    • Native C++ Call (thiscall)

      函数参数通过栈来传递,压栈顺序为从右往左, this 指针通过 ecx 寄存器传递,由函数体( callee)清栈

    • COM (__stdcall for C++)

      函数参数通过栈来传递,压栈顺序为从右往左, this 指针通过 ecx 寄存器传递,由函数体( callee)清栈

    • __fastcall

      前两个 DWORD 型参数通过 ECX 和EDX 传递,剩下的参数通过栈传递,压栈顺序为从右往左,由函数体( callee)清栈

    • __cdecl

      函数参数通过栈来传递,压栈顺序为从右往左,由函数调用方( caller)清栈。函数定义中包含可变长度的参数,均为 __cdecl 函数调用

  • X64 的调用约定可以参考 X64 Calling Convention;

    X64 调用约定相对简单,其调用过程中通过 rcx,rdx,r8,r9 传递前四个整形参数或者指针,如果前四个参数中包含浮点数,则使用对应的 xmm0-xmm3替代即可。其余参数均通过栈来传递。整型数或者整型指针返回值保存在 rax 中,如果是浮点数,则保存在 xmm0 中。对于含有 this 指针的,则传递方法和第一个参数相同,即 rcx 或者 xmm0

  • ARM 的调用约定可以参考 Overview of ARM32 ABI conventions;
  • ARM64 的调用约定可以参考 Overview of ARM64 ABI conventions;

函数的序言和尾声/Prologue and Epilogue

在函数调用中,我们通常需要在起始处分配栈空间,保存非易失性寄存器,或者使用异常处理等;在退出函数前,则需要释放分配的栈空间,从栈空间弹出保存的非易失性寄存器。

对于 x86 和 x64 常见的序言和尾声参考如下:

X86 函数的序言和尾声示例
    /* Prologue */
    push ebp       //保存基地址
    mov ebp, esp   //将当前栈地址保存到基地址寄存器
    sub esp, N     //为本地变量和临时变量保留栈空间
    push edi       //保存 edi
    push esi       //保存 esi
    push ebx       //保存ebx,如果函数体中使用到的非易失性寄存器,也需要依次保存
    ...
    /*Epilogue*/
    pop ebx        //还原 ebx,如果函数体中使用到的非易失性寄存器,则还原顺序与序言中顺序相反
    pop esi        //还原 esi
    pop edi        //还原 edi
    add esp,N      //清空本地变量和临时变量的保留栈,通常不需要,会被优化
    mov esp, ebp   //还原栈顶指针,该指令会释放本地变量和临时变量保留栈空间
    pop ebp        //还原基地址
    ret

X64 函数的序言和尾声示例
    /* Prologue */
    mov    [RSP + 8], RCX
    push   R15     //保存 R15
    push   R14     //保存 R14
    push   R13     //保存 R13
    ...            //保存其他函数体中使用到的非易失性寄存器
    mov    RAX,  fixed-allocation-size
    call   __chkstk  //检查栈溢出
    sub    RSP, RAX  //为本地变量和临时变量保留栈空间
    lea    R13, 128[RSP]

    ......

    /*Epilogue*/
    lea      RSP, -128[R13]
    ; epilogue proper starts here
    add      RSP, fixed-allocation-size  //清空本地变量和临时变量的保留栈空间
    ...            //依次还原函数体中使用到的非易失性寄存器,与序言中顺序相反
    pop      R13   //还原 R13
    pop      R14   //还原 R14
    pop      R15   //还原 R15
    ret

文档信息

Document Information

Search

    LuyaoWechat

    路遥之家

    Table of Contents