Hola Esta vez seguimos riéndonos de la llamada al método normal. Propongo familiarizarme con la llamada al método con parámetros sin pasar parámetros. También intentaremos convertir el tipo de referencia en un número: su dirección, sin usar punteros y
código inseguro .
Descargo de responsabilidad
Antes de continuar con la historia, le recomiendo que lea la
publicación anterior
sobre StructLayout . Aquí usaré algunas características que se describieron allí.
También me gustaría advertir que este artículo no contiene material que deba usarse en proyectos reales.
Alguna información inicial
Antes de comenzar a practicar, recordemos cómo el código C # se convierte en código ensamblador.
Examinemos un ejemplo simple.
public class Helper { public virtual void Foo(int param) { } } public class Program { public void Main() { Helper helper = new Helper(); var param = 5; helper.Foo(param); } }
Este código no contiene nada difícil, pero las instrucciones generadas por JiT contienen varios puntos clave. Propongo buscar solo en un pequeño fragmento del código generado. en mis ejemplos usaré código ensamblador para máquinas de 32 bits.
1: mov dword [ebp-0x8], 0x5 2: mov ecx, [ebp-0xc] 3: mov edx, [ebp-0x8] 4: mov eax, [ecx] 5: mov eax, [eax+0x28] 6: call dword [eax+0x10]
En este pequeño ejemplo, puede observar la convención de llamada rápida que usa registros para pasar parámetros (los dos primeros parámetros de izquierda a derecha en los registros ecx y edx), y los parámetros restantes se pasan a través de la pila de derecha a izquierda. El primer parámetro (implícito) es la dirección de la instancia de la clase en la que se llama el método (para métodos no estáticos).
En nuestro caso, el primer parámetro es la dirección de la instancia, el segundo es nuestro valor
int .
Entonces, en la
primera línea vemos la variable local 5, no hay nada interesante aquí.
En la
segunda línea, copiamos la dirección de la instancia de Helper en el registro ecx. Esta es la dirección del puntero a la tabla de métodos.
En la
tercera línea hay copia de la variable local 5 en el registro edx
En la
cuarta línea podemos ver la copia de la dirección de la tabla de métodos en el registro eax
La quinta línea contiene la carga del valor de la memoria en la dirección 40 bytes más grande que la dirección de la tabla de métodos: el inicio de las direcciones de métodos en la tabla de métodos. (La tabla de métodos contiene información diversa que se almacena antes. Por ejemplo, la dirección de la tabla de métodos de la clase base, la dirección de clase EEC, varios indicadores, incluido el indicador del recolector de basura, etc.). Por lo tanto, la dirección del primer método de la tabla de métodos ahora se almacena en el registro eax.
Nota: en .NET Core se modificó el diseño de la tabla de métodos. Ahora hay un campo (con un desplazamiento de 32/64 bits para sistemas de 32 y 64 bits respectivamente) que contiene la dirección del inicio de la lista de métodos.En la
sexta línea, el método se llama en el desplazamiento 16 desde el principio, es decir, el quinto en la tabla de métodos. ¿Por qué nuestro único método es el quinto? Les recuerdo que el
objeto tiene 4 métodos virtuales (
ToString (), Equals (), GetHashCode () y Finalize () ), que tendrán todas las clases, respectivamente.
Goto Practice;
Práctica:
Es hora de comenzar una pequeña demostración. Sugiero un espacio en blanco tan pequeño (muy similar al espacio en blanco del artículo anterior).
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public Test1 Test1; [FieldOffset(0)] public Test2 Test2; } public class Test1 { public virtual int Useless(int param) { Console.WriteLine(param); return param; } } public class Test2 { public virtual int Useless() { return 888; } } public class Stub { public void Foo(int stub) { } }
Y usemos esas cosas de esa manera:
class Program { static void Main(string[] args) { Test2 fake = new CustomStructWithLayout { Test2 = new Test2(), Test1 = new Test1() }.Test2; Stub bar = new Stub(); int param = 55555; bar.Foo(param); fake.Useless(); Console.Read(); } }
Como puede suponer, por la experiencia del artículo anterior, se
llamará al método
Useless (int j) de tipo
Test1 .
¿Pero qué se mostrará? El lector atento, creo, ya ha respondido esta pregunta. Se muestra "55555" en la consola.
Pero aún veamos los fragmentos de código generados.
mov ecx, [ebp-0x20] mov edx, [ebp-0x10] cmp [ecx], ecx call Stub.Foo(Int32) mov ecx, [ebp-0x1c] mov eax, [ecx] mov eax, [eax+0x28] call dword [eax+0x10]
Creo que reconoce el patrón de llamada al método virtual, comienza después de la
llamada Stub.Foo (Int32) . Como podemos ver, como se esperaba, ecx se llena con la dirección de la instancia en la que se llama el método. Pero como el compilador piensa que llamamos a un método de tipo Test2, que no tiene parámetros, no se escribe nada en edx. Sin embargo, tenemos otro método llamado antes. Y allí hemos usado edx para pasar parámetros. Y, por supuesto, no tenemos instrucciones, ese claro edx. Entonces, como puede ver en la salida de la consola, se utilizó el valor edx anterior.
Hay otro matiz interesante. Usé específicamente el tipo significativo. Sugiero intentar reemplazar el tipo de parámetro del método Foo del tipo Stub con cualquier tipo de referencia, por ejemplo, una cadena. Pero el tipo de parámetro del método
Useless () no cambia. A continuación puede ver el resultado en mi máquina con información aclaratoria: WinDBG y Calculadora :)
Imagen en la que se puede hacer clicLa ventana de salida muestra la dirección del tipo de referencia en notación decimal.
Total
Actualizamos el conocimiento de los métodos de llamada utilizando la convención de llamada rápida e inmediatamente utilizamos el maravilloso registro edx para pasar un parámetro en 2 métodos a la vez. También escupimos en todos los tipos y con el conocimiento de que todo es solo bytes, obtuvimos fácilmente la dirección del objeto sin usar punteros y código inseguro. Además, planeo usar la dirección recibida para propósitos aún más inaplicables.
Gracias por la atencion!
El código PS C # se puede encontrar
aquí