2023年1月

1、对于反应性事件:“将工程师带到现场,因为这样可以更容易地隔离问题。”

这是我听到的最常见的误解。让我解释一下:大多数复杂的问题都需要深入的调试会话。
收集必要的信息是很容易的,可以远程或由客户完成。但是,调试转储文件可能需要几个小时或几天。由于我们可能无法访问我们的私有符号,也无法与具有特定技术知识的同事协作访问,因此在现场工作实际上会减慢进程。
很多时候,在现场工作的一个重要价值是充当远程工程师的耳目,或者更好地理解我们无法通过电子邮件或电话很好理解的复杂问题。

2、“我们需要代码检查,因为我们的应用程序有性能问题。”

有时我会收到代码评审请求,但实际上,客户需要的是问题隔离。那有什么区别,我怎么知道我需要什么?
代码检查的目标是检查源代码并指出代码中不遵循最佳实践的部分,或者表示安全漏洞的部分,或者还可以针对速度进行优化的部分。
问题隔离的目标是隔离导致特定应用程序症状的问题。例如,崩溃、挂起、内存泄漏和性能瓶颈。
让我解释一下:想象一个ASP.NET应用程序性能不佳的场景。如果我代码审查应用程序,我可能会找到可以优化速度的方法。但是,如果应用程序的性能很慢,因为在数据库端或网络上存在瓶颈,那么通过代码检查获得的性能增益并不能解决问题。最坏的情况是,它甚至可能不明显。
如果您希望确保应用程序不存在通过实现最佳实践可以避免的潜在问题,或者如果您认为可以进一步优化应用程序以获得更高的速度,那么代码检查非常好。但是,只有在应用程序没有遇到问题时有一个基线,才能测量速度增益。重要的是,通常性能的提高不如消除瓶颈那么重要。

3、”因此,在修复这个问题后,性能/内存问题将被规范化,对吗?”

实际情况是,可能会有不同的问题导致相同的症状,如性能低下、挂起或内存问题。
这是什么意思?这意味着在解决了最重要和最明显的问题之后,我们需要监视应用程序,因为其他小问题可能会导致相同的症状。此外,在修复了主要瓶颈之后,这些其他小问题应该变得可见和易于隔离。识别和解决应用程序问题是一个迭代过程。

4、“我们使用的是.NET,所以我不需要担心内存管理。”

如果你有一个纯.NET的应用程序,我倾向于同意。然而,大多数商业应用程序都与本机世界有某种交互,比如C Dlls、COM对象或API调用。

CLR非常适合管理纯.NET应用程序中的内存。如果应用程序正在与本机代码交互,则开发人员有责任确保资源已释放/关闭。

5、我需要收集什么信息?我需要收集多少信息?

在信息不足和信息过多之间有一条细线。对我们来说最重要的是得到正确的信息。当遇到这种症状时,从有问题的应用程序收集一个转储文件是非常有价值的。运行正常时从有问题的应用程序中收集的五个转储文件很可能没有太大帮助。
如果应用程序崩溃,则希望在应用程序崩溃时收集转储文件。如果您在任何其他时间收集转储文件,它将没有来自异常的信息。如果强制收集一个巨大的内核转储文件,则最终将从计算机的所有进程中获得一个巨大的转储文件,但同样,该转储文件不会包含导致应用程序崩溃的异常的信息。

6、“我们需要对体系结构进行检查,因为我们的应用程序存在性能问题。”

这与上文第2项类似。架构评审并不是解决当前问题的最佳方法。
此外,架构评审甚至可能不是解决大多数应用程序问题的正确方法,因为这些应用程序问题通常过于细粒度。这意味着客户的应用程序从架构的角度正确设计,但与架构的设计方式无关的问题。
让我举几个例子。假设您还没有为.NET框架安装影响应用程序的重要更新。或者您的SharePoint应用程序没有释放它正在使用的内部SharePoint对象。在这些例子中,架构评审不会揭示这些问题。

7、有时候,找到合适的起点是最困难的。

想象一下这个场景:
“我们需要一个IIS工程师,因为我的W3WP.EXE占用了太多内存。这可能是一个IIS错误,“用户、管理员和开发人员将如何体验这个问题?

  • 最终用户:我认为浏览器有问题,应用程序很慢。
  • IIS管理员:我认为问题出在ASP.NET应用程序上。
  • 开发人员:ASP.NET应用程序运行良好;问题可能出在数据库方面。
  • DBA:SQL服务器运行良好;我认为瓶颈与网络有关。
  • 网络管理员:网络没有问题。

