Hacer que su lenguaje .NET avance correctamente en el depurador

135

En primer lugar, me disculpo por la longitud de esta pregunta.

Soy el autor de IronScheme . Recientemente he estado trabajando duro para emitir información de depuración decente, para poder usar el depurador .NET 'nativo'.

Si bien esto ha sido parcialmente exitoso, me encuentro con algunos problemas iniciales.

El primer problema está relacionado con los pasos.

Debido a que Scheme es un lenguaje de expresión, todo tiende a estar entre paréntesis, a diferencia de los principales lenguajes .NET que parecen estar basados ​​en sentencias (o líneas).

El código original (Esquema) se ve así:

(define (baz x)
  (cond
    [(null? x) 
      x]
    [(pair? x) 
      (car x)]
    [else
      (assertion-violation #f "nooo" x)]))

A propósito he presentado cada expresión en una nueva línea.

El código emitido se transforma en C # (a través de ILSpy) se ve así:

public static object ::baz(object x)
{
  if (x == null)
  {
    return x;
  }
  if (x is Cons)
  {
    return Builtins.Car(x);
  }
  return #.ironscheme.exceptions::assertion-violation+(
     RuntimeHelpers.False, "nooo", Builtins.List(x));
}

Como puedes ver, bastante simple.

Nota: Si el código se transformó en una expresión condicional (? :) en C #, todo sería solo un paso de depuración, tenlo en cuenta.

Aquí está la salida IL con números de fuente y línea:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       56 (0x38)
    .maxstack  6
    .line 15,15 : 1,2 ''
//000014: 
//000015: (define (baz x)
    IL_0000:  nop
    .line 17,17 : 6,15 ''
//000016:   (cond
//000017:     [(null? x) 
    IL_0001:  ldarg.0
    IL_0002:  brtrue     IL_0009

    .line 18,18 : 7,8 ''
//000018:       x]
    IL_0007:  ldarg.0
    IL_0008:  ret

    .line 19,19 : 6,15 ''
//000019:     [(pair? x) 
    .line 19,19 : 6,15 ''
    IL_0009:  ldarg.0
    IL_000a:  isinst [IronScheme]IronScheme.Runtime.Cons
    IL_000f:  ldnull
    IL_0010:  cgt.un
    IL_0012:  brfalse    IL_0020

    IL_0017:  ldarg.0
    .line 20,20 : 7,14 ''
//000020:       (car x)]
    IL_0018:  tail.
    IL_001a:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_001f:  ret

    IL_0020:  ldsfld object 
         [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_0025:  ldstr      "nooo"
    IL_002a:  ldarg.0
    IL_002b:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
//000021:     [else
//000022:       (assertion-violation #f "nooo" x)]))
    IL_0030:  tail.
    IL_0032:  call object [ironscheme.boot]#::
       'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_0037:  ret
  } // end of method 'eval-core(033)'::'::baz'

Nota: Para evitar que el depurador simplemente resalte todo el método, hago que el punto de entrada del método tenga solo 1 columna de ancho.

Como puede ver, cada expresión se asigna correctamente a una línea.

Ahora el problema con los pasos (probado en VS2010, pero el mismo problema / similar en VS2008):

Estos son con IgnoreSymbolStoreSequencePointsno aplicado.

  1. Llame baz con argumento nulo, funciona correctamente. (nulo? x) seguido de x.
  2. Llame baz con Cons arg, funciona correctamente. (nulo? x) luego (par? x) luego (carro x).
  3. Llama a baz con otro argumento, falla. (nulo? x) luego (par? x) luego (carro x) luego (afirmación-violación ...).

Al aplicar IgnoreSymbolStoreSequencePoints(según lo recomendado):

  1. Llame baz con argumento nulo, funciona correctamente. (nulo? x) seguido de x.
  2. Llama a baz con Cons arg, falla. (nulo? x) entonces (par? x).
  3. Llama a baz con otro argumento, falla. (nulo? x) luego (par? x) luego (carro x) luego (afirmación-violación ...).

También encuentro en este modo que algunas líneas (no mostradas aquí) están resaltadas incorrectamente, están apagadas en 1.

Aquí hay algunas ideas sobre cuáles podrían ser las causas:

  • Tailcalls confunde al depurador
  • Las ubicaciones superpuestas (no mostradas aquí) confunden al depurador (lo hace muy bien al establecer un punto de interrupción)
  • ????

El segundo problema, pero también grave, es que el depurador no logra romper / alcanzar puntos de interrupción en algunos casos.

El único lugar donde puedo hacer que el depurador se rompa correctamente (y de manera consistente), es en el punto de entrada del método.

La situación mejora un poco cuando IgnoreSymbolStoreSequencePointsno se aplica.

Conclusión

Puede ser que el depurador VS sea simplemente defectuoso :(

Referencias

  1. Hacer un lenguaje CLR / .NET depurable

Actualización 1:

Mdbg no funciona para ensamblados de 64 bits. Entonces eso está fuera. No tengo más máquinas de 32 bits para probarlo. Actualización: estoy seguro de que esto no es un gran problema, ¿alguien tiene una solución? Editar: Sí, tonto, simplemente inicia mdbg en el símbolo del sistema x64 :)

Actualización 2:

Creé una aplicación C # e intenté diseccionar la información de la línea.

Mis hallazgos:

  • Después de cualquier brXXXinstrucción, debe tener un punto de secuencia (si no es válido, también conocido como '#line hidden', emita a nop).
  • Antes de cualquier brXXXinstrucción, emita un '#line hidden' y a nop.

Sin embargo, aplicar esto no soluciona los problemas (¿solo?).

Pero al agregar lo siguiente, se obtiene el resultado deseado :)

  • Después ret, emita una '# línea oculta' y a nop.

Esto está usando el modo donde IgnoreSymbolStoreSequencePointsno se aplica. Cuando se aplica, algunos pasos aún se omiten :(

Aquí está la salida IL cuando se ha aplicado lo anterior:

  .method public static object  '::baz'(object x) cil managed
  {
    // Code size       63 (0x3f)
    .maxstack  6
    .line 15,15 : 1,2 ''
    IL_0000:  nop
    .line 17,17 : 6,15 ''
    IL_0001:  ldarg.0
    .line 16707566,16707566 : 0,0 ''
    IL_0002:  nop
    IL_0003:  brtrue     IL_000c

    .line 16707566,16707566 : 0,0 ''
    IL_0008:  nop
    .line 18,18 : 7,8 ''
    IL_0009:  ldarg.0
    IL_000a:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_000b:  nop
    .line 19,19 : 6,15 ''
    .line 19,19 : 6,15 ''
    IL_000c:  ldarg.0
    IL_000d:  isinst     [IronScheme]IronScheme.Runtime.Cons
    IL_0012:  ldnull
    IL_0013:  cgt.un
    .line 16707566,16707566 : 0,0 ''
    IL_0015:  nop
    IL_0016:  brfalse    IL_0026

    .line 16707566,16707566 : 0,0 ''
    IL_001b:  nop
    IL_001c:  ldarg.0
    .line 20,20 : 7,14 ''
    IL_001d:  tail.
    IL_001f:  call object [IronScheme]IronScheme.Runtime.Builtins::Car(object)
    IL_0024:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_0025:  nop
    IL_0026:  ldsfld object 
      [Microsoft.Scripting]Microsoft.Scripting.RuntimeHelpers::False
    IL_002b:  ldstr      "nooo"
    IL_0030:  ldarg.0
    IL_0031:  call object [IronScheme]IronScheme.Runtime.Builtins::List(object)
    .line 22,22 : 7,40 ''
    IL_0036:  tail.
    IL_0038:  call object [ironscheme.boot]#::
      'ironscheme.exceptions::assertion-violation+'(object,object,object)
    IL_003d:  ret

    .line 16707566,16707566 : 0,0 ''
    IL_003e:  nop
  } // end of method 'eval-core(033)'::'::baz'

Actualización 3:

Problema con el 'semi-arreglo' anterior. Peverify informa errores en todos los métodos debido a lo nopposterior ret. Realmente no entiendo el problema. ¿Cómo puede una nopverificación de interrupción después de a ret. Es como un código muerto (excepto que ni siquiera es código) ... Oh, bueno, la experimentación continúa.

Actualización 4:

De vuelta a casa ahora, eliminé el código 'no verificable', ejecuté en VS2008 y las cosas son mucho peores. Quizás ejecutar el código no verificable en aras de una depuración adecuada podría ser la respuesta. En el modo 'lanzamiento', toda la salida aún sería verificable.

Actualización 5:

Ahora he decidido que mi idea anterior es la única opción viable por ahora. Aunque el código generado no es verificable, aún no he encontrado ninguno VerificationException. No sé cuál será el impacto en el usuario final con este escenario.

Como beneficio adicional, mi segundo problema también se ha resuelto. :)

Aquí hay un pequeño screencast de lo que terminé. Alcanza los puntos de interrupción, realiza pasos adecuados (entrada / salida / sobre), etc. En general, el efecto deseado.

Sin embargo, todavía no estoy aceptando esto como la forma de hacerlo. Se siente demasiado hacky para mí. Tener una confirmación sobre el problema real sería bueno.

Actualización 6:

Acabo de tener el cambio para probar el código en VS2010, parece haber algunos problemas:

  1. La primera llamada ahora no pasa correctamente. (afirmación-violación ...) es golpeado. Otros casos funciona bien. Algunos códigos antiguos emitían posiciones innecesarias. Eliminado el código, funciona como se esperaba. :)
  2. Más en serio, los puntos de interrupción fallan en la segunda invocación del programa (al usar la compilación en memoria, volcar el ensamblado en el archivo parece hacer felices nuevamente los puntos de interrupción).

Ambos casos funcionan correctamente bajo VS2008. La principal diferencia es que bajo VS2010, la aplicación completa se compila para .NET 4 y bajo VS2008, compila a .NET 2. Ambos ejecutan 64 bits.

Actualización 7:

Como se mencionó, obtuve mdbg ejecutándose bajo 64 bits. Desafortunadamente, también tiene el problema del punto de interrupción donde no se rompe si vuelvo a ejecutar el programa (esto implica que se vuelve a compilar, por lo que no usa el mismo ensamblaje, pero sigue usando la misma fuente).

Actualización 8:

He presentado un error en el sitio de MS Connect con respecto al problema del punto de interrupción.

Actualización: arreglado

Actualización 9:

Después de pensarlo mucho, la única forma de hacer feliz al depurador parece estar haciendo SSA, por lo que cada paso puede ser aislado y secuencial. Sin embargo, todavía tengo que demostrar esta noción. Pero parece lógico. Obviamente, limpiar las temperaturas de SSA interrumpirá la depuración, pero eso es fácil de alternar, y dejarlas no tiene mucha sobrecarga.

leppie
fuente
Cualquier idea es bienvenida, las probaré y agregaré los resultados a la pregunta. Primeras 2 ideas, prueba mdbg y escribe el mismo código en C # y compara.
leppie
66
¿Cuál es la pregunta?
George Stocker
9
Creo que su pregunta está en la línea de "¿por qué mi idioma no funciona correctamente?"
McKay
1
Si no pasa Peverify, no podrá ejecutarse en situaciones de confianza parcial, como desde una unidad asignada o en muchas configuraciones de alojamiento de complementos. ¿Cómo está generando su IL, reflexión, emisión o vía texto + ilasm? En cualquier caso, siempre puede ingresar la línea.
Hippiehunter
@Hippiehunter: Gracias. Desafortunadamente, sin la nops, el paso falla (lo verificaré nuevamente para estar seguro). Es un sacrificio que supongo que tendría que hacer. No es que VS incluso pueda ejecutarse sin derechos de administrador :) Por cierto, usando Reflection.Emit a través del DLR (uno muy ramificado que se ha pirateado).
leppie

