一文详解C#中方法重载的底层玩法

目录
  • 一:为什么 C 不支持
  • 二:C++ 符号表突破
  • 三:C#如何实现突破

最近在看 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 这个符号,奇怪的是他们属于不同的地址: 03B13A2h03B1302h,这就太奇怪了,哈哈,字典类符号表肯定是没有问题的,问题是 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++ 这一套。

以上就是一文详解C#中方法重载的底层玩法 的详细内容,更多关于C#方法重载的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详细分析c# 运算符重载

    您可以重定义或重载 C# 中内置的运算符.因此,程序员也可以使用用户自定义类型的运算符.重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的.与其他函数一样,重载运算符有返回类型和参数列表. 例如,请看下面的函数: public static Box operator+ (Box b, Box c) { Box box = new Box(); box.length = b.length + c.length; box.breadth = b.breadth

  • C#中多态、重载、重写区别分析

    本文实例总结了C#中多态.重载与重写的概念与区别.对于初学C#的朋友来说有不错的参考价值.分享给大家供大家参考.具体分析如下: 重写:是指重写基类的方法,在基类中的方法必须有修饰符virtual,而在子类的方法中必须指明override. 格式如下: 1.在基类中: public virtual void myMethod() { } 2.在子类中: public override void myMethod() { } 重写以后,用基类对象和子类对象访问myMethod()方法,结果都是访问在

  • 基于C#方法重载的总结详解

    1.首先解释一下什么是方法重载? 方法重载是指在同一个类中方法同名,参数不同,调用时根据实参的形式,选择与他匹配的方法执行操作的一种技术. 这里所说的参数不同是指以下几种情况: ①  参数的类型不同 ②  参数的个数不同 ③  参数的个数相同时他们的先后顺序不同 注意:系统会认为是同一个方法的两种情况,这样的两个方法不可以在同一个类里,否则系统会报错. ⑴返回类型不同,方法名和参数个数.顺序.类型都相同的两个方法 ⑵返回类型相同,方法名和参数的个数.顺序.类型都相同的两个方法,但是参数的名字不同

  • C#中重载与重写区别分析

    一.前言        接触面向对象的思想已经有一段时光了,为什么要学习面向对象呢?因为面向对象具有可复用性.可维护性.可扩展性等优点.         本人之前一直接触的是VB语言,所以正式接触面向对象的编程语言还要从C#开始.前段时间,刚学完C#,其中重载.重写傻傻分不清楚.如今通过查阅资料对这两者有了一个大概的理解,不足之处还请多多指教. 二.定义         重载:在同一定义域内(比如一个类里面).方法名相同,参数个数.次序.类型不同.因此重载对返回值没有要求,可以相同,也可以不同.

  • 一文详解C#中方法重载的底层玩法

    目录 一:为什么 C 不支持 二:C++ 符号表突破 三:C#如何实现突破 最近在看 C++ 的方法重载,我就在想 C# 中的重载底层是怎么玩的,很多朋友应该知道 C 是不支持重载的,比如下面的代码就会报错. #include <stdio.h> int say() { return 1; } int say(int i) { return i; } int main() { say(10); return 0; } 从错误信息看,它说 say 方法已经存在了,尴尬... 一:为什么 C 不支

  • 详解Java中方法重写和方法重载的6个区别

    目录 1.方法重写 1.1 基本用法 1.2 使用场景 1.3 注意事项 2.方法重载 2.1 基本使用 2.2 使用场景 2.3 注意事项 3.方法重写 VS 方法重载 总结 方法重写(Override)和方法重载(Overload)都是面向对象编程中,多态特性的不同体现,但二者本身并无关联,它们的区别犹如马德华之于刘德华的区别,除了名字长得像之外,其他的都不像. 接下来咱们就来扒一下二者的具体区别. 1.方法重写 方法重写(Override)是一种语言特性,它是多态的具体表现,它允许子类重新

  • 一文详解C++中动态内存管理

    目录 前言 1.C/C++程序的内存开辟 2.C语言中动态内存管理方式:malloc/calloc/realloc/free 2.1malloc.calloc.realloc区别? 3.C++内存管理方式 3.1 new/delete操作内置类型 3.2 new和delete操作自定义类型 3.3new和malloc处理失败 4.operator new与operator delete函数 4.1 operator new与operator delete函数 4.1.1 我们看看operator

  • 一文详解Java中的Stream的汇总和分组操作

    目录 前言 一.查找流中的最大值和最小值 二.汇总 三.连接字符串 四.分组 1.分组 2.多级分组 3.按子组数据进行划分 后记 前言 在前面的文章中其实大家也已经看到我使用过collect(Collectors.toList()) 将数据最后汇总成一个 List 集合. 但其实还可以转换成Integer.Map.Set 集合等. 一.查找流中的最大值和最小值 static List<Student> students = new ArrayList<>(); ​ static

  • 一文详解JS中的事件循环机制

    目录 前言 1.JavaScript是单线程的 2.同步和异步 3.事件循环 前言 我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件循环机制. 1.JavaScript是单线程的 JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事.在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行.在

  • 一文详解Java中的类加载机制

    目录 一.前言 二.类加载的时机 2.1 类加载过程 2.2 什么时候类初始化 2.3 被动引用不会初始化 三.类加载的过程 3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 四.父类和子类初始化过程中的执行顺序 五.类加载器 5.1 类与类加载器 5.2 双亲委派模型 5.3 破坏双亲委派模型 六.Java模块化系统 一.前言 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程

  • 一文详解C++中运算符的使用

    目录 一.算术运算符 二.关系运算符 三.逻辑运算符 四.位运算符 五.赋值运算符 六.杂项运算符 一.算术运算符 运算符 描述 + 把两个操作数相加 - 从第一个操作数中减去第二个操作数 * 把两个操作数相乘 / 分子除以分母 % 取模运算符,整除后的余数 ++ 自增运算符,整数值增加 1 – 自减运算符,整数值减少 1 通过下面的例子可以让我们更好的理解C++中的运算符的意义与使用方法. #include <iostream> using namespace std; int main()

  • 一文详解Python中生成器的原理与使用

    目录 什么是生成器 迭代器和生成器的区别 创建方式 生成器表达式 基本语法 生成器函数 yield关键字 yield和return yield的使用方法 生成器函数的基本使用 send的使用 可迭代对象的优化 总结 我们学习完推导式之后发现,推导式就是在容器中使用一个for循环而已,为什么没有元组推导式? 原因就是“元组推导式”的名字不是这样的,而是叫做生成器表达式. 什么是生成器 生成器表达式本质上就是一个迭代器,是定义迭代器的一种方式,是允许自定义逻辑的迭代器.生成器使用generator表

  • 一文详解Java中Stream流的使用

    目录 简介 操作1:创建流 操作2:中间操作 筛选(过滤).去重 映射 排序 消费 操作3:终止操作 匹配.最值.个数 收集 规约 简介 说明 本文用实例介绍stream的使用. JDK8新增了Stream(流操作) 处理集合的数据,可执行查找.过滤和映射数据等操作. 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询.可以使用 Stream API 来并行执行操作. 简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式. 特点 不是数据结构

  • 一文详解Python中PO模式的设计与实现

    目录 什么是PO模式 PO 三层模式 PO 设计模式的优点 将改写的脚本转为PO设计模式 构建基础的 BasePage 层 构建首页的 Page 层(HomePage) 构建登录页的 Page 层(LoginPage) 构建 首页 - 订单 - 支付 流程的 Page 层(OrderPage) PO 设计模式下测试Case的改造 在使用 Python 进行编码的时候,会使用自身自带的编码设计格式,比如说最常见的单例模式,稍微抽象一些的抽象工厂模式等等… 在利用 Python 做自动化测试的时候,

随机推荐