资讯详情

聊聊 C# 方法重载的底层玩法

最近在看 C 我在想重载的方法。 C# 很多朋友应该知道重载底层是怎么玩的。 C 不支持重载,比如下面的代码会报错。

#include<stdio.h>  intsay(){ return1; } intsay(inti){ returni; }  intmain() { say(10); return0; }  

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

一:为什么 C 不支持

如果你想找到答案,你需要知道一点底层知识,那就是编译器在编译 C 方法时会将函数名添加到符号中符号表中,这个符号表就是 call 到say方法字节码中间的载体大概就是这样画的。

简而言之,call 先跳转到符号表, 然后再 jmp 到 say 这里出现了方法和问题,它是一种类字典结构,不能出现符号同样的情况。对了,在 windbg 我们可以用它x命令搜索这些符号,

为了论证我的说法,您可以在汇编层面进行验证。修改代码如下:

#include<stdio.h>  intsay(inti){ returni; }  intmain() { say(10); return0; }  

接下来看汇编。

---------------say(10)-----------  00C41771push0Ah 00C41773call_say(0C412ADh)  ---------------符号表-----------  00C412ADjmpsay(0C417B0h)  ---------------saybody-----------  00C417B0pushebp 00C417B1movebp,esp 00C417B3subesp,0C0h 00C417B9pushebx 00C417BApushesi 00C417BBpushedi 00C417BCmovedi,ebp 00C417BExorecx,ecx 00C417C0moveax,0CCCCCCCCh 00C417C5repstosdwordptres:[edi] 00C417C7movecx,offset_2440747F_ConsoleApplication6@c(0C4C008h) ...  

了解原理后,我们再来看看。 C 是如何在符号表唯一唯一突破。

二:C 符号表突破

为了方便讲述,我们先上一段 C 方法重载代码。

usingnamespacestd;  classPerson { public: voidsayhello(inti){ cout<<i<<endl; } voidsayhello(constchar*c){ cout<<c<<endl; } };  intmain(intargc) { Personperson;  person.sayhello(10); person.sayhello("helloworld"); }  

按理说sayhello有很多,肯定是无法突破的。带着好奇,我们来看看它的反汇编代码。

----------person.sayhello(10);----------------  003B2E5Fpush0Ah 003B2E61leaecx,[person] 003B2E64callPerson::sayhello(03B13A2h)  ------------person.sayhello("helloworld");----------------  003B2E69pushoffsetstring"helloworld"(03B9C2Ch) 003B2E6Eleaecx,[person] 003B2E71callPerson::sayhello(03B1302h)  

从汇编代码来看, 调的都是Person::sayhello奇怪的是,它们属于不同的地址:03B13A2h,03B1302h,太奇怪了,哈哈,字典符号表肯定没问题。Visual Studio 20222在调试过程中,反汇编窗口进行了一些内部转换,可以算是蒙蔽了我们的眼睛,

真可气!!!运行时汇编代码不够彻底,现在怎么继续挖?IDA看看这个程序静态反汇编代码,截图如下:

从代码上的注释可以清楚地看到:

  1. Person::sayhello(int)变成了 j_?sayhello@Person@@QAEXH@Z

  2. Person::sayhello(char cont *) 变成了  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); -----------

00007FFB82134DFC  mov         ecx,0Ah  
00007FFB82134E01  call        Method stub for: ConsoleApp1.Program.Say(Int32) (07FFB81F6F118h)  
00007FFB82134E06  nop  

----------- Say("hello world");  -----------

00007FFB82134E07  mov         rcx,qword ptr [1A8C65E8h]  
00007FFB82134E0F  call        Method stub for: ConsoleApp1.Program.Say(System.String) (07FFB81F6F120h)  
00007FFB82134E14  nop  

从输出信息看,同样也是两个符号表地址,然后由符号表地址 jmp 到最后的方法体。

----------- Say(10); -----------
00007FFB82134E01  call        Method stub for: ConsoleApp1.Program.Say(Int32) (07FFB81F6F118h)  

----------- 符号表 -----------
00007FFB81F6F118  jmp         ConsoleApp1.Program.Say(Int32) (07FFB82134F10h)  

----------- Say body -----------

00007FFB82134F10  push        rbp  
00007FFB82134F11  push        rdi  
00007FFB82134F12  push        rsi  
00007FFB82134F13  sub         rsp,20h  
00007FFB82134F17  mov         rbp,rsp  
00007FFB82134F1A  mov         dword ptr [rbp+40h],ecx  
00007FFB82134F1D  cmp         dword ptr [7FFB82036B80h],0  
00007FFB82134F24  je          ConsoleApp1.Program.Say(Int32)+01Bh (07FFB82134F2Bh)  
00007FFB82134F26  call        00007FFBE1C2CC40  

暂时还不知道怎么看 JIT 改名后 方法名,有知道的朋友可以留言一下哈,但总的来说还是 C++ 这一套。

好了本篇就聊到这里,希望对你有帮助。

标签: dd70f120三社二极管模块

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台