Respuestas:

26

Soy ingeniero en el equipo de Visual Studio Debugger.

Corríjame si me equivoco, pero parece que el único problema que queda es que al cambiar de PDB al formato de símbolo de compilación dinámica .NET 4 se pierden algunos puntos de interrupción.

Probablemente necesitemos una repro para diagnosticar exactamente el problema, sin embargo, aquí hay algunas notas que podrían ayudar.

  1. VS (2008+) puede ejecutarse como no administrador
  2. ¿Se carga algún símbolo por segunda vez? Puede realizar la prueba interviniendo (mediante una excepción o llamando a System.Diagnostic.Debugger.Break ())
  3. Suponiendo que los símbolos se cargan, ¿hay alguna repro que nos pueda enviar?
  4. La diferencia probable es que el formato de símbolo para el código compilado dinámico es 100% diferente entre .NET 2 (flujo PDB) y .NET 4 (IL DB, creo que lo llamaron).
  5. El 'nop' suena bien. Vea las reglas para generar puntos de secuencia implícitos a continuación.
  6. En realidad no necesitas emitir cosas en diferentes líneas. Por defecto, VS irá a "declaraciones de símbolos" donde, como escritor del compilador, puede definir qué significa "declaración de símbolos". Entonces, si desea que cada expresión sea una cosa separada en el archivo de símbolos, eso funcionará bien.

