Disclaimer

First — theory

public class StubClass { public static int StubMethod(int fromEcx, int fromEdx, int fromStack) { int local = 5; return local + fromEcx + fromEdx + fromStack; } public static void CallingMethod() { int local1 = 7, local2 = 8, local3 = 9; int result = StubMethod(local1, local2, local3); } }

StubClass.StubMethod(Int32, Int32, Int32) 1: push ebp 2: mov ebp, esp 3: sub esp, 0x10 4: mov [ebp-0x4], ecx 5: mov [ebp-0x8], edx 6: xor edx, edx 7: mov [ebp-0xc], edx 8: xor edx, edx 9: mov [ebp-0x10], edx 10: nop 11: mov dword [ebp-0xc], 0x5 12: mov eax, [ebp-0xc] 13: add eax, [ebp-0x4] 14: add eax, [ebp-0x8] 15: add eax, [ebp+0x8] 16: mov [ebp-0x10], eax 17: mov eax, [ebp-0x10] 18: mov esp, ebp 19: pop ebp 20: ret 0x4 StubClass.CallingMethod() 1: push ebp 2: mov ebp, esp 3: sub esp, 0x14 4: xor eax, eax 5: mov [ebp-0x14], eax 6: xor edx, edx 7: mov [ebp-0xc], edx 8: xor edx, edx 9: mov [ebp-0x8], edx 10: xor edx, edx 11: mov [ebp-0x4], edx 12: xor edx, edx 13: mov [ebp-0x10], edx 14: nop 15: mov dword [ebp-0x4], 0x7 16: mov dword [ebp-0x8], 0x8 17: mov dword [ebp-0xc], 0x9 18: push dword [ebp-0xc] 19: mov ecx, [ebp-0x4] 20: mov edx, [ebp-0x8] 21: call StubClass.StubMethod(Int32, Int32, Int32) 22: mov [ebp-0x14], eax 23: mov eax, [ebp-0x14] 24: mov [ebp-0x10], eax 25: nop 26: mov esp, ebp 27: pop ebp 28: ret

Small example

