首页 > 开发 > 综合 > 正文

CLR 调试接口的架构与应用 [2] 调试框架

2024-07-21 02:17:14
字体:
来源:转载
供稿:网友
如 don box 在《.net本质论 第1卷:公共语言运行库》一书的第10章中介绍, clr 调试框架是一个由 clr 提供的,面向工具开发商的,支持调试功能的最小功能集。与 jvm 的 jdi (java debug interface)不同,clr 调试框架不仅仅关注于虚拟机一级的调试,同时也提供了 native 一级调试的统一接口。使得现有工具开发商能够以最小代价移植并支持 clr 调试功能。而对 clr 调试更高层次或更细粒度的支持,则是由前面提到的 profiling api 完成。
clr 调试接口主要通过 mscordbi.dll 提供的 icordebug 接口,让调试器通过进程内或进程外方式,对被调试 clr 进行监控。而 icordebug 接口可以通过 .net framework sdk 中 includecordebug.idl 或 includecordebug.h 直接使用。对 c#/delphi 也可以直接 reference/import 在 sdk 的 lib 目录下的 cordebug.tlb 类型库,获得调用包装类。下面示例将都使用 c# 作为描述语言。
在使用时,可以直接获取icordebug接口,并调用其initialize/terminate方法进行初始化和析构操作,框架代码如下:


以下为引用:

using cordblib;

namespace cordbg
{
public class debugger : idisposable
{
private icordebug _dbg;

public void run()
{
_dbg = new cordebugclass();

try
{
_dbg.initialize();

// 构造调试环境

// 处理调试事件
}
finally
{
_dbg.terminate();
}
}
...
}
[mtathread]
static void main(string[] args)
{
using(debugger dbg = new debugger())
{
dbg.run();
}
}
}





注意 clr 调试环境必须在 mta 的线程套间上下文(thread apartment context)中运行,因此必须将入口函数的 stathread 属性改成 mtathread,否则会在调试接口调用回调函数时出现异常。对应于 com 中的 coinitializeex(null, coinit_multithreaded) 调用。
在创建了 icordebug 调试接口后,需要针对托管和非托管调试事件,提供调试事件回调接口。可以将实现了调试事件接口 icordebugmanagedcallback/icordebugunmanagedcallback 的实例,使用 icordebug 接口的 setmanagedhandler/setunmanagedhandler 方法,挂接到调试系统上,在适当的时候由调试系统回调,通知调试器有调试事件发生。实现上可以通过 managedeventhandler/unmanagedeventhandler 两个单独的类,抽象出对托管和非托管调试事件的处理机制,将之挂接到调试器上,如:

以下为引用:

namespace cordbg
{
public class debugeventhandler
{
protected debugger _dbg;

public debugeventhandler(debugger dbg)
{
this._dbg = dbg;
}
}

public class managedeventhandler : debugeventhandler, icordebugmanagedcallback
{
public managedeventhandler(debugger dbg) : base(dbg)
{
}

// 实现 icordebugmanagedcallback 接口
}

public class unmanagedeventhandler : debugeventhandler, icordebugunmanagedcallback
{
public unmanagedeventhandler(debugger dbg) : base(dbg)
{
}

// 实现 icordebugunmanagedcallback 接口
}

public class debugger : idisposable
{
public void run()
{
//...

_dbg.setmanagedhandler(new managedeventhandler(this));
_dbg.setunmanagedhandler(new unmanagedeventhandler(this));

//...
}
}
}





在准备好了调试事件处理器后,就可以根据需要,创建或者附加到目标调试进程上。icordebug 提供了 createprocess 方法对 win32 api 中 createprocess 函数进行了包装。

以下为引用:

public abstract interface icordebug
{
public abstract new void createprocess (
string lpapplicationname,
string lpcommandline,
_security_attributes lpprocessattributes,
_security_attributes lpthreadattributes,
int binherithandles,
uint dwcreationflags,
intptr lpenvironment,
system.string lpcurrentdirectory,
uint lpstartupinfo,
uint lpprocessinformation,
cordebugcreateprocessflags debuggingflags,
icordebugprocess ppprocess)
}

bool createprocess(
lpctstr lpapplicationname,
lptstr lpcommandline,
lpsecurity_attributes lpprocessattributes,
lpsecurity_attributes lpthreadattributes,
bool binherithandles,
dword dwcreationflags,
lpvoid lpenvironment,
lpctstr lpcurrentdirectory,
lpstartupinfo lpstartupinfo,
lpprocess_information lpprocessinformation
);





可以看到这两个函数的参数基本上是一一对应的,只不过icordebug.createprocess函数多了一个输入debuggingflags参数指定调试标志和一个输出ppprocess参数返回创建进程的控制接口。
两个 _security_attributes 类型的安全属性,一般来说可以设置为空,使用缺省设置。

以下为引用:

_security_attributes sa = new _security_attributes();

sa.nlength = (uint)marshal.sizeof(sa);
sa.binherithandle = win32.bool.false;
sa.lpsecuritydescriptor = intptr.zero;





值得注意的是 dwcreationflags 指定了创建进程是否支持 native 模式的调试,也就是前面 setunmanagedhandler 方法调用的接口是否起作用。可以根据情况如命令行选项决定是否支持 native 调试模式,如

以下为引用:

namespace win32
{
public struct creationflag
{
public const uint debug_process = 0x00000001;
public const uint debug_only_this_process = 0x00000002;

public const uint create_suspended = 0x00000004;

public const uint detached_process = 0x00000008;

public const uint create_new_console = 0x00000010;

public const uint normal_priority_class = 0x00000020;
public const uint idle_priority_class = 0x00000040;
public const uint high_priority_class = 0x00000080;
public const uint realtime_priority_class = 0x00000100;

public const uint create_new_process_group = 0x00000200;
public const uint create_unicode_environment = 0x00000400;

public const uint create_separate_wow_vdm = 0x00000800;
public const uint create_shared_wow_vdm = 0x00001000;
public const uint create_forcedos = 0x00002000;

public const uint below_normal_priority_class = 0x00004000;
public const uint above_normal_priority_class = 0x00008000;

public const uint create_breakaway_from_job = 0x01000000;
}
}

namespace cordbg
{
public class debugger : idisposable
{
private void run()
{
//...

uint dwcreationflag = creationflag.create_new_console;

if(options.nativemode)
{
dwcreationflag |= creationflag.debug_only_this_process | creationflag.debug_process;
}

//...
}
}
}





比较麻烦的是指定启动参数的 lpstartupinfo 参数和返回进程信息的 lpprocessinformation 参数。c# 在导入 cordebug.tlb 类型库时,都没有处理这两个类型,必须自己定义之:

以下为引用:

[structlayout(layoutkind.sequential, charset=charset.auto)]
public struct startupinfo
{
public uint cb;
public string lpreserved;
public string lpdesktop;
public string lptitle;
public uint dwx;
public uint dwy;
public uint dwxsize;
public uint dwysize;
public uint dwxcountchars;
public uint dwycountchars;
public uint dwfillattribute;
public uint dwflags;
public ushort wshowwindow;
public ushort cbreserved2;
public intptr lpreserved2;
public intptr hstdinput;
public intptr hstdoutput;
public intptr hstderror;
}

[structlayout(layoutkind.sequential)]
public struct process_information
{
public intptr hprocess;
public intptr hthread;
public uint dwprocessid;
public uint dwthreadid;
}





使用的时候则需要先在堆栈中构造此结构的值类型对象,然后通过 unsafe 形式指针,或者 marshal 手工处理将之转换为地址。这里为了避免使用较为 dirty 的 unsafe 方式,通过 marshal.allochglobal 分配全局内存;然后调用 marshal.structuretoptr 将结构复制到内存中;调用 createprocess 时使用此内存的地址;调用返回后使用 marshal.ptrtostructure 从内存中获得结构的内容;最后调用 marshal.freehglobal 释放全局内存。简要代码如下:

以下为引用:

//...

startupinfo si = new startupinfo(); // 构造时所有字段已清零

si.cb = (uint)marshal.sizeof(si);

process_information pi = new process_information();

intptr ppi = marshal.allochglobal(marshal.sizeof(pi)),
psi = marshal.allochglobal(marshal.sizeof(si));

marshal.structuretoptr(si, psi, true);
marshal.structuretoptr(pi, ppi, true);

_dbg.createprocess(options.fileinfo.fullname, options.commandline,
ref sa, ref sa, bool.false, dwcreationflag, intptr.zero,
options.currentdirectory, (uint)psi.toint32(), (uint)ppi.toint32(),
cordebugcreateprocessflags.debug_no_special_options, out _proc);

pi = (process_information)marshal.ptrtostructure(ppi, typeof(process_information));

marshal.freehglobal(ppi);
marshal.freehglobal(psi);

native.closehandle(pi.hprocess);

//...





而将调试器附加到现有进程上则相对简单得多,接口方法如下:

以下为引用:

public abstract interface icordebug
{
public abstract new void debugactiveprocess(uint id, int win32attach, icordebugprocess ppprocess)
}

bool debugactiveprocess(
dword dwprocessid
);





与 win32 api 的 debugactiveprocess 相比,icordebug.debugactiveprocess 增加的 win32attach 指定是否允许 native 模式调试,ppprocess 返回目标调试进程的控制接口。

以上简要介绍了 clr 调试接口在使用时如何构造调试环境,以及对调试目标进程的创建和附加的方法。下一节将整体上对托管和非托管的各种调试事件做一个介绍,然后再针对不同的调试功能开始详细介绍。
菜鸟学堂:
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表