El JIT crea un punto de secuencia implícito basado en las siguientes reglas: 1. Instrucciones IL nop 2. Puntos vacíos de la pila IL 3. La instrucción IL inmediatamente después de una instrucción de llamada

Si resulta que necesitamos un repro para resolver su problema, puede presentar un error de conexión y cargar archivos de forma segura a través de ese medio.

Actualizar:

Alentamos a otros usuarios que experimenten este problema a que prueben la Vista previa para desarrolladores de Dev11 desde http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=27543 y comenten con cualquier comentario. (Debe apuntar 4.5)

Actualización 2:

Leppie ha verificado que la solución funciona para él en la versión Beta de Dev11 disponible en http://www.microsoft.com/visualstudio/11/en-us/downloads como se indica en el error de conexión https://connect.microsoft. com / VisualStudio / feedback / details / 684089 / .

Gracias,

Luke

Luke Kim
fuente
Muchas gracias. Intentaré hacer repro, si es necesario.
leppie
En cuanto a la confirmación, sí, tienes razón, pero aún así preferiría resolver los problemas de los pasos sin romper la verificabilidad. Pero como dije, estoy dispuesto a sacrificar eso si pudiera demostrar que nunca arrojará un tiempo de ejecución VerificationException.
leppie
En cuanto al punto 6, simplemente lo hice 'basado en líneas' para esta pregunta. De hecho, el depurador ingresa correctamente las expresiones de entrada / salida / sobre como pretendo que funcione :) El único problema con el enfoque es establecer puntos de interrupción en las expresiones 'internas' si una expresión 'externa' lo cubre; el depurador en VS tiende a querer hacer la expresión más externa como punto de interrupción.
leppie
Creo que he aislado el problema del punto de ruptura. Mientras se ejecuta bajo CLR2, los puntos de interrupción parecen ser reevaluados cuando se ejecuta el código nuevamente, aunque sea un código nuevo. En CLR4, los puntos de interrupción solo se 'adhieren' al código original. Hice una pequeña transmisión de pantalla del comportamiento CLR4 @ screencast.com/t/eiSilNzL5Nr . Observe que el nombre del tipo cambia entre invocaciones.
leppie
He presentado un error en conectar. La reproducción es bastante fácil :) connect.microsoft.com/VisualStudio/feedback/details/684089
leppie
3

