c# asp.net asp.net-mvc asp.net-mvc-4 nullreferenceexception

c# - Cómo puede un punto de trazado de apilamiento a la línea incorrecta(la declaración de "retorno")-40 líneas desactivadas



asp.net asp.net-mvc (4)

Ya he visto dos veces una NullReferenceException registrada desde una aplicación web Production ASP.NET MVC 4 y he iniciado sesión en la línea incorrecta. No está mal por una línea o dos (como lo haría con una discrepancia de PDB), pero está mal por la duración de toda la acción del controlador. Ejemplo:

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 }

Esto ha sucedido dos veces para acciones en el mismo controlador. El segundo caso fue registrado

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

Esto es muy perturbador. NullReferenceException es muy fácil de arreglar una vez que sabes en qué línea ocurre. ¡No es tan fácil si la excepción podría haber sucedido en cualquier lugar dentro de la acción del controlador!

¿Alguien ha visto algo así en absoluto, ya sea en ASP.NET MVC o en otro lugar? Estoy dispuesto a creer que es la diferencia entre una compilación Release y una compilación Debug, pero aún así, ¿estar fuera de las 40 líneas?

EDITAR:

Para ser claro: soy el autor original de " ¿Qué es una NullReferenceException y cómo la soluciono? ". Sé lo que es una NullReferenceException . Esta pregunta se trata de por qué la traza de pila podría estar tan lejos. He visto casos en los que un seguimiento de pila está desactivado por una línea o dos debido a una discrepancia de PDB. He visto casos en los que no hay AP, así que no obtienes números de línea. Pero nunca he visto un caso en el que el seguimiento de la pila esté desactivado en 32 líneas.

EDICION 2:

Tenga en cuenta que esto ha sucedido con dos acciones de controlador separadas dentro del mismo controlador. Su código es bastante diferente el uno del otro. De hecho, en el primer caso, la NullReferenceException ni siquiera se produjo en un condicional, era más como esto:

SomeMethod(someObject.SomeProperty);

Hubo alguna posibilidad de que el código se reorganizara durante la optimización, de modo que la NullReferenceException real se produjo más cerca de la return , y el AP estaba, de hecho, solo apagado por unas pocas líneas. Pero no veo la oportunidad de reorganizar una llamada a un método de forma tal que el código se mueva en 32 líneas. De hecho, solo miré la fuente descompilada, y no parece haber sido reorganizada.

Lo que estos dos casos tienen en común son:

  1. Ocurren en el mismo controlador (hasta ahora)
  2. En ambos casos, el seguimiento de la pila apunta a la instrucción return , y en ambos casos, la NullReferenceException produjo a 30 o más líneas de la declaración return .

EDIT 3:

Acabo de hacer un experimento. Acabo de reconstruir la solución utilizando la configuración de compilación "Producción" que hemos implementado en nuestros servidores de producción. Ejecuté la solución en mi IIS local, sin cambiar la configuración de IIS.

El seguimiento de pila mostró el número de línea correcto.

EDIT 4:

No sé si esto es relevante, pero la circunstancia que causa la NullReferenceException es tan inusual como este problema de "número de línea incorrecta". Parece que estamos perdiendo el estado de la sesión sin una buena razón (no hay reinicios ni nada). Eso no es muy extraño. La parte extraña es que nuestro Session_Start debería estar redirigiendo a la página de inicio de sesión cuando eso sucede. Cualquier intento de reproducir la pérdida de sesión provoca el redireccionamiento a la página de inicio de sesión. Posteriormente, al utilizar el botón "Atrás" del navegador o al ingresar manualmente la URL anterior, vuelve directamente a la página de inicio de sesión sin tocar el controlador en cuestión.

Así que tal vez dos problemas extraños es realmente un problema muy extraño.

EDIT 5:

Pude obtener el archivo .PDB y mirarlo con dia2dump . Pensé que era posible que el PDB estuviera en mal estado, y solo tenía la línea 72 para el método. Ese no es el caso. Todos los números de línea están presentes en el PDB.

EDICION 6:

Para el registro, esto acaba de pasar de nuevo, en un tercer controlador. El seguimiento de la pila apunta directamente a la declaración de devolución de un método. Esta declaración de devolución es simplemente un return model; . No creo que haya ninguna forma de que cause una NullReferenceException .

Editar 6a:

De hecho, simplemente miré más de cerca el registro y encontré varias excepciones que no son NullReferenceException , y que todavía tienen el punto de seguimiento de la pila en la declaración de return . Ambos casos están en métodos llamados desde la acción del controlador, no directamente en el método de acción mismo. Una de estas fue una InvalidOperationException explícitamente lanzada, y una era una FormatException simple.

Aquí hay algunos hechos que hasta ahora no había considerado relevantes:

  1. Application_Error en global.asax es lo que hace que estas excepciones se registren. Recoge las excepciones utilizando Server.GetLastError() .
  2. El mecanismo de registro registra el mensaje y el seguimiento de la pila por separado (en lugar de registrar ex.ToString() , que hubiera sido mi recomendación). En particular, el seguimiento de pila que he estado preguntando proviene de ex.StackTrace .
  3. FormatException se generó en System.DateTime.Parse , llamado desde System.Convert.ToDate , llamado desde nuestro código. La línea de seguimiento de la pila que apunta a nuestro código es la línea que apunta a " return model; ".

¿Pueden los PDB estar fuera de más de 2 o 3 líneas?

Usted da la declaración de que nunca ha visto los PDB desactivados más que unas pocas líneas. 40 líneas parecen ser demasiado, especialmente cuando el código descompilado no parece tener mucha diferencia.

Sin embargo, eso no es cierto y puede probarse con un trazador de líneas 2: cree un objeto String, ToString() como null y llame a ToString() . Compilar y ejecutar A continuación, inserte un comentario de 30 líneas, guarde el archivo, pero no vuelva a compilar. Ejecute la aplicación nuevamente. La aplicación aún falla, pero da una diferencia de 30 líneas en lo que informa (línea 14 vs. 44 en la captura de pantalla).

