65.9K
CodeProject 正在变化。 阅读更多。
Home

Bug Finder,一个真正的 Win32 可扩展的被动调试器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (25投票s)

2013年6月6日

GPL3

6分钟阅读

viewsIcon

77166

downloadIcon

984

Win32、与编译器无关且可扩展的被动调试器

引言

这是我的第一篇文章,基于我托管在 平台上的 Bug Finder 开源项目。您可以从 项目主页 获取。

背景

这个项目诞生于几年前,当时我在生产环境中遇到一个致命错误,可惜在我的开发机上无法重现。

我花费了大量时间寻找解决方案,包括使用免费和商业的第三方工具和库,但都没有帮助到我,因为那个错误导致进程崩溃,而上述工具都无法在进程终止前捕获异常。

而且,当时无法在生产机器上安装开发环境,所以唯一的解决方案是开发一个不需要任何用户/开发者交互的调试器。

我使用了许多开源资源,然后决定将 Bug Finder 作为一个开源项目。

它构建在可插拔的架构之上,以支持除 Borland Delphi(我用来开发我的故障应用程序和 Bug Finder 的语言)之外的其他语言。

项目结构

Bug Finder 是一个基于组件堆栈构建的工具,如下图所示的框图。

现在我们将逐一 examining 它们

  • 核心应用程序:这是实现整个调试逻辑的调试器核心模块。
  • 异常提供程序:对于使用特定编译器(如 Borland Delphi)编译的可执行文件,需要处理每个堆栈帧指针的地址以获取异常名称、描述和有效虚拟地址等信息。
    • Delphi 异常提供程序:这是我唯一需要实现的。
  • 调试符号提供程序:每个编译器都有自己的调试符号格式,因此在这种情况下,您也需要一个自定义的解释器插件。

特点

Bug Finder 是一个真正的 Win32 调试器,完全用 Borland Delphi 编写,它分析您的应用程序执行流程,因此您可以

  1. 在主可执行文件、外部 DLL、主线程和工作线程上捕获异常。
  2. 为每个异常生成详细的堆栈跟踪。
  3. 设置符号断点,以便在程序调试断点出现时,获取完整的堆栈跟踪日志消息(动态跟踪)。
  4. 生成详细且可轮换的日志文件,用于检查批处理应用程序的行为。
  5. OutputDebugString API 的输出捕获到日志文件中(以便您自己直接在代码中提供额外的调试信息)。
  6. 跟踪进程、线程和 DLL 的活动。
Important! Bug Finder is tool written by Delphi language, 
  therefore it is native Win32 application, for this reason it can debug any other Win32 application 
  produced by other compilers. You have only to implement 
  a new symbols provider if not yet provided by the BF.  

配置参数

要配置 Bug Finder,您可以选择两种方法

  1. 使用配置向导实用程序
  2. 自己提供一个配置文件,作为参数传递给 Bug Finder 的主可执行文件

配置向导非常直观,所以我只向您解释存储在每个 INI 配置文件中的配置参数。

节:配置

参数 必需 类型 描述

AppFileName

字符串

应用程序可执行文件的完整路径

AppParameters 字符串 应用程序命令行参数

节:符号提供程序

在此处为每种支持的调试符号类型添加一个条目,格式如下

<Descriptive name> = <DLL file name>

节:异常提供程序

在此处为每种支持的异常提供程序添加一个条目,格式如下

<Descriptive name> = <DLL file name> 

节:日志记录

参数 必需 类型 默认值 描述
LogViewLinesLimit 整数 1000

日志窗口中显示的日志行数限制,以优化内存使用。

LogFileName 字符串 BugFinder.log 日志文件的完整路径。
SpoolToFile 整数 1 启用/禁用日志记录功能。
LogFileRotation 整数 0

设置日志轮换策略

  • 0:每日
  • 1:每周
  • 2:每月
SuppressBreakpointSourceDetails 整数 0 启用(0)/禁用(1)跟踪断点源代码调试符号的日志记录。
SuppressDllEvents 整数 0 启用(0)/禁用(1)DLL 加载/卸载事件的日志记录。
SuppressOutputDebugStringEvents 整数 0 启用(0)/禁用(1)OutputDebugString API 调用事件的日志记录。
SuppressProcessEvents 整数 0 启用(0)/禁用(1)调试进程创建/终止事件的日志记录。
SuppressThreadEvents 整数 0 启用(0)/禁用(1)线程创建/终止事件的日志记录。
StackDepth 整数 3 捕获异常时设置堆栈跟踪深度
PopUpOnErrors 整数 1 发生异常时启用(1)/禁用(0)日志窗口的自动弹出。

节:断点

在此处为要跟踪的每个方法调用添加一个条目,遵循以下语法

