堆栈跟踪如何指向错误的行(“return”语句)- 关闭 40 行

How Can a Stack Trace Point to the Wrong Line (the "return" Statement) - 40 Lines Off

提问人:John Saunders 提问时间:11/22/2014 最后编辑:CommunityJohn Saunders 更新时间:12/14/2014 访问量:6455

问:

我现在已经两次看到从生产 ASP.NET MVC 4 Web 应用程序登录的 - 并且登录了错误的行。不会错一两行(就像 PDB 不匹配一样),但整个控制器操作的长度会出错。例:NullReferenceException

public ActionResult Index()
{
    var someObject = GetObjectFromService();
    if (someObject.SomeProperty == "X") { // NullReferenceException here if someObject == null
        // do something
    }

    // about 40 more lines of code

    return View();    // Stack trace shows NullReferenceException here
}

对于同一控制器上的操作,这种情况已发生两次。第二个案例已登录

// someObject is known non-null because of earlier dereferences
return someObject.OtherProperty
    ? RedirecToAction("ViewName", "ControllerName")
    : RedirectToAction("OtherView", "OtherController");

这是非常令人不安的。 一旦你知道它发生在哪条线上,就很容易修复。如果异常可能发生在控制器操作中的任何位置,那就不是那么容易了!NullReferenceException

有没有人见过这样的东西,无论是在 ASP.NET MVC 还是其他地方?我愿意相信这是发布版本和调试版本之间的区别,但仍然要偏离 40 行?


编辑:

需要明确的是:我是“什么是 NullReferenceException 以及如何修复它?我知道什么是。这个问题是关于为什么堆栈跟踪会离得这么远。我见过由于 PDB 不匹配而导致堆栈跟踪偏离一两行的情况。我见过没有 PDB 的情况,所以你不会得到行号。但我从未见过堆栈跟踪偏离 32 行的情况。NullReferenceException

编辑2:

请注意,这是在同一控制器中的两个单独的控制器操作中发生的。它们的代码彼此完全不同。事实上,在第一种情况下,它甚至没有出现在条件中 - 它更像是这样的:NullReferenceException

SomeMethod(someObject.SomeProperty);

有可能在优化过程中对代码进行了重组,以便实际代码更接近 ,而 PDB 实际上只偏离了几行。但是我没有看到机会以会导致代码移动 32 行的方式重新排列方法调用。事实上,我只是看了反编译的源代码,它似乎没有被重新排列。NullReferenceExceptionreturn

这两种情况的共同点是:

  1. 它们发生在同一个控制器中(到目前为止)
  2. 在这两种情况下,堆栈跟踪都指向语句,并且在这两种情况下,发生的事件都与语句相距 30 行或更多行。returnNullReferenceExceptionreturn

编辑3:

我刚刚做了一个实验 - 我刚刚使用我们部署到生产服务器的“生产”构建配置重新构建了解决方案。我在本地 IIS 上运行了该解决方案,而根本没有更改 IIS 配置。

堆栈跟踪显示了正确的行号。

编辑4:

我不知道这是否相关,但导致这种情况的情况与这个“错误的行号”问题本身一样不寻常。我们似乎无缘无故地失去会话状态(没有重新启动或其他任何事情)。这并不奇怪。奇怪的是,当这种情况发生时,我们的Session_Start应该重定向到登录页面。任何重现会话丢失的尝试都会导致重定向到登录页面。随后,使用浏览器的“后退”按钮或手动输入上一个 URL 将直接返回登录页面,而不会点击相关控制器。NullReferenceException

所以也许两个奇怪的问题真的是一个非常奇怪的问题。

编辑5:

我能够获得 .PDB 文件,并使用 dia2dump 查看它。我认为 PDB 可能搞砸了,并且只有第 72 行用于该方法。事实并非如此。所有行号都存在于 PDB 中。

编辑6:

作为记录,这种情况再次发生,在第三个控制器中。堆栈跟踪直接指向方法的 return 语句。这个 return 语句很简单。我不认为有任何方法可以导致.return model;NullReferenceException

编辑 6a:

事实上,我只是更仔细地查看了日志,发现了几个异常,这些异常不是,并且在语句中仍然具有堆栈跟踪点。这两种情况都存在于从控制器操作调用的方法中,而不是直接在操作方法本身中。其中一个是显式抛出的,另一个是简单的.NullReferenceExceptionreturnInvalidOperationExceptionFormatException


