最近在看 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
看看这个程序静态反汇编代码
,截图如下:
从代码上的注释可以清楚地看到:
-
Person::sayhello(int)
变成了j_?sayhello@Person@@QAEXH@Z
。 -
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++ 这一套。
好了本篇就聊到这里,希望对你有帮助。