如何使用堆中的类型对象定位结构实例的虚拟方法?

Posted

技术标签:

【中文标题】如何使用堆中的类型对象定位结构实例的虚拟方法?【英文标题】:How does a struct instance's virtual method get located using its type object in heap? 【发布时间】:2021-05-02 00:49:02 【问题描述】:

以下是书中的代码示例,用于显示值类型何时被装箱:

internal struct Point 

   private readonly Int32 m_x, m_y;
   public Point(Int32 x, Int32 y) 
      m_x = x;
      m_y = y;
   
   
   //Override ToString method inherited from System.ValueType
   public override string ToString() 
      return String.Format("(0, 1)", m_x.ToString(), m_y.ToString());
   


class Program

    static void Main(string[] args) 
       Point p1 = new Point(10, 10);
       p1.ToString();       
    

作者说:

在对 ToString 的调用中,p1 不必被装箱。起初,您会认为 p1 必须被装箱,因为 ToString 是从基类型 System.ValueType 继承的虚拟方法。通常,为了调用虚方法,CLR 需要确定对象的类型以定位该类型的方法表。因为 p1 是一个未装箱的值类型,所以没有类型对象指针。但是,即时 (JIT) 编译器看到 Point 覆盖了 ToString 方法,并且它发出直接(非虚拟)调用 ToString 的代码,而无需进行任何装箱。编译器知道多态在这里无法发挥作用,因为 Point 是一个值类型,并且没有任何类型可以从它派生来提供此虚拟方法的另一种实现。

我有点明白它的意思,因为Point 覆盖了System.ValueType 中的ToString,CLR 不需要检查类型对象来定位类型的方法表,编译器可以发出调用 ToString 的 IL 代码直接地。很公平。

但是假设p1 也从System.ValueType 调用GetHashCode

class Program

    static void Main(string[] args) 
       Point p1 = new Point(10, 10);
       p1.ToString();  
       p1.GetHashCode();     
    

由于Point struct 没有从System.ValueType 覆盖GetHashCode(),那么编译器这次不能直接发出IL 代码,CLR 需要定位该类型的方法表来查找GetHashCode 方法,但正如作者说p1是未装箱的值类型,没有类型对象指针,那么CLR如何在Point结构体堆中查找GetHashCode方法呢?

【问题讨论】:

因为任何结构变量的类型在编译时都是已知的,所以将被调用的方法也是已知的并且可以硬编码。然而,这种硬编码只可能在运行时编译您的代码时发生。如果您想检查 C# 是如何实际编译为 IL 的,sharplab.io 是一个有用的工具。 @JeremyLakeman 如果是这种情况,那么结构变量需要堆中的类型对象吗?根据我之前提出的问题,结构实例确实在堆中有其类型对象,如果将调用的方法是已知的并且可以在编译时进行硬编码,那么为什么需要类型对象? 在某种意义上说 Assembly.GetType(...) 或 localVariable.GetType(...) 将返回类型?是的。必须加载所有类型,并且在构造任何实例(或值...)或执行静态方法之前执行它们的静态构造函数。 @amjad 类型对象仅供 CLI 参考,例如 JITted 代码的位置、可用字段数量和可用字段等 @Oliver Rogier 你提到类和结构的方法代码不在堆或堆栈中分配。也许你是C背景的,这句话是对的。但是对于.Net,本机代码是在堆中分配的,类型对象持有它的引用。通过 C# 检查书籍 clr。顺便说一句,我认为我在c和汇编,CPU方面有扎实的背景,你一定知道有一本经典的书叫csapp,我在这门课上得到了很好的分数。老实说,我认为你没有理解我的问题。 【参考方案1】:

如果我们查看生成的 MSIL,我们会看到以下内容:

IL_0000:  ldloca.s    00 // p1
IL_0002:  ldc.i4.s    0A 
IL_0004:  ldc.i4.s    0A 
IL_0006:  call        System.Drawing.Point..ctor
IL_000B:  ldloca.s    00 // p1
IL_000D:  constrained. System.Drawing.Point
IL_0013:  callvirt    System.Object.ToString
IL_0018:  pop         
IL_0019:  ldloca.s    00 // p1
IL_001B:  constrained. System.Drawing.Point
IL_0021:  callvirt    System.Object.GetHashCode
IL_0026:  pop     

让我们在constrained. 上查找ECMA-335 Part III.2.1:

constrained. 前缀只允许在 callvirt 指令中使用。 ptr 的类型必须是指向 thisType 的托管指针 (&)。约束前缀旨在允许以统一的方式生成 callvirt 指令,而与 thisType 是值类型还是引用类型无关。