以下是我直到现在才认为相关的几个事实:

  1. global.asax 中的 是导致记录这些异常的原因。它通过使用 来获取异常。Application_ErrorServer.GetLastError()
  2. 日志记录机制分别记录消息和堆栈跟踪(而不是日志记录,这是我的建议)。特别是,我一直在询问的堆栈跟踪来自 .ex.ToString()ex.StackTrace
  3. 在 , 调用自 , 从我们的代码中调用。指向代码的堆栈跟踪行是指向 “” 的行。FormatExceptionSystem.DateTime.ParseSystem.Convert.ToDatereturn model;
C# asp.net asp.net-mvc-4 nullreferenceexception

评论

1赞 Azhar Khorasany 11/22/2014
有时,视图本身可能存在具有 null 引用的逻辑。示例模型属性,也可以是模型本身。检查您的视图,确保其中没有出现故障。
1赞 Brian Driscoll 11/22/2014
我支持爱资哈尔的评论。我的猜测是,您的视图正在调用模型上为空的内容。
2赞 Alexei Levenkov 11/22/2014
有趣的是,为什么人们会投票否决“我应该在这里得到例外,但相反,在那里得到它”的问题......
4赞 Erik Philips 11/22/2014
@BrianDriscoll 不调用视图,因此视图中的任何 NRE 都不会被抛出到控制器的方法中。我倾向于认为示例代码已经减少到永远无法重现异常的程度。就目前而言,这个问题是无法回答的,因为它没有最小、完整和可验证的示例View()Execute()
3赞 Mike 11/25/2014
使用 msft 中的调试诊断。这将允许您在发生访问冲突时获取用户转储。然后你可以加载到 windbg 中,看看你得到了什么堆栈。苔丝的博客可能会有所帮助 blogs.msdn.com/b/tess

答:

10赞 Thomas Weller 11/25/2014 #1

PDB 可以关闭超过 2 或 3 条线路吗?

你说你从未见过超过几行的 PDB。40 行似乎太多了,尤其是当反编译的代码看起来没有太大区别时。

然而,事实并非如此,可以通过 2 行来证明:创建一个 String 对象,将其设置为并调用 .编译并运行。接下来,插入 30 行注释,保存文件,但不要重新编译。再次运行应用程序。应用程序仍然崩溃,但报告的内容相差 30 行(屏幕截图中的第 14 行与第 44 行)。nullToString()

