最近在看 C++ 的方法過載,我就在想 C# 中的過載底層是怎麼玩的,很多朋友應該知道 C 是不支援過載的,比如下面的程式碼就會報錯。
#include <stdio.h>
int say() {
return 1;
}
int say(int i) {
return i;
}
int main()
{
say(10);
return 0;
}
從錯誤資訊看,它說 say
方法已經存在了,尷尬。。。
要想尋找答案,需要了解一點點底層知識,那就是編譯器在編譯 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++ 方法過載的程式碼。
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# 的底層 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++ 這一套。
好了本篇就聊到這裡,希望對你有幫助。