最近在看 C++ 的方法重載,我就在想 C# 中的重載底層是怎么玩的,很多朋友應該知道 C 是不支持重載的,比如下面的代碼就會報錯。
#include?<stdio.h>int?say()?{return?1;
}
int?say(int?i)?{return?i;
}int?main()
{say(10);return?0;
}

從錯誤信息看,它說 say
方法已經存在了,尷尬。。。
一:為什么 C 不支持
要想尋找答案,需要了解一點點底層知識,那就是編譯器在編譯 C 方法時會將 函數名
作為符號添加到 符號表
中,這個 符號表
就是 call 到 say方法字節碼
?中間的一個載體,畫個圖大概就是這樣。

簡而言之,call 先跳轉到 符號表
, 然后再 jmp 到 say 方法,問題就出現在這里,符號表是一種類字典結構,是不可以出現 符號
相同的情況。對了,在 windbg 中我們可以用 x
命令去搜索這些符號,
為了論證我的說法,可以在匯編層面給大家驗證下,修改代碼如下:
#include?<stdio.h>int?say(int?i)?{return?i;
}int?main()
{say(10);return?0;
}
接下來再看下匯編。
---------------?say(10)?-----------00C41771??push????????0Ah??
00C41773??call????????_say?(0C412ADh)??---------------?符號表?-----------00C412AD??jmp?????????say?(0C417B0h)??---------------?say?body?-----------00C417B0??push????????ebp??
00C417B1??mov?????????ebp,esp??
00C417B3??sub?????????esp,0C0h??
00C417B9??push????????ebx??
00C417BA??push????????esi??
00C417BB??push????????edi??
00C417BC??mov?????????edi,ebp??
00C417BE??xor?????????ecx,ecx??
00C417C0??mov?????????eax,0CCCCCCCCh??
00C417C5??rep?stos????dword?ptr?es:[edi]??
00C417C7??mov?????????ecx,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);??----------------003B2E5F??push????????0Ah??
003B2E61??lea?????????ecx,[person]??
003B2E64??call????????Person::sayhello?(03B13A2h)?------------??person.sayhello("hello?world");?----------------003B2E69??push????????offset?string?"hello?world"?(03B9C2Ch)??
003B2E6E??lea?????????ecx,[person]??
003B2E71??call????????Person::sayhello?(03B1302h)
從匯編代碼看, 調的都是 Person::sayhello
這個符號,奇怪的是他們屬于不同的地址: 03B13A2h
, 03B1302h
,這就太奇怪了,哈哈,字典類符號表
肯定是沒有問題的,問題是 Visual Studio 20222
的反匯編窗口在調試時做了一些內部轉換,算是蒙蔽了我們雙眼吧,
真是可氣!!!居然運行時匯編代碼都還不夠徹底,那現在我們怎么繼續挖呢?可以用 IDA
去看這個程序的 靜態反匯編代碼
,截圖如下:

從代碼上的注釋可以清楚的看到,原來:
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);?-----------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++ 這一套。
好了本篇就聊到這里,希望對你有幫助。