它与编译的代码完全无关。这样的事情很容易发生:

  • 代码重新格式化,例如按可见性对方法进行排序,因此方法向上移动了 40 行
  • 代码重新格式化,例如在 80 个字符处中断长行,通常这会使内容向下移动
  • 优化 usings (R#),删除了 30 行不需要的导入,因此该方法上移了
  • 插入注释或换行符
  • 在部署的版本(与 PDB 匹配)来自主干(或类似)时切换到分支

PDBs off by 30 lines

在你的情况下,这怎么会发生?

如果它真的像你所说的那样,并且你认真地审查了你的代码,那么有两个潜在的问题:

  • EXE 或 DLL 与 PDB 不匹配,PDB 可以轻松检查
  • PDB 与源代码不匹配,更难识别

多线程可以将对象设置为最不期望的时间,即使它之前已经初始化过。在这种情况下,NullReferenceExceptions 不仅可以在 40 行之外,甚至可以位于完全不同的类中,因此可以位于文件中。null

如何继续

捕获转储

我首先会尝试转储情况。这允许您捕获状态并详细查看所有内容,而无需在开发人员计算机上重现它。

有关 ASP.NET,请参阅 MSDN 博客 Steps to Trigger a User Dump of a Process with DebugDiag when a Specific .net Exception is ThrownTess 的博客

在任何情况下,请始终捕获包含完整内存的转储。还要记住从发生崩溃的计算机收集所有必要的文件(SOS.dll 和 mscordacwks.dll)。可以使用 MscordacwksCollector(免责声明:我是它的作者)。

检查符号

查看 EXE/DLL 是否真的与您的 PDB 匹配。在 WinDbg 中,以下命令很有帮助

!sym noisy
.reload /f
lm
!lmi <module>

在 WinDbg 之外,但仍使用适用于 Windows 的调试工具:

symchk /if <exe> /s <pdbdir> /av /od /pf

第三方工具,ChkMatch

chkmatch -c <exe> <pdb>

检查源代码

如果 PDB 与 DLL 匹配,则下一步是检查源代码是否属于 PDB。如果将 PDB 与源代码一起提交到版本控制,这是最好的方法。如果这样做,则可以在源代码管理中搜索匹配的 PDB,然后获取源代码和 PDB 的相同修订版。

如果你没有这样做,你就很不幸了,你可能不应该使用源代码,而只使用 PDB。对于 .NET,这效果很好。我在没有收到源代码的情况下使用 WinDbg 在第三方代码中调试了很多内容,我可以走得很远。

如果使用 WinDbg,则以下命令很有用(按此顺序)

.symfix c:\symbols
.loadby sos clr
!threads    
~#s
!clrstack
!pe

为什么代码在 StackOverflow 上如此重要

另外,我查看了 View() 方法的代码,它无法抛出 NullReferenceException

好吧,其他人以前也发表过类似的声明。很容易忽略某些东西。

下面是一个真实世界的示例,只是最小化了,并且是伪代码。在第一个版本中,该语句尚不存在,DoWork() 可以从多个线程调用。很快,声明就被提出来了,一切都很顺利。离开锁时,永远会是一个有效的对象,对吧?locklocksomeobj

var someobj = new SomeObj(); 
private void OnButtonClick(...)
{
    DoWork();
}

var a = new object();   
private void DoWork()
{
    lock(a) {
        try {
            someobj.DoSomething();
            someobj = null;
            DoEvents();             
        }
        finally
        {
            someobj = new SomeObj();
        }
    }   
}

直到一个用户再次报告了相同的错误。我们确信该错误已修复,这是不可能的。但是,这是一个“双击用户”,即双击任何可以单击的内容的人。

DoEvents() 调用当然不在如此显眼的位置,它导致同一线程再次输入锁(这是合法的)。这一次,是 ,在似乎不可能为 null 的地方导致 NullReferenceException。someobjnull

第二次,是返回boolValue?RedirectToAction(“A1”,“C1”) : RedirectToAction(“A2”, “C2”).boolValue 是一个表达式,它不可能抛出 NullReferenceException

为什么不呢?什么是boolValue?带有吸气剂和二传手的属性?还要考虑以下(可能有点偏离)情况,其中仅采用常量参数,看起来像一个方法,抛出异常但仍然不在调用堆栈上。这就是为什么在 StackOverflow 上查看代码如此重要的原因......RedirectToAction

Screenshot: method with constant parameters not on callstack

3赞 mreyeros 11/25/2014 #2

只是一个想法,但我能想到的一件事是,也许您的构建定义/配置可能会推出应用程序 dll 的不同步编译版本,这就是为什么当您从堆栈跟踪中查找行号时,您会在机器上看到差异。

评论

1赞 John Saunders 11/25/2014
我将检查日期,但这是此应用程序首次部署到生产环境,因此不可能有 40 行差异的先前版本。
9赞 Vikas Gupta 11/27/2014 #3

曾经在生产代码中看到过这种行为。虽然细节有点模糊(那是大约 2 年前的事了,虽然我可以找到电子邮件,但我无法再访问代码,也无法访问转储等)

仅供参考,这是我写给团队的内容(大邮件中的非常小的部分)——

// Code at TeamProvider.cs:line 34
Team securedTeam = TeamProvider.GetTeamByPath(teamPath); // Static method call.

“这里不可能发生空引用异常。”

后来,经过更多的倾倒潜水

“调查结果——

  1. 该问题发生在 DBI 中,因为它没有 root/BRH 团队。UI 没有正常处理 CLib 返回的 null,因此会出现异常。
  2. UI 上显示的堆栈跟踪具有误导性,这是由于 Jitter 和 CPU 可以优化/重新排序指令,导致堆栈跟踪“撒谎”。

深入研究进程转储发现了问题所在,并已确认 DBI 确实没有上述团队。


我认为,这里要注意的是上面粗体的陈述,与您的分析和陈述形成鲜明对比——

"我只是看了一下反编译的源代码,它似乎没有被重新排列。

"在我的本地计算机上运行的生产版本显示正确的行号。"

这个想法是优化可以在不同的层面上进行。而那些在编译时完成的只是其中的一部分。今天,特别是对于像这样的托管环境,在发出 IL 时所做的优化实际上相对较少(为什么 10 种不同 .Net 语言的 10 个编译器要尝试做相同的优化集,而发出的中间语言代码将进一步转换为机器代码,无论是通过 ngen 还是 Jitter)。.Net

因此,您所观察到的只能通过查看生产机器转储中的抖动机器代码(又名程序集)来确认。


我可以看到的一个问题是 - 为什么 Jitter 会在生产机器上发出不同的代码,与您的机器相比,对于相同的构建?

回答 - 我不知道。我不是 Jit 专家,但我相信它可以......因为正如我上面所说..今天,与 5-10 年前使用的技术相比,这些东西要复杂得多。谁知道,所有因素是什么..比如“内存、CPU 数量、CPU 负载、32 位与 64 位、Numa 与非 Numa、方法执行的次数、方法的大小、谁调用它、它调用什么、多少次、内存位置的访问模式等”,它在进行这些优化时会查看。

对于您的情况,到目前为止,只有您可以重现它,并且只有您可以访问您的 jitted 生产环境中的代码。因此,(如果我可以这么说的话:))这是任何人都能想到的最好的答案。


