¿Hay alguna forma de saber los argumentos del método que llama a una función en C#?

Estoy trabajando en un mecanismo de registro de no intervención para mi aplicación c#.

Así es como me gustaría que se viera:

función a(arg1, arg2, arg 3.....) función de llamadas b(arg4,arg5,arg6....), que a su vez llama log() que es capaz de detectar el stacktrace (esto se puede hacer a través de Environment.StackTrace) y los valores con los que cada función (ej. a y b) en el stacktrace se llama.

Quiero que funcione en modo de depuración y lanzamiento (o, al menos, en modo de depuración).

¿Es posible hacerlo en .net?

preguntado el 24 de agosto de 12 a las 21:08

Pregunta interesante... no es posible en C#, pero ¿quizás en IL o usando la reflexión? -

Mi apuesta es que es imposible. Sería un buen no convencional, habiertos Sin embargo, la pregunta de la entrevista (imagina que es posible, ¿cómo crees que funcionaría?), mucho mejor que la mayoría de las típicas "preguntas raras"; un buen punto de partida para revelar el conocimiento general de C# y CLR, etc. -

Teóricamente, eso DEBERÍA ser posible. Stacktrace ordinal devuelve información sobre las funciones que se están llamando, así como sus tipos de argumentos, y todas las variables utilizadas para llamar a las funciones deben almacenarse en la pila en algún lugar (aunque, probablemente, como variables locales). -

@ArsenZahray No hay razón para que estén en la pila; el espacio de la pila podría reutilizarse para otro local si no se volvieron a usar después, y la pila IL no tiene que corresponder a una pila en memoria, ya que un local solo podría estar en un registro. -

Esa es más o menos la misma pregunta a la que acabamos de decir "no". He agregado un poco más a mi respuesta. -

4 Respuestas

Probablemente no es posible:

Para el momento b se llama, el espacio en la pila utilizado por a's arg1 (la pila IL, por lo que posiblemente nunca se colocó en una pila, pero se registró en la llamada) no se garantiza que aún sea utilizada por arg1.

Por extensión, si arg1 es un tipo de referencia, no se garantiza que el objeto al que hace referencia no haya sido recolectado como basura, si no se usa después de la llamada a b.

Editar:

Un poco más de detalle, ya que su comentario sugiere que no está asimilando esto y aún cree que debería ser posible.

Las convenciones de llamada utilizadas por el jitter no se especifican en las especificaciones de ninguno de los estándares relevantes, lo que da a los implementadores la libertad de realizar mejoras. De hecho, difieren entre las versiones de 32 bits y 64 bits, y las diferentes versiones.

Sin embargo, los artículos de personas con EM sugieren que la convención utilizada es similar a la __llamada rápida convención. En su llamada a a, arg1 se colocaría en el registro ECX*, y arg2 en el registro EDX (estoy simplificando asumiendo x32 de 86 bits, con amd64 se registran incluso más argumentos) del núcleo en el que se ejecuta el código. arg3 se colocaría en la pila y, de hecho, existiría en la memoria.

Tenga en cuenta que en este punto, no hay ninguna ubicación de memoria en la que arg1 y arg2 existen, solo están en un registro de CPU.

En el curso de la ejecución del método en sí, los registros y la memoria se utilizan según sea necesario. Y el b se llama.

