Llevamos los parámetros a operaciones inseguras en código seguro

Hola a todos Esta vez seguimos riéndonos de la llamada al método normal. Le sugiero que se familiarice con la llamada al método con parámetros sin pasar parámetros. También intentamos convertir el tipo de referencia en un número: su dirección, sin usar punteros y código inseguro.

Descargo de responsabilidad


Antes de comenzar la historia, le recomiendo que lea la publicación anterior sobre StructLayout , porque Las cosas acordadas allí no se repetirán aquí.

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, recordemos cómo se convierte el código C #.
Tomemos un ejemplo simple. Permíteme recordarte que para divertirme con StructLayout, solo uso métodos virtuales.

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 complicado, pero las instrucciones generadas por JiT contienen varios puntos clave. Propongo analizar solo un pequeño fragmento del código generado.

  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 llamada rápida: un acuerdo para pasar parámetros a través de registros (los dos primeros parámetros de izquierda a derecha en los registros ecx y edx), y los parámetros restantes se pasan de derecha a izquierda en la pila. El primer parámetro (implícito) es la dirección de la instancia de clase en la que se llama el método. Se pasa como el primer parámetro implícito para cada método de instancia. El segundo parámetro es una variable local de tipo int (en nuestro caso).

Entonces, en la primera línea vemos la variable local 5, aquí no hay nada interesante.
En la segunda línea, copiamos la dirección de la instancia de Helper en el registro ecx. Esta es la dirección de la tabla de métodos en sí.
La tercera línea contiene la copia de la variable local 5 en el registro edx
La cuarta línea copia la dirección de la tabla de métodos en el registro eax
La quinta línea contiene un desplazamiento del registro eax en 40 bytes, cargando un valor de la memoria en una dirección 40 bytes mayor que la dirección de la tabla de métodos: la dirección del comienzo de los métodos en la tabla de métodos. (La tabla de métodos contiene información diversa que se almacena antes. Dicha información, por ejemplo, incluye la dirección de la tabla de métodos de la clase base, la dirección EEClass, varios indicadores, incluido el indicador del recolector de basura, etc.). En consecuencia, la dirección del primer método de la tabla de métodos ahora se almacena en el registro eax.
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, en consecuencia, estarán en todas las clases.

Pasemos a practicar


Es hora de comenzar una pequeña demostración. Propongo aquí un espacio en blanco (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 el siguiente relleno del método Main:

  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 imaginar, por la experiencia del artículo anterior, se llamará al método Useless (int j) de tipo Test1.

Pero, ¿qué se deducirá? Un lector atento, creo, ya ha respondido esta pregunta. 55555 se muestra en la consola.

Pero echemos un vistazo a los fragmentos del código generado.

  mov ecx, [ebp-0x20] mov edx, [ebp-0x10] cmp [ecx], ecx call Stub.Foo(Int32) nop 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 L00cc: nop. Como podemos ver, en ecx se espera que se escriba la dirección de la instancia en la que se llama al método. Pero desde Si llamamos a un método como Test2, que no tiene parámetros, no se escribe nada en edx. Sin embargo, antes de eso, se llamó al método, que pasó el parámetro a través del registro edx, respectivamente, y el valor permaneció en él. y podemos observarlo en la ventana de salida.

Hay otro matiz interesante. Usé específicamente un 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 inútil no cambia. A continuación puede ver el resultado en mi máquina con algunos elementos de aclaración: WinDBG y Calculadora :)


Imagen en la que se puede hacer clic

La ventana de salida muestra la dirección del tipo de referencia en el sistema de números decimales.

Resumen


Refrescaron el conocimiento de los métodos de llamada usando la convención de llamada rápida e inmediatamente usaron el maravilloso registro edx para pasar los métodos del parámetro 2 a la vez. Tampoco les importaban todos los tipos y recordaban que solo hay bytes que reciben 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 su atencion!

El código PS C # se puede encontrar aquí

Source: https://habr.com/ru/post/es424011/


All Articles