我们作为开发人员PFEs的目标是帮助我们的客户跨不同的技术隔离问题,并在现场或远程提供不同团队之间的跨组协作。

8、我需要什么技能来帮助我调试应用程序?

如果应用程序需要调试,则不需要知道如何管理或安装产品的工程师。你需要的是一个了解应用程序内部以及如何调试它们的工程师。好消息是,这些知识并不依赖于应用程序。
即使Microsoft工程师以前从未见过您的应用程序,他/她也可以调试您的应用程序。这同样适用于我们自己的产品。
如果在某个时候我们将问题隔离到我们的某个产品上,那么我们就需要产品团队中的一名工程师,因为他/她对产品中的问题和缺陷有很深的了解支持。

9、“我执行了!clrstack和大多数运行很长时间的线程正在尝试从数据库中检索数据。瓶颈可能在数据库方面。”

让我告诉你一件事:我曾经对我们的新工程师或那些想了解更多关于.NET调试的人说,如果你想在.NET调试中胜过,你必须学习本机代码调试,这也暗示了C/C++编程的一些知识。
不相信我?如果你最喜欢的博主只知道.NET调试,那就问问他们.NET调试的情况。
就这么说!clrstack是学习.NET调试的人最喜欢的命令。很酷;您可以看到调用堆栈的托管端,它通常比本机端的级别更高。然而,有时您仍然需要看到本机端才能真正理解线程在做什么,否则,如果只关注托管端,您可能会得出错误的结论。
底线是:如果您想提高.NET调试技能,请进一步了解本机调试。

10、“我的两个服务器是相同的,但问题只发生在服务器XYZ上。”

当对这样的场景进行故障排除时,永远不要假设服务器是相同的。相反,收集数据来证明这一点。
一个很好的开始是运行MPSReport/SPSReport工具。此工具将从每个服务器收集所有信息并进行比较。至少在一个服务器完全相同的情况下,根本的问题是应用程序正在访问其中一个服务器,因此它正在过载。

11、“从事件日志中,我可以看到导致应用程序崩溃且调用堆栈指向Windows的异常。我认为这是一个Windows错误。”

这与上文第7项和一个常见的误解有关。有时从第二次机会呼叫堆栈
异常(应用程序未处理的异常,从而导致应用程序崩溃)将来自Windows的dll作为顶部框架。这是正常的,并不意味着Windows会导致崩溃。

例子:

 ChildEBP RetAddr
0013bcd0 7c90de7a ntdll!KiFastSystemCall+0x2
0013bdd0 7c81cdfe kernel32!_ExitProcess+0x62
0013bde4 79f944b0 kernel32!ExitProcess+0x14
0013c00c 79f2c09a mscorwks!SafeExitProcess+0x11b
0013c018 79eff585 mscorwks!DisableRuntime+0xd1
0013c0a8 79011628 mscorwks!CorExitProcess+0x242
0013c0b8 77c39d3c mscoree!CorExitProcess+0x46
0013c0c4 77c39e78 msvcrt!__crtExitProcess+0x29
0013c0d4 77c39e90 msvcrt!_cinit+0xee
0013c0e8 0e68d21e msvcrt!exit+0x12
0013c580 0e256834 testappl!FuTestInterface::init+0x34 <<< This is where you should start the investigation.
0013c5a4 0e1d8c01 testapp!WBNARiskReportInterface::getResults+0x442a

因此,不要认为是ntdll或kernel32导致了这个问题。这些操作系统dll中的api是由于应用程序可能导致的异常而调用的。尝试将最新的应用程序方法调用标识为初始调查点。在上面的例子中,这是testappl!FuTestInterface::init。分析它,如有必要,分析前一帧等。

12、“我们从崩溃的C++应用程序中收集转储文件。我们认为这是堆损坏,所以调用堆栈应该指示罪魁祸首,对吧?”

堆损坏不像以前那样频繁,因为.NET应用程序越来越常见。然而,在COM对象和C DLL的时代,堆损坏是一个典型的问题。
要从实际损坏堆的方法中获取调用堆栈是为了启用页堆,请重新启动应用程序,以便它可以使用新的堆管理器设置并收集转储文件。使用这种方法,您可以轻松地隔离堆损坏问题。
可以使用不同的工具(如Page Heap.exe、GFlags.exe、Application)启用页堆验证者和其他人。某些页堆设置(如完整页堆)在每次内存分配后创建一个只读页,因此每当应用程序试图覆盖缓冲区时,它都会命中只读页,从而导致访问冲突。

