Hello. This time we continue to laugh at the normal method call. I propose to get acquainted with the method call with parameters without passing parameters. We will also try to convert the reference type to a number — its address, without using pointers and unsafe code.


Disclaimer


Before proceeding with the story, I strongly recommend that you read the previous post about StructLayout. Here I will use some features, that were described there.

Also I would like to warn that this article does not contain material that should be used in real projects.

Some initial information


Before we start practicing, let's remember how the C# code is converted into assembler code.
Let us examine a simple example.

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

This code does not contain anything difficult, but the instructions generated by JiT contain several key points. I propose to look only on a small fragment of the generated code. in my examples I will use assembler code for 32 bit machines.

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]

In this small example, you can observe fastcall — calling convention that uses registers to pass parameters (the first two parameters from left to right in the ecx and edx registers), and the remaining parameters are passed through the stack from right to left. The first (implicit) parameter is the address of the instance of the class on which the method is called (for non-static methods).

In our case first parameter is the address of the instance, second one is our int value.

So int the first line we see the local variable 5, there is nothing interesting here.
In the second line, we copy the address of the Helper instance into the ecx register. This is the address of the pointer to method table.
In the third line there is copying of local variable 5 into the edx register
In the fourth line we can see copying of the method table address into the eax register
Fifth line contains loading of the value from memory at the address 40 bytes larger than the address of the method table: the start of the methods addresses in the method table. (The method table contains various information that is stored before. For example address of the base class method table, the EEClass address, various flags, including the garbage collector flag, and so on). Thus, the address of the first method from the method table is now stored in the eax register.
Note: In .NET Core the layout of the method table was changed. Now there is field (at 32/64 bit offset for 32 and 64 bit systems respectively) that contains the address of the start of method list.
In the sixth line, the method is called at offset 16 from the beginning, that is, the fifth one in the method table. Why is our only method is fifth? I remind you that object has 4 virtual methods (ToString(), Equals(), GetHashCode() and Finalize()), which all classes will have, respectively.

Goto Practice;


Practive:
It's time to start a small demonstration. I suggest such small blank (very similar to the blank from the previous article).

    [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) { }
    }

And let's use that stuff in such way:

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

As you might guess, from the experience of the previous article, the Useless(int j) method of type Test1 will be called.

But what will be displayed? The attentive reader, I believe, has already answered this question. «55555» is displayed on the console.

But let's still look at the generated code fragments.

     mov ecx, [ebp-0x20]
     mov edx, [ebp-0x10]
     cmp [ecx], ecx
     call Stub.Foo(Int32)
     mov ecx, [ebp-0x1c]
     mov eax, [ecx]
     mov eax, [eax+0x28]
     call dword [eax+0x10]

I think you recognize the virtual method call pattern, it starts after Stub.Foo(Int32) call. As we can see, as expected ecx is filled with the address of the instance on which the method is called. But since the compiler think that we call a method of type Test2, which has no parameters, nothing is written to edx. However, we have another method call before. And there we have used edx to pass parameter. And of course we don't have instructions, that clear edx. So, as you can see in console output, previous edx value was used.

There is another interesting nuance. I specifically used the meaningful type. I suggest trying to replace the parameter type of the Foo method of the Stub type with any reference type, for example, a string. But the parameter type of the method Useless() does not change. Below you can see the result on my machine with some clarifying information: WinDBG and Calculator :)


Clickable image

The output window displays the address of the reference type in decimal notation.

Total


We refreshed the knowledge of calling methods using the fastcall convention and immediately used the wonderful edx register to pass a parameter in 2 methods at a time. We also spat on all types and with the knowledge that everything is only bytes easily obtained the address of the object without using pointers and unsafe code. Further I plan to use the received address for even more inapplicable purposes!

Thanks for attention!

P.S. C# code can be found here

Комментарии (0)