Code is in the spoiler using System; using System.Runtime.InteropServices; namespace Magic { public class StubClass { public StubClass(int id) { Id = id; } public int Id; } [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public Test1 Test1; [FieldOffset(0)] public Test2 Test2; } public class Test1 { public virtual void Useless(int skipFastcall1, int skipFastcall2, StubClass adressOnStack) { adressOnStack.Id = 189; } } public class Test2 { public virtual int Useless() { return 888; } } class Program { static void Main() { Test2 objectWithLayout = new CustomStructWithLayout { Test2 = new Test2(), Test1 = new Test1() }.Test2; StubClass adressOnStack = new StubClass(3); objectWithLayout.Useless(); Console.WriteLine($"MAGIC - {adressOnStack.Id}"); // MAGIC - 189 } } }



I propose to look at the internals that are behind the simple lines of initializing of the objects, calling methods, and passing parameters. And, of course, we will use this information in practice — we will subtract the stack of the calling method.Before proceeding with the story, I strongly recommend you to read the first post about StructLayout , there is an example that will be used in this article.All code behind the high-level one is presented for themode, because it shows the conceptual basis. JIT optimization is a separate big topic that will not be covered here.I would also like to warn that this article does not contain material that should be used in real projects.Any code eventually becomes a set of machine commands. Most understandable is their representation in the form of Assembly language instructions that directly correspond to one (or several) machine instructions.Before turning to a simple example, I propose to get acquainted with stack.is primarily a chunk of memory that is used, as a rule, to store various kinds of data (usually they can be called). It is also worth remembering that the stack grows towards smaller addresses. That is the later an object is placed on the stack, the less address it will have.Now let's take a look on the next piece of code in Assembly language (I’ve omitted some of the calls that are inherent in the debug mode).C#:Asm:The first thing to notice is theand theregisters and operations with them.A misconception that theregister is somehow related to the pointer to the top of the stack is common among my friends. I must say that it is not.Theregister is responsible for pointing to the top of the stack. Correspondingly, with eachinstruction (putting a value on the top of the stack) the value ofregister is decremented (the stack grows towards smaller addresses), and with eachinstruction it is incremented. Also, thecommand pushes the return address on the stack, thereby decrements the value of theregister. In fact, the change of theregister is performed not only when these instructions are executed (for example, when interrupt calls are made, the same thing happens with theinstructions).Will considerIn the first line, the content of theregister is saved (it is put on a stack). Before returning from a function, this value will be restored.The second line stores the current value of the address of the top of the stack (the value of the registeris moved to). Next, we move the top of the stack to as many positions as we need to store local variables and parameters (third row). Something like memory allocation for all local needs —. At the same time, theregister is a starting point in the context of the current call. Addressing is based on this value.All of the above is calledAfter that, variables on the stack are accessed via the storedregister, which points on the place where the variables of this method begin. Next comes the initialization of local variables.reminder: in .net, thecalling convention is used.The calling convention governs the location and the order of the parameters passed to the function.The first and second parameters are passed via theandregisters, respectively, the subsequent parameters are transmitted via the stack. (This is for 32-bit systems, as always. In 64-bit systems four parameters passed through registers())For non-static methods, the first parameter is implicit and contains the address of the instance on which the method is called (this address).In lines 4 and 5, the parameters that were passed through the registers (the first 2) are stored on the stack.Next is cleaning the space on the stack for local variables () and initializing local variables.It is worth be mentioned that the result of the function is in the registerIn lines 12-16, the addition of the desired variables occurs. I draw your attention to line 15. There is a accessing value by the address that is greater than the beginning of the stack, that is, to the stack of the previous method. Before calling, the caller pushes a parameter to the top of the stack. Here we read it. The result of the addition is obtained from the registerand placed on the stack. Since this is the return value of the, it is placed again in. Of course, such absurd instruction sets are inherent only in the debug mode, but they show exactly how our code looks like without smart optimizer that does the lion’s share of the work.In lines 18 and 19, both the previous(calling method) and the pointer to the top of the stack are restored (at the time the method is called). The last line is the returning from function. About the value 0x4 I will tell a bit later.Such a sequence of commands is called a function epilogue.Now let's take a look at. Let's go straight to line 18. Here we put the third parameter on the top of the stack. Please note that we do this using theinstruction, that is, thevalue is decremented. The other 2 parameters are put into registers (). Next comes themethod call. Now let's remember theinstruction. Here the following question is possible: what is 0x4? As I mentioned above, we have pushed the parameters of the called function onto the stack. But now we do not need them. 0x4 indicates how many bytes need to be cleared from the stack after the function call. Since the parameter was one, you need to clear 4 bytes.Here is a rough image of the stack:Thus, if we turn around and see what lies on the stack right after the method call, the first thing we will see, that was pushed onto the stack (in fact, this happened in the first line of the current method). The next thing will be the return address. It determines the place, there to resume the execution after our function is finished (used by). And right after these fields we will see the parameters of the current function (starting from the 3rd, first two parameters are passed through registers). And behind them the stack of the calling method hides!The first and second fields mentioned before (and return address) explain the offset in +0x8 when we access parameters.Correspondingly, the parameters must be at the top of the stack in a strictly defined order before function call. Therefore, before calling the method, each parameter is pushed onto the stack.But what if they do not push, and the function will still take them?So, all the above facts have caused me an overwhelming desire to read the stack of the method that will call my method. The idea that I am only in one position from the third argument (it will be closest to the stack of the calling method) is the cherished data that I want to receive so much, did not let me sleep.Thus, to read the stack of the calling method, I need to climb a little further than the parameters.When referring to parameters, the calculation of the address of a particular parameter is based only on the fact that the caller has pushed them all onto the stack.But implicit passing through theparameter (who is interested — previous article ) makes me think that we can outsmart the compiler in some cases.The tool I used to do this is called StructLayoutAttribute (al features are in the first article ). //One day I will learn a bit more than only this attribute, I promiseWe use the same favorite method with overlapped reference types.At the same time, if overlapping methods have a different number of parameters, the compiler does not push the required ones onto the stack (at least because it does not know which ones).However, the method that is actually called (with the same offset from a different type), turns into positive addresses relative to its stack, that is, those where it plans to find the parameters.But nobody passes parameters and method begins to read the stack of the calling method. And the address of the object(with Id property, that is used in the) is in the place, where the third parameter is expected.I will not give the assembly language code, everything is pretty clear there, but if there are any questions, I will try to answer them in the commentsI understand perfectly that this example cannot be used in practice, but in my opinion, it can be very useful for understanding the general scheme of work.