Soy ingeniero en el equipo de depurador de SharpDevelop :-)

¿Resolviste el problema?

¿Intentaste depurarlo en SharpDevelop? Si hay un error en .NET, me pregunto si necesitamos implementar alguna solución alternativa. No estoy al tanto de este problema.

¿Intentaste depurarlo en ILSpy? Especialmente sin símbolos de depuración. Depuraría el código C #, pero nos diría si las instrucciones de IL son muy fáciles de depurar. (Tenga en cuenta que el depurador de ILSpy es beta)

Notas rápidas sobre el código IL original:

  • .line 19,19: 6,15 '' ocurre dos veces?
  • .line 20,20: 7,14 '' no comienza en el punto de secuencia implícito (la pila no está vacía). Estoy preocupado
  • .line 20,20: 7,14 '' incluye el código para "car x" (bueno) así como "#f nooo x" (¿malo?)
  • con respecto al nop después de ret. ¿Qué pasa con stloc, ldloc, ret? Creo que C # usa este truco para hacer ret un punto de secuencia distinto.

David

dsrbecky
fuente
+1 Gracias por los comentarios, como primer punto, desafortunado efecto secundario del generador de código, pero parece inofensivo. Segundo punto, esto es exactamente lo que necesito, pero veo tu punto, lo investigaré. No estoy seguro de lo que quieres decir con el último punto.
leppie
El tercer punto fue que .line 22,22: 7,40 '' debería estar antes de IL_0020. O debería haber algo antes de IL_0020, de lo contrario el código todavía cuenta como .line 20,20: 7,14 ''. El cuarto punto es "ret, nop" puede ser reemplazado por "sloc, .line, ldloc, ret". He visto el patrón antes, tal vez era redundante, pero tal vez tenía una razón.
dsrbecky
No puedo hacer el stloc, ldloc antes de ret, ya que perderé la llamada de cola. Ah te entiendo, ¿te referías a la salida original?
leppie