<Descriptive name> = <"Binary module", "Source module", "Method names"> 

以上三个参数可以通过您的源代码或符号调试表获得。

为 Bug Finding 准备您的应用程序

使用 Bug Finder 查找 bug 绝对是一项简单的任务,如下所述

  • 如果需要,可以在代码中添加对 OputDebugString 函数的调用。
  • 使用您首选的(受支持的)符号表编译模块。
  • 使用以下基本选项构建配置文件
    • 配置AppFileName = { 您的应用程序的完整路径 }。
    • 异常提供程序:{ 例如,Delphi 异常的 DelphiEP = DelphiEP.dll }。
    • 符号提供程序:{ 例如,Borland MAP 文件的 MapFile = MapSP.dll如果您链接了由不同编译器构建的 DLL,则可以添加多个}。
    • 日志记录:
      • LogFileName = { 您的自定义日志文件名 }
      • SpoolToFile = 1
  • 通过命令行提供您的配置文件来运行 Bug Finder。

如何支持新的调试符号格式

要支持新的调试符号格式,您需要编写自己的调试符号提供程序。以下是完成此操作的基本步骤

  • 符号提供程序是一个实现唯一接口(ISymbolProvider)的 DLL。
  • 创建一个 DLL 项目。
  • 删除任何默认单元包含。
  • 将以下文件包含到项目中
    • intf/hSymProvider.pas
    • intf/uSymProvider.pas
  • 为您的提供程序创建一个单元。
  • 通过您自己的类扩展 TSymProvider 类并实现以下方法
    • QuerySymbol
    • QueryAddress
  • 用您自己的类扩展 TSymProviderFactory 类并实现 AcceptModule 方法。
  • 在初始化部分,通过调用 RegisterFactory 方法注册工厂。

这是从 Bug Finder 源代码中的 COFF 文件格式提供程序提取的示例(您可以从 Source Forge 下载整个项目)。

unit uCoffSP;

interface

uses
  hCoffHelpers,
  hCoreServices,
  hSymProvider,
  SysUtils,
  uCoffHelpers,
  uSymProvider,
  Windows;

type
  TCoffSPFactory = class(TSymProviderFactory)
  public
    
    function AcceptModule(
      const AServices   : ICoreServices;
      const AModuleName : String;
      AModuleData       : PLoadDLLDebugInfo;
      out AProvider     : ISymbolProvider
    ): Boolean; override;

  end;

  TCoffSP = class(TSymProvider)
  private
    function    ExtractModuleName(const ASourceFileName: String): String;
  protected
    function    QuerySymbol(ARawAddress, ARelativeAddress: DWORD): ISymbol; override;
    function    QueryAddress(AUnitName, AProcName: PChar; 
                  ACodeBase: DWORD; out AAddress: DWORD): BOOL; override;
  end;

implementation

{ TCoffSPFactory }

function TCoffSPFactory.AcceptModule(
  const AServices   : ICoreServices;
  const AModuleName : String;
  AModuleData       : PLoadDLLDebugInfo;
  out AProvider     : ISymbolProvider
): Boolean;

var
  BaseAddr : DWORD;
  hFile    : THandle;

begin
  if not Assigned(AModuleData) then begin
    BaseAddr := DWORD(AServices.ProcessDebugInfo^.lpBaseOfImage);
    hFile    := 0;
  end else begin
    BaseAddr := DWORD(AModuleData^.lpBaseOfDll);
    hFile    := AModuleData^.hFile;
  end;

  Result := hlpInitialize(AServices.ProcessDebugInfo^.hProcess, hFile, AModuleName, BaseAddr);

  if Result then
    AProvider := TCoffSP.Create(AServices, AModuleName, AModuleData)
  else
    AProvider := nil;

  hlpFinalize(AServices.ProcessDebugInfo^.hProcess, BaseAddr);
end;

{ TCoffSP }

function TCoffSP.QueryAddress(AUnitName, 
  AProcName: PChar; ACodeBase: DWORD; out AAddress: DWORD): BOOL;
var
  Symbol   : PIMAGEHLP_SYMBOL;
  SymSize  : DWORD;
begin
  Result   := False;
  AAddress := DWORD(-1);
  Symbol   := nil;

  { Init }

  if hlpInitialize(FServices.Process, 0, FModuleName, GetModuleBase) then

    try
      { Symbol: Unit name ignored!!! }

      SymSize := SizeOf(IMAGEHLP_SYMBOL) + MAX_SYM_NAME;
      GetMem(Symbol, SymSize);
      FillChar(Symbol^, SymSize, 0);

      with Symbol^ do begin
        SizeOfStruct  := SymSize;
        MaxNameLength := MAX_SYM_NAME;
      end;

      Result := SymGetSymFromName(FServices.Process, PChar(AProcName), Symbol);
      if Result then
        AAddress := Symbol^.Address;
    finally
      FreeMem(Symbol);
    end;

  { Finalize }

  hlpFinalize(FServices.Process, GetModuleBase);