1. 下载dmp文件所有相关模块的symbols,缓存到共享路径,便于其它人快速下载
symchk.exe
/id c:\MyApplication.dmp /s
SRV*\\symbols_server\WinSymbols\*http://msdl.microsoft.com/download/symbols

2. 下载某个已运行进程所有相关模块的symbols,缓存到共享路径,便于其它人快速下载
symchk.exe /ie qq.exe /s SRV*\\symbols_server\WinSymbols\*http://msdl.microsoft.com/download/symbols

3. 下载某个exe/dll文件对应的symbols,例如user32.dll,输入命令行:
symchk.exe c:\windows\system32\user32.dll /s SRV*c:\symbols\*http://msdl.microsoft.com/download/symbols

4. 下载整个目录下(例如system32)所有模块的symbols:
symchk.exe /r c:\windows\system32\ /s SRV*c:\symbols\*http://msdl.microsoft.com/download/symbols

加载DLL 的时候断

  • sxe ld:[dll]

    比如:

    sxe ld:wininet.dll  (在wininet.dll 被装载的时候断点)

    这里DLL名字是支持通配符的

    比如:

    sxe ld:wini*.dll

  • 也可以在事件过滤器里设置

卸载DLL 的时候断

  • sxe ud:[dll]
  • 也可以在事件过滤器里设置

直接在DllMain下断

bu wininet!DllMain

低层次思考,我指的是从应用程序内部思考的重要性,有时是在机器代码级别。

大多数人认为,要知道如何调试应用程序,只需要学习如何使用调试器。但事实上,学习如何使用调试器只是解决复杂软件问题所需的一部分。因此,我觉得有必要解释在处理应用程序问题(如挂起、崩溃、内存泄漏、应用程序错误和性能问题)时,低层思考是多么重要。

硬件断点的原理

Intel 80306以上的CPU给我们提供了调试寄存器用于软件调试,硬件断点是通过设置调试寄存器实现的。

 

上图为Intel手册提供的32位操作系统下8个调试寄存器的图示(Intel手册卷3 17章第二节 Debug Registers,有兴趣的朋友可以查阅),根据介绍,DR0-DR3为设置断点的地址,DR4和DR5为保留,DR6为调试异常产生后显示的一些信息,DR7保存了断点是否启用、断点类型和长度等信息。

我们在使用硬件断点的时候,就是要设置调试寄存器,将断点的位置设置到DR0-DR3中,断点的长度设置到DR7的LEN0-LEN3中,将断点的类型设置到DR7的RW0-RW3中,将是否启用断点设置到DR7的L0-L3中。设置硬件断点需要的DR0-DR3很简单,就是下断点的地址,DR7寄存器很复杂,位段信息结构体如下:

typedef struct_DBG_REG7
{
/*// 局部断点(L0~3)与全局断点(G0~3)的标记位*/unsigned L0 :1; //对Dr0保存的地址启用 局部断点 unsigned G0 : 1; //对Dr0保存的地址启用 全局断点 unsigned L1 : 1; //对Dr1保存的地址启用 局部断点 unsigned G1 : 1; //对Dr1保存的地址启用 全局断点 unsigned L2 : 1; //对Dr2保存的地址启用 局部断点 unsigned G2 : 1; //对Dr2保存的地址启用 全局断点 unsigned L3 : 1; //对Dr3保存的地址启用 局部断点 unsigned G3 : 1; //对Dr3保存的地址启用 全局断点 /*// 【以弃用】用于降低CPU频率,以方便准确检测断点异常*/unsigned LE :1;
unsigned GE :
1;/*// 保留字段*/unsigned Reserve1 :3;/*// 保护调试寄存器标志位,如果此位为1,则有指令修改条是寄存器时会触发异常*/unsigned GD :1;/*// 保留字段*/unsigned Reserve2 :2;

unsigned RW0 :
2; //设定Dr0指向地址的断点类型 unsigned LEN0 : 2; //设定Dr0指向地址的断点长度 unsigned RW1 : 2; //设定Dr1指向地址的断点类型 unsigned LEN1 : 2; //设定Dr1指向地址的断点长度 unsigned RW2 : 2; //设定Dr2指向地址的断点类型 unsigned LEN2 : 2; //设定Dr2指向地址的断点长度 unsigned RW3 : 2; //设定Dr3指向地址的断点类型 unsigned LEN3 : 2; //设定Dr3指向地址的断点长度 }DBG_REG7, *PDBG_REG7;