No está relacionado en absoluto con el código que se compila. Tales cosas pueden suceder fácilmente:

  • formato de código, que, por ejemplo, ordena los métodos por visibilidad, por lo que el método se movió hasta 40 líneas
  • reformateo de código, que, por ejemplo, rompe líneas largas a 80 caracteres, por lo general, esto mueve las cosas hacia abajo
  • optimizar los usos (R #) que elimina 30 líneas de importaciones innecesarias, por lo que el método se movió hacia arriba
  • inserción de comentarios o nuevas líneas
  • haber cambiado a una rama mientras que la versión implementada (que coincide con el PDB) es de troncal (o similar)

¿Cómo puede suceder esto en tu caso?

Si es realmente lo que dice y revisó seriamente su código, hay dos posibles problemas:

  • EXE o DLL no coincide con los PDB, que se pueden verificar fácilmente
  • Los PDB no coinciden con el código fuente, que es más difícil de identificar

Multithreading puede establecer objetos a null cuando menos lo esperas, incluso si se ha inicializado antes. En tal caso, NullReferenceExceptions no solo puede estar a 40 líneas de distancia, incluso puede estar en una clase totalmente diferente y, por lo tanto, en un archivo.

Cómo continuar

Captura un vertedero

Primero trataría de hacer un cambio de situación. Esto le permite capturar el estado y ver todo en detalle sin la necesidad de reproducirlo en su máquina de desarrollo.

Para ASP.NET, consulte el blog de MSDN Pasos para activar un volcado de usuario de un proceso con DebugDiag cuando se lanza una excepción .net específica o el blog de Tess .

En cualquier caso, siempre capture un volcado que incluya memoria completa. Recuerde también recopilar todos los archivos necesarios (SOS.dll y mscordacwks.dll) de la máquina donde se produjo el bloqueo. Puede usar MscordacwksCollector (Descargo de responsabilidad: yo soy el autor).

Verifica los símbolos

Vea si el EXE / DLL realmente coincide con sus PDB. En WinDbg, los siguientes comandos son útiles

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

Fuera de WinDbg, pero aún usando herramientas de depuración para Windows:

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

Herramienta de terceros, ChkMatch :

chkmatch -c <exe> <pdb>

Verifica el código fuente

Si los PDB coinciden con los DLL, el siguiente paso es verificar si el código fuente pertenece a los PDB. Esto es lo mejor posible si compromete PDB para control de versiones junto con el código fuente. Si lo hizo, puede buscar los PDB coincidentes en el control de fuente y luego obtener la misma revisión de código fuente y PDB.

Si no hiciste eso, no tienes suerte y probablemente no deberías usar el código fuente sino trabajar con PDB solamente. En el caso de .NET, esto funciona bastante bien. Estoy depurando mucho en código de terceros con WinDbg sin recibir el código fuente y puedo llegar bastante lejos.

Si usa WinDbg, los siguientes comandos son útiles (en este orden)

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

Por qué el código es tan importante en

Además, miré el código del método View (), y no hay forma de que arroje una NullReferenceException

Bueno, otras personas han hecho declaraciones similares antes. Es fácil pasar por alto algo.

El siguiente es un ejemplo del mundo real, simplemente minimizado y en pseudo código. En la primera versión, la instrucción de lock todavía no existía y DoWork () se podía llamar desde varios subprocesos. Muy pronto, se introdujo la declaración de lock y todo salió bien. Al salir de la cerradura, someobj siempre será un objeto válido, ¿verdad?

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

Hasta que un usuario reportó el mismo error nuevamente. Estábamos seguros de que el error estaba solucionado y esto era imposible. Sin embargo, este fue un "usuario de doble clic", es decir, alguien que hace doble clic en cualquier cosa que se pueda hacer clic.

La llamada DoEvents (), que por supuesto no estaba en un lugar tan prominente, hizo que el bloqueo se ingresara de nuevo con el mismo hilo (lo que es legal). Esta vez, someobj era null , causando una NullReferenceException en un lugar donde parecía imposible ser nulo.

Esa segunda vez, fue return boolValue? RedirectToAction ("A1", "C1"): RedirectToAction ("A2", "C2"). El boolValue era una expresión que no podría haber arrojado la NullReferenceException

Por qué no? ¿Qué es boolValue? Una propiedad con getter y setter? Considere también el siguiente caso (quizás un poco desactivado), donde RedirectToAction solo toma parámetros constantes, se parece a un método, arroja una excepción pero todavía no está en la pila de llamadas. Es por eso que es tan importante ver el código en ...


El problema y sus síntomas huelen a un problema de hardware, por ejemplo:

Parece que estamos perdiendo el estado de la sesión sin una buena razón (no hay reinicios ni nada).

Si se utiliza el interruptor de almacenamiento de estado de sesión de InProc a fuera de proceso. Esto lo ayudará a aislar el problema de perder sesiones a partir del síntoma de los números de línea PDB no coincidentes en el NRE que está informando. Si usa almacenamiento fuera de proceso, ejecute algunas utilidades de diagnóstico en el servidor.

ps publicar el resultado de DebugDiag. Probablemente debería haber puesto esta respuesta como un comentario, pero ya hay demasiados, es necesario espaciarlos y comentar diferentes pasos de diagnóstico por separado.


He visto este tipo de comportamiento en el código de producción una vez . Aunque los detalles son un poco vagos (Fue hace aproximadamente 2 años, y aunque puedo encontrar el correo electrónico, ya no tengo acceso al código, ni a los vertederos, etc.)

Para tu información, esto es lo que escribí al equipo (partes muy pequeñas del correo grande) -

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

"De ninguna manera la excepción de referencia nula puede suceder aquí".

Más tarde, después de más inmersiones

"Hallazgos -

  1. El problema estaba sucediendo en DBI porque no tenía un equipo root / BRH. UI no está manejando el nulo devuelto por CLib con gracia, y por lo tanto la excepción.
  2. El seguimiento de la pila que se muestra en la interfaz de usuario fue engañoso, y se debió al hecho de que Jitter y la CPU pueden optimizar / reordenar las instrucciones, lo que hace que las huellas de la pila "mientan".

Indagar en un vertedero de procesos reveló el problema, y ​​se ha confirmado que DBI de hecho no tenía el equipo mencionado anteriormente ".

Creo que lo que hay que notar aquí es la afirmación en negrita anterior, en contraste con su análisis y declaración:

" Acabo de ver la fuente descompilada, y no parece haber sido reorganizada ", o

" La compilación de producción que se ejecuta en mi máquina local muestra el número de línea correcto " .

La idea es que las optimizaciones pueden suceder en diferentes niveles ... y las realizadas en tiempo de compilación son solo algunas de ellas. Hoy en día, especialmente en entornos administrados como .Net , en realidad se realizan relativamente menos optimizaciones mientras se emite IL (¿Por qué 10 compiladores para 10 diferentes lenguajes .Net intentan hacer el mismo conjunto de optimizaciones, cuando el código de Lenguaje Intermedio emitido se transformará aún más en código de máquina, ya sea por ngen o Jitter).

Por lo tanto, lo que ha observado, solo puede confirmarse mirando el código máquina jit (también conocido como ensamblaje) desde un volcado de la máquina de producción .

Una pregunta que puedo ver es: ¿Por qué Jitter emitiría un código diferente en la máquina de producción, en comparación con su máquina, para la misma construcción?

Respuesta - No sé. No soy un experto en Jit, pero sí creo que puedo ... porque como dije antes ... Hoy estas cosas son mucho más sofisticadas en comparación con las tecnologías utilizadas hace 5-10 años. Quién sabe, todos los factores ... como "memoria, número de CPU, carga de la CPU, 32 bit vs 64 bit, Numa vs Non-Numa, número de veces que se ha ejecutado un método, qué tan pequeño o grande es un método, quién lo llama , ¿cómo se llama, cuántas veces, patrones de acceso para ubicaciones de memoria, etc.? "lo observa al hacer estas optimizaciones.

Para su caso, hasta ahora solo usted puede reproducirlo, y solo usted tiene acceso a su código apilado en producción. Por lo tanto, (si se me permite decirlo :)) esta es la mejor respuesta que cualquiera puede encontrar.