如果 thisType 是值类型并且 thisType 实现 methodptr 未经修改地作为“this”指针传递对 thisType

实现的 method 的调用

如果 thisType 是值类型并且 thisType 没有实现 methodptr 被取消引用、装箱并传递作为 method

的 callvirt 的 'this' 指针

只有在 System.ObjectSystem.ValueTypeSystem.Enum 上定义了方法并且没有被 thisType 覆盖时,才会出现最后一种情况。在最后一种情况下,装箱会生成原始对象的副本,但是由于System.ObjectSystem.ValueTypeSystem.Enum 上的所有方法都不会修改对象的状态,因此无法检测到这一事实。

所以,是的,这确实会导致装箱,但只有在没有覆盖的情况下,因为System.Object 方法需要一个类,而不是值类型。但是如果被覆盖了,那么方法的this指针必须是托管指针,和其他的valuetype方法一样。

【讨论】:

【参考方案2】:

相关答案:How does the heap and stack work for instances and members of struct in C#?How boxing a value type work internally in C#?Is everything in .NET an object?

提前为我糟糕的英语和我肯定会写的错误或不确定性提前道歉,这些东西很遥远,而且我在 IL/CLR/CTS 方面没有那么先进......还有什么时候我会说说 808x,除了 6809 之外,我也是从这里来的,它是为了简化关于 @​​987654324@ 和 CISC 历史的事情。我尽力为这幅画画肖像,让我们创作音乐,开辟研究道路。

这样的关于代码和数据、堆栈和堆、类和结构等的问题,是一个非常有趣和基础,但复杂而艰难,广泛而广泛的主题:它是现代计算技术的一大根源基于transistors 和silicon integrated circuit 用于我们基于microprocessors 的计算机、服务器、智能手机……以及更多具有electronic components 的设备。

它们是关于 CPU 底层如何工作的,无论是高级构建在低级之上,OOP 在非 OOP 上发明,结构化在非结构化之上创建,功能模仿在过程之上,我们使用的技术。

我们还没有支持 .NET 的微处理器,除了以下信息之外:

What and where are the stack and heap?

Stack and heap in c sharp

Stack and Heap allocation

Memory allocation: Stack vs Heap?

How does the heap and stack work for instances and members of struct in C#?

https://***.com/questions/65929369/does-structs-have-type-objects-created-in-heap

Why methods return just one kind of parameter in normal conditions?

How would the memory look like for this object?

从根本上说,类和结构的方法代码既不位于也不分配在堆或堆栈中。基本上,本机代码,指令,除了2GB的DATA SEGMENT(我希望我不要不要说错了,那是旧的......)当我们点击 .exe 文件并加载一些 DLL 时。

换句话说:方法的实现代码是在进程启动时从二进制文件EXE和DLLs中加载的,并存储在CODE SEGMENT中,作为所有数据,静态(文字)和动态(实例) 在DATA SEGMENT 中,我想,甚至是由 JIT 翻译的,或者类似的东西,如果自 x32 和保护模式以来事情没有改变的话。非虚拟方法的表以及virtual 方法的表不存储在每个对象实例的数据段中。我不记得细节,但这些表是用于代码的。

此外,继承类的类会继承概念中的成员数据和行为。这是一个架构,一个计划,一个平局。当我们创建一个对象时,您可以在一个地方获取所有数据。对于层次结构中的每个类,我们没有每个对象有多个对象:这只是我们头脑中和源代码中的概念设计。

Where in memory is vtable stored?

此外,对象的每个实例的数据都是其定义及其祖先的投影,在一个地方,一个完整实例。引用是指向数据段中相关其他空间的“指针”。想象一个对象是火车上的货车(内存):文字和引用是椅子,而引用(类和结构,类的特殊情况,或contrary)指向另一个货车。

Memory segmentation

x86 memory segmentation

堆栈是我写的here 并解释了here:一个大房间(堆)中的橱柜(堆栈),并且使用慢速MOVs 的标准内存访问无法访问此堆栈位置,但快速CPU堆栈寄存器,因此速度更快,但空间有限。

.NET IL 代码被翻译成机器相关代码,因此在我们的技术上,类似 Intel 或 ARM 我想,例如,所有 CPU 在某种方式上都是相同的(它是硅技术),它是相同的正如我们在 C 或 x86 ASM 中学到的那样...... DotNet 是一个虚拟机。简而言之,执行 IL 代码的 CLR 将其转换为类似 INTEL 的代码。就这样。 CPU registers 是物理 CPU 寄存器,即使我们想要其他东西,它们也不会是另一回事。而CPU Stack就是CPU tack。以此类推。

