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

我有两次现在看到从Production 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就很容易修复。 如果exception可能发生在控制器动作中的任何地方,那就不那么容易了!

有没有人见过这样的东西,无论是在ASP.NET MVC还是其他地方? 我愿意相信它是Release版本和Debug版本之间的区别,但仍然需要关闭40行?


编辑:

要明确:我是“ 什么是NullReferenceException以及如何解决它? ”的原始作者。 我知道NullReferenceException是什么。 这个问题是关于为什么堆栈跟踪如此遥远。 我已经看到由于PDB不匹配导致堆栈跟踪被一两行关闭的情况。 我见过没有PDB的情况,所以你没有得到行号。 但我从未见过堆栈跟踪偏离32行的情况。

编辑2:

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

 SomeMethod(someObject.SomeProperty); 

有可能在优化期间重新组织了代码,以便实际的NullReferenceException发生在更接近return ,而PDB实际上只有几行。 但我没有看到有机会以一种会导致代码移动32行的方式重新排列方法调用。 事实上,我只是看了反编译的源代码,它似乎没有被重新排列。

这两种情况的共同点是:

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

编辑3:

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

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

编辑4:

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

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

编辑5:

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

编辑6:

为了记录,这只是在第三个控制器中再次发生。 堆栈跟踪直接指向方法的return语句。 这个 return语句只是return model; 。 我认为没有办法导致NullReferenceException

编辑6a:

实际上,我只是仔细查看了日志,发现了几个不是 NullReferenceExceptionexception,并且在return语句中仍然有堆栈跟踪点。 这两种情况都是在控制器操作调用的方法中,而不是直接在操作方法本身中调用 。 其中一个是显式抛出的InvalidOperationException ,其中一个是简单的FormatException


以下是我直到现在还没有想到的一些事实:

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

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

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

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

“这里不会发生空引用exception。”

之后,更多的倾倒潜水

“发现 –

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

挖掘进程转储揭示了这个问题,并且已经证实DBI确实没有上面提到的团队。“


我想,这里要注意的是上面的粗体声明,与你的分析和陈述形成对比

我只看了反编译的来源,它似乎没有重新排列。 ”或者

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

我们的想法是优化可以在不同的层次上进行..而在编译时完成的只是其中的一部分。 今天,尤其是像.Net这样的托管环境,实际上在发送IL时进行的优化相对较少(为什么10个不同的.Net语言的10个编译器会尝试进行同一组优化,当发出的中间语言代码将进一步转换为机器代码,由ngen或Jitter提供)。

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


我能看到的一个问题是 – 为什么Jitter会在生产机器上发出不同的代码,与机器相比,同样的构建?

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

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


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

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


编辑2概念certificate – 即抖动优化可能导致堆叠迹线。

自己运行以下代码, Release build, x64 ,Optimizations on ,所有TRACEDEBUG 关闭Visual Studio Hosting Process 关闭 。 从visual studio编译,但从资源管理器运行并尝试猜测堆栈跟踪将告诉您exception的哪一行?

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

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

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

你声明你从未见过多于几行的PDB。 40行似乎太多了,特别是当反编译的代码看起来没什么区别时。

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

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

  • 代码重新格式化,例如,通过可见性对方法进行排序,因此该方法向上移动了40行
  • 代码重新格式化,例如将长行分成80个字符,通常这会使事情发生变化
  • 优化使用(R#),删除30行不需要的导入,因此该方法向上移动
  • 插入评论或换行符
  • 切换到分支,而部署版本(匹配PDB)来自主干(或类似)

PDB关闭了30行

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

如果它真的如您所说并且您认真审核了代码,则存在两个潜在问题:

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

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

怎么继续

捕获转储

我首先尝试了解情况。 这使您可以捕获状态并详细查看所有内容,而无需在开发人员计算机上重现它。

对于ASP.NET,请参阅MSDN博客当抛出特定的.netexception或Tess的博客 时,使用DebugDiag触发进程的用户转储的步骤 。

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

检查符号

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

 !sym noisy .reload /f lm !lmi  

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

 symchk /if  /s  /av /od /pf 

第三方工具, ChkMatch :

 chkmatch -c   

检查源代码

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

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

如果使用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()调用当然不在如此突出的位置,导致锁被同一个线程再次输入(这是合法的)。 这次, someobjnull ,在一个似乎无法为null的地方导致NullReferenceException。

第二次,它是返回boolValue? RedirectToAction(“A1”,“C1”):RedirectToAction(“A2”,“C2”)。 boolValue是一个无法抛出NullReferenceException的表达式

为什么不? 什么是boolValue? 有吸气剂和二传手的财产? 还要考虑以下(可能有点偏)的情况,其中RedirectToAction仅采用常量参数,看起来像一个方法,抛出exception但仍然不在callstack上。 这就是为什么在StackOverflow上查看代码非常重要…

屏幕截图:不在callstack上的常量参数的方法

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

问题及其症状都有硬件问题,例如:

我们似乎没有充分的理由失去会话状态(没有重启或任何事情)。

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

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