Как трассировка стека указывает на неправильную линию (выражение "возврат" ) - 40 строк
Я уже дважды видел, что NullReferenceException
зарегистрировался из веб-приложения Production ASP.NET MVC 4 и записал неверную строку. Неправильно ли строка или две (например, вы получили бы несоответствие 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 строк?
EDIT:
Чтобы быть ясным: я автор оригинала Что такое исключение NullReferenceException и как его исправить?". Я знаю, что такое NullReferenceException
. Этот вопрос связан с тем, почему трассировка стека может быть настолько далека. Я видел случаи, когда трассировка стека отключена на строку или две из-за несоответствия PDB. Я видел случаи, когда нет PDB, поэтому вы не получаете номера строк. Но я никогда не видел случая, когда трассировка стека отключена на 32 строки.
ИЗМЕНИТЬ 2:
Обратите внимание, что это произошло с двумя отдельными действиями контроллера внутри одного контроллера. Их код сильно отличается друг от друга. Фактически, в первом случае NullReferenceException
даже не встречался в условном выражении: это было примерно так:
SomeMethod(someObject.SomeProperty);
Был некоторый шанс, что код был реорганизован во время оптимизации, так что фактический NullReferenceException
стал ближе к return
, и PDB фактически был только выключен несколькими строками. Но я не вижу возможности переупорядочить вызов метода таким образом, чтобы код мог перемещаться на 32 строки. Фактически, я просто посмотрел на декомпилированный источник, и он, похоже, не был перестроен.
Что общего у этих двух случаев:
- Они встречаются в одном контроллере (пока)
- В обоих случаях трассировка стека указывает на оператор
return
, и в обоих случаях NullReferenceException
произошло в 30 или более строках от оператора return
.
ИЗМЕНИТЬ 3:
Я только что сделал эксперимент - я только что перестроил решение, используя конфигурацию сборки "Производство", которую мы развернули на наших производственных серверах. Я запускал решение на своем локальном IIS без изменения конфигурации IIS.
Трассировка стека показала правильный номер строки.
EDIT 4:
Я не знаю, является ли это актуальным, но обстоятельство, вызывающее NullReferenceException
, столь же необычно, как и сам номер "неправильной строки". Похоже, мы теряем состояние сеанса без уважительной причины (никаких перезагрузок или чего-то еще). Это не слишком странно. Странная часть заключается в том, что наш Session_Start должен перенаправляться на страницу входа в систему, когда это произойдет. Любая попытка воспроизвести потерю сеанса вызывает перенаправление на страницу входа. Впоследствии, используя кнопку браузера "Назад" или вручную введя предыдущий URL-адрес, вы вернетесь на страницу входа в систему, не нажимая на соответствующий контроллер.
Так что, может быть, две странные проблемы - действительно одна очень странная проблема.
РЕДАКТИРОВАТЬ 5:
Мне удалось получить файл .PDB и посмотреть на него с dia2dump. Я думал, что возможно, что PDB перепутался, и для метода была только строка 72. Это не так. Все номера строк присутствуют в PDB.
EDIT 6:
Для записи это снова произошло в третьем контроллере. Трассировка стека указывает прямо на оператор возврата метода. Этот оператор возврата просто return model;
. Я не думаю, что для этого есть способ .
Изменить 6a:
Фактически, я просто более внимательно посмотрел на журнал и нашел несколько исключений, которые не являются NullReferenceException
, и которые по-прежнему имеют точку трассировки стека в инструкции return
. Оба эти случая находятся в методах, вызванных действием контроллера, а не непосредственно в самом методе действий. Один из них был явно брошен InvalidOperationException
, а один из них был простым FormatException
.
Вот несколько фактов, которые я до сих пор не считал актуальными:
-
Application_Error
в global.asax - это то, что заставляет эти исключения регистрироваться. Он выбирает исключения с помощью Server.GetLastError()
. - Механизм регистрации регистрирует трассировку сообщений и стека отдельно (вместо записи
ex.ToString()
, что было бы моей рекомендацией). В частности, трассировка стека, о которой я просил, происходит от ex.StackTrace
.
-
FormatException
был поднят в System.DateTime.Parse
, вызванном из System.Convert.ToDate
, вызванным из нашего кода. Строка трассировки стека, указывающая на наш код, - это строка, указывающая на "return model;
".
Ответы
Ответ 1
Я видел такое поведение в производственном коде один раз. Хотя детали немного расплывчаты (это было примерно 2 года назад, и хотя я могу найти электронное письмо, у меня больше нет доступа к коду, и не дампы и т.д.)
FYI, это то, что я написал команде (очень мелкие части из большой почты) -
// Code at TeamProvider.cs:line 34
Team securedTeam = TeamProvider.GetTeamByPath(teamPath); // Static method call.
"Никоим образом не может быть исключение ссылочной ссылки".
Позже, после более дайвинга
"Выводы -
- Проблема произошла в DBI, потому что у нее не было команды root/BRH. UI не обрабатывает null, возвращенный CLIB изящно, и, следовательно, исключение.
- Трассировка стека, отображаемая в пользовательском интерфейсе, вводит в заблуждение и объясняется тем, что Jitter и CPU могут оптимизировать/переупорядочить инструкции, заставляя трассировки стека "лежать".
Копание в свалке процесса выявило проблему, и было подтверждено, что DBI действительно не имел вышеупомянутой команды.
Я думаю, здесь стоит отметить выражение, выделенное жирным шрифтом выше, в контраст с вашим анализом и утверждением -
" Я просто посмотрел на декомпилированный источник, и он, похоже, не был перестроен." или
" Производственная сборка, запущенная на моей локальной машине, показывает правильный номер строки."
Идея состоит в том, что оптимизация может происходить на разных уровнях.. и те, которые выполняются во время компиляции, - это лишь некоторые из них. Сегодня, особенно с управляемой средой, такой как .Net
, во время испускания ИЛ на самом деле относительно меньше оптимизаций (почему 10 компиляторов для 10 разных .Net-языков пытаются выполнить тот же набор оптимизаций, когда испускаемый Промежуточный Код языка будет далее преобразован в машинный код, либо ngen, либо Jitter).
Следовательно, то, что вы наблюдали, может быть подтверждено только при просмотре машинного кода (ака сборки) из дампа с производственной машины.
Один вопрос, который я вижу, - . Почему Jitter выбрасывает другой код на Production Machine по сравнению с вашей машиной для той же сборки?
Ответ. Не знаю. Я не эксперт по джитту, но я уверен, что это может... потому что, как я сказал выше, сегодня эти вещи более сложны по сравнению с технологиями, используемыми 5-10 лет назад. Кто знает, что все факторы.. как "память, количество процессоров, загрузка процессора, 32-битное и 64-битное число, Numa vs Non-Numa, количество раз, когда был выполнен метод, как маленький или большой метод, кто его называет, что он вызывает, сколько раз, шаблоны доступа к ячейкам памяти и т.д. и т.д.", на это он смотрит, делая эти оптимизации.
В вашем случае пока вы можете воспроизвести только его, и только у вас есть доступ к вашему джиттеру
код в производстве. Следовательно, (если я могу так сказать:)), это лучший ответ, который любой может придумать.
ИЗМЕНИТЬ:
Важное различие между дрожанием на одной машине и другим, также может быть версией самого джиттера. Я бы предположил, что, поскольку несколько патчей и KBs выпущены для .net-структуры, кто знает, какие различия в динамическом поведении дрожания могут иметь даже отличия версии незначительные.
Другими словами, это не, достаточный, чтобы предположить, что обе машины имеют одну и ту же основную версию фреймворка (скажем,.Net 4.5 SP1). У продукта могут не быть исправлений, которые выпускаются каждый день, но ваш dev/private machine может быть выпущен в прошлый вторник.
РЕДАКТИРОВАТЬ 2: Доказательство концепции - т.е. оптимизация дрожания может привести к трассировке лежащих стеков.
Запустите следующий код самостоятельно, Release
build, x64
, Оптимизации на, все TRACE
и DEBUG
повернули выключено, 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, которая не документирована и, следовательно, трудно догадаться.
Ответ 2
Может ли PDB отключить более 2 или 3 строк?
Вы даете утверждение, что вы никогда не видели PDB с нескольких строк. 40 строк, кажется, слишком много, особенно если декомпилированный код не имеет большого значения.
Однако это неверно и может быть доказано 2-х линейными: создать объект String, установить его на null
и вызвать ToString()
. Скомпилируйте и запустите. Затем вставьте комментарий в 30 строк, сохраните файл, но не перекомпилируйте. Запустите приложение еще раз. Приложение по-прежнему выходит из строя, но дает разницу в 30 строк в том, что она сообщает (строка 14 против 44 на скриншоте).
Он вообще не связан с кодом, который компилируется. Такие вещи могут легко произойти:
- переформатировать код, который, например, сортирует методы по видимости, поэтому метод перемещался на 40 строк
- переформатировать код, который, например, разбивает длинные строки на 80 символов, обычно это перемещает вещи вниз.
- оптимизировать использование (R #), который удаляет 30 строк ненужного импорта, поэтому метод перемещается вверх
- вставка комментариев или новых строк
- переключившись на ветвь, в то время как развернутая версия (соответствующая PDB) находится из соединительной линии (или аналогичной)
![PDBs off by 30 lines]()
Как это может произойти в вашем случае?
Если это действительно так, как вы говорите, и вы серьезно пересмотрели свой код, есть две потенциальные проблемы:
- EXE или DLL не соответствуют PDB, которые можно легко проверить.
- PDB не соответствуют исходному коду, который сложнее идентифицировать
Многопоточность может устанавливать объекты на null
, когда вы меньше всего этого ожидаете, даже если они были инициализированы ранее. В этом случае NullReferenceExceptions могут быть не только на расстоянии 40 строк, но даже в совершенно другом классе и, следовательно, файле.
Как продолжить
Захват дампа
Сначала я попытался получить свалку ситуации. Это позволяет вам фиксировать состояние и подробно смотреть на все без необходимости воспроизведения его на вашей машине разработчика.
Для ASP.NET см. блог MSDN Шаги для запуска дампа пользователя процесса с помощью DebugDiag, когда выбрано конкретное исключение .net или блог Тесс.
В любом случае всегда записывайте дамп, включая полную память. Также не забудьте собрать все необходимые файлы (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
Ну, другие люди делали подобные заявления раньше. Легко упустить что-то.
Ниже приведен пример реального мира, только сведенный к минимуму и в псевдокоде. В первой версии оператор 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(), который, конечно же, не был таким заметным местом, заставлял замок вводить снова тот же поток (что является законным). На этот раз someobj
был null
, вызывая исключение NullReferenceException в месте, где казалось невозможным быть нулевым.
Что второй раз, это было возвращение boolValue? RedirectToAction ( "A1", "C1" ): RedirectToAction ( "A2", "C2" ). BoolValue было выражением, которое не могло бы вызвать исключение NullReferenceException
Почему бы и нет? Что такое boolValue? Свойство с геттером и сеттером? Также рассмотрите следующий (возможно, бит) случай, где RedirectToAction
принимает только постоянные параметры, выглядит как метод, выдает исключение, но все еще не входит в стоп-код. Вот почему так важно видеть код в StackOverflow...
![Screenshot: method with constant parameters not on callstack]()
Ответ 3
Просто мысль, но одна вещь, о которой я могу думать, заключается в том, что, возможно, есть вероятность, что ваше определение/конфигурация сборки выталкивает из синхронизированной скомпилированной версии ваших приложений dll (s), и именно поэтому вы см. расхождение на вашем компьютере, когда вы просматриваете номер строки из stacktrace.
Ответ 4
Проблема и ее симптомы пахнут аппаратной проблемой, например:
Мы, кажется, теряем состояние сеанса без уважительной причины (без перезагрузки или что-то еще).
Если использование InProc Session State Storage переключится в нерабочее. Это поможет вам изолировать проблему потери сеансов от симптома несогласованных номеров номеров PDB в NRE, о котором вы сообщаете. Если вы используете вне хранилища процессов, запустите на сервере некоторые диагностические утилиты.
ps выводит результат из DebugDiag. Вероятно, мне следовало бы поместить этот ответ в качестве комментария, но их уже слишком много, нужно их просто пропустить и отдельно прокомментировать различные диагностические шаги.