【问题标题】:How Can a Stack Trace Point to the Wrong Line (the "return" Statement) - 40 Lines Off堆栈跟踪如何指向错误的行(“return”语句) - 40 行
【发布时间】:2015-01-20 04:03:09
【问题描述】:

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

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 或其他地方见过类似的东西?我愿意相信这是 Release 构建和 Debug 构建之间的区别,但仍然相差 40 行?


编辑:

明确一点:我是“What is a NullReferenceException and how do I fix it?”的原作者。我知道NullReferenceException 是什么。这个问题是关于为什么堆栈跟踪会如此远离。我见过由于 PDB 不匹配而导致堆栈跟踪关闭一两行的情况。我见过没有 PDB 的情况,所以你没有得到行号。但我从未见过堆栈跟踪偏离 32 行的情况。

编辑 2:

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

SomeMethod(someObject.SomeProperty);

在优化过程中,有可能代码已被重新​​组织,因此实际的NullReferenceException 更接近return,而 PDB 实际上只偏离了几行。但是我看不到重新排列方法调用的机会,这种方式会导致代码移动 32 行。其实我只是看了一下反编译的源码,好像没有重新整理过。

这两种情况的共同点是:

  1. 它们出现在同一个控制器中(到目前为止)
  2. 在这两种情况下,堆栈跟踪都指向return 语句,在这两种情况下,NullReferenceException 都出现在距return 语句 30 行或更多行的地方。

编辑 3:

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

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

编辑 4:

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

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

编辑 5:

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

编辑 6:

为了记录,这又发生了,在第三个控制器中。堆栈跟踪直接指向方法的返回语句。 这个返回语句就是return model;。我认为没有任何方法可以导致NullReferenceException

编辑 6a:

事实上,我只是更仔细地查看了日志并发现了几个不是 NullReferenceException 的异常,它们仍然在return 语句处具有堆栈跟踪点。这两种情况都在控制器动作中调用的方法中,而不是直接在动作方法本身中。其中一个是明确抛出的InvalidOperationException,一个是简单的FormatException


以下是一些我之前认为不相关的事实:

  1. global.asax 中的Application_Error 是导致记录这些异常的原因。它通过使用Server.GetLastError() 来获取异常。
  2. 日志记录机制分别记录消息和堆栈跟踪(而不是记录ex.ToString(),这本来是我的建议)。特别是,我一直在询问的堆栈跟踪来自 ex.StackTrace
  3. FormatExceptionSystem.DateTime.Parse 中引发,从 System.Convert.ToDate 调用,从我们的代码调用。指向我们代码的堆栈跟踪行是指向“return model;”的行。

【问题讨论】:

  • 有时视图本身可能存在具有空引用的逻辑。示例模型属性或可能是模型本身。检查您的视图以确保其中没有失败的内容。
  • 我赞同 Azhar 的评论。我的猜测是你的视图在你的模型上调用了一些空的东西。
  • 有趣的是,为什么人们对“我应该在这里获得例外,而不是在那里获得例外”的问题投反对票...
  • @BrianDriscoll View() 不会在视图上调用Execute(),因此视图中的任何 NRE 都不会在控制器的方法中抛出。我倾向于认为示例代码已经减少到永远无法重现异常的程度。就目前而言,这个问题无法回答,因为它没有Minimal, Complete, and Verifiable example
  • 使用来自 msft 的调试诊断。这将允许您在发生访问冲突时获取用户转储。然后你可以加载到windbg,看看你得到了什么堆栈。苔丝的博客可能会有所帮助blogs.msdn.com/b/tess

标签: c# asp.net asp.net-mvc asp.net-mvc-4 nullreferenceexception


【解决方案1】:

PDB 可以关闭超过 2 或 3 行吗?

您声明您从未见过 PDB 关闭超过几行。 40 行似乎太多了,尤其是反编译后的代码看起来差别不大的时候。

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

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

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

你的情况怎么会这样?

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

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

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

如何继续

捕获转储

我会首先尝试了解情况。这允许您捕获状态并详细查看所有内容,而无需在您的开发人员机器上复制它。

