大家好 这次我们继续嘲笑常规方法调用。 我建议您熟悉不带参数的带参数的方法调用。 我们还尝试将引用类型转换为数字-其地址,而不使用指针和不安全的代码。
免责声明
在开始讲故事之前,我强烈建议您阅读
有关StructLayout的上一篇
文章 ,因为 商定的事情在这里将不再重复。
我也要警告,本文不包含实际项目中应使用的材料。
一些初步信息
在开始之前,让我们回顾一下如何转换C#代码。
让我们看一个简单的例子。 让我提醒您,为了享受StructLayout的乐趣,我仅使用虚拟方法。
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); } }
该代码不包含任何复杂的内容,但是JiT生成的指令包含几个关键点。 我建议仅解析生成代码的一小部分。
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]
在这个小示例中,您可以观察到快速调用-关于通过寄存器传递参数的协议(在ecx和edx寄存器中,前两个参数从左到右),其余参数在堆栈中从右到左传递。 第一个(隐式)参数是在其上调用该方法的类实例的地址。 它作为每个实例方法的第一个隐式参数传递。 第二个参数是int类型的局部变量(在我们的例子中)。
因此,在
第一行中,我们看到了局部变量5,在这里没有什么有趣的。
在
第二行中,我们将Helper实例的地址复制到ecx寄存器中。 这是方法表本身的地址。
第三行包含将局部变量5复制到edx寄存器
第四行将方法表的地址复制到eax寄存器
第五行包含一个
40字节的
eax寄存器移位,从内存中加载一个值,该地址的值比方法表的地址大40个字节:方法表中方法的开始地址。 (方法表包含以前存储的各种信息。例如,此类信息包括基类的方法表的地址,EEClass地址,各种标志(包括垃圾收集器的标志,等等)。 因此,方法表中第一种方法的地址现在存储在eax寄存器中。
在
第六行中,该方法从起始位置偏移16,即方法表中的第五行处调用。 为什么我们唯一的方法是第五种? 我提醒您,对象具有4个虚拟方法(ToString,Equals,GetHashCode和Finalize),因此,它们将在所有类中使用。
让我们继续练习
现在该开始一个小型演示了。 我在这里提出了这样的空白(非常类似于上一篇文章的空白)。
[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) { } }
以及以下Main方法的填充:
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(); } }
您可能会想到,根据前一篇文章的经验,将调用Test1类型的Useless(int j)方法。
但是,可以推断出什么呢? 我相信,细心的读者已经回答了这个问题。 55555显示在控制台上。
但是,让我们看一下所生成代码的片段。
mov ecx, [ebp-0x20] mov edx, [ebp-0x10] cmp [ecx], ecx call Stub.Foo(Int32) nop mov ecx, [ebp-0x1c] mov eax, [ecx] mov eax, [eax+0x28] call dword [eax+0x10]
我认为您认识到虚拟方法调用模式,它在L00cc:nop之后开始。 正如我们所看到的,期望在ecx中写入调用该方法的实例的地址。 但是因为 如果我们调用像Test2这样的没有参数的方法,则不会将任何内容写入edx。 但是,在此之前,该方法被调用,该方法分别将参数传递给edx寄存器,并且值保留在其中。 我们可以在输出窗口中观察它。
还有另一个有趣的细微差别。 我专门使用了有意义的类型。 我建议尝试用任何引用类型(例如字符串)替换Stub类型的Foo方法的参数类型。 但是Useless方法的参数类型不会更改。 下面您可以在计算机上查看结果,其中包含一些说明性元素:WinDBG和Calculator :)
可点击的图片输出窗口显示十进制数字系统中引用类型的地址
总结
他们使用fastcall约定刷新了调用方法的知识,并立即使用奇妙的edx寄存器一次传递参数2方法。 他们也没有对所有类型一概而论,并记住,只有字节很容易就可以接收对象的地址,而无需使用指针和不安全的代码。 此外,我计划将收到的地址用于更多不适用的目的!
感谢您的关注!
PS C#代码可以在
这里找到