一文详解C#中方法重载的底层玩法

目录

  • 一:为什么 C 不支持
  • 二:C++ 符号表突破
  • 三:C#如何实现突破
最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错。
#include int say() { return 1; }int say(int i) { return i; }int main(){ say(10); return 0; }

一文详解C#中方法重载的底层玩法
文章图片

从错误信息看,它说 say 方法已经存在了,尴尬。。。

一:为什么 C 不支持 要想寻找答案,需要了解一点点底层知识,那就是编译器在编译 C 方法时会将函数名作为符号添加到符号表中,这个符号表 就是call到say方法字节码中间的一个载体,画个图大概就是这样。
一文详解C#中方法重载的底层玩法
文章图片

简而言之,call 先跳转到符号表, 然后再 jmp 到 say 方法,问题就出现在这里,符号表是一种类字典结构,是不可以出现符号相同的情况。对了,在 windbg 中我们可以用 x 命令去搜索这些符号,
为了论证我的说法,可以在汇编层面给大家验证下,修改代码如下:
#include int say(int i) { return i; }int main(){ say(10); return 0; }

接下来再看下汇编。
--------------- say(10) -----------00C41771push0Ah00C41773call_say (0C412ADh)--------------- 符号表 -----------00C412ADjmpsay (0C417B0h)--------------- say body -----------00C417B0pushebp00C417B1movebp,esp00C417B3subesp,0C0h00C417B9pushebx00C417BApushesi00C417BBpushedi00C417BCmovedi,ebp00C417BExorecx,ecx00C417C0moveax,0CCCCCCCCh00C417C5rep stosdword ptr es:[edi]00C417C7movecx,offset _2440747F_ConsoleApplication6@c (0C4C008h)...

知道了原理后,我们再看看 C++ 是如何在符号表上实现唯一性突破。

二:C++ 符号表突破 为了方便讲述,我们先上一段 C++ 方法重载的代码。
using namespace std; class Person{public: void sayhello(int i) {cout << i << endl; } void sayhello(const char* c) {cout << c << endl; }}; int main(int argc){ Person person; person.sayhello(10); person.sayhello("hello world"); }

按理说 sayhello 有多个,肯定是无法突破的,带着好奇心我们看下它的反汇编代码。
----------person.sayhello(10); ----------------003B2E5Fpush0Ah003B2E61leaecx,[person]003B2E64callPerson::sayhello (03B13A2h) ------------person.sayhello("hello world"); ----------------003B2E69pushoffset string "hello world" (03B9C2Ch)003B2E6Eleaecx,[person]003B2E71callPerson::sayhello (03B1302h)

从汇编代码看, 调的都是 Person::sayhello 这个符号,奇怪的是他们属于不同的地址: 03B13A2h, 03B1302h,这就太奇怪了,哈哈,字典类符号表肯定是没有问题的,问题是 Visual Studio 20222 的反汇编窗口在调试时做了一些内部转换,算是蒙蔽了我们双眼吧,
真是可气!!!居然运行时汇编代码都还不够彻底,那现在我们怎么继续挖呢? 可以用 IDA 去看这个程序的静态反汇编代码,截图如下:
一文详解C#中方法重载的底层玩法
文章图片

从代码上的注释可以清楚的看到,原来:
  • Person::sayhello(int) 变成了 j_?sayhello@Person@@QAEXH@Z
  • Person::sayhello(char const *) 变成了 j_?sayhello@Person@@QAEXPBD@Z
到这里终于搞清楚了,原来 C++ 为了支持方法重载,将方法名做了重新编码,这样确实可以突破符号表的唯一性限制。

三:C#如何实现突破 我们都知道 C# 的底层 CLR 是由 C++ 写的,所以大概率玩法都是一样,接下来上一段代码:
internal class Program{static void Main(string[] args){//故意做一次重复Say(10); Say("hello world"); Say(10); Say("hello world"); Console.ReadLine(); }static void Say(int i){Console.WriteLine(i); }static void Say(string s){Console.WriteLine(s); }}

由于 C# 的方法是由 JIT 在运行时动态编译的,并且首次编译方法会先跳转到 JIT 的桩地址,所以断点必须下在第二次调用 Say(10) 处才能看到方法的符号地址,汇编代码如下:
----------- Say(10); -----------00007FFB82134DFCmovecx,0Ah00007FFB82134E01callMethod stub for: ConsoleApp1.Program.Say(Int32) (07FFB81F6F118h)00007FFB82134E06nop----------- Say("hello world"); -----------00007FFB82134E07movrcx,qword ptr [1A8C65E8h]00007FFB82134E0FcallMethod stub for: ConsoleApp1.Program.Say(System.String) (07FFB81F6F120h)00007FFB82134E14nop

从输出信息看,同样也是两个符号表地址,然后由符号表地址 jmp 到最后的方法体。
----------- Say(10); -----------00007FFB82134E01callMethod stub for: ConsoleApp1.Program.Say(Int32) (07FFB81F6F118h)----------- 符号表 -----------00007FFB81F6F118jmpConsoleApp1.Program.Say(Int32) (07FFB82134F10h)----------- Say body -----------00007FFB82134F10pushrbp00007FFB82134F11pushrdi00007FFB82134F12pushrsi00007FFB82134F13subrsp,20h00007FFB82134F17movrbp,rsp00007FFB82134F1Amovdword ptr [rbp+40h],ecx00007FFB82134F1Dcmpdword ptr [7FFB82036B80h],000007FFB82134F24jeConsoleApp1.Program.Say(Int32)+01Bh (07FFB82134F2Bh)00007FFB82134F26call00007FFBE1C2CC40

暂时还不知道怎么看 JIT 改名后方法名,有知道的朋友可以留言一下哈,但总的来说还是 C++ 这一套。
【一文详解C#中方法重载的底层玩法】以上就是一文详解C#中方法重载的底层玩法 的详细内容,更多关于C#方法重载的资料请关注脚本之家其它相关文章!

    推荐阅读