从生产环境中的 .NET 应用程序获取信息






4.89/5 (9投票s)
通过托盘图标获取简单的调试信息。
引言
新: appCompanion 现在有一个 codeplex 页面 (欢迎贡献)。
这个小型的 mdbg 程序可以帮助您在客户(非开发)环境中获取有关程序运行的内部信息,而无需安装 VisualStudio 或学习如何使用 windbg。
使用此工具,您可以:
- 实时获取所有线程的应用程序调用堆栈。
- 进行小型性能快照,显示时间浪费在哪里。
- 查看系统的调试控制台 (dbgView)
- 监听异常(第一次和第二次)
- 为进程生成挂起转储。
这个工具的帮助方式比最初看起来的要多,例如,获取调用堆栈可以帮助您通过打开一个对话框并查找其在调用堆栈中的名称来发现代码中未知的流程,或者诊断一个挂起的进程是由应用程序背后隐藏的对话框引起的。
在制作这个程序的过程中,我还使用了 Christian-Birkl 关于使用 C# 复制 DebugView 功能的优秀 CodeProject 文章。
32/64 位支持
本文的第一个版本是 x86 编译的,这限制了它的范围。现在已修复。- 由于调试器是平台特定的,您不能使用 x64 调试器来调试 x86 进程,反之亦然。
- AnyCPU 将在 x64 操作系统上作为 x64 运行,在 x86 操作系统上作为 x86 运行。
- 要与 AnyCPU/x64 应用程序一起使用,您需要使用 AnyCPU 编译 AppCompanion。
- 对于 x86 应用程序,请在 exe 项目属性中使用 x86 编译 AppCompanion。
- 演示 zip 现在包含两个版本。
- 当尝试附加到具有不正确平台的进程时,AppCompanion 将显示一个错误气球并退出。
背景
在使用 windbg+sos 从预生产和生产环境中获取代码调试信息多年后,我发现了 MSE,但随着 .NET 4.0 的发布,它被淘汰了,我又失去了它。(今天有几种替代方案)
最近我一直在想,我可以使用 mdbg 创建一些可以帮助我跟踪我的应用程序的东西,它能以一种优雅的方式提供比 MSE 更多(但比 windbg 少)的功能。
我试图收集一些有用的函数供开发(以及 QA 人员)使用,以便在应用程序提供的信息不足时获取更多数据。
使用 AppCompanion 演示
编辑 AppCompanion\AppStateMonitor.exe.config 文件,将 myProgram 更改为您的程序名称,不带 ".exe" 扩展名。
- 运行时, AppCompanion 会创建一个小的托盘图标(您可能需要自定义 Windows 托盘以显示它)。
- 当它识别出目标应用程序时,它会“窃取”它的图标并将其用作自己的图标。
- 右键单击此图标可获得本文第一段中描述的所有选项。
关于源代码
使 mdbg 工作
我使用了 Microsoft 的 mdbg 4.0 示例来附加和获取进程信息。由于调试器需要 MTA 线程,而图形需要 STA 线程。为了解决这个冲突,我使用了 TPL 任务 来运行调试器代码,并且由于 TPL 线程(以及基本上所有托管线程)默认是 MTA,因此解决了这个问题。
我仍在考虑将调试器代码放在一个带有自定义消息循环的单个线程中,使用阻塞集合,但找不到一个真正的好理由……
//attaching
Task t = Task.Factory.StartNew(() =>
{
MDbgEngine debugger = new MDbgEngine();
MDbgProcess process = DebuggerUtils.AttachToProcess(m_processId, debugger);
process.CorProcess.Stop(0);
process.Detach();
});
return t;
//detaching
Task t = Task.Factory.StartNew(() =>
{
if (m_process.IsAlive)
{
m_process.CorProcess.Stop(0);
m_process.Detach();
}
});
t.Wait();
进行性能采样
采样/性能代码的工作方式与某些性能分析器进行采样的方式相同。
- 每隔 x 毫秒进行一次快照。
- 将样本合并到调用树中。
- 根据看到此调用的次数来增加每个节点。
这可以很好地了解在每个节点上花费了多少时间,并且可以轻松地乘以两次采样之间的毫秒数,从而对实际时间有一个很好的了解。
调试器控制台
我添加了这个,因为 我发现它对我自己很有用,代码不是我的 而是 Christian-Birkl 的,如上所述。
进行挂起/崩溃转储
转储是使用对 dbghelp.dll 中的 MiniDumpWriteDump
函数的 pinvoke 调用来获取的:
// Overload supporting MiniDumpExceptionInformation == NULL
[DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump",
CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile,
uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam);
[DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
static extern uint GetCurrentThreadId();
public static bool WriteDump(int pid, int threadWithExceptionId,
string filename, DumpOptions options, bool hasException)
{
using (FileStream dumpFile = File.OpenWrite(filename))
{
SafeHandle fileHandle = dumpFile.SafeFileHandle;
Process currentProcess = Process.GetProcessById(pid);
IntPtr currentProcessHandle = currentProcess.Handle;
uint currentProcessId = (uint)currentProcess.Id;
MiniDumpExceptionInformation exp;
exp.ThreadId = threadWithExceptionId;//GetCurrentThreadId();
exp.ClientPointers = false;
exp.ExceptionPointers = IntPtr.Zero;
if (hasException)
{
exp.ExceptionPointers = System.Runtime.InteropServices.Marshal.GetExceptionPointers();
}
bool bRet = false;
if (exp.ExceptionPointers == IntPtr.Zero)
{
bRet = MiniDumpWriteDump(currentProcessHandle, currentProcessId,
fileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
else
{
bRet = MiniDumpWriteDump(currentProcessHandle, currentProcessId,
fileHandle, (uint)options, ref exp, IntPtr.Zero, IntPtr.Zero);
}
return bRet;
}
}
- 转储可以随时进行,不取决于调试器活动。
- 当监听异常并且
ProcessListener.WriteDumpOnUncaughtExceptions
设置为 true 时(默认值为 false),应用程序将捕获未捕获的异常并生成转储。
潜在的未来功能 和改进
- 添加创建断点的能力。
- 在断点处进行代码评估。
- 使用移动窗口/直到停止而不是恒定时间进行采样。
我希望您发现这对于作为工具或作为 mdbg 使用的示例代码都很有用。
如果有很多需求,我可能会考虑将其开源,但目前它只是我在业余时间制作的东西。=)