Non reflection based access to private fields

Started by RawCode, August 11, 2016, 09:47:27 PM

Previous topic - Next topic

RawCode

subj
tl:dr section in very end


[StructLayout(LayoutKind.Explicit,Size=8)]
public unsafe struct s_rpret
{
[FieldOffset(0)] public object _o;
[FieldOffset(0)] public void*  _v;
}


no reason to store in static fields, this struct always allocated from stack, accessing static field will take more time.
void* can be safely replaced by any other pointer, they all have same size anyway.
If you think that IntPtr is better - open source code of that class and you will be surprised.


s_rpret caster = new s_rpret();
caster._o = targetObject;


Alternative is ugly __makeref() left for reference, result is same.


TypedReference rawref = __makeref(object);
int** actualref = (int**)&rawref;
Console.WriteLine(*actualref[1]); //result is pointer to object


Now lets talk about reading and writing objects.

All classes have struct layout auto and all structs have layout sequential, this can and will cause issues if ignored.
Classes have vtable and syncblock both have size of native int (32\64) (long on 64 bit)

Layout auto allows runtime to "move" fields around in order to reach most effective allocation, result is deterministic, two sets of fields with layout auto will be stored in same manner, always.
Looks like mono embedded with game does not perform any optimizations, still, you must take this in account.

If you extend class and add new fields, runtime will add them separately in increment manner, optimizations done at block level, so extension will never alter field order of base class.

Lets begin, out "victim" class:


public class PrivateClass
{
private int NoAccessA = 99998;
private int NoAccessB = 88888;
private int NoAccessC = 77777;

public void print()
{
Console.WriteLine(NoAccessA);
Console.WriteLine(NoAccessB);
Console.WriteLine(NoAccessC);
}
}


Write "same" field struct:

public unsafe struct smap_PrivateClass
{
public void* vtable;
public void* syncblock;
public int NoAccessA;
public int NoAccessB;
public int NoAccessC;
}

don't forget about special fields, struct do not have them by default.

final code will looks like:

PrivateClass pc = new PrivateClass();
s_rpret caster = new s_rpret();
caster._o = pc;
((smap_PrivateClass*)caster._v)->NoAccessA = 1;
((smap_PrivateClass*)caster._v)->NoAccessB = 1;
((smap_PrivateClass*)caster._v)->NoAccessC = 1;
pc.print();

fields are set without usage of reflection, same code can be used for reading.

Special note related to storing smap_PrivateClass in variables:

expected result:
smap_PrivateClass* mirror = (smap_PrivateClass*)caster._v;

mirror->NoAccessA = 1;
mirror->NoAccessB = 1;
mirror->NoAccessC = 1;

local bitwise copy:
smap_PrivateClass mirror = *(smap_PrivateClass*)caster._v;

mirror.NoAccessA = 1;
mirror.NoAccessB = 1;
mirror.NoAccessC = 1;

pc.print();



complete code:

public class PrivateClass
{
private int NoAccessA = 99998;
private int NoAccessB = 88888;
private int NoAccessC = 77777;

public void print()
{
Console.WriteLine(NoAccessA);
Console.WriteLine(NoAccessB);
Console.WriteLine(NoAccessC);
}
}

public unsafe struct smap_PrivateClass
{
public void* vtable;
public void* syncblock;
public int NoAccessA;
public int NoAccessB;
public int NoAccessC;
}


[StructLayout(LayoutKind.Explicit,Size=8)]
public unsafe struct s_rpret
{
[FieldOffset(0)] public object _o;
[FieldOffset(0)] public void*  _v;
}

//TypedReference ttz = __makeref(pc);
//int** ggv = (int**)&ttz;
//Console.WriteLine(*ggv[1]);

static void ext()
{
PrivateClass pc = new PrivateClass();
s_rpret caster = new s_rpret();
caster._o = pc;

smap_PrivateClass* mirror = (smap_PrivateClass*)caster._v;

mirror->NoAccessA = 1;
mirror->NoAccessB = 1;
mirror->NoAccessC = 1;

pc.print();

//((smap_PrivateClass*)caster._v)->NoAccessA = 1;
//((smap_PrivateClass*)caster._v)->NoAccessB = 1;
//((smap_PrivateClass*)caster._v)->NoAccessC = 1;

}

1000101

Very interesting, I can see it's use for time sensitive code which reflection could cause a major delay when accessing a lot of private members.  For methods which aren't called a lot and/or access very few private members I would still recommend people use reflection though.  I also wonder about getting field offsets directly from the reflection and they using pointer access from that point on.
(2*b)||!(2*b) - That is the question.
There are 10 kinds of people in this world - those that understand binary and those that don't.

Powered By

Fluffy (l2032)

I was lead to understand that the slow part of reflection is the inital meta-data grab, e.g. the Method/Field/Property-Info, and that subsequent call/read/write ops werent noticeably slower than normal access. Is that incorrect? This method feels a bit complex and fragile, unless it's tremendously faster?

RawCode

Quote from: Fluffy (l2032) on August 12, 2016, 03:03:28 PM
I was lead to understand that the slow part of reflection is the inital meta-data grab, e.g. the Method/Field/Property-Info, and that subsequent call/read/write ops werent noticeably slower than normal access. Is that incorrect? This method feels a bit complex and fragile, unless it's tremendously faster?

well, we can run benchmark, to keep results "clean" reflection based code should be done by someone else (not me).
also, please clarify why this should be "fragile", it uses exactly same routines as runtime does behind the scene.
all my snippets grabbed from sources of mono, i never "invented" anything.

yes, you can cache offsets for fields extracted from runtime with reflection (or some specific native method) and use offsets directly.
such method will somewhat slower, check rest of post for details.

only real way to evaluate, is checking native code result...


static void ext(s_rpret caster)
{
smap_PrivateClass* mirror = (smap_PrivateClass*)caster._v;

mirror->NoAccessA = 0x11223344;
//mirror->NoAccessB = 1;
//mirror->NoAccessC = 1;

//pc.print();

//((smap_PrivateClass*)caster._v)->NoAccessA = 1;
//((smap_PrivateClass*)caster._v)->NoAccessB = 1;
//((smap_PrivateClass*)caster._v)->NoAccessC = 1;
}


compile into (full, run before fetching code) overhead (stack frame allocation) stripped
mov eax,DWORD PTR [ebp+0x8] //loading pointer to struct into eax, ebp+0x8 is argument stored on stack
mov DWORD PTR [eax+0x8],0x11223344 //loading value 0x11223344 into pointer eax+0x8


basically, this is simple atomic set operation, setting field for normal struct without using of any magic result into same native code.
that code equals to
((smap_PrivateClass*)caster._v)->NoAccessA = 0x11223344;
and equals to
((int*)caster._v)[2] = 0x11223344

if you manage to use static field to store offset, like

static public int OFFSETVAR = 2;

result will be

mov eax,DWORD PTR [ebp+0x8] //argument to eax
mov ecx,DWORD PTR ds:0x3dd8d8 //field to ecx
shl ecx,0x2 //multiplication by 4 via shift, this value will change based off size of pointer
add eax,ecx //eax + field*4
mov DWORD PTR [eax],0x11223344 //actual set


overhead is 3 operations, mov, shift and addition, this basically double number of processor ticks required to perform operation and consume one more register.