No respeto la encapsulación o el uso de un tipo diferente de tabla de métodos para llamar rápidamente a métodos privados

Hola a todos Me gustaría compartir un ejemplo de uso de StructLayout para algo más interesante que ejemplos con bytes, ints y otros números, en los que todo sucede un poco más de lo esperado.

Antes de proceder con una violación de encapsulación ultrarrápida, vale la pena recordar en pocas palabras qué es StructLayout. Estrictamente hablando, esto es incluso un StructLayoutAttribute, es decir, un atributo que le permite crear estructuras y clases similares a la unión en C ++. Con más detalle, este atributo le permite tomar el control de la ubicación de los miembros de la clase en la memoria. En consecuencia, se coloca por encima de la clase. Por lo general, si una clase tiene 2 campos, esperamos que se organicen secuencialmente, es decir, serán independientes entre sí (no se superpongan). Sin embargo, StructLayout permite especificar que la ubicación de los campos será determinada no por el entorno, sino por el usuario. Para indicar explícitamente desplazamientos de campo, use el parámetro LayoutKind.Explicit. Para indicar en qué desplazamiento relativo al comienzo de la clase / estructura (en lo sucesivo, la clase) queremos colocar el campo, debemos colocar el atributo FieldOffset encima, que toma como parámetro el número de bytes, la sangría desde el comienzo de la clase. No podrá transmitir un valor negativo, por lo que no piense en estropear los punteros a la tabla de métodos o al índice del bloque de sincronización, será un poco más complicado.

Comencemos a escribir el código. Para comenzar, sugiero comenzar con un ejemplo simple. Cree una clase de la siguiente forma:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); } 

A continuación, usamos el mecanismo anterior para configurar explícitamente las compensaciones de campo.
 [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; } 

Por ahora, pospondré las explicaciones y usaré la clase escrita de la siguiente manera:
  class Program { static void Main(string[] args) { CustomStructWithLayout instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClass(); instance.Str = "4564"; Console.WriteLine(instance.SomeInstance.GetType()); //System.String Console.WriteLine(instance.SomeInstance.ToString()); //4564 Console.Read(); } } 

Total La llamada al método GetType () devuelve una cadena, el método ToString () es travieso y nos da la cadena "4564".
Descarga para el cerebro: ¿qué se mostrará cuando se llame a la propiedad virtual CustomClass?

Como habrás adivinado, inicializamos CustomStructWithLayout, ambos enlaces son nulos, luego inicializamos un campo de nuestro tipo y luego asignamos una cadena al campo Str. Como resultado, queda un poco más de CustomClass que nada. Encima había una línea con toda su estructura interna, incluida una tabla de métodos y un índice de un bloque de sincronización. Pero el compilador ve que el campo todavía es del tipo de nuestra clase.
Como prueba, le daré un pequeño recorte de WinDbg:

Aquí puedes ver algunas cosas inusuales. La primera es que en el objeto las direcciones en las tablas de métodos tienen diferentes campos para los campos de clase, como se esperaba, pero la dirección del valor del campo es uno. El segundo: puede ver que ambos campos están ubicados en el desplazamiento 4. Creo que la mayoría lo entenderá, pero por si acaso, lo explicaré, directamente en la dirección del objeto hay un enlace a la tabla de métodos. Los campos comienzan con un desplazamiento de 4 bytes (para z 32 bits), y el índice del bloque de sincronización se encuentra con un desplazamiento de -4.

Ahora que ha descubierto lo que está sucediendo, puede intentar usar compensaciones para llamar a algo que no debería llamar.
Para hacer esto, repetí la estructura de la clase de cadena en una de mis clases. Es cierto, repetí solo el principio, ya que la clase de cadena es muy voluminosa.
  public class CustomClassLikeString { public const int FakeAlignConst = 3; public const int FakeCharPtrAlignConst = 3; public static readonly object FakeStringEmpty; public char FakeFirstChar; public int FakeLength = 3; public const int FakeTrimBoth = 3; public const int FakeTrimHead = 3; public const int FakeTrimTail = 3; public CustomClassLikeString(){} public CustomClassLikeString(int a){} public CustomClassLikeString(byte a){} public CustomClassLikeString(short a){} public CustomClassLikeString(string a){} public CustomClassLikeString(uint a){} public CustomClassLikeString(ushort a){} public CustomClassLikeString(long a){ } public void Stub1(){} public virtual int CompareTo(object value) { return 800; } public virtual int CompareTo(string value) { return 801; } } 

Bueno, la estructura con Layout cambia un poco
  [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; } 

Además, al llamar a FakeLength o al método CompareTo (), debido al desplazamiento idéntico de estos miembros de clase en relación con la dirección del objeto en sí, se llamará al método de cadena correspondiente (en este caso). Me llevó bastante tiempo llegar al primer método privado en la línea que puedo usar, así que me decidí por el público. Pero el campo es privado, todo es justo. Por cierto, los métodos se hacen virtuales para proteger contra todo tipo de optimizaciones, lo que interfiere con el trabajo (por ejemplo, incrustación), y también para que el método sea llamado por un desplazamiento en la tabla de métodos.

Entonces, rendimiento. Es un hecho que un competidor directo en el desafío de lo que no es necesario causar y en la violación de la encapsulación es el reflejo. Creo que ya está claro que somos más rápidos que esto, todos nosotros no analizamos metadatos. Valores exactos:
MétodoTrabajoMediaErrorStddevMediana
StructLayoutFieldClr0,0597 ns0,0344 ns0,0396 ns0,0498 ns
Campo de reflexiónClr197.1257 ns1.9148 ns1.7911 ns197,4787 ns
StructLayoutMethodClr3.5195 ns0,0382 ns0.0319 ns3.5285 ns
Método de reflexiónClr743.9793 ns13.7378 ns12.8504 ns743.8471 ns
Aquí hay un largo código con cómo medí el rendimiento (si alguien lo necesita):
Código
  [ClrJob] [RPlotExporter, RankColumn] [InProcessAttribute] public class Benchmarking { private CustomStructWithLayout instance; private string str; [GlobalSetup] public void Setup() { instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClassLikeString(); instance.Str = "4564"; str = "4564"; } [Benchmark] public int StructLayoutField() { return instance.SomeInstance.FakeLength; } [Benchmark] public int ReflectionField() { return (int)typeof(string).GetField("m_stringLength", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(str); } [Benchmark] public int StructLayoutMethod() { return instance.SomeInstance.CompareTo("4564"); } [Benchmark] public int ReflectionMethod() { return (int)typeof(string).GetMethod("CompareTo", new[] { typeof(string) }).Invoke(str, new[] { "4564" }); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<Benchmarking>(); } } 

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


All Articles