EDITAR : Una diferencia importante entre el Jitter en una máquina frente a la otra, también puede ser la versión de la misma fluctuación. Me imagino que a medida que se lanzan varios parches y KBs para el framework .net, quién sabe qué diferencias de la fluctuación de la conducta de optimización pueden tener, incluso las diferencias menores de la versión.

En otras palabras, no es suficiente suponer que ambas máquinas tienen la misma versión principal del marco (digamos .Net 4.5 SP1). Es posible que la producción no tenga parches que se publiquen todos los días, pero es posible que su desarrollador / máquina privada haya sido liberado el martes pasado.

EDIT 2 : Prueba de concepto - es decir, las optimizaciones de Jitter pueden conducir a los rastros de pila de mentiras.

Ejecute el código siguiente usted mismo, Release build, x64 , Optimizaciones activadas, todos los TRACE y DEBUG desactivados , Visual Studio Hosting Process se apagó . Compila desde el estudio visual, pero ejecuta desde el explorador . ¿Y tratar de adivinar en qué línea el rastro de la pila te dirá la excepción?

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

Lamentablemente, después de algunos intentos, todavía no puedo reproducir el problema exacto que ha visto (es decir, el error en la declaración de devolución), porque solo usted tiene acceso al código exacto y a cualquier patrón de código específico que pueda tener. O, una vez más, es alguna otra optimización de Jitter, que no está documentada y, por lo tanto, es difícil de adivinar.


Sólo un pensamiento, pero lo único que se me ocurre es que quizás exista la posibilidad de que la definición / configuración de su compilación esté sacando una versión compilada fuera de sincronización de los archivos DLL de su aplicación y es por eso que ve la discrepancia en su máquina cuando busca el número de línea de stacktrace.