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:
- Ocurren en el mismo controlador (hasta ahora)
- En ambos casos, el seguimiento de la pila apunta a la instrucción
return
, y en ambos casos, laNullReferenceException
produjo a 30 o más líneas de la declaraciónreturn
.
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
.
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:
-
Application_Error
en global.asax es lo que hace que estas excepciones se registren. Recoge las excepciones utilizandoServer.GetLastError()
. - 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 deex.StackTrace
. -
FormatException
se generó enSystem.DateTime.Parse
, llamado desdeSystem.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 -
- 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.
- 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.