end;

function TCoffSP.QuerySymbol(ARawAddress, ARelativeAddress: DWORD): ISymbol;
var
  dwDispl  : DWORD;
  Symbol   : PIMAGEHLP_SYMBOL;
  SymbolLn : IMAGEHLP_LINE;
  symFName : String;
  symLine  : DWORD;
  symName  : String;
  SymSize  : Integer;
begin
  Result := nil;
  Symbol := nil;

  { Init }

  if hlpInitialize(FServices.Process, 0, FModuleName, GetModuleBase) then
    try
      { Symbol }

      SymSize := SizeOf(IMAGEHLP_SYMBOL) + MAX_SYM_NAME;
      GetMem(Symbol, SymSize);
      FillChar(Symbol^, SymSize, 0);

      with Symbol^ do begin
        SizeOfStruct  := SymSize;
        MaxNameLength := MAX_SYM_NAME;
      end;

      dwDispl := 0; { Optional for SymGetSymFromAddr }

      if SymGetSymFromAddr(FServices.Process, ARawAddress, @dwDispl, Symbol) then begin
        symName := Trim(StrPas(@Symbol^.Name));

        { Line }

        FillChar(SymbolLn, SizeOf(IMAGEHLP_LINE), 0);
        SymbolLn.SizeOfStruct := SizeOf(IMAGEHLP_LINE);

        dwDispl               := 0; { Not optional for SymGetLineFromAddr!!! }

        if SymGetLineFromAddr(FServices.Process, ARawAddress, @dwDispl, @SymbolLn) then begin
          symFName := StrPas(SymbolLn.FileName);
          symLine  := SymbolLn.LineNumber;
        end else begin
          symFName := 'N/A';
          symLine  := 0;
        end;

        Result := TSymbol.Create(
          ExtractFileName(symFName),
          ExtractModuleName(symFName),
          symName,
          ARawAddress,
          symLine
        );
      end;
    finally
      FreeMem(Symbol);
    end;

  { Finalize }

  hlpFinalize(FServices.Process, GetModuleBase);
end;

function TCoffSP.ExtractModuleName(const ASourceFileName: String): String;
var
  Ext : String;
begin
  Result := ExtractFileName(ASourceFileName);
  Ext    := ExtractFileExt(Result);

  if (Ext <> '') then
    Result := Copy(Result, 1, (Length(Result) - Length(Ext)));
end;

begin
  RegisterFactory(TCoffSPFactory);
end.

编写新的异常提供程序

如前所述,异常提供程序是一个特殊的、可选的插件,用于将特定编译器生成的异常堆栈帧数据转换为适用于 Bug Finder 的信息。在撰写本文时,我只需要为 Borland Delphi 编译器实现它。

编写新的异常提供程序需要对编译器及其内部动态和数据结构有深入的了解,所以您必须做一些黑客工作来编写一个!

作为示例,我将为您粘贴我为 Delphi 异常提供程序编写的代码。

unit uDelphiEP;

interface

uses
  hCoreServices,
  hDelphiEP,
  hExcProvider,
  SysUtils,
  uExcProvider,
  uDebugUtils,
  Windows;

type
  TDelphiEPFactory = class(TExcProviderFactory)
  public
    function AcceptException(const AServices: ICoreServices; 
      AException: PExceptionRecord; out AProvider: IExceptionProvider): Boolean; override; 
  end;

  TDelphiEP = class(TExcProvider)
  private
    function GetExceptionDescription(AProcess: THandle; AExceptionObject: Pointer): String;
    function GetExceptionName(AProcess: THandle; AExceptionObject: Pointer): String;
    function GetExceptionVMT(AProcess: THandle; AExceptionObject: Pointer): DWORD;
  protected
    function GetDescription: PChar; override;
    function HandleException(AException: PExceptionRecord): BOOL; override;
    function TranslateExceptionAddress(AException: PExceptionRecord): DWORD; override;
  end;

implementation

{ TDelphiEPFactory }

function TDelphiEPFactory.AcceptException(const AServices: ICoreServices; 
  AException: PExceptionRecord; out AProvider: IExceptionProvider): Boolean;
begin
  Result := Assigned(AException) and (AException^.ExceptionCode = cDelphiException);

  if Result then
    AProvider := TDelphiEP.Create(AServices)
  else
    AProvider := nil;
end;

{ TDelphiEP }

function TDelphiEP.GetExceptionVMT(AProcess: THandle; AExceptionObject: Pointer): DWORD;
var
  lpVMT : DWORD;
