What happens behind the scenes C#: the basics of working with the stack

Original author: Pavel Romash
  • Translation
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.

Disclaimer


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 the debug mode, 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.

First — theory


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. Stack is primarily a chunk of memory that is used, as a rule, to store various kinds of data (usually they can be called temporal data). 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#:

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);
    }
}

Asm:

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

The first thing to notice is the EBP and the ESP registers and operations with them.

A misconception that the EBP register is somehow related to the pointer to the top of the stack is common among my friends. I must say that it is not.

The ESP register is responsible for pointing to the top of the stack. Correspondingly, with each PUSH instruction (putting a value on the top of the stack) the value of ESP register is decremented (the stack grows towards smaller addresses), and with each POP instruction it is incremented. Also, the CALL command pushes the return address on the stack, thereby decrements the value of the ESP register. In fact, the change of the ESP register is performed not only when these instructions are executed (for example, when interrupt calls are made, the same thing happens with the CALL instructions).

Will consider StubMethod().

In the first line, the content of the EBP register 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 register ESP is moved to EBP). 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 — stack frame. At the same time, the EBP register is a starting point in the context of the current call. Addressing is based on this value.

All of the above is called the function prologue.

After that, variables on the stack are accessed via the stored EBP register, which points on the place where the variables of this method begin. Next comes the initialization of local variables.

Fastcall reminder: in .net, the fastcall calling 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 the ECX and EDX registers, 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(RCX, RDX, R8, R9))

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 (stack frame) and initializing local variables.

It is worth be mentioned that the result of the function is in the register EAX.

In 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 register EAX and placed on the stack. Since this is the return value of the StubMethod(), it is placed again in EAX. 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 EBP (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 CallingMethod(). 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 the PUSH instruction, that is, the ESP value is decremented. The other 2 parameters are put into registers ( fastcall). Next comes the StubMethod() method call. Now let's remember the RET 0x4 instruction. 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 EBP, 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 RET). 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 (EBP 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?

Small example


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 the EDX parameter (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 promise

We 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 WriteLine()) is in the place, where the third parameter is expected.

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 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 comments

I 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.
Share post

Similar posts

Comments 0

Only users with full accounts can post comments. Log in, please.