聊聊 C# 方法重载的底层玩法
<p>最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错。</p><pre><code class="language-C++">
#include <stdio.h>
int say() {
return 1;
}
int say(int i) {
return i;
}
int main()
{
say(10);
return 0;
}
</code></pre>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4dbcd8b1c6c544fcade861204b678774~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<p>从错误信息看,它说 <code>say</code> 方法已经存在了,尴尬。。。</p>
<h2 id="一为什么-c-不支持">一:为什么 C 不支持</h2>
<p>要想寻找答案,需要了解一点点底层知识,那就是编译器在编译 C 方法时会将 <code>函数名</code> 作为符号添加到 <code>符号表</code> 中,这个 <code>符号表</code> 就是 call 到 <code>say方法字节码</code>中间的一个载体,画个图大概就是这样。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e96aafb96880428890de94739b4b4b82~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<p>简而言之,call 先跳转到 <code>符号表</code>, 然后再 jmp 到 say 方法,问题就出现在这里,<strong>符号表</strong>是一种类字典结构,是不可以出现 <code>符号</code> 相同的情况。对了,在 windbg 中我们可以用 <code>x</code> 命令去搜索这些符号,</p>
<p>为了论证我的说法,可以在汇编层面给大家验证下,修改代码如下:</p>
<pre><code class="language-C++">
#include <stdio.h>
int say(int i) {
return i;
}
int main()
{
say(10);
return 0;
}
</code></pre>
<p>接下来再看下汇编。</p>
<pre><code class="language-C++">
--------------- say(10) -----------
00C41771push 0Ah
00C41773call _say (0C412ADh)
--------------- 符号表 -----------
00C412ADjmp say (0C417B0h)
--------------- say body -----------
00C417B0push ebp
00C417B1mov ebp,esp
00C417B3sub esp,0C0h
00C417B9push ebx
00C417BApush esi
00C417BBpush edi
00C417BCmov edi,ebp
00C417BExor ecx,ecx
00C417C0mov eax,0CCCCCCCCh
00C417C5rep stos dword ptr es:
00C417C7mov ecx,offset _2440747F_ConsoleApplication6@c (0C4C008h)
...
</code></pre>
<p>知道了原理后,我们再看看 C++ 是如何在 <code>符号表</code> 上实现唯一性突破。</p>
<h2 id="二c-符号表突破">二:C++ 符号表突破</h2>
<p>为了方便讲述,我们先上一段 C++ 方法重载的代码。</p>
<pre><code class="language-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");
}
</code></pre>
<p>按理说 <code>sayhello</code> 有多个,肯定是无法突破的,带着好奇心我们看下它的反汇编代码。</p>
<pre><code class="language-C++">
---------- person.sayhello(10);----------------
003B2E5Fpush 0Ah
003B2E61lea ecx,
003B2E64call Person::sayhello (03B13A2h)
------------person.sayhello("hello world"); ----------------
003B2E69push offset string "hello world" (03B9C2Ch)
003B2E6Elea ecx,
003B2E71call Person::sayhello (03B1302h)
</code></pre>
<p>从汇编代码看, 调的都是 <code>Person::sayhello</code> 这个符号,奇怪的是他们属于不同的地址: <code>03B13A2h</code>, <code>03B1302h</code>,这就太奇怪了,哈哈,<code>字典类符号表</code> 肯定是没有问题的,问题是 <code>Visual Studio 20222</code> 的反汇编窗口在调试时做了一些内部转换,算是蒙蔽了我们双眼吧,</p>
<p>真是可气!!!居然运行时汇编代码都还不够彻底,那现在我们怎么继续挖呢? 可以用 <code>IDA</code> 去看这个程序的 <code>静态反汇编代码</code>,截图如下:</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f9a9a0876ff34b08b75c369d319f3bba~tplv-k3u1fbpfcp-zoom-1.image" alt="" loading="lazy"></p>
<p>从代码上的注释可以清楚的看到,原来:</p>
<ol>
<li><code>Person::sayhello(int)</code> 变成了<code>j_?sayhello@Person@@QAEXH@Z</code>。</li>
<li><code>Person::sayhello(char const *)</code> 变成了<code>j_?sayhello@Person@@QAEXPBD@Z</code></li>
</ol>
<p>到这里终于搞清楚了,原来 C++ 为了支持方法重载,将 <code>方法名</code> 做了重新编码,这样确实可以突破 <code>符号表</code> 的唯一性限制。</p>
<h3 id="三c-如何实现突破">三:C# 如何实现突破</h3>
<p>我们都知道 C# 的底层 CLR 是由 C++ 写的,所以大概率玩法都是一样,接下来上一段代码:</p>
<pre><code class="language-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);
}
}
</code></pre>
<p>由于 C# 的方法是由 <code>JIT</code> 在运行时动态编译的,并且首次编译方法会先跳转到 JIT 的桩地址,所以断点必须下在第二次调用 <code>Say(10)</code> 处才能看到方法的符号地址,汇编代码如下:</p>
<pre><code class="language-C#"> ----------- Say(10); -----------
00007FFB82134DFCmov ecx,0Ah
00007FFB82134E01call Method stub for: ConsoleApp1.Program.Say(Int32) (07FFB81F6F118h)
00007FFB82134E06nop
----------- Say("hello world"); -----------
00007FFB82134E07mov rcx,qword ptr
00007FFB82134E0Fcall Method stub for: ConsoleApp1.Program.Say(System.String) (07FFB81F6F120h)
00007FFB82134E14nop
</code></pre>
<p>从输出信息看,同样也是两个符号表地址,然后由符号表地址 jmp 到最后的方法体。</p>
<pre><code class="language-C#">
----------- Say(10); -----------
00007FFB82134E01call Method stub for: ConsoleApp1.Program.Say(Int32) (07FFB81F6F118h)
----------- 符号表 -----------
00007FFB81F6F118jmp ConsoleApp1.Program.Say(Int32) (07FFB82134F10h)
----------- Say body -----------
00007FFB82134F10push rbp
00007FFB82134F11push rdi
00007FFB82134F12push rsi
00007FFB82134F13sub rsp,20h
00007FFB82134F17mov rbp,rsp
00007FFB82134F1Amov dword ptr ,ecx
00007FFB82134F1Dcmp dword ptr ,0
00007FFB82134F24je ConsoleApp1.Program.Say(Int32)+01Bh (07FFB82134F2Bh)
00007FFB82134F26call 00007FFBE1C2CC40
</code></pre>
<p>暂时还不知道怎么看 JIT 改名后 <code>方法名</code>,有知道的朋友可以留言一下哈,但总的来说还是 C++ 这一套。</p>
<p>好了本篇就聊到这里,希望对你有帮助。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/16378081.html
頁:
[1]