begin
  if not ReadProcMem(AProcess, AExceptionObject, @lpVMT, SizeOf(DWORD)) then
    Result := DWORD(nil)
  else
    Result := lpVMT;
end;

function TDelphiEP.GetExceptionName(AProcess: THandle; AExceptionObject: Pointer): String;

var
  tmpResult : String;

  function InternalGetExceptionName(AVmtOfs: Integer): String;
  var
    lpClassName   : Pointer;
    lplpClassName : Pointer;
    lpVMT         : Pointer;
    szClassName   : ShortString;
  begin
    Result := '';
    lpVMT  := Pointer(GetExceptionVMT(AProcess, AExceptionObject));
    
    if Assigned(lpVMT) then begin                           { TClass(VMT) }
      lplpClassName := Pointer(DWORD(lpVMT) + AVmtOfs);

      if ReadProcMem(AProcess, lplpClassName, @lpClassName, SizeOf(DWORD)) then  
                                             { *ClassName }
        if ReadProcMem(AProcess, lpClassName, @szClassName[0], 1) then  
                                             { ClassName length }
          if ReadProcMem(AProcess, Pointer(DWORD(lpClassName) + 1), 
                @szClassName[1], Byte(szClassName[0])) then  { ClassName data }
            Result := szClassName;
    end;
  end;

begin
  Result := 'Unknown!';

  {

    TClass(VMT) = ExceptionObject^
    *ClassName  = VMT + vmtClassName

  }

  tmpResult := InternalGetExceptionName(VMT_CLASSNAME_Dx);
  if IsValidIdent(tmpResult) then begin
    Result := tmpResult;
    Exit;
  end;

  tmpResult := InternalGetExceptionName(VMT_CLASSNAME_D3);
  if IsValidIdent(tmpResult) then
    Result := tmpResult;
end;

function TDelphiEP.GetExceptionDescription(AProcess: THandle; 
                   AExceptionObject: Pointer): String;
var
  dwSize : DWORD;
  lpMsg  : Pointer;
  lpSize : PDWORD;
  lpVars : Pointer;
  szMsg  : PChar;
begin
  Result := 'Unknown!';
  lpVars := Pointer(DWORD(AExceptionObject) + SizeOf(Pointer) { VMT ptr } );

  {
    TObjectInstanceData = record
      VMT          : Pointer;
      InstanceData : ...

      ...
    end;
  }

  if ReadProcMem(AProcess, lpVars, @lpMsg, SizeOf(Pointer)) then begin
    lpSize := PDWORD(DWORD(lpMsg) - 4 { AnsiString length offset } );
    szMsg  := nil;

    if ReadProcMem(AProcess, lpSize, @dwSize, SizeOf(DWORD)) then
      if (dwSize > 0) then
        try
          szMsg := StrAlloc(dwSize + 1);

          if ReadProcMem(AProcess, lpMsg, szMsg, dwSize) then begin
            PByte(DWORD(szMsg) + dwSize)^ := 0;
            Result                        := StrPas(szMsg);
          end;

        finally

          try
            if Assigned(szMsg) then
              StrDispose(szMsg);
          except
          end;

        end;

  end;
end;

function TDelphiEP.GetDescription: PChar;
begin
  Result := EXCEPTION_DESCRIPTION;
end;

function TDelphiEP.HandleException(AException: PExceptionRecord): BOOL;
var
  ExceptObj : Pointer;
begin
  ExceptObj := PSysExceptionRecord(AException)^.ExceptObject;

  FServices.LogMessage(PChar(Format('  Class name  : %s', 
    [GetExceptionName(FServices.ProcessInfo^.hProcess, ExceptObj)])), True);
  FServices.LogMessage(PChar(Format('  Error mesg. : "%s"', 
    [GetExceptionDescription(FServices.ProcessInfo^.hProcess, ExceptObj)])), True);

  Result := True;
end;

function TDelphiEP.TranslateExceptionAddress(AException: PExceptionRecord): DWORD;
begin
  Result := DWORD(PSysExceptionRecord(AException).ExceptAddr);
end;

begin
  RegisterFactory(TDelphiEPFactory);
end.

致谢

历史

  • 2013年6月6日
    • 首次发布文章
  • 2013年6月7日
    • 更新了引言部分
    • 更新了框图
    • 更新了配置部分
    • 更新了项目功能部分
    • 更新了鸣谢部分
    • 添加了“为 Bug Finding 准备您的应用程序”部分
    • 添加了“异常提供程序”部分
    • 更改了调试符号提供程序示例代码
  • 2013年6月10日
    • 更新了项目功能部分
    • 更新了“编写新的异常提供程序”部分
  • 2013年6月11日
    • 更新了下载链接
    • 更改了副标题
  • 2013年7月15日
    • 文章排版
© . All rights reserved.