Ahora si a va a necesitar arg1 or arg2 tendrá que empujar eso antes de que llame b. Pero si no es así, entonces no será así, y las cosas podrían incluso reordenarse para reducir esta necesidad. Por el contrario, es posible que esos registros ya se hayan utilizado para otra cosa en este punto: la fluctuación no es estúpida, por lo que si necesita un registro o una ranura en la pila y hay uno sin usar para el resto del método, va para reutilizar ese espacio. (De hecho, en el nivel superior a este, el compilador de C# reutilizará las ranuras en la pila virtual que usa el IL producido).

Así que cuando b se llama, arg4 se coloca en el registro ECX, arg5 en EDX y arg6 empujado en la pila. En este punto, arg1 y arg2 no existen y ya no puedes descubrir lo que eran como leer un libro después de haberlo reciclado y convertido en papel higiénico.

(Una nota interesante es que es muy común que un método llame a otro con los mismos argumentos en la misma posición, en cuyo caso ECX y EDX pueden dejarse solos).

Entonces, b devuelve, poniendo su valor de retorno en el registro EAX, o par EDX:EAX o en memoria con EAX apuntándolo, dependiendo del tamaño, a hace un poco más de trabajo antes de poner su retorno en ese registro, y así sucesivamente.

Ahora, esto supone que no se han realizado optimizaciones. Es posible que, de hecho, b no fue llamado en absoluto, sino que su código estaba en línea. En este caso, si los valores estaban en los registros o en la pila, y en el último caso, dónde estaban en la pila, ya no tiene nada que ver con bla firma de y todo lo relacionado con dónde están los valores relevantes durante a's ejecución, y sería diferente en el caso de otra "llamada" a b, o incluso en el caso de otra "llamada" a b de a, ya que toda la convocatoria de a incluyendo su llamado a b podría haber sido alineado en un caso, no alineado en otro, y alineado de manera diferente en otro. Si por ejemplo, arg4 vino directamente de un valor devuelto por otra llamada, podría estar en el registro EAX en este punto, mientras que arg5 estaba en ECX ya que era lo mismo que arg1 y arg6 estaba en algún lugar a mitad de camino en el medio del espacio de pila que estaba siendo utilizado por a.

Otra posibilidad es que la llamada a b fue una llamada de cola que fue eliminada: Porque la llamada a b iba a tener su valor de retorno inmediatamente devuelto también por a (o algunas otras posibilidades), entonces, en lugar de empujar a la pila, los valores que utiliza a se reemplazan en el lugar y la dirección de devolución se cambia para que la devolución desde b salta de nuevo al método que llamó a, omitiendo parte del trabajo (y reduciendo el uso de la memoria en la medida en que algunos enfoques de estilo funcional que desbordarían la pila funcionan y, de hecho, funcionan bien). En este caso, durante la llamada a b, los parámetros para a es probable que hayan desaparecido por completo, incluso aquellos que habían estado en la pila.

Es muy discutible si este último caso debería considerarse una optimización; algunos idiomas dependen en gran medida de que se haga, ya que con él dan un buen rendimiento y sin ellos dan un rendimiento horrible si incluso funcionan (en lugar de desbordar la pila).

Puede haber todo tipo de otras optimizaciones. Ahí debe ser todo tipo de otras optimizaciones: si el equipo .NET o el equipo Mono hacen algo que hace que mi código sea más rápido o use menos memoria, pero se comporte de la misma manera, sin que yo tenga que hacer nada, ¡no me quejaré!

Y eso suponiendo que la persona que escribió el C# en primer lugar nunca cambió el valor de un parámetro, lo que ciertamente no será cierto. Considere este código:

IEnumerable<T> RepeatedlyInvoke(Func<T> factory, int count)
{
  if(count < 0)
    throw new ArgumentOutOfRangeException();
  while(count-- != 0)
    yield return factory();
}

Incluso si el compilador de C# y el jitter se hubieran diseñado de manera tan derrochadora que pudiera garantizar que los parámetros no se cambiaran de la manera descrita anteriormente, ¿cómo podría saber qué count ya había sido desde dentro la invocación de factory? Incluso en la primera llamada es diferente, y no es como si el código anterior fuera extraño.

Entonces, en resumen:

  1. Jitter: Los parámetros a menudo se registran. Puede esperar que x86 coloque 2 parámetros de puntero, referencia o enteros en los registros y que amd64 coloque 4 parámetros de puntero, referencia o enteros y 4 parámetros de punto flotante en los registros. No tienen ubicación desde donde leerlos.
  2. Fluctuación: los parámetros en la pila a menudo se sobrescriben.
  3. Jitter: es posible que no haya una llamada real, por lo que no hay lugar para buscar parámetros, ya que podrían estar en cualquier parte.
  4. Jitter: La "llamada" puede estar reutilizando el mismo cuadro que el anterior.
  5. Compilador: el IL puede reutilizar espacios para locales.
  6. Humano: El programador puede cambiar los valores de los parámetros.

A partir de todo eso, ¿cómo diablos va a ser posible saber qué arg1 ¿estaba?

Ahora, agregue la existencia de recolección de basura. Imagínense si mágicamente pudiéramos saber qué arg1 fue de todos modos, a pesar de todo esto. Si fuera una referencia a un objeto en el montón, aún podría no servirnos de nada, porque si todo lo anterior significara que no hay más referencias activas en el montón, y debe quedar claro que esto definitivamente sucede. y el GC se activa, entonces el objeto podría haber sido recolectado. Entonces, todo lo que podemos obtener mágicamente es una referencia a algo que ya no existe; de ​​hecho, muy posiblemente a un área en el montón que ahora se usa para otra cosa, ¡bang va toda la seguridad de tipos de todo el marco!

No es en lo más mínimo comparable a la reflexión de obtener el IL, porque:

  1. El IL es estático, en lugar de solo un estado en un momento dado. Del mismo modo, podemos obtener una copia de nuestros libros favoritos de una biblioteca mucho más fácilmente que recuperar nuestra reacción la primera vez que los leímos.
  2. El IL no refleja el impacto de la alineación, etc. de todos modos. Si una llamada se insertara cada vez que se usara realmente, y luego usáramos la reflexión para obtener una MethodBody de ese método, el hecho de que normalmente esté en línea es irrelevante.

Las sugerencias en otras respuestas sobre perfiles, AOP e intercepción son lo más cercano que obtendrá.

*Realmente, this es el primer parámetro real para los miembros de la instancia. Finjamos que todo es estático para no tener que seguir señalando esto.

Respondido 25 ago 12, 20:08

Es imposible en .net. En el tiempo de ejecución, JITter puede decidir usar registros de CPU en lugar de pilas para almacenar parámetros de métodos o incluso reescribir los valores iniciales (aprobados) en la pila. Por lo tanto, sería muy costoso para el rendimiento de .net permitir registrar parámetros en cualquier punto del código fuente.

Hasta donde sé, la única forma en que puede hacerlo en general es usar la API de creación de perfiles .net CLR. (El marco Typemock, por ejemplo, puede hacer tales cosas y usa la API de creación de perfiles CLR)

Si solo necesita interceptar llamadas de funciones/propiedades virtuales (incluidos los métodos/propiedades de las interfaces), puede usar cualquier marco de interceptación (Unity o Castle, por ejemplo).

Hay alguna información sobre la API de creación de perfiles de .net:

Revista MSDN

MSDN Blogs

blog de brian long

Respondido 24 ago 12, 22:08

Esto no es posible en C#, debe usar un enfoque AOP y realizar un registro de argumentos de método cuando se llama a cada método. De esta manera, puede centralizar su código de registro, hacerlo reutilizable y luego solo tendrá que marcar qué métodos requieren el registro de argumentos.

Creo que esto podría lograrse fácilmente usando un marco AOP como PostSharp.

Respondido 24 ago 12, 22:08

Posiblemente no va a suceder sin burlarse de tipos o alguna magia de ICorDebug. Incluso el marco de pila class solo enumera los miembros que le permiten obtener información sobre la fuente, y no los parámetros.

Sin embargo, la funcionalidad que busca existe como IntelliTrace con registro de métodos. Puede filtrar lo que necesita para su revisión.

Respondido 24 ago 12, 22:08

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.