一文详解C#中方法重载的底层玩法
作者:一线码农 发布时间:2022-03-08 11:11:03
最近在看 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++ 这一套。
来源:https://www.cnblogs.com/huangxincheng/p/16378081.html
猜你喜欢
- 如何在多线程中使用随机数生成器(Random)避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致
- 让我们来看看这段代码: import java.util.BitSet;import java.util.concurrent.C
- 1. 抽象类关键字:abstract类:用来描述一类具体的事物抽象类:抽象的、模糊的、不具体的类在Java的普通类中是不允许多继承的,原因是
- 一、什么是JSONJSON: JavaScript Object Notation JS对象简谱,是一种类似于XML的语言。相比于XML,它
- 当我们需要与 NIO Channel 进行交互时, 我们就需要使用到 NIO Buffer, 即数据从 Buffer读取到 Channel
- 对接支付宝支付接口,官方文档已经写的很清楚了,但是也有很多像我一样的小白,第一次对接支付宝支付接口,会有些迷茫,所以我在此写下这篇文章,给我
- 对一个集合中的对象进行排序,根据对象的某个指标的大小进行升序或降序排序。代码如下:进行降序排列 进行降序排列 Co
- 一、技术介绍线上演示地址:http://chat.breez.work实时通信(Instant Messaging,简称IM)是一个实时通信
- Spring 使用Junit单元测试并配置数据源一、问题描述由于公司项目中的数据源是配置在Tomcat中的server.xml中的,所以在使
- 前言:线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、
- Spring是一个非常流行的Java Web开发框架,它提供了强大的依赖注入、面向切面编程、声明式事务管理等功能,为开发者提供了高效、快速地
- 重新认识 Java 的 System.in以前也写过不少命令行的程序,处理文件时总需要通过参数指定路径,直到今天看资料时发现了一种我自己从来
- 前言昨晚想在Android应用中增加一个int映射到String的字典表,使用HashMap实现的时候,Eclipse给出了一个警告,昨晚项
- static和@Component遇到的bug今天在编写util的时候,发现不能调用到工具类里面的方法,转眼一看,原来不是工具类里面的方法是
- 如果不知道,类的静态变量存储在那? 方法的局部变量存储在那? 赶快收藏Java内存区域主要可以分为共享内存,堆、方法区和线程私有内存,虚拟机
- 刚开始我以为熔断和降级是一体的,以为他们必须配合使用; 只不过名字不一样而已,但是当我经过思考过后,发现他们其实不是一个东西;降级什么是服务
- Feign使用@RequestLine遇到的坑如何在微服务项目中调用其它项目的接口试使用spring cloud feign声明式调用。/*
- Java 常量池的实例详解Java的常量池中包含了类、接口、方法、字符串等一系列常量值。常量池在编译期间就已经确定,并保存在*.class文
- 本文实例讲述了C#使用Dispose模式实现手动对资源的释放。分享给大家供大家参考。具体实现方法如下://单一类的实现class MyCla
- 前言文件的上传和下载都是基于io复制,只不过文件上传是浏览器向服务器发送报文文件下载是服务器向浏览器发送报文提示:以下是本篇文章正文内容,下