Подтвердить что ты не робот

Как трассировка стека указывает на неправильную линию (выражение "возврат" ) - 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;".
4b9b3361

Ответ 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. Вероятно, мне следовало бы поместить этот ответ в качестве комментария, но их уже слишком много, нужно их просто пропустить и отдельно прокомментировать различные диагностические шаги.