编辑: 一台机器与另一台机器上的抖动之间的重要区别也可能是抖动本身的版本。我想,随着 .net 框架发布了几个补丁和 KB,谁知道即使是微小的版本差异也会有什么优化行为抖动的差异。

换句话说,仅仅假设两台计算机具有相同的框架主要版本(例如 .Net 4.5 SP1)是不够的。生产环境可能没有每天发布的补丁,但您的开发/私有机器可能在上周二发布了补丁。


编辑 2概念验证 - 即抖动优化可能导致堆栈跟踪。

自己运行以下代码,构建,优化打开,全部关闭,关闭。从 Visual Studio 编译,但从资源管理器运行并尝试猜测堆栈跟踪会告诉您异常在哪一行?Releasex64TRACEDEBUGVisual Studio Hosting Process

class Program
{
    static void Main(string[] args)
    {
        string bar = ReturnMeNull();

        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine(i);
        }

        for (int i = 0; i < bar.Length; i++)
        {
            Console.WriteLine(i);
        }

        Console.ReadLine();

        return;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static string ReturnMeNull()
    {
        return null;
    }
}

不幸的是,经过几次尝试,我仍然无法重现您看到的确切问题(即返回语句上的错误),因为只有您可以访问确切的代码,以及它可能具有的任何特定代码模式。或者,再一次,它是其他一些抖动优化,没有记录在案,因此很难猜测。

评论

1赞 bzlm 11/27/2014
但行号是原始源代码行号,例外是托管异常。为什么在 IL 和机器代码之间所做的重新排列会对 IL 和原始源代码行号之间的关系产生任何影响?
0赞 Vikas Gupta 11/27/2014
因为在构建异常堆栈跟踪时,运行时必须遍历堆栈以构建堆栈跟踪。它在运行时所拥有的是机器代码中的堆栈,而不是 IL 代码(当机器代码被重新排列时,我认为机器代码不能那么容易地追溯到 IL..至少应用程序不需要它才能正常运行,显然,缺乏机器代码到 IL 代码的映射,是它咬人的那种场景)。
0赞 John Saunders 11/28/2014
维卡斯,非常感谢。这是一个非常有趣的想法。我必须检查的一件事是:我们的生产服务器是负载平衡的。我想知道它们是否都运行相同的硬件。当然,该硬件与我的本地机器(笔记本电脑)不同。
1赞 Jeremy Thompson 11/29/2014 #4

该问题及其症状闻起来是硬件问题,例如:

我们似乎无缘无故地失去会话状态(没有重新启动 或任何东西)。

如果使用 InProc 会话状态存储,请切换到进程外。这将帮助您将丢失会话的问题与您报告的 NRE 上 PDB 行号不匹配的症状隔离开来。如果使用进程外存储,请在服务器上运行一些诊断实用程序。

ps 发布 DebugDiag 的输出。我可能应该把这个答案作为评论,但已经有太多了,需要将它们隔开并分别评论不同的诊断步骤。

评论

0赞 Jeremy Thompson 11/29/2014
如果您无法公开提供转储,那么也许您最好的停靠港是 Microsoft PSS。这是一项很好的服务,他们也有 windbg 专家和私人符号。很可能会让你从阅读苔丝的博客和他们真正感兴趣的话题中节省几天时间,祝你好运!
1赞 John Saunders 11/29/2014
杰里米,我建议他们试试PSS。顺便说一句,我们使用的是 SQL Server 会话状态。我偶尔会看到有关无法连接到 SQL Server 的异常。远远少于“我们丢失了会话状态”异常的数量,并且在时间上没有明显的相关性。不幸的是,用户没有报告问题,所以这不是一个高优先级。