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






4.94/5 (25投票s)
Win32、与编译器无关且可扩展的被动调试器
引言
这是我的第一篇文章,基于我托管在 和
平台上的 Bug Finder 开源项目。您可以从 项目主页 获取。
背景
这个项目诞生于几年前,当时我在生产环境中遇到一个致命错误,可惜在我的开发机上无法重现。
我花费了大量时间寻找解决方案,包括使用免费和商业的第三方工具和库,但都没有帮助到我,因为那个错误导致进程崩溃,而上述工具都无法在进程终止前捕获异常。
而且,当时无法在生产机器上安装开发环境,所以唯一的解决方案是开发一个不需要任何用户/开发者交互的调试器。
我使用了许多开源资源,然后决定将 Bug Finder 作为一个开源项目。
它构建在可插拔的架构之上,以支持除 Borland Delphi(我用来开发我的故障应用程序和 Bug Finder 的语言)之外的其他语言。
项目结构
Bug Finder 是一个基于组件堆栈构建的工具,如下图所示的框图。
现在我们将逐一 examining 它们
- 核心应用程序:这是实现整个调试逻辑的调试器核心模块。
- 异常提供程序:对于使用特定编译器(如 Borland Delphi)编译的可执行文件,需要处理每个堆栈帧指针的地址以获取异常名称、描述和有效虚拟地址等信息。
- Delphi 异常提供程序:这是我唯一需要实现的。
- 调试符号提供程序:每个编译器都有自己的调试符号格式,因此在这种情况下,您也需要一个自定义的解释器插件。
- COFF (通用对象文件格式):由 Microsoft 编译器生成的符号。
- Borland MAP 文件:当启用 MAP 文件生成选项时,由 Borland Delphi 编译器生成的文件。
- Borland TDS (Turbo Debugger 符号):当启用 TD32 符号生成选项时,由 Borland Delphi 编译器生成的符号。
特点
Bug Finder 是一个真正的 Win32 调试器,完全用 Borland Delphi 编写,它分析您的应用程序执行流程,因此您可以
- 在主可执行文件、外部 DLL、主线程和工作线程上捕获异常。
- 为每个异常生成详细的堆栈跟踪。
- 设置符号断点,以便在程序调试断点出现时,获取完整的堆栈跟踪日志消息(动态跟踪)。
- 生成详细且可轮换的日志文件,用于检查批处理应用程序的行为。
- 将
OutputDebugString
API 的输出捕获到日志文件中(以便您自己直接在代码中提供额外的调试信息)。 - 跟踪进程、线程和 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,您可以选择两种方法
- 使用配置向导实用程序
- 自己提供一个配置文件,作为参数传递给 Bug Finder 的主可执行文件
配置向导非常直观,所以我只向您解释存储在每个 INI 配置文件中的配置参数。
节:配置
参数 | 必需 | 类型 | 描述 |
| 是 |
| 应用程序可执行文件的完整路径 |
AppParameters | 否 | 字符串 | 应用程序命令行参数 |
节:符号提供程序
在此处为每种支持的调试符号类型添加一个条目,格式如下
<Descriptive name> = <DLL file name>
节:异常提供程序
在此处为每种支持的异常提供程序添加一个条目,格式如下
<Descriptive name> = <DLL file name>
节:日志记录
参数 | 必需 | 类型 | 默认值 | 描述 |
LogViewLinesLimit | 否 | 整数 | 1000 | 日志窗口中显示的日志行数限制,以优化内存使用。 |
LogFileName | 否 | 字符串 | BugFinder.log | 日志文件的完整路径。 |
SpoolToFile | 否 | 整数 | 1 | 启用/禁用日志记录功能。 |
LogFileRotation | 否 | 整数 | 0 | 设置日志轮换策略
|
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.
致谢
- Free Pascal 编译器:用于断点步进技术
- JEDI Code Library:用于 Borland 调试符号和 MAP 文件解释
- LMD Innovative:用于 LMD-Tools 特别版组件
- Nullsoft:用于 NSIS 安装程序
- Hector Mauricio Rodriguez Segura:用于 NSIS 编辑器
历史
- 2013年6月6日
- 首次发布文章
- 2013年6月7日
- 更新了引言部分
- 更新了框图
- 更新了配置部分
- 更新了项目功能部分
- 更新了鸣谢部分
- 添加了“为 Bug Finding 准备您的应用程序”部分
- 添加了“异常提供程序”部分
- 更改了调试符号提供程序示例代码
- 2013年6月10日
- 更新了项目功能部分
- 更新了“编写新的异常提供程序”部分
- 2013年6月11日
- 更新了下载链接
- 更改了副标题
- 2013年7月15日
- 文章排版