对于 ASP.NET,请参阅 MSDN 博客 Steps to Trigger a User Dump of a Process with DebugDiag when a Specific .net Exception is ThrownTess' blog

在任何情况下,始终捕获包含完整内存的转储。还要记住从发生崩溃的机器上收集所有必要的文件(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 在 3rd 方代码中调试了很多,我可以走得很远。

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

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

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

另外,我看了View()方法的代码,没有办法抛出NullReferenceException

嗯,其他人之前也发表过类似的声明。很容易忽略一些东西。

以下是一个真实的示例,只是最小化的伪代码。在第一个版本中,lock 语句尚不存在,并且可以从多个线程调用 DoWork()。很快,lock 声明被引入,一切顺利。离开锁时,someobj 将始终是有效对象,对吧?

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() 调用,当然不是在这么显眼的地方,导致 same 线程再次进入锁(这是合法的)。这一次,someobjnull,导致在一个似乎不可能为 null 的地方出现 NullReferenceException。

第二次是 return boolValue ? RedirectToAction(“A1”,“C1”):RedirectToAction(“A2”,“C2”)。 boolValue 是一个不能抛出 NullReferenceException 的表达式

为什么不呢?什么是布尔值?具有 getter 和 setter 的属性?还要考虑以下(可能有点偏离)的情况,其中RedirectToAction 只接受常量参数,看起来像一个方法,抛出异常但仍然不在调用堆栈上。这就是为什么在 StackOverflow 上查看代码如此重要的原因......

【讨论】:

    【解决方案2】:

    我在生产代码中见过这种行为一次。虽然细节有点模糊(大约是 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 确实没有上述团队。”


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

    我只是看了反编译的源码,好像没有重新排列。”,或者

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

    这个想法是优化可以发生在不同的级别。在编译时完成的只是其中的一部分。今天,尤其是像.Net 这样的托管环境,在发出 IL 时实际上完成的优化相对较少(为什么 10 个编译器针对 10 种不同的 .Net 语言尝试做相同的优化,当发出 Intermediate 语言代码将通过 ngen 或 Jitter 进一步转换为机器代码。

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


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

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

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


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

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


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

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

    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;
        }
    }
    

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

    【讨论】:

    • 但是行号是原始源代码行号,异常是托管异常。为什么 IL 和机器码之间的重新排列会影响 IL 和原始源代码行号之间的关系?
    • 因为在构建异常堆栈跟踪时,运行时必须遍历堆栈以构建堆栈跟踪.. 而它在运行时拥有的是机器代码中的堆栈,而不是 IL 代码(当机器代码已经重新排列,我不认为机器代码可以那么容易地追溯到 IL.. 至少应用程序不需要正确运行,显然,缺少机器代码到 IL 代码映射,是它咬人的那种场景)。
    • 维卡斯,非常感谢。这是一个非常有趣的想法。我必须检查一件事:我们的生产服务器是负载平衡的。我想知道他们是否都运行相同的硬件。当然,该硬件与我的本地机器(笔记本电脑)不同。
    【解决方案3】:

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

    【讨论】:

    • 我会检查日期,但这是此应用程序首次部署到生产环境,因此之前的版本不可能有 40 行差异。
    【解决方案4】:

    问题及其症状有硬件问题的味道,例如:

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

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

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

    【讨论】:

    • 如果您不能公开提供转储,那么您最好的停靠港可能是 Microsoft PSS。这是一项很好的服务,他们也有windbg专家和私人符号。很可能会节省您阅读 Tess 的博客和他们真正感兴趣的主题的时间,祝您好运!
    • Jeremy,我建议他们试试 PSS。顺便说一句,我们正在使用 SQL Server 会话状态。我偶尔会看到有关无法连接到 SQL Server 的异常。远远少于“我们丢失会话状态”异常的数量,并且在时间上明显不相关。不幸的是,用户没有报告问题,所以这不是一个高优先级。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-19
    • 1970-01-01
    • 1970-01-01
    • 2011-08-10
    • 1970-01-01
    相关资源
    最近更新 更多