如果有一天我们有了新一代计算机(在大约 8086/8088 之后的当前硅之后),情况可能会发生变化,并且 .NET 将生成不同的代码,例如使用量子计算或 DNA 计算。因此,任何关于 .NET 和 CLR 的问题实际上并最终都会得到一个标准和经典的 808x 答案,因为 .NET 不会改变 CPU 寄存器的工作方式、总线大小或其他所有内容。

我看到一些帮助者在某些情况下对某些主题的某些问题非常了解 .NET 规范的详细信息,并回答说“不能保证”,因为文档让门打开了到新一代,什么都不说能够在新一代计算机上生成不同的机器代码......

CLR 只是将虚拟代码转换为目标架构的真实机器代码。 CLR 不是真机,它是virtual machine。这个虚拟机不能像在 CPU 上那样在现实世界中存在和工作。没有像真正的 .NET 机器这样的东西。它还不存在,我希望它会存在,这会很棒(据我所知,创建一个 .NET CPU 是微软多年来的计划之一)。

换句话说:目前还没有 .NET CPU。所以,所有 .NET 技术都被翻译成 x86/x32/i64 技术……就是这样。因此,所有关于堆栈、堆、方法、类、结构的问题都无法用这种想象中的 .NET-ready CPU 作为与实际芯片不同的新技术来回答。

我们的实际机器只能执行machine code,因此是汇编代码,可以在我们的CPU上运行。没有其他的。其余的都是人类的概念。所有语言和所有虚拟机都需要翻译成适用于 CPU 架构的机器代码。我们所有的高级语言和任何虚拟架构,如 Java 和 .NET 或其他,甚至是 80 年代解释的 BASIC,在现实中都不存在。永远不会为我们的硅一代。我们的 CPU 不存在 IL 代码:它只是一个虚拟 ASM。

因此,.NET 以及 C 或我们源代码以外的任何语言“不存在”。甚至bytecode 最终都被翻译成机器代码,由 CPU 执行,这是一个非常基本、简单、机械、自动且不是很进化的东西,甚至是现代的。我们可以发明的任何语言的所有源代码和中间代码指令都被翻译成我们当前一代的 Intel、AMD、ARM 机器可以理解的机器代码……甚至还有保护模式、x64 模式、多核、超线程,以此类推,1970 年和 2020 年的 CPU 基本上是一样的,就像老大众甲壳虫和最后一辆保时捷之间的区别一样。

如果不去想象不存在的事物,就不可能通过认为 .NET 可以从机器的不同角度存在并且 CPU 可以与当前架构不同的运行方式来回答这样的问题,这将在代码段和数据、堆栈和堆以及所有其他方面与进程不同。 DotNet 在 CPU 上的运行方式与使用 CPU 本身的运行方式不同。不可能。即使是量子模拟平台,最终也能将内容转换为当前硅 CPU 的机器代码。

CLR 使用真机架构。因此,CPU 及其寄存器以及 RAM 内存,以及我们所知道的一切。 CLR 将 IL 代码转换为 CPU 代码。就是这样:它不断地变戏法。

我们可以使用 Visual Studio 窗口 Debug > Windows > Machine code 查看此机器代码,以检查在我们的 CPU 上实时执行的真实代码,没错,这就是 x86/x32/i64 ASM。 .. 不是我们可以看到的 MSIL,例如使用 ILSpy。

除了机器码之外的所有东西都不存在,除了在我们的头脑和我们的工作文件中,如源代码和字节码,从 CPU 的角度来看,它“根本不是真实的”。

要了解 .NET 在内部的工作原理,您可以阅读以下书籍和在线资源:

List of CIL instructions

.NET OpCodes Class

Expert .NET 2.0 IL Assembler.

还有:

Stack register

The Concept of Stack and Its Usage in Microprocessors

Introduction of Stack based CPU Organization

What is the role of stack in a microprocessor?

此外,一切都在这里,也就是说,在其他示例中:

Protected mode software architecture

Operating System concepts

为了更好地理解和提高您的计算技能,您可能会感兴趣地研究一下assembly language 是什么以及CPU 的工作原理。您可以从IL 和modern Intel 开始,但从过去的8086 to i386/i486 开始可能更简单、更具形成性和互补性。

祝你阅读愉快!

也许您可以在https://superuser.com 上询问有关这些主题的高级问题:

Understanding Windows Process Memory Layout

【讨论】:

以上是关于如何使用堆中的类型对象定位结构实例的虚拟方法?的主要内容,如果未能解决你的问题,请参考以下文章

Java对象的访问方式

「每天五分钟,玩转 JVM」:对象访问定位

Java虚拟机(JVM)详解

深入理解Java虚拟机——对象的访问定位

深入理解Java虚拟机——对象的访问定位

Java虚拟机的内存结构