具体实施方式
概览
某些软件开发方法包括从源代码生成中间代码。存在各种中间代码,其具有各种特性。某些技术部署JIT编译(“即时”编译)的中间代码。通常,此类中间代码以抽象栈机器为目标。特别地,将该抽象机器映射到使用实际硬件寄存器的有形机器上涉及对该机器的寄存器的分配。高效的寄存器分配通常是用于JIT编译,或实际上是用于产生机器语言的任何编译的设计准则。然而,可接受的寄存器分配方案可能是复杂的,从而使其实现起来相对困难,并且使得其在时间和其他计算资源方面成为编译的相对大的一部分。JIT编译还可执行机器无关的优化,这使得从中间代码到机器语言的转换进一步变复杂。
编译的一方面可以是将字段偏移量和虚槽号插入到引用它们的代码中。某些方法在加载时构造偏移量表,但是这具有有限的灵活性,并且未完全解决脆弱二进制接口问题。由此,尚待在某种程度上例如用假设其修改的地址中的固定改变的链接器来修补代码,或者对该代码使用某种间接手段。间接手段降低了执行时间性能。代码修补可以涉及JIT编译的中间代码,其具有附带的寄存器分配成本。
相反,此处描述的某些实施例提供了接近机器的中间代码,因为即使尚未指定偏移量,寄存器也已经被分配。该中间代码在典型方式中不是JIT编译的,因为寄存器分配已经完成并且被嵌入在中间代码中。相反,该中间代码经历客户机器上的绑定,这一绑定可以比涉及寄存器分配的熟悉的编译工作更简单。然而,尽管有嵌入的寄存器分配,该中间代码在面对对于对象字段大小、对象字段布局、无用信息收集(garbage collection)细节、结构分配细节、方法调用实现细节的改变以及对具体执行环境中的可执行代码的其他方面的改变时,可以是有回复力的。
例如,某些实施例提供了中间语言代码,其中寄存器分配足够完整以便允许高效执行,并且还具有“抽象”实现细节的伪指令。即,该伪指令被设计成使某些实现细节从编程接口解耦,编程接口包括遵从中间代码句法但是在实践中可被绑定到一个或多个机器语言指令的指令。
在某些实施例中,提供了抽象对象字段布局和虚方法槽指派的伪指令,从而对诸如C++等非托管语言减轻或消除了脆弱基类问题。在某些实施例中,由中间语言代码和绑定器提供的能力扩展到托管执行环境,并且包括抽象代码和数据中的无用信息收集器信息、对象分配、类型描述符的表示、异常处理、用于虚或接口调用的机制、和/或软件执行的其他方面的伪指令。某些实施例提供了部署的中间代码,该部署的中间代码对于执行引擎中的改变并且对于该执行引擎预期以该引擎为目标的任何代码都要遵从的数据结构和软件约定中的改变是有回复力的。
现在将参考诸如附图中示出的示例性实施例,且此处将使用具体语言来描述这些实施例。但是相关领域且拥有本发明的技术人员将想到的、此处所示的特征的更改和进一步修改以及此处所示的原理的其他应用应被认为是在权利要求书的范围内。
各术语的意义在本发明中阐明,因此权利要求书应仔细注意这些阐明来阅读。给出了具体示例,但是相关领域的技术人员将理解,其他示例也落入所使用的术语的意义内,且在一个或多个权利要求的范围内。各术语不必在此具有与其一般使用中、特定行业的使用中、或特定字典或字典集的使用中相同的意义。与各种措辞一起使用附图标记来帮助显示术语的宽度。给定一段文本中省略附图标记并不一定意味着该附图的内容没有被该文本讨论。发明人声称并行使其对于其自己的词典编纂的权利。各术语此处可在详细描述和/或申请文件的别处显式或隐式地定义。
如此处所使用的,“计算机系统”可包括例如一个或多个服务器、主板、处理节点、个人计算机(便携式或非便携式)、个人数字助理、蜂窝或移动电话、和/或提供至少部分地由指令来控制的一个或多个处理器的设备。指令可以采用存储器和/或专用电路中的软件的形式。具体而言,尽管可以想到许多实施例在工作站或膝上型计算机上运行,但其他实施例可以在其他计算设备上运行,且任何一个或多个此类设备可以是给定实施例的一部分。
“多线程化”计算机系统是支持多个执行线程的计算机系统。术语“线程”应被理解为包括能够或经历同步的任何代码,并且可用另一名称来称呼,如“任务”、“进程”或“协同例程”。线程可以并行地、顺序地、或以并行执行(例如,多处理)和顺序执行(例如,时间片)的组合来运行。以各种配置设计了多线程化环境。执行线程可以并行地运行,或者线程可以被组织供并行执行但实际顺序地轮流执行。多线程化可以例如通过在多处理环境中在不同核上运行不同线程、通过对单个处理器核上的不同线程进行时间分片、或通过时间分片和多处理器线程化的某种组合来实现。线程上下文切换可以例如由内核的线程调度器、由用户空间信号、或由用户空间和内核操作的组合来发起。线程可轮流在共享数据上操作,或者每一线程可以例如在其自己的数据上操作。
“逻辑处理器”或“处理器”是单个独立的硬件线程处理单元。例如,每一个核运行两个线程的超线程化四核芯片具有8个逻辑处理器。处理器可以是通用的,或者它们可以对诸如图形处理、信号处理、浮点算术处理、加密、I/O处理等特定使用来定制。
“多处理器”计算机是具有多个逻辑处理器的计算机系统。多处理器环境以各种配置出现。在一给定配置中,所有处理器可以在功能上是等价的,而在另一配置中,某些处理器可以借助具有不同硬件能力、不同软件分配或两者而不同于其他处理器。取决于配置,处理器可在单个总线上彼此紧耦合,或者它们可以是松耦合的。在某些配置中,处理器共享中央存储器,在某些配置中它们各自具有其自己的本地存储器,且某些配置中存在共享和本地存储器两者。
“内核”包括操作系统、系统管理程序、虚拟机、以及类似的硬件接口软件。
“代码”意味着处理器指令、数据(包括常量、变量和数据结构)或指令和数据两者。
关于诸如涉及“变换”、“产生”或“准备”代码的操作等绑定操作的讨论,对“指令”的引用意味着“指令和任何对应的数据”。元数据是一种类型的数据。例如,如此处所讨论的,对变换无用信息收集写屏障(garbage-collection-write-barrier)伪指令的引用意味着该变换接收某些中间语言代码(指令和相关联的元数据/数据),并产生某些本机代码(指令和相关联的元数据/数据,如无用信息收集表)。即,在绑定的上下文中对术语“指令”的使用不排除对与该指令相关联的数据的产生或其他使用。
关于中间语言代码的诸如“寄存器分配是执行就绪的”等短语意味着可从该中间语言代码创建可执行代码,而没有从程序源代码变量到处理器寄存器的任何附加或不同映射。结果,绑定该中间语言代码不需要执行寄存器分配来从中间语言代码产生机器语言代码。换言之,中间代码中分配给寄存器的每一用户定义的或编译器生成的变量将在最终可执行代码中使用相同的寄存器。
在产生系统中,可能发生整个中间语言代码具有寄存器分配执行就绪,或者情况可以是整个中间语言代码的仅仅一适当的子集具有寄存器分配执行就绪。换言之,假定X是对于其寄存器分配是执行就绪的中间语言代码,假定指令Y不是寄存器分配执行就绪的,且假定Y被添加到X。因此,Y的添加不破坏X的寄存器分配执行就绪状态。例如,在以下讨论的称为“MDIL”的一具体中间语言中,存在非本机指令,对于这些非本机指令,通过绑定产生的本机指令序列将使用绑定器所分配的临时寄存器。然而,这一类型的寄存器分配在MDIL中具有有限的范围,且其存在不会更改以下事实:MDIL的大部分不包括对于其寄存器分配是执行就绪的伪指令。
“自动”意味着通过使用自动化(例如,由软件针对此处讨论的具体操作配置的通用计算硬件),与不使用自动化相对。具体而言,“自动”执行的步骤并不是在纸张上用手执行或在人的脑海中执行的;它们是用机器来执行的。
贯穿本文,对任选的复数的使用意味着存在一个或多个所指示的特征。例如,“指令”意味着“一个或多个指令”,或等价地,“至少一个指令”。
只要参考了数据或指令,就理解这些项目配置了计算机可读存储器,从而将其变换为特定物品,而非简单地存在于纸张上、人的脑海中、或作为例如线路上的瞬时信号。
贯穿本文,除非明确地另外规定,否则对过程中的步骤的任何引用假定该步骤可以由感兴趣的一方直接执行和/或由该方通过干预机器和/或干预实体来间接执行,且仍在该步骤的范围内。即,感兴趣的一方对步骤的直接执行并不是必需的,除非直接执行是明确规定的要求。例如,涉及感兴趣的一方的诸如向目的地“传送”、“发送”或“传递”等动作的步骤可涉及某一其他方的诸如转发、复制、上传、下载、编码、解码、压缩、解压、加密、解密等中介动作,但仍被理解为由该感兴趣的一方直接执行。具体地,即使涉及了中介机制和/或中介实体,诸如此处讨论的编译、生成、绑定和产生等动作也可以由一方直接执行。
操作环境
参考图1,用于一个实施例的操作环境100可包括计算机系统102。计算机系统102可以是多处理器计算机系统,也可以不是。操作环境可包括给定计算机系统中的一个或多个机器,这些机器可以是集群的、客户机-服务器联网的、和/或对等联网的。
人类用户104可以通过使用显示器、键盘和其他外围设备106来与计算机系统102交互。系统管理员、开发者、工程师和最终用户各自都是一具体类型的用户104。代表一个或多个人来行动的自动化代理也可以是用户104。存储设备和/或联网设备在某些实施例中可被认为是外围设备。图1中未示出的其他计算机系统可以使用经由例如网络接口设备到网络108的一个或多个连接来与计算机系统102或与另一系统实施例交互。
计算机系统102包括至少一个逻辑处理器110。如其他合适的系统的计算机系统102还包括一个或多个计算机可读非瞬态存储介质112。介质112可以是不同的物理类型。介质112可以是易失性存储器、非易失性存储器、固定在原处的介质、可移动介质、磁介质、光介质、和/或其他类型的非瞬态介质(与诸如仅传播信号的线路等瞬态介质形成对比)。具体地,诸如CD、DVD、记忆棒或其他可移动非易失性存储介质等配置的介质114在被插入或以其他方式安装时可以变为计算机系统的功能部分,从而使得其内容可被存取来供处理器110使用。可移动的配置的介质114是计算机可读存储介质112的一个示例。计算机可读存储介质112的某些其他示例包括内置RAM、ROM、、硬盘、和不可由用户104容易地移动的其他存储设备。
介质114用可由处理器110执行的指令116来配置;“可执行”在此以宽泛的意义被使用来包括机器代码、可解释代码、以及在例如虚拟机上运行的代码。介质114还用通过指令116的执行创建、修改、引用和/或以其他方式使用的数据118来配置。指令116和数据118配置它们所驻留的介质114;当该存储器是给定计算机系统的功能部分时,指令116和数据118还配置该计算机系统。在某些实施例中,数据118的一部分代表了诸如产品特性、库存、物理测量、设置、图像、读数、目标、量等的真实项目。此类数据也如此处所讨论的例如通过绑定、重新绑定、指令插入、槽指派、符号变换、解析、部署、执行、修改、显示、创建、修订、加载和/或其他操作来变换。
计算机软件源代码120可包括例如模块122、基类124、具有字段128的对象126、虚方法130、结构144和/或其他项目。编译器132和/或代码生成器可用于从源代码生成本机指令134,即处理器110可识别的指令。链接器和/或其他工具组合由本机指令和修补组成的对象模块来形成可执行代码136。处理器110具有寄存器138。处理器110和诸如内核等系统软件帮助定义执行引擎140。源代码120和图中所示和/或此处注明的其他项目可以部分或完全地驻留在一个或多个介质112内,从而配置这些介质。操作环境还可包括其他硬件,例如总线、电源、加速计等。
给定操作环境100可包括集成开发环境(IDE)142,其向开发者提供了一组协调的软件开发工具。具体地,用于某些实施例的合适的操作环境中的某一些包括或帮助创建被配置成支持程序开发的
Visual
开发环境(微软公司的标记)。某些合适的操作环境包括
环境(Sun微系统公司的标记),并且某些合适的操作环境包括利用诸如C++或C#(“C-Sharp”)等语言的环境,但是此处的教示适用于各种各样的编程语言、编程模型和程序,以及使用中间语言,即在源代码和本机指令之间的代码的软件开发领域本身之外的尝试。
在图1中以轮廓形式示出了IDE 142来强调它不一定是所示操作环境的一部分,但是可以与此处讨论的操作环境中的项目互操作。在任何附图或任何实施例中,不能得出不采用轮廓形式的项目就一定是需要的。
系统
图2示出了适用于某些实施例的体系结构。源代码120由生成器204变换成中间语言代码202(此处也称为中间代码)。在诸如图3所示的某些实施例中,生成器204与编译器132集成,例如,生成器204可以在编译的代码生成阶段期间被调用。在诸如图4所示的实施例之类的其他实施例中,生成器204与编译器分开,并且接收由编译器132输出的内容作为输入代码。
除非另外清楚指明,否侧此处对中间代码(或中间语言代码)的引用指的是其中已经完成了寄存器分配206但是尚未指定可执行代码136的其他方面的中间代码202。本文之外的对“中间代码”的某些使用可在当前意义上在对不是中间代码202的代码的引用中使用短语“中间代码”,例如因为尚未完成寄存器分配206。在实践中,某些配置可采用多于一种中间代码,包括中间代码202和作为源代码和本机代码之间的中介但不是此处定义的中间代码202的某种其他代码。
在某些实施例中,例如,对象字段布局208未完全在中间代码202中被指定,但寄存器分配206被完全指定。在某些实施例中,虚方法槽指派210未在中间代码202中被完全指定。在某些实施例中,中间代码202中的伪指令212标识了可执行代码136的未在该代码202中指定的各方面。这些未指定的各方面使用绑定器214来被确定和指定。如此处所教示的,绑定器214将中间代码202伪指令212变换成本机指令134。
在本文的附图和文字中,“PI”指的是“伪指令”,即伪代码指令。类似地,“NI”指的是“本机指令”,即本机代码指令。“MDIL”代表“机器相关中间语言”,且是中间语言代码202的某些方面的示例。
给定实施例可包括以下各种伪指令212中的一种或多种:
无用信息收集器探测伪指令216可被插入到紧凑循环(tight loop)中来检查待决的无用信息收集。该检查可通过绑定器214将无用信息收集器探测伪指令216变换成本机指令134来实现。即,无用信息收集器探测伪指令216可用作以下绑定器指定的机器指令的占位符:该绑定器指定的机器指令在执行时将检查待决的无用信息收集。如何执行检查以及检查使用什么无用信息收集信息可取决于执行引擎140;可使用熟悉的无用信息收集机制。
对象分配伪指令218可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将分配存储器来保持中间语言代码202中指定的类型的对象。如何执行分配可取决于执行引擎140;可使用熟悉的存储器分配机制。
异常抛出伪指令220可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将抛出新异常、重新抛出当前异常、和/或以其他方式执行异常操作。如何执行异常可取决于执行引擎140;可使用熟悉的异常处理机制。
虚调用方法伪指令222可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将作出对指定方法的虚调用。如何执行虚调用可取决于执行引擎140;可使用虚表和/或其他熟悉的虚调用机制。虚方法在某些情形中可被非虚地调用,因此在作出对方法的虚调用(在这一情况下方法是虚的)和作出对虚方法的调用(在这一情况下调用本身可以是虚的或可以不是虚的)之间存在区别。虚和非虚调用之间的区别是:对于虚调用,调用的地址由对象的虚表或另一查找机制中该方法的地址的运行时查找来确定,而对于非虚调用,该方法的地址是静态地确定的。
静态调用方法伪指令224可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将作出对指定方法的非虚调用。如何执行该调用可取决于执行引擎140和其他因素;可使用熟悉的调用机制。
尾调用(tail-call)方法伪指令226可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将作出对指定方法的尾调用,即,调用的后面紧跟着对调用者的返回。如何执行尾调用可取决于执行引擎140;可使用熟悉的机制。
执行引擎服务调用伪指令228可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将作出对指定的运行时助手,例如执行引擎140服务例程作出调用。可用服务通常将取决于执行引擎140。在某些情况下,引擎服务可通过代码202获得,但是在其他情况下,引擎服务通过其他机制来访问。在一给定实施例中,执行引擎服务可包括以下类别中的一个或多个,或者不包括任何以下类别;这些示例中的某一些是特定于实现的,和/或与MDIL HELPER_CALL(助手调用)伪指令228相关,并且不一定涉及每一实施例:
用于机器本机不支持的运算——例如32位机器上的64位乘法——的算术助手。
浮点和整型数据类型之间的转换,任选地具有错误检验。
用于抛出各种运行时异常,例如用于运行时范围检查的助手。
用于安全检查的助手。
用于访问“远程”对象的助手。在对象可以在同一机器上的另一进程中的意义上,和/或在对象可以完全在另一机器上的意义上,对象可以是远程的。
用于运行时的各种类型检查的助手调用。
与调用非托管代码相关的助手调用。
与对象分配相关的助手调用。在MDIL中,这些是由中间代码202中的ALLOC_OBJECT(分配对象)或ALLOC_ARRAY(分配数组)来表示的。一个例外是CORINFO_HELP_NEW_MDARR_NO_LBOUNDS(用于无下界的新多维数组的助手的协作信息),其分配所有下界被设为0的多维数组。
与无用信息收集器交互的助手调用。例如,GC_PROBE(GC探测)伪指令216可被转换成助手调用CORINFO_HELP_POLL_GC(用于轮询GC的助手的协作信息),但是取决于执行环境,可以有实现GC_PROBE的更多的高效方式。服务CORINFO_HELP_ASSIGN_REF_ECX(用于指派引用ECX的助手的协作信息)是该类别中的另一调用;其帮助实现写屏障,且因此可以由中间代码202中的STORE_REF(存储引用)伪指令更紧凑地表示。
运行时类型直接调用方法伪指令230可用作对绑定器214的请求,请求绑定器不执行虚调用,而是从编译器132或其他中间代码202生成器204提供的运行时类型中确定虚或接口调用的目标方法。由此,可要求绑定器214插入机器指令来代替伪指令230,以便作出对中间语言代码202中通过符号标识的运行时类型的方法的直接调用。例如,MDIL CONSTRAINT(MDIL约束)伪指令230指定虚调用在其上被执行的对象的运行时类型。CONSTRAINT作为对绑定器214的请求来操作,请求绑定器完全不执行虚调用,而是从编译器提供的运行时类型中找出虚或接口调用的目标方法。
托管对象字段访问伪指令232可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将访问托管的对象126的字段128,例如其分配由执行引擎140管理的对象。访问可采用例如加载、添加、存储或另一操作的形式。由绑定器置于本机代码134内的实际字段偏移量可例如由于托管汇编件和执行引擎140的版本化(versioning)而改变。在某些实施例中,字段访问伪指令232可使用各种寻址模式。一般而言,这些伪指令模式直接映射到本机寻址模式。在某些情况下,使用额外指令以及可能还有临时寄存器。MDIL还包括数组访问寻址模式,其抽象数组头部的布局,例如数组的长度存储在何处,以及第一个数组元素在何处与该数组的起始地址相关。
堆指针指定伪指令234可用于向绑定器214传达哪些寄存器或栈位置在方法中的什么位置处包含哪一类的指针。绑定器214可使用此类信息来确定从方法内的位置到包含指向无用信息收集堆的指针的一组指针位置的运行时映射。
在MDIL中,堆指针指定伪指令234的某些示例包括REF_UNTR、REF_BIRTH和REF_DEATH,因为它们各自帮助确定从方法主体执行点到指向无用信息收集堆的位置的映射。一般而言,这些位置在方法的执行的特定部分期间仅包含gc指针。可以将无用信息收集认为是在一特定执行点中断方法的执行。进行无用信息收集的执行系统可能很需要在执行被中断的该特定点处确定哪些寄存器和栈位置包含指向无用信息收集堆的指针。概念上,这可通过扫描中间代码直到执行被中断的点并维护如下包含指向无用信息收集堆的指针的位置列表来确定:该列表以空开始。对于每一REF_UNTR_xxx伪指令,将该位置添加到该列表。对于每一REF_BIRTH_xxx伪指令,将该位置添加到该列表。对于每一REF_DEATH_xxx伪指令,将该位置添加从该列表删除。在一给定实现中,中间代码不必被保留或被扫描;取而代之的是,构建与每一方法相关联且实现从方法内的执行点到包含指向无用信息收集堆的指针的一组位置(寄存器或栈位置)的相同映射的附加数据结构。
实例化查找伪指令236可用作以下绑定器指定的机器指令134的占位符:该绑定器指定的机器指令在执行时将查找例如类属类型或方法的实例化或实例。标识类属项的具体实例的实例化参数可作为伪指令的参数来传递、在附加到“this”指针的字典中提供、或通过另一机制来提供。
更一般地,某些实施例包括对类属代码的支持,由此实际本机代码序列可依赖于作为参数提供的某一类型。中间代码202可被共享,或者取决于类型参数的种类,可以有若干风格的中间代码。例如,可以有用于整型类型的中间代码方法主体、用于浮点类型的中间代码方法主体、用于引用类型的中间代码方法主体、用于用户定义的值类型(结构体类型)的中间代码方法主体、等等。在共享的类属代码中,实际机器代码在类属类型或类属方法的相似实例之间共享。在某些情况下,这意味着执行对类型、字段、方法等的运行时查找。
可中断区域伪指令238可用于向绑定器214传递哪些代码部分能够访问无用信息收集器信息,特别地,除调用返回位置之外的哪些代码部分具有这样的访问。无用信息收集的细节可取决于执行引擎140;可使用熟悉的机制。
在MDIL中,START_FULLY_INTERRUPTIBLE(开始完全可中断)和END_FULLY_INTERRUPTIBLE(结束完全可中断)伪指令238界定了其中无用信息收集信息在每一指令边界而非仅在调用位置(call sites)是精确的区域。这对于多线程化程序中的情形可能是重要的,其中一个线程分配存储器,而另一线程执行不作出任何调用的长期运行循环。在这一情形中,可能期望能够停止第二个线程,并且能够准确地报告所有无用信息收集指针。
无用信息收集指针伪指令240可用于向绑定器214指示包含无用信息收集器指针的自变量被压入栈上何处、从栈上何处弹出、或者在何处在没有被弹出的情况下变得对无用信息收集无效。在某些实施例中,无用信息收集指针伪指令240可用于指示不是无用信息收集器指针的值已被压到栈上。
在MDIL中,REF_PUSH(引用压入)和NONREF_PUSH(非引用压入)是无用信息收集指针伪指令240的示例。具体地,NONREF_PUSH指示不是无用信息收集器指针的值已被压到栈上。结果,无用信息收集器知道某些值不是无用信息收集器指针,而是可能碰巧看上去很像的某一其他值。该NONREF_PUSH指示可在无用信息收集器决定压缩无用信息收集器堆时尤其有帮助,在此期间,它可调整程序保持的无用信息收集器指针中的一部分或全部;调整不是指针的常规整数将是不正确的。该NONREF_PUSH指示还可帮助无用信息收集器知道返回地址被存储在栈上的何处。
无用信息收集写屏障伪指令242可用于向绑定器214传达无用信息收集写屏障的状态。写屏障和无用信息收集的其他方面的细节,如写屏障实现中使用的卡表(card table)数据结构的大小,可取决于执行引擎140;可使用熟悉的机制。
在MDIL中,STORE_REF(存储引用)伪指令242可用于支持世代无用信息收集器。该指令242生成被称为写屏障的东西,即,对执行引擎的暗示,其暗示指向较年轻对象的指针可能已经被存储在较老的对象中。STORE_REF指令可在可中断区域的内部或外部出现。在对目标执行引擎适当的时候,STORE_REF将由绑定器变换成实现无用信息收集器写屏障的机器指令序列。
MDIL还包括地址模式修改器。在严格观点下,这些地址模式修改器本身不是指令,而是被用作指令的一部分。然而,此处为方便起见,“地址模式修改器伪指令”是包括地址模式修改器的任何伪指令212。因此,某些伪指令212位于至少两个类别中,例如,给定指令212可以既是托管对象字段访问伪指令232,又是地址模式修改器伪指令244。地址模式修改器伪指令244可用作包括地址模式修改的绑定器指定的机器指令134的各部分的占位符。在MDIL中,这些伪指令244在执行引擎中隐藏数组的布局,例如包含长度的数组头部的布局。但是它们的角色不必限于隐藏数组布局。在MDIL中,这些伪指令244还具有支持在版本化期间改变大小的数组元素的规定。
静态基址伪指令246可用作包括或提供用于访问静态变量存储区域的基址的绑定器指定的机器指令134的占位符。在MDIL中,GET_STATIC_BASE(获得静态基址)伪指令246可用于获得不包含无用信息收集指针的静态量(例如,int、double、bool)的基址。GET_STATIC_BASE_GC(获得静态基址GC)伪指令246可用于获得包含无用信息收集指针以及用户定义的结构体类型的静态量的基址。在MDIL中,这些伪指令隐藏(将指定推迟到绑定时)到类的静态字段的精确访问路径,并且还隐藏是否应运行类初始化器。
某些伪指令包括对字段、类型和/或其他项的符号引用248。例如,可使用令牌来标识字段或类型而不同时指定诸如字段的偏移量或类型的大小等细节;绑定器214在将具有伪指令的代码变换成没有伪指令的完全本机代码时添加执行所需的细节。
在某些实施例中,诸如人类用户I/O设备(屏幕、键盘、鼠标、图形输入板、话筒、扬声器、运动传感器等)等的外围设备106将存在于与一个或多个处理器110和存储器的可操作通信中。然而,一实施例还可被深嵌入在系统中,使得没有人类用户104直接与该实施例交互。软件进程可以是用户104。
在某些实施例中,该系统包括通过网络连接的多个计算机。联网接口设备可使用诸如例如存在于计算机系统中的分组交换网络接口卡、无线收发机、或电话网络接口等组件来提供对网络108的接入。然而,一实施例也可通过直接存储器存取、可移动非易失性介质、或其他信息存储-检索和/或传输方法来通信,或者计算机系统中的一实施例可以在不与其他计算机系统通信的情况下操作。
参考图1和2,某些实施例提供了具有逻辑处理器110和存储器介质112的计算机系统102,该存储器介质由电路、固件和/或软件配置来如此处所描述的将包含伪指令的中间代码变换成本机代码。例如,某些实施例提供了与存储器112、与存储器中(例如,RAM和/或盘上的文件中)的中间语言代码202和可执行代码136进行可操作通信的处理器110的系统。中间语言代码和可执行代码符合结构对应性,表现在中间语言代码中的每一寄存器分配206在可执行代码中具有相同的寄存器分配206。
另外,在某些实施例中,中间语言代码和可执行代码关于对象字段布局和/或虚方法槽指派是一致的。如果中间语言代码中用于访问对象126的伪指令212包括对应于可执行代码中使用数值字段偏移量的机器指令134的符号字段引用,则中间语言代码202和可执行代码136关于对象字段布局是一致的。绑定器214将符号字段引用变换成可由作为绑定器的目标的执行引擎140识别的数值偏移量。
可以说“对象字段布局迄今尚未被绑定”来指示中间语言代码使用符号字段引用而非直接偏移量。例如,如果对象字段布局迄今尚未被绑定,则字段的次序在中间语言中未被指定。中间语言是字段次序无关的,因为两个功能上相同的可执行代码可以从相同的中间语言代码创建,这两个可执行代码区别仅在于某一对象的两个字段的RAM内的相对次序。
如果中间语言代码中用于调用虚方法130的伪指令212包括对应于可执行代码中使用数值虚方法表槽的机器指令134的符号槽引用,则中间语言代码202和可执行代码136关于虚方法槽指派是一致的。绑定器214将符号槽引用变换成可由作为绑定器的目标的执行引擎140识别的数值虚方法表槽(例如,索引)。可以说“虚方法槽指派迄今尚未被绑定”来指示虚方法槽号在中间语言中未被指定。
在某些实施例中,中间代码可对虚方法使用符号引用,可让虚槽指派是未指定的,并且可以让虚方法的次序是未指定的。由此,可生成两个可执行的功能上等效的程序,它们之间的区别仅在于虚方法的虚表中的相对次序,即虚槽指派。
某些实施例包括实现中间语言代码202和可执行代码136之间的结构对应性的绑定器214。在某些实施例中,绑定器214比JIT编译器更快且更容易实现/维护/移植,因为寄存器分配206已经在到达该绑定器的中间语言代码202中完成。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中通过符号引用类型146的伪指令212对应于可执行代码136中指定描述该类型的数据结构的地址的机器指令134。
此处所称的与中间代码202和本机指令134/可执行代码136相关的“结构对应性”不限于程序数据结构的管理,尤其不限于对象126或结构144,而是在某些实施例中可以扩展来包含无用信息收集、异常处理、方法调用、以及软件的其他方面,如此处所讨论的。然而,某些伪指令直接针对管理诸如C结构体、Pascal记录、以及这些和其他编程语言中相似的多字段/多成员数据结构之类的结构144。
在某些实施例中,例如,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中用于复制其大小在中间语言代码中未被指定的结构144(例如,结构体或记录)的伪指令212对应于可执行代码中与特定结构大小相一致的机器指令134。例如,伪指令212可以声明具有一类型的变量,该类型被指定为对该类型的符号引用248,该类型的实际大小在绑定器中而非在中间代码生成器中确定。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中用于零初始化其大小在中间语言代码中未被指定的结构144(例如,局部变量)的伪指令212对应于可执行代码中与特定结构大小相一致的机器指令。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中用于在例程中声明其类型在中间语言代码中通过符号指定的局部变量的伪指令212对应于可执行代码中符合在该例程中通过符号声明的至少一个局部变量的至少一个特定局部变量大小的机器指令。
例如,伪指令212可以零初始化某一类型的一部分存储器,该类型在中间代码202中通过符号指定。该存储器可以是嵌入在某一其他类型内的字段、数组的元素等等。
还要注意,机器指令134可以通过结构对应性与中间代码202相一致而不必以某种方式显式地包含结构大小。例如,如果大小为8字节的结构在中间代码202中零初始化,则所生成的本机代码134序列可由以下三个指令构成:第一个指令将零值加载到机器寄存器中,第二个和第三个指令将该机器寄存器存储到该结构中的第一和第二个四字节存储器部分中。在某些情况下,方法主体中仅全部伪指令212的累积大小(以及它们的对齐要求)才被表现为所生成的机器代码134中的显式常量。当访问符号局部变量时,可执行代码中存在的是局部变量偏移量,其可取决于不仅仅是变量本身的大小和对齐要求,而且还有在前的局部变量的大小和对齐要求。
现在从特别针对结构144的伪指令转开,在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中用于类型的类型布局250描述的伪指令212至少部分地定义了虚槽索引到对该类型的方法主体的运行时映射,其中可执行代码136中的至少一个方法主体指针将虚方法130实现为以下各项之一:新的虚方法、基类型中的虚方法的覆盖。
在某些实施例中,绑定器214构建与类型布局描述相一致的虚方法表(槽指派210)。虚方法表可被视为指向方法的指针的数组。每一类型具有其自己的虚方法表,但是默认地,指向方法的指针从基类型复制,除了在以下情况下。首先,当类型布局描述指定新的虚方法时,将新槽添加到针对该类型的虚方法表,指向该新方法的主体。其次,当类型布局描述指定了基类中的虚方法的覆盖(override)时,绑定器查找该基类型中的方法的虚槽号。然后使派生类型的虚方法表中的该槽指向覆盖方法(来自派生类型)而非来自基类型的方法。
在某些实施例中,该系统建立虚槽索引到针对其布局被描述的类型的方法主体的映射,即,新的虚方法创建新的槽,并将其映射到新方法主体,而覆盖将现有的虚槽映射到新方法。这可在不需要曾经在中间代码202中显式地提到虚槽号的情况下完成。相反,代码202被认为是实际上说出了“该方法获得新槽”或者“该方法使用与该其他现有方法相同的槽”。绑定器214取得虚方法的该声明性描述,以及什么虚方法覆盖什么其他虚方法来构造供在运行时使用的高效映射。
在某些实施例中,绑定器214生成既用于方法调用又用于虚方法调用的本机指令。对于方法调用,绑定器可能通过在模块加载时填充的间接单元来提供地址以调用。对于虚方法调用,绑定器将合成包含该虚槽号的机器代码序列,这涉及来自在其上调用该虚方法的对象的一个或多个间接手段。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的无用信息收集器探测伪指令212、216在位置上对应于可执行代码136中在执行时将检查待决无用信息收集的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的对象分配伪指令212、218在位置上对应于可执行代码136中在执行时将分配存储器来保持中间语言代码中指定的类型的对象126的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的异常抛出伪指令212、220在位置上对应于可执行代码中在执行时将抛出在中间语言代码中指定的寄存器138中标识的异常对象的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的虚调用方法伪指令212、222在位置上对应于可执行代码中在执行时将作出对在中间语言代码中通过符号标识的方法的虚调用的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的静态调用方法伪指令212、224在位置上对应于可执行代码中在执行时将作出对在中间语言代码中通过符号标识的方法的静态调用的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的尾调用方法伪指令212、226在位置上对应于可执行代码中在执行时将作出对在中间语言代码中通过符号标识的方法的尾调用的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的运行时类型直接调用方法伪指令212、230在位置上对应于可执行代码中在执行时将作出对在中间语言代码中通过符号标识的运行时类型的方法的直接调用的机器指令134。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码中的托管对象字段访问伪指令212、232在位置上对应于可执行代码中在执行时将使用字段偏移量来访问无用信息收集堆上的对象126的字段128的机器指令134,该字段偏移量在中间语言代码中未被指定。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码包含指示以下的至少一项的无用信息收集指针伪指令212、240:无用信息收集器指针被压到栈上、无用信息收集器指针从栈中弹出、无用信息收集器指针变为无效、不是无用信息收集器指针的值被压到栈上。
在某些实施例中,中间语言代码202和可执行代码136符合结构对应性,表现在中间语言代码包含指示无用信息收集器写屏障的状态的无用信息收集写屏障伪指令214、242。
在某些实施例中,类型可以嵌入其他类型。在某些实施例中,栈帧的一些部分可使用符号布局,例如,局部变量可以是其大小和布局由绑定器来计算的类型。
代码的大小通常是要考虑的问题,尤其是对于中间代码和本机代码,并且改变代码大小的操作可使得嵌入在代码内的跳转和其他地址无效。在某些实施例中,绑定器214将占据X字节空间的伪指令212变换成具有不同字节数的本机指令。本机代码可以大于或小于对应的中间代码。在某些实施例中,绑定器214确定并插入基于改变代码大小的跳转/调用地址,而非如熟悉的链接器所完成的基于给定地址和固定偏移量的地址。即,此类绑定器能够正确地处理链接器不能正确处理的地址生成/更新情形。绑定器214可确定指令何时能使用较小的编码(取决于字段偏移量的大小和虚槽号),并相应地调整分支距离。
某些实施例在“云”计算环境和/或“云”存储环境中操作。例如,源代码120可以在联网云中的多个设备/系统102上,对应的中间代码202可以被存储在云内的又一些其他设备上,且对应的可执行代码136可以配置另外一些其他云设备/系统102上的存储器。例如,编译源代码120来产生中间语言代码202可在一个机器上发生,而绑定该中间语言代码202来产生可执行代码136可在中间语言代码202被部署到不同机器之后发生。
过程
图3到7示出了某些过程实施例。图5到7共同形成一流程图500。图中所示的过程在某些实施例中可以例如由中间代码生成器204和绑定器214在需要极少或不需要用户输入的脚本的控制下自动执行。各过程可以部分地自动执行,且部分地手动执行,除非另外指明。在给定实施例中,可以重复一过程的零或多个所示步骤,可能对不同参数或数据进行操作。一实施例中的各步骤也可按与图5到7中列出的从上到下次序不同的次序来完成。各步骤可以串行地、以部分重叠的方式、或完全并行地执行。遍历流程图500来指示在一过程期间执行的步骤的次序可以在过程的一种执行与过程的另一种执行之间变化。流程图遍历次序也可在一个过程实施例与另一过程实施例之间变化。各步骤可被省略、组合、重命名、重组合、或采用其他方式,而不脱离所示流程,只要所执行的过程是可操作的且符合至少一个权利要求。
此处提供了各示例来帮助说明该技术的各方面,但是本文中给出的示例并未描述所有可能的实施例。各实施例不限于此处提供的具体实现、排列、显示、特征、方法或情形。给定实施例可以例如包括另外的或不同的特征、机制和/或数据结构,并且还可另外脱离此处提供的示例。
图3示出了过程和其他实施例,特别关注基类124的处理中的版本回复力。源代码120(本实例中包括至少一个基类124)被输入到编译器132中,编译器生成用于至少一个模块122的中间语言代码202,在该中间代码202中包括伪指令212和本机代码指令134。中间代码202进而被馈送到绑定器214,绑定器214例如通过基于类型布局250信息来计算字段128的数值偏移量,并通过使用这些数值偏移量来替换曾经在伪指令中使用的符号字段引用,将伪指令212解析成本机代码指令134。
图4示出了过程和其他实施例,特别关注处理图2中所枚举的无用信息收集和/或其他伪代码类别时的版本回复力。源代码120被输入到编译器132中,编译器生成MSIL,其是一种熟悉的中间语言代码;MSIL代表“微软中间语言”。与MSIL代码指令相关联的是熟悉的元数据404。包括其元数据的MSIL被馈送到MDIL生成器,MDIL生成器是生成器204的一个示例。MDIL生成器生成MDIL代码408及其相关联的元数据410。MDIL代表“机器相关中间语言”,且是中间语言代码202的一个示例。在MDIL本机指令和底层的MSIL指令之间可以存在某些重叠,并且在MDIL元数据和MSIL元数据之间可以存在某些重叠。然而,MSIL不包括此处讨论的属于MDIL的伪指令212。MDIL中间代码202被馈送到绑定器214,绑定器214通过置换、映射和/或此处讨论的其他方式,将伪指令212解析成本机代码指令134,包括诸如虚方法槽指派表和无用信息收集卡表等本机代码数据结构412。
现在转向图5到7,将介绍各种步骤。这些步骤的各方面也在本文的别处讨论。
在基类源代码获得步骤502期间,一实施例获得包括至少一个基类124的声明的源代码120。步骤502例如可以使用文件系统、源代码编辑器和/或其他熟悉的机制来完成。
在中间代码生成步骤504期间,一实施例生成中间代码202。在某些配置或某些实施例中,仅生成中间代码指令,但是在其他配置或实施例中,步骤504生成元数据(例如,MDIL元数据410)作为中间代码202的一部分。步骤504可以使用语法分析、树遍历、表查找和/或适用于如此处所教示地生成具有伪指令212的中间代码202的其他熟悉的机制来完成。
在对象字段布局改变回复力保持步骤506期间,一实施例保持对象字段布局208在中间代码202中未绑定。例如,对象字段128偏移量可以是符号的而非硬编码的(此处称为“数值的”),其开发依赖于绑定器214来在中间代码202被绑定到特定目标引擎140时确定并插入数值字段偏移量。
在虚方法槽指派改变回复力保持步骤508期间,一实施例保持虚方法槽指派210在中间代码202中未绑定。例如,虚方法130槽指派可以未被指定,其开发依赖于绑定器214来在中间代码202被绑定到特定目标引擎140时确定并使用数值虚方法槽指派。
在符号引用使用步骤510期间,一实施例例如通过放置符号引用和/或通过将符号引用变换成数值引用,来在中间代码202中使用符号引用248。
在通过符号标识步骤512期间,一实施例通过符号标识中间代码202中的字段128,而非例如将字段标识为数值偏移量。步骤512是步骤510的一个示例,其中符号引用指的是字段。
在字段次序独立性保持步骤514期间,一实施例保持字段次序在中间代码202中未绑定。当对象字段128偏移量通过符号来表示而非被表示为数值字段偏移量时,步骤514的一个实例可以是例如步骤512和506的示例。
在伪指令表达步骤516期间,使用一个或多个伪指令212来表达一个多个项,如字段、类型、虚方法、调用、或关于无用信息收集的指针状态等。图2枚举了某些种类的伪指令,本文也讨论了其他示例。表达步骤516可以在中间语言代码生成步骤504期间进行。以下讨论的步骤518到543是表达步骤516的某些示例,其聚焦于某些种类的伪指令212。
在字段访问表达步骤518期间,一实施例例如使用诸如托管对象字段访问伪指令212、232等一个或多个伪指令212来表达字段128访问520(读和/或写)。
在方法调用表达步骤522期间,一实施例使用一个或多个伪指令212,如静态调用方法伪指令212、224,尾调用方法伪指令212、226,执行引擎服务调用伪指令212、228,或运行时类型直接调用方法伪指令212、230,来表达非虚方法调用524。
在虚调用表达步骤526期间,一实施例例如使用诸如虚调用方法伪指令212、222等一个或多个伪指令212来表达虚调用528。
在方法前序处理表达步骤526期间,一实施例使用一个或多个伪指令212来表达方法前序处理532。
在方法结尾处理表达步骤534期间,一实施例使用一个或多个伪指令212来表达方法结尾处理536。
在绑定步骤538期间,一实施例将伪指令212绑定到本机指令134,在伪指令212的基础上选择本机指令134,基于伪指令212产生本机代码数据结构412,和/或如此处所教示地在产生本机指令134时以其他方式处理伪指令212。重新绑定步骤也被称为步骤538;在重新绑定期间,绑定器214可再次绑定该绑定器(或另一绑定器)先前绑定538的某些中间代码。
在可执行代码产生步骤540(其也可被称为代码准备步骤540)期间,一实施例至少部分地基于底层的伪指令212来产生、准备、更新、生成、和/或以其他方式提供本机指令134以供在可执行代码136中使用。步骤540可以例如由绑定器214来执行。
在修订步骤542期间,一实施例或者自动地或者在人类用户104的指示和详细控制下,修订源代码120。
在代码执行步骤544期间,一实施例执行本机指令134,如部分地在绑定器214的努力下产生的可执行代码136中的本机指令。
在长度变换步骤602期间,一实施例将伪指令212或伪指令212的集合变换成其表示占据了与伪指令212的表示不同的字节数的本机指令134。如同此处对“变换”和相似术语的其他讨论一样,底层伪指令212可经历了变换之后仍存在,和/或伪指令212可能不再存在而是被原地替换。
在局部变量大小确定和指定步骤期间,其对应于且此处为了方便起见被单独且联合地称为步骤604,一实施例确定其大小未在底层伪指令212中被指定的局部变量的本机代码大小。步骤604可以作为例如绑定步骤538的一部分来执行。
在栈帧偏移量大小确定和指定步骤期间,其对应于且此处为了方便起见被单独且联合地称为步骤606,一实施例确定未在底层伪指令212中被指定的本机代码栈帧偏移量。步骤606可以作为例如绑定步骤538的一部分来执行。
在栈帧对齐确定和指定步骤期间,其对应于且此处为了方便起见被单独且联合地称为步骤608,一实施例确定未在底层伪指令212中被指定的本机代码栈帧对齐。步骤608可以作为例如绑定步骤538的一部分来执行。可以理解,步骤608并不对齐栈帧本身。相反,栈帧中的各个变量具有在步骤608期间确定的对齐要求。绑定器也根据这些对齐要求来指派栈帧偏移量。
在类型符号引用变换步骤610(也可被称为类型解析)期间,一实施例将对位于伪指令212中的类型的符号引用248变换成对位于本机指令134中的具体类型的使用。不像符号类型引用,具体类型具有例如数值大小,并且如果类型具有字段/成员,则具体类型也具有具体字段/成员次序。步骤610可以作为例如绑定步骤538的一部分来执行。
在字段符号引用变换步骤612(也可被称为字段解析)期间,一实施例将对位于伪指令212中的字段的符号引用248变换成对位于本机指令134中的具体字段的使用。不像符号字段引用,具体字段具有例如数值大小和偏移量。步骤612可以作为例如绑定步骤538的一部分来执行。
在成员符号引用变换步骤614(也可被称为成员解析)期间,一实施例将对位于伪指令212中的成员的符号引用248变换成对位于本机指令134中的具体成员的使用。不像符号成员引用,具体成员具有例如数值大小和偏移量。步骤614可以作为例如绑定步骤538的一部分来执行。
在槽符号引用变换步骤616(也可被称为槽解析或槽指派)期间,一实施例将对位于伪指令212中的虚方法槽的符号引用248变换成对位于本机指令134中的具体槽的使用。不像符号槽引用,具体槽例如在诸如虚方法表等运行时数据结构中具有数值。步骤616可以作为例如绑定步骤538的一部分来执行。
在对应性实现步骤618期间,一实施例实现伪指令212和本机指令134之间的结构对应性620。步骤618可以例如通过执行以下步骤的一个或多个来完成:重新绑定538、变换602、610、612、614、616。
在复制伪指令使用步骤622期间,一实施例使用以下伪指令212:该伪指令将向绑定器214指示需要某些本机指令134,即在被执行时将复制结构624的本机指令134。结构624的大小未在伪指令212中指定,但却是/将在本机指令134中指定。伪指令212可通过例如将其放置在中间语言代码202中和/或通过将伪指令212变换成本机指令134来被使用622。
在零伪指令使用步骤626期间,一实施例使用以下伪指令212:该伪指令将向绑定器214指示需要某些本机指令134,即在被执行时将使结构624置零的本机指令134。结构624的大小未在伪指令212中指定,但却是/将在本机指令134中指定。伪指令212可通过例如将其放置在中间语言代码202中和/或通过将伪指令212变换成本机指令134来被使用626。
在局部变量声明伪指令使用步骤628期间,一实施例使用以下伪指令212:该伪指令将向绑定器214指示需要某些本机指令134,即在被执行时将声明具有某一类型的局部变量630的本机指令134。类型可通过使用510符号类型引用来指定。在某些实施例中,局部变量630及其类型的各方面未在伪指令212中指定,但却是/将在本机指令134中指定。这些方面可包括例如局部变量和/或局部变量的类型的大小、对齐要求、以及栈帧偏移量。伪指令212可通过例如将其放置在中间语言代码202中和/或通过将伪指令212变换成本机指令134来被使用628。
在虚方法槽映射步骤632期间,例如,作为实现618对应性630的一部分,一实施例将一个或多个虚方法槽映射到一个或多个方法主体。步骤632可以是(重新)绑定步骤538的一部分。
在存储器配置步骤636期间,存储器介质112由中间语言代码202或其包含伪指令212的一部分、由用于生成这一代码202的生成器204、由用于绑定这一代码202的绑定器214、和/或关于此处讨论的改变回复力代码的其他方式来配置。
在无用信息收集器探测伪指令使用步骤702期间,一实施例通过将指令216放在中间代码202中和/或通过将伪指令216变换成例如在执行时将检查待决无用信息收集的机器指令134(以及可能的相关联的数据/元数据)来使用无用信息收集器探测伪指令216。
在对象分配伪指令使用步骤704期间,一实施例通过将指令218放在中间代码202中和/或通过将伪指令218变换成例如在执行时将为对象126分配存储器空间的机器指令134(以及可能的相关联的数据/元数据)来使用对象分配伪指令218。
在异常抛出伪指令使用步骤706期间,一实施例通过将指令220放在中间代码202中和/或通过将伪指令220变换成例如在执行时将抛出异常的机器指令134(以及可能的相关联的数据/元数据)来使用异常抛出伪指令220。
在虚调用方法伪指令使用步骤708期间,一实施例通过将指令222放在中间代码202中和/或通过将伪指令222变换成例如在执行时将对指令222所标识的方法作出虚调用的机器指令134(以及可能的相关联的数据/元数据)来使用虚调用方法伪指令222。
在静态调用方法伪指令使用步骤710期间,一实施例通过将指令224放在中间代码202中和/或通过将伪指令224变换成例如在执行时将对指令224所标识的方法作出静态调用的机器指令134(以及可能的相关联的数据/元数据)来使用静态调用方法伪指令224。
在尾调用方法伪指令使用步骤712期间,一实施例通过将指令226放在中间代码202中和/或通过将伪指令226变换成例如在执行时将对指令226所标识的方法作出尾调用的机器指令134(以及可能的相关联的数据/元数据)来使用尾调用方法伪指令226。
在执行引擎服务调用伪指令使用步骤714期间,一实施例通过将指令228放在中间代码202中和/或通过将伪指令228变换成例如在执行时将对指令228所标识的执行引擎140服务作出调用的机器指令134(以及可能的相关联的数据/元数据)来使用执行引擎服务调用伪指令224。
在运行时类型直接调用过方法伪指令使用步骤716期间,一实施例通过将指令230放在中间代码202中和/或通过将伪指令230变换成例如在执行时将对指令230通过符号标识的方法作出调用的机器指令134(以及可能的相关联的数据/元数据)来使用运行时类型直接调用方法伪指令230。
在托管对象字段访问伪指令使用步骤718期间,一实施例通过将指令232放在中间代码202中和/或通过将伪指令232变换成例如在执行时将访问指令232所标识的对象126的字段128的机器指令134(以及可能的相关联的数据/元数据)来使用托管对象字段访问伪指令232。
在堆指针指定伪指令使用步骤720期间,一实施例通过将指令234放在中间代码202中和/或通过将伪指令234变换成例如在执行时将使用指令234所标识的指针来管理用于堆的无用信息收集的机器指令134(以及可能的相关联的数据/元数据)来使用堆指针指定伪指令234。
在实例化查找伪指令使用步骤722期间,一实施例通过将指令236放在中间代码202中和/或通过将伪指令236变换成例如在执行时将使用指令236所标识的类属代码实例化的机器指令134(以及可能的相关联的数据/元数据)来使用实例化查找伪指令236。
在可中断区域伪指令使用步骤724期间,一实施例通过将指令238放在中间代码202中和/或通过将伪指令238变换成例如在执行时将管理用于紧凑循环和/或至少部分地由指令238来界定或以其他方式来标识的其他代码区域的无用信息收集的机器指令134(以及可能的相关联的数据/元数据)来使用可中断区域伪指令238。
在无用信息收集指针伪指令使用步骤726期间,一实施例通过将指令240放在中间代码202中和/或通过将伪指令240变换成例如在执行时将使用指令240所标识的指针来管理无用信息收集的机器指令134(以及可能的相关联的数据/元数据)来使用无用信息收集指针伪指令240。
在无用信息收集写屏障伪指令使用步骤728期间,一实施例通过将指令242放在中间代码202中和/或通过将指令242变换成例如在执行时将管理无用信息收集写屏障的机器指令134(以及可能的相关联的数据/元数据)来使用无用信息收集写屏障伪指令242。
在地址模式修改器伪指令使用步骤730期间,一实施例通过将指令244放在中间代码202中和/或通过将伪指令244变换成例如在执行时将由地址模式修改来控制的机器指令134(以及可能的相关联的数据/元数据)来使用地址模式修改器伪指令244。伪指令244中的地址模式修改器修改作为从包含地址模式修改器的伪指令244的转换获得的本机指令134的一部分的本机地址模式。
在静态基址伪指令使用步骤732期间,一实施例通过将指令246放在中间代码202中和/或通过将伪指令246变换成例如在执行时将提供静态变量存储区域的基址的机器指令134(以及可能的相关联的数据/元数据)来使用静态基址伪指令246。在某些实施例中,所生成的机器指令还可检查类是否已经被初始化,并且如果尚未完成则触发初始化。
在不同机器使用步骤734期间,一实施例可使用与用于生成504代码202的计算机或设备不同的计算机或其他设备来绑定538代码202,或者该实施例可使用与将用于绑定538代码202的计算机或设备不同的计算机或其他设备来生成504代码202。换言之,步骤734可在绑定或在生成时进行,只要已经或将用于绑定和用于生成的机器能在进行步骤734时被标识,并且只要这些机器不是同一个机器。如果将用于绑定代码的机器在生成代码时是未知的,则在生成代码时不进行步骤734。出于步骤734的目的,如果两个机器具有单独可控的电源,即,如果人类用户有可能关闭一个机器而不用因此也关闭另一个机器,和/或如果人类用户有可能打开一个机器而不用因此也打开另一机器,则这两个机器构成不同机器。
上述步骤及其相互关系将在下文结合各实施例来更详细讨论。
某些实施例提供了方便对涉及软件产品的基类124的改变的管理的过程。该过程包括在存储器内获得502基类源代码120,以及在存储器内从该基类源代码生成中间语言代码202,在该中间语言代码中,寄存器分配206是执行就绪的,但是以下的至少一项尚未绑定:对象字段布局208、虚方法槽指派210。
例如,在一个实施例中,生成步骤生成504其中对象字段布局208尚未绑定的中间语言代码202,并且至少部分地通过生成使用510符号引用248而非直接偏移量来标识字段128的中间语言代码来这样做。作为另一示例,在一个实施例中,生成步骤生成504其中对象字段布局208尚未绑定的中间语言代码202,并且至少部分地通过生成字段次序无关的中间语言代码来这样做。
在某些实施例中,生成步骤生成504既包括本机指令134又包括伪指令212的中间语言代码202。具体地,一实施例可生成其中使用伪指令来表达516以下的至少一项的代码202:字段访问、方法调用、虚方法调用、整个方法前序处理、方法前序处理的至少一部分、整个方法结尾处理、方法结尾处理的至少一部分。
在某些实施例中,该过程聚焦于生成504代码202,且不必包括绑定538。在其他实施例中,该过程的确包括绑定538中间语言代码202且因此产生540可执行代码136,即,可由至少一个设备上的处理器110执行的代码。
绑定538可在不同上下文中进行。例如,在某些实施例中,该过程包括首先绑定538中间语言代码202且因此产生540与执行引擎140的第一版本一起使用的第一可执行代码136,其次绑定相同的中间语言代码202且因此产生540与执行引擎140的第二版本一起使用的第二可执行代码136。换言之,在某些情况下,相同的中间代码可用于为引擎140的不同版本产生不同可执行代码,由此示出了中间代码202对于执行引擎改变的回复力。
某些实施例提供了管理涉及软件产品的基类124的改变的过程。该过程包括获得502用于模块A的初始源代码A,包括在该初始源代码A中声明的初始基类A,并且获得502用于模块B的源代码B,包括对初始基类A的依赖关系。该过程还包括通过编译初始源代码A来生成504初始中间语言代码A,并通过编译源代码B来生成504初始中间语言代码B。这些初始中间语言代码用于创建可执行代码。即,该过程包括通过绑定538初始中间语言代码A来准备540初始可执行代码A,并通过绑定538初始中间语言代码B来准备540初始可执行代码B。
以上步骤可被视为预备性的,或上下文的。在其执行的上下文中,一实施例获得502用于模块A中的修订的基类A的修订的542源代码A,修订的基类A在以下方面的至少一个中不同于初始基类A:添加了字段、移除了私有字段、重新安排了字段次序、添加了虚方法、重新安排了虚方法次序。该过程实施例通过编译修订的源代码A在手边生成504修订的中间语言代码A。
接着,该实施例通过绑定538修订的中间语言代码A来准备540修订的可执行代码A,并通过在引用修订的中间语言代码A时重新绑定538初始中间语言代码B来准备540修订的可执行代码B。以此方式,该过程避免了源代码B的重新编译。然后可以按照在修订的基类A的执行期间没有任何字段偏移量冲突且没有任何虚方法槽指派冲突的方式,用修订的可执行代码B来执行修订的可执行代码A。即,该过程示出了在面对对基类124的改变时中间语言代码202的回复力。
作为回复力的进一步说明,考虑涉及至少两个模块的情形,例如,包含基类“Base”的模块“A”和或者包含从“Base”派生的类“Derived”或者也许仅包含使用来自“Base”的字段或方法的代码的模块“B”。两个模块的初始版本被编译成中间语言,然后被绑定来获得A和B的可执行模块。然后,模块A按照上述方式(添加字段、移除私有字段、重新安排字段次序、添加虚方法、重新安排虚方法次序)之一来改变,并且仅重新编译模块A。然后,重新绑定538两个模块来获得新的可执行代码。
在这些情形中,可通过确保类型“Base”的布局在模块A和B两者的可执行代码中是一致的来促进正确性。例如,如果在模块A和B中都访问“Base”中的字段,则两个模块使用相同的偏移量;如果偏移量不同则不正确程序行为的风险会变高。类似地,如果在两个模块中都访问了虚方法,则确保它们使用相同的虚槽号降低了模块A中的改变所导致的不正确程序行为的风险。如果模块B中的派生类型添加更多字段,则确保其偏移量不会与A中的任何字段偏移量冲突降低了不正确程序行为的风险;如果对不同字段使用相同的偏移量则会发生毁坏。类似地,如果模块B中的派生类型添加更多虚方法,则确保其虚槽不与A使用的任何虚槽冲突降低了风险;如果对不同方法使用相同的槽号则会发生毁坏。某些实施例通过确保即使添加了新方法和字段或者重新安排了现有方法和字段,用于类型、方法和字段的符号标签跨各版本也保持相同,来帮助降低风险。这些符号标记此处有时被称为“令牌”或符号引用248。
在某些实施例中,绑定538中间语言代码202涉及将访问存储器112的中间语言伪指令212变换成具有与中间语言伪指令不同的长度(以字节为单位)的对应的本机语言指令134。结果,绑定器214可以比熟悉的链接器更复杂。绑定器214可被配置成调整本机代码134中的跳转,使得它们仍然跳转到其预期的目标,其位置取决于具体伪指令212和其各自的本机代码134之间的变化的长度差别。相反,熟悉的修补可以简单地向每一跳转添加固定的量。
在某些实施例中,在以上讨论的初始中间语言代码B中未指定局部变量的大小,并且重新绑定538初始中间语言代码B涉及确定604该大小并在修订的可执行代码B中指定该大小。在某些实施例中,在初始中间语言代码B中未指定局部变量的栈帧偏移量,并且重新绑定538初始中间语言代码B涉及确定606该栈帧偏移量并在修订的可执行代码B中指定该栈帧偏移量。在某些实施例中,在初始中间语言代码B中未指定局部变量的栈帧偏移量对齐要求,并且重新绑定538初始中间语言代码B涉及确定608该栈帧偏移量对齐要求,并且根据该栈帧偏移量对齐要求来确定局部变量的栈帧偏移量。
例如,关于局部变量的对齐要求,局部变量的栈帧偏移量可能需要除以2的某一幂(通常是1、2、4、8或16),取决于该变量的类型。这一对齐要求可以在版本化期间改变。例如,如果结构体仅包含32位整数,则其偏移量必须被对齐到4的倍数,但是如果稍后添加了8字节的双精度字段,则该变量偏移量必须是8的倍数,取决于处理器架构和操作系统约定。某些实施例例如通过使用510符号引用和/或伪指令212的其他方面来提供关于这一对齐要求改变的改变回复力。
在某些实施例中,初始中间语言代码B包括对在以上讨论的修订的基类A中定义的类型的符号引用248,并且重新绑定538初始中间语言代码B涉及将该符号引用变换成利用描述该类型的数据结构的地址的修订的可执行代码B中的引用。换言之,可执行代码136最终包含描述该类型的数据结构(类型描述符)的地址,或者它包含含有该类型描述符的地址的间接单元的地址。前一种引用类型的方式一般在模块内使用,后一种方式用于引用驻留在另一模块中的类型描述符。出于步骤510的目的,“利用”(即,使用)类型描述符地址,而不管其是直接使用的还是经由间接单元使用的。
在某些实施例中,上述初始中间语言代码B包括对字段128的符号引用248,并且重新绑定538初始中间语言代码B涉及将该符号引用变换成修订的可执行代码B中的数值偏移量。在某些情况下,字段不是原语类型(其中大小是已知的),而是其大小可在版本化期间改变的复合类型。某些实施例因此使用622、626伪指令,如本文别处讨论的MDIL INIT_STRUCT(初始化结构体)、COPY_STRUCT(复制结构体)、PUSH_STRUCT(压入结构体)伪指令212。
在某些实施例中,上述初始中间语言代码B包括对模块A中定义的成员的符号引用248,并且重新绑定538初始中间语言代码B包括将该符号引用变换成修订的可执行代码B中的以下各项中的至少一项:数值偏移量、指向方法主体的直接指针、指向方法主体的间接指针、虚槽号。例如,在MDIL中,memberref(成员引用)令牌可以指字段或方法。在字段的情况下,他们通过绑定538被变换成数值偏移量。在方法的情况下,通过绑定538,它们或者成为指向该方法的主体的(直接或间接)指针(这是对于普通方法调用),或者它们成为插入到可执行代码序列中的虚槽号(在虚调用的情况下)。
某些实施例提供了涉及对于软件产品的改变回复中间语言代码202的过程。该过程包括在存储器内获得502源代码(对于基类和/或其他软件项),以及在存储器内从该源代码生成504其中寄存器分配206是执行就绪的中间语言代码202。所生成的中间语言代码202在该代码202内的特定位置包含图2中按照名称枚举的伪指令212中的至少一个,即伪指令216到242中的至少一个。
配置的介质
某些实施例包括配置的计算机可读存储介质112。介质112可包括盘(磁、光或其他)、RAM、EEPROM或其他ROM、和/或其他可配置存储器,包括在特定的非瞬态计算机可读介质(与线路和其他传播信号介质形成对比)。配置的存储介质可以特别地是诸如CD、DVD或闪存等可移动存储介质114。可以是可移动或不可移动并且可以是易失性或非易失性的通用存储器可被配置成使用数据118和指令116形式的从可移动介质114和/或诸如网络连接等另一源读取的诸如伪指令212和/或绑定器214等项目来形成配置的介质的实施方式。配置的介质112能够使计算机系统执行用于如此处公开的生成和/或变换回复中间语言代码202的过程步骤。图1到7因而帮助示出配置的存储介质实施方式和进程实施方式,以及系统和进程实施方式。具体而言,图3到7所示或此处另外教示的过程步骤中的任一个可用于帮助配置存储介质来形成配置的介质实施例。
其他示例
以下提供其他细节和设计考虑。如同此处的其他示例一样,在一给定实施例中,所描述的特征可个别和/或组合地使用,或完全不使用。
本领域的技术人员将理解,实现细节可涉及诸如具体API和具体样本程序等具体代码,因此不需要出现在每一实施例中。本领域的技术人员还将理解,讨论细节时所使用的程序标识符和某些其他术语,包括例如要求和结果的陈述,是特定于实现的,且因此不需要属于每一实施例。
具体地,在讨论MDIL的各示例中对术语“伪指令”的使用可以更宽泛,或者另外与在上文中对该术语的使用不一致,例如,通过将MDIL指令称为伪指令,即使它们可能缺乏上述伪指令212的某些特性。同样,上述绑定器214不必具有以下讨论的MDIL绑定器的每一个特征或能力。
尽管如此,虽然不要求它们一定存在于此,但是提供了关于MDIL的这些细节,因为它们可通过提供上下文来帮助一些读者。MDIL还示出了以上结合中间语言代码202、伪指令212和绑定器214所讨论的技术的许多可能的实现中的某一些。
MDIL指令集:解释了MDIL代码中的指令格式和约定。
本节讨论了MDIL指令的二进制编码和语义。
介绍
什么是MDIL?MDIL是比MSIL更低级的中间语言。它对机器独立性的目标不抱希望——事实上,缩写MDIL代表了机器相关中间语言。MDIL不与MSIL竞争——相反,经历MDIL是编译流水线从如C#、Visual
托管C++等语言中的源代码经由MSIL到本机代码(微软公司的标记)中的另一步骤。MDIL不试图抽象目标处理器的特性,但是它仍提供了针对托管代码和正在为其编译MDIL代码的执行引擎的版本化的隔离层。
MDIL不是直接可执行的。相反,绑定器读取MDIL代码并从中产生可执行机器代码。MDIL代码因此让人想起经历链接器来产生最终可执行机器代码的未托管代码的编译所得的目标文件。然而,MDIL中的伪指令可以比传统的目标文件中的修补更进一步——并非仅仅修补指令的各部分(像地址字段),MDIL伪指令可以生成长度可能不同于伪指令的新的本机指令。由此,MDIL绑定器比链接器更复杂,因为绑定器必须调整所得的本机代码中的跳转,如此它们仍跳转到其预期目标。随着这一增加的复杂性还带来了增加的能力——MDIL代码对于添加新字段或虚方法的基类型能够是稳健的,因此它可被认为是对“脆弱基类问题”的解决方案。
MDIL代码示例
此处是一个采用C#的简单的介绍性代码示例:
并且,此处是在MDIL中该示例可能编译成什么,其中添加了一些注释:
该示例展示了MDIL如何能接近实际机器并且仍提供了一抽象层来作为针对执行引擎、托管代码的布局或支持数据结构的布局中的改变的保护。
例如,该方法的前序处理大多数经由伪指令来表达。这帮助遵守执行引擎限制,并传达关于栈帧的布局的信息。
并且,MDIL代码可援引本机机器指令。
并且,分配对象经由伪指令(本情况中的ALLOC_OBJECT)来表达。这提供了灵活性,因为应用于最高效的分配方式的规则和限制可能在将来改变。
并且,无用信息收集器(GC)信息经由嵌入在代码流中的显式伪指令(本示例中的REF_BIRTH_ESI和REF_DEATH_ESI)来表达。
同样,访问托管对象中的字段经由伪指令(本示例中的LOAD、ADD、STORE)来完成。这提供了灵活性,因为字段偏移量可能由于托管汇编件的版本化及其执行引擎的版本化而改变。MDIL具有用于将MDIL寻址模式装配在一起的构造的丰富供应。
同样,调用方法也经由伪指令(本示例中的CALL_DEF和CALL_REF)来完成。这允许MDIL代码表达意图(做出调用)而不会陷入可能取决于执行引擎的约定且因此经受改变的细节中。
同样,方法的结尾处理被表达为单个伪指令。这是因为结尾处理包含了关于能够从中构造该结尾处理的栈帧的足够信息。以此方式,能够避免嵌入像压入或弹出被调用者被保存的(callee-saved)寄存器的次序这样的细节。另一方面,寄存器分配基本上或完全由产生MDIL代码的编译器来完成。
要记住,以上仅是如何以此MDIL格式表达优化的托管代码的一个示例,本节对MDIL的讨论的其余部分更仔细地考虑这些概念,伪指令用于支持这些概念,以及如何在二进制级编码伪指令。
MDIL文件格式
在一个原型系统中,编译成MDIL代码的结果被存储在单独的文件中,其按照约定具有.mdil扩展名。某些实施例可将MDIL代码与元数据和MSIL代码一起放在一个包中;此处提供的文件格式信息在不同的实施例中可不同。
MDIL文件头
在该具体实现中,文件头是简单的C++结构,其具有某些版本化信息以及其后所跟的各个节的大小,以元素或字节数为单位:
MDIL文件节
在MDIL文件之后,跟着按照以下给出的次序的若干节。
公知的类型表(WellKnownType)。如果该头部的标志字段中的WellKnownTypesPresent(存在公知类型)位打开(on),则后面跟着定义特定基本系统类型的typedef(类型定义)令牌的表。该表旨在仅对基本系统库,例如公共语言运行时(CLR)的上下文中的mscorlib.dll存在,但是一个原型编译器始终将其放在其中。该表是简单的dwords(双字)数组,其中的槽由以下C++枚举类型来定义:
类型映射节。这是将typedef(类型定义)令牌映射到压缩类型布局节中的偏移量的简单dword条目数组。条目0未使用,但应存在。
方法映射节。这是将methoddef(方法定义)令牌映射到MDIL代码节中的偏移量的简单dword条目数组。在该偏移量处,MDIL方法头开始——见下文。dword可以将其高位置位,该高位表示偏移量改为是对类属实例节的偏移量。
类属实例节。类属方法或类属类型中的方法可具有多个MDIL代码主体,每一MDIL代码主体适用于不同种类的类型自变量。有多少不同的主体,以及哪一主体适用于什么种类的类型自变量由类属实例描述符来描述。每一描述符以头部开始:
字段m_instCount描述了对该具体方法存在多少不同的主体。字段m_arity描述了类型自变量的总数,即,类级类型自变量和方法级类型自变量两者的和。在头部之后,跟着类型自变量掩码的矩形矩阵。其具有m_instCount行(对每一主体一行)和m_arity列(对每一类型自变量一列)。每一类型自变量掩码是简单的位掩码,每一位表示该主体对于一具体种类的类型的适用性。位的编号跟在CorElementType枚举之后,具有几个添加:
-ELEMENT_TYPE_BOOLEAN =0x02,
-ELEMENT_TYPE_CHAR =0x03,
-ELEMENT_TYPE_CHAR =0x03,
-ELEMENT_TYPE_I1 =0x04,
-ELEMENT_TYPE_U1 =0x05,
-ELEMENT_TYPE_I2 =0x06,
-ELEMENT_TYPE_U2 =0x07,
-ELEMENT_TYPE_I4 =0x08,
-ELEMENT_TYPE_U4 =0x09,
-ELEMENT_TYPE_I8 =0x0a,
-ELEMENT_TYPE_U8 =0x0b,
-ELEMENT_TYPE_R4 =0x0c,
-ELEMENT_TYPE_R8 =0x0d,
-ELEMENT_TYPE_VALUETYPE =0x11,
-ELEMENT_TYPE_CLASS =0x12,
-NULLABLE =0x17,
-SHARED_VALUETYPE =0x1e
-SHARED_NULLABLE =0x1f,
因此,如果一具体方法具有两个类型自变量,且如果第一自变量为浮点或双精度而第二自变量为整型,则一具体主体适用,该主体的行中的第一列将具有值3000h(在C++表示法中为(1<<0x0c)|(1<<0x0d)),且第二列将具有值100h(在C++表示法中为(1<<0x08))。如果绑定器正在查找取浮点和整型作为参数的主体,则该行将匹配。
在该类型掩码的矩形矩阵之后,跟着m_instCount DWORD对的一维数组。每一对中的第一个DWORD是MDIL代码偏移量(即,到MDIL代码池的偏移量),而每一对中的第二个DWORD是调试信息偏移量(即,对调试信息池的偏移量)。
由此,如果绑定器的该实施例需要适用于某一组类型自变量的MDIL代码主体,则它将首先查找类型自变量掩码矩阵中的匹配行。它然后将使用该行索引来索引到MDIL代码偏移量数组中来找到正确的主体。行将从顶部开始顺序地被搜索。这意味着编译器在更一般且因此较不优化的版本之前应放置更为优化且专门化的版本。
外部模块节。这是简单的双字条目数组,每一条目包含对名称池(Namepool)节的偏移量——这些是所引用的其他模块的名称。
外部类型节。这是通过以下C++结构来描述的条目数组:
该模块只是对外部模块节的索引,并且序数是另一模块中的typedef的编号。
外部成员节。这是通过以下C++结构来描述的条目数组:
extTypeRid字段或者是对外部类型节的索引,或者是对类型规范节的索引,如由isTypeSpec字段所指示的。ordinal字段是类型中的字段或方法的索引,而isField指示正在引用字段还是方法。
类型规范节。这是包含了引用压缩类型布局节的偏移量的简单双字条目数组。
方法规范节。这是包含了引用压缩类型布局节的偏移量的简单双字条目数组。
名称池节。这是包含了外部模块和P/调用入口点的名称的简单的字节数组。
压缩类型布局节。该节包含了三种数据。压缩类型布局描述了引用或值类型、其基类型、实现的接口、字段、方法等。类型规范描述描述了构造的类型,如数组类型、类属实例化等。方法规范描述描述了方法实例化。
用户串池节。这是包含用户串的节。
MDIL代码节。这是包含实际MDIL代码的节。用于给定methoddef令牌的MDIL代码的起始偏移量通过索引到方法映射节来找到。对于类属类型中的方法,或对于类属方法,该偏移量将使其高位置位,这意味着该偏移量是到类属实例节的偏移量,类属实例节描述了为方法编译的类属代码的不同风格,以及它们适用于什么种类的类型自变量。否则,该偏移量指向MDIL代码节,并且前几个字节是MDIL方法头部。
调试映射节。这是将methoddef令牌映射到调试信息中的偏移量的简单的双字条目数组。在该偏移量处,调试信息开始——见下文。
调试信息节。这是包含方法调试信息的节,见下文的调试信息。
也可存在平台专用数据节,其具有专用于某一平台或某一种类的平台的数据。
MDIL方法头部
该头部是描述了后面所跟的MDIL方法的大小以及主体后跟的任选异常子句表中的异常子句的数量的一个或多个字节的数据结构。
编码方案对于没有异常子句的小方法来优化。以下是它如何工作的:
·如果第一个字节在范围00h..0DFh内,这意味着例程大小是第一字节,则没有异常表条目。
·如果第一个字节在范围0E0h..0FFh内,则
·位0..2编码该例程的大小:
0..5:大小=下一字节+(位0..2)*256
6:大小=下一字
7:大小=下一双字
·位3..4编码异常条目的数量:
0..2:(位3..4)个异常条目
3:下一字节是异常条目的数量
如果nextByte(下一字节)是0xff,则下一双字是异常条目的数量
调试信息
每一方法任选地与一调试信息数据结构相关联。该数据结构由诸如
Visual
调试器、mdbg和windbg(微软公司的标记)等调试器来使用。该数据结构包含两种类型的信息。偏移量映射信息将中间语言(IL)指令偏移量映射到MDIL指令偏移量。绑定器将这些转换成对IL指令偏移量到本机指令偏移量的映射,并将其存储在本机映像中。与PDB文件(其包含IL指令偏移量到源行的映射)一起,该映射允许源代码级调试。变量信息存储方法参数和局部变量的位置。这允许调试器示出这些变量的值。
逻辑上,调试信息数据结构是如下结构的集合。该集合包括源类型结构:
该集合还包括寄存器结构:
该集合还包括变量位置结构:
该集合还包括附加结构:
对于存储在栈上的每一变量,调试信息或者包含该变量的相对于寄存器的偏移量(如果VLT_MDIL_SYMBOLIC位未被置位),或者包含MDIL变量号(如果VLT_MDIL_SYMBOLIC位被置位)。在后一情况下,绑定器将负责将变量号转换成实际栈偏移量,然后将VLT_MDIL_SYMBOLIC位清零。变量信息中的varNumber(变量号)或者是IL变量号,或者是枚举ILNUM中的值之一。
由于调试信息数据结构中的所有值是相对小的整数,因此使用基于半字节的压缩方案来减小该数据结构的大小。该压缩方案使用以下算法将每一值转换成一个或多个半字节(4位值):
半字节按照它们被生成的次序被存储在存储器中。在每一字节内,第一个半字节被存储在四个最低有效位中,且第二个半字节被存储在四个最高有效位中。每一无符号整数(包括枚举值)用WriteEncodedU32写入半字节流中,且每一有符号整数用WriteEncodedI32写入,具有以下例外。变量信息中的endOffset(结束偏移量)用WriteEncodedU32(endOffset-startOffset)来写入。变量信息中的varNumber用WriteEncodedU32(varNumber-MAX_ILNUM)来写入。当VLT_MDIL_SYMBOLIC位未被置位时,栈偏移量在用WriteEncodedI32写出以前除以sizeof(DWORD)。注意,当前的CLR实现不支持未在DWORD边界上对齐的栈变量。当VLT_MDIL_SYMBOLIC位被置位时,MDIL变量号用WriteEncodedU32来写入。
以下三个值始终在字节边界上开始:cbOffsetMapping、iOffsetMapping和iVariableInfo。如有需要,前导字节可以用未使用的半字节来填充。所有其他值不必在字节边界上开始。
援引本机机器指令
本机机器指令经由担当援引的伪指令族来嵌入在MDIL中。该伪指令包含紧跟在后面的本机机器指令的字节数。在本机代码中,大多数机器指令块是相当短的,因此在MDIL中有16个短形式来嵌入机器指令字节的0到15字节。还存在分别将机器指令字节的数量表达为无符号字节、无符号字和无符号双字的三个较长的形式。
由此,具有以下伪指令族:
·LIT_MACHINE_INST_0..LIT_MACHINE_INST_15(操作码00h..0Fh):后跟本机机器指令字节的0..15字节
·LIT_MACHINE_INST_B<byte count>:(操作码10h):后面的字节给出了后跟的机器指令字节数
·LIT_MACHINE_INST_W<word count>:(操作码11h):后面的(按照小尾序)字给出了后跟的机器指令字节数
·LIT_MACHINE_INST_L<dword count>:(操作码10h):后面的(按照小尾序)双字给出了后跟的机器指令字节数
作为一个示例MDIL代码字节,考虑:
05 0f af d1 8b c2
这给出了分解成以下指令的五字节本机指令序列:
imul edx,ecx
mov eax,edx
当将该序列转换成本机代码时,绑定器将仅剥离伪指令援引(在这一情况下是初始的05字节),并且将本机机器指令的其余5字节逐字地复制到输出代码流中。
MDIL伪指令的编码中的一般概念
本章介绍了对MDIL伪指令的频繁出现的分量进行编码的方式。
类型令牌的编码。许多MDIL伪指令具有类型令牌作为自变量(例如,分配、类型强制转换),并且类型令牌也可出现在即值(immediates)和寻址模式中。MDIL使用类型令牌的压缩编码,其在通常情况下是紧凑的。
在MDIL中(如同MSIL中一样),类型可以由三种不同的类型令牌来指定:
·Typedef令牌(具有最高有效字节02h)用于表达对当前模块中定义的类型的引用。
·Typeref令牌(具有最高有效字节01h)用于表达对其他模块中定义的类型的引用。
·Typespec令牌(具有最高有效字节1Bh)用于表达对像数组类型、类属实例化、类型参数等构造类型的引用。
在合理大小的模块中,通常使用数千个typedef令牌、少得多的typeref令牌、以及还要少的typespec令牌。MDIL在大多数情况下使用以下编码来以2字节表达类型令牌:
·如果令牌是typedef令牌,且typedef令牌在范围02000000h..0200BFFFh内,则该令牌被编码为该类型令牌的低字的高字节,之后是低字的低字节。
·如果令牌是typeref令牌,且typeref令牌在范围01000000h..01002FFFh内,则该令牌被编码为0C0h加上该类型令牌的低字的高字节,之后是低字的低字节。
·如果令牌是typespec令牌,且typespec令牌在范围1B000000h..1B000EFFh内,则该令牌被编码为0F0h加上该类型令牌的低字的高字节,之后是低字的低字节。
·否则,该令牌被编码为字节0FFh,之后是以小尾序表示该类型令牌的4字节。
类型令牌编码示例。这里是一个示例:
6d 00 03 ALLOC_OBJECT 02000003
在该示例中,字节“6d”是MDIL伪指令ALLOC_OBJECT的操作码,且后面的2字节是类型令牌编码。由此,该类型令牌的低字是0003,且高字隐含为0200,因此整个令牌是02000003h。
字段令牌的编码。字段令牌通常被编码为寻址模式的部分。在MDIL中(如同在MSIL中一样),字段令牌可以是两种主要种类:
·它可以是fielddef令牌(最高有效字节04h),其用于引用当前模块中的非类属类型的字段
·或者,它可以是memberref令牌(最高有效字节0Ah),其用于引用其他模块中的类型的字段,或实例化的类属类型的字段。
作为一个特例,在MDIL中,字段令牌也可以是类型令牌。这用于引用值类型的装箱(boxed)表示的内容。如同类型令牌一样,字段令牌对于通常情况具有短的表示:
·如果令牌是范围04000000h..0400BFFFh内的fielddef令牌,则其被编码为该令牌的低字的高字节(保证为0BFh或更小),之后是低字的低字节。
·如果令牌是范围0A000000h..0A001FFFH内的memberref令牌,则其被编码为0C0h加上该类型令牌的低字的高字节,之后是低字的低字节。
·如果令牌是范围0400C000h..040FFFFFFh内的fielddef令牌,则其被编码为0E0h加上该令牌的高字的低字节,之后是低字的高字节,之后是低字的低字节。
·如果令牌是范围0A002000h..0A0EFFFFh内的memberref令牌,则其被编码为0F0h加上该令牌的高字的低字节,之后是低字的高字节,之后是低字的低字节。
·否则,该令牌被编码为字节0FFh,之后是以小尾序表示该字段令牌的4字节。
字段令牌编码示例。这里是一个示例:
13 06 00 11 LOAD eax,[esi].04000011(Test.a)
此处,字节‘13’是LOAD伪指令的操作码。字节‘06’编码了基址和目的地寄存器,并且还暗示后面跟有字段令牌。字段令牌由字节‘00’和‘11’组成,它们是按照大尾序的字段令牌的低字。由此,字段令牌的低字是0011h,且整个字段令牌是04000011h。
串令牌的编码。串令牌由MDIL用于引用串文字(在LOAD_STRING和PUSH_STRING伪指令中)。串令牌的最高有效字节是70h。用于这些令牌的编码再一次试图使得通常情况是紧凑的:
·如果令牌在范围70000000h..7000BFFFh内,则其被编码为该令牌的低字的高字节,之后是低字的低字节。
·如果令牌在范围7000C000h..703EFFFFh内,则其被编码为0C0h加上该令牌的高字的低字节,之后是低字的高字节,之后是低字的低字节。
·否则,该令牌被编码为字节0FFh,之后是以小尾序表示该串令牌的4字节。
串令牌编码示例。这里是一个示例:
9e 01 00 13 LOAD_STRING ecx,70000013
‘9e’字节是LOAD_STRING伪指令的操作码。后面的‘01’字节包含目标寄存器(本情况中是ECX)。后面的两个字节是用于串令牌70000013h的串令牌编码。
即值的编码。MDIL出于以下两个原因使用即值,即绑定时常量的特殊编码。
·大多数常量是小的,且MDIL代码大小可能是重要的,因此对这一情况进行优化是有意义的。
·某些常量在编译时不是常量,因此编译器需要用符号来表达它们。示例:值类型的大小在绑定时是常量,但是它们可能对编译器不是已知的。
编码可以用以下语法来描述:
其中:
SignedByte=不包括0bbb、0bdh、0dbh、0bdh的单个字节//表示有符号字节。
SignedWord=0xbb低字节高字节//表示有符号字(按照小尾序)
DWord=0xdd低字高字//表示双字(按照小尾序)
ArrayElementSize=0bdh类型令牌//作为数组元素的类型的大小(以字节为单位)
ArgumentSize=0dbh类型令牌//作为自变量的类型的大小(以字节为单位)
Multiplier=MDILImmediate
AdditiveConstant=MDILImmediate
符号常量(以0bdh和0dbh开始的编码)主要在以下三个上下文中在地址算术中使用:
·在对varargs或cdecl函数的调用之后弹出栈自变量
·类似地,指示对于gc信息,在调用之后栈上的多少值已被弹出或被无效
·走查具有(内部)指针的值类型的数组
注意,类型在MDIL中可具有两种不同大小——如果该类型的值被用作栈自变量则是一种大小,且如果其被用作数组元素则是另一种大小(有时候较小)——这解释了为什么一个实现既使用ArrayElementSize(数组元素大小)又使用ArgumentSize(自变量大小)。例如,对于类型System.Int16。ArrayElementSize将为2,但是ArgumentSize将为4。这是因为栈自变量的大小始终被上舍入到4的倍数。
这里是一些MDIL即值编码示例:
·03h代表常量3或0x00000003
·0fch代表常量-4(或0xfffffffc)
·0bbh 0bbh 00h代表常量187(或0x000000bb)
·0ddh 78h 56h 34h 12h代表常量305419896(或0x12345678)
·0dbh 00h 0dh 03h 04h代表ArgumentSize(0200000d)*3+4。这可以用于实例来在调用了取3个(值类型)0200000d自变量和一个Dword的方法之后清理栈。
寻址模式
访问存储器。存在用于访问存储器的MDIL指令族。它们被转换成等效的本机指令,像字段偏移量或访问大小等某些细节由绑定器来填充。
例如,简单的特性获取器可获得传入寄存器ECX中的this指针,并且可能希望在寄存器EAX中返回离开this的字段。
用于这一指令的MDIL代码字节可能看上去为:
13 01 00 09
MDIL分解器将其变为:
LOAD eax,[ecx].04000009
注意,这看上去与以下的本机代码指令非常相似:
mov eax,dword ptr[ecx+8]
其实际上是绑定器可能将该具体指令转变成的指令。
在MDIL指令与其本机对应物之间存在多个区别:
·MDIL指令使用字段令牌(04000009)而非直接偏移量来指示它引用什么字段。因此,这是对字段的符号引用(即使其仍是各种数字)——绑定器将对该对象布局,并确定该具体字段的偏移量。对字段的某些种类的声明不在MDIL特性中完成,而是在相关的元数据信息中完成。
·访问的大小(本情况中为DWORD)不是由MDIL操作码(13)来确定的,而是由该字段的类型来确定的。因此,如果想要改为加载类型字段byte,则可使用相同的操作码,但是使用不同的字段令牌——类型字段byte的令牌。绑定器然后可以将其转换成访问字节的本机指令,如mov al,byte ptr[ecx+0ch]。
·在MDIL指令的大小(本情况中为4字节)和本机指令的大小(3字节)之间没有简单关系。如果由绑定器确定的字段偏移量较大(大于127字节),则绑定器将必须使用较大的本机指令,但是MDIL指令将完全不变。
当然,对象字段不仅仅是能够用MDIL伪指令来访问的唯一的东西——还有局部变量、数组元素和更多,并且还有其组合。如同在本机代码中一样,MDIL具有指令(其传达对数据项要做什么)和寻址模式(其用于指示正被操作的数据项)之间的区别。MDIL寻址模式如何工作的细节在以下段落中更详细讨论。
什么种类的访问可被表达为MDIL地址模式?MDIL地址模式表达了对堆上和栈上的对象(可版本化的结构体、类属)、对常量池中的文字常量、以及对静态字段的访问。有时候访问数据对象的部分——对象的字段、数组和串的元素、数组和串的长度字段。本节描述了用于x86和x64处理器的地址模式。
抽象了什么方面?首先,要理解,“抽象”等此处在软件开发的意义上使用,而不是在专利案件法律意义上使用。在软件开发中,抽象涉及关于在何处以及何时使实现细节发挥作用。由此,诸如C++或LISP等编程语言抽象诸如将使用哪一处理器以及有多少存储器可用于执行程序等细节。类似地,文档节标题、章节标题、目录、索引、以及当然书、文章或其他文档中的摘要抽象了文档内容的细节。
在某些实施例中,诸如MDIL等中间语言隐藏,即抽象了诸如以下的细节:
·堆和栈对象的确切布局(字段在哪里,其排序是什么等等)。
·在类属代码中,有时是确切类型,因此可以使用相同的机器代码而不管代码参数的大小、gc布局和有无符号。
·在某些情况下(栈上的可版本化结构体、类属代码),还有必要在某种程度上隐藏栈布局——换言之,一些或全部自变量和变量栈偏移量由绑定器而非编译器来分配。
地址模式的元素。此处是可用于构造简单或更复杂的MDIL地址模式的地址模式的元素。
·字段偏移量
·显式类型
·索引
·数组和串长度
·显式偏移量
·缩放、隐式或显式
地址模式的编码。地址模式的编码是非常灵活的——许多元素可任选地存在。然而,在大多数情况下,在指令中需要基址寄存器和源/目的地寄存器。由此,目前定义的MDIL中的地址模式始终以包含两个寄存器字段的单个字节开始,在x64代码中每一寄存器字段为4位宽:
AddrRegsByte:rrrrbbbb,其中
rrrr=4位源/目的地寄存器字段,以及
bbbb=4位基址寄存器字段
在x86 MDIL代码的情况下,每一寄存器字段只有3位宽,且因此有另外两个位可用于频繁地描述地址模式:
AddrRegsByte:ffrrrbbb,其中
ff=2位标志字段(见下文)
rrr=3位源寄存器字段,以及
bbb=3位基址寄存器字段
在x64代码中,AddrRegsByte总是跟有一个或多个地址模式修饰符,在x86代码中,这只是AddrRegsByte中的ff字段具有值11b时的情况——ff字段的其他可能值用于编码常见地址模式(以下更详细描述)。某些指令不需要源/目的地寄存器字段。在这些情况下,改为使用该字段来保持子操作码,或标志。
地址模式修饰符。地址模式修饰符用于调整地址模式的含义:
·能够指定(类或结构体的)字段——这意味着字段偏移量被添加到已经存在的任何偏移量,并且该字段的类型变为地址模式的类型——因此当字段例如是类型char时,地址模式现在引用大小为2字节的无符号值。作为一种特殊类型,引用值类型的类型令牌也可用作字段令牌——这用于引用值类型的装箱表示的内容。
·能够指定索引——这意味着指定索引寄存器是什么,并且还指定数组的元素类型。元素类型变为地址模式的类型。
·能够添加显式偏移量。
·能够指定该地址模式引用方法的常量池。
·能够指定地址模式引用局部变量。
·能够指定地址模式直接引用基址寄存器,而非引用基址寄存器所指向的内容。
·能够指定希望引用单维或多维数组的长度字段。这将长度字段的偏移量添加到地址模式的偏移量,并且类型变为长度字段的类型。
·能够显式地改变地址模式的类型——这有时候对于类型强制转换是有用的,例如当希望引用作为字节的双字大小字段的时候。
地址模式修饰符可被串接——例如,为了对结构体数组的结构体元素中的长字段的高双字寻址,可以指定索引(来到达数组元素),然后指定字段(来到达结构体内的字段),然后指定显式偏移量4(来到达高双字),并最终指定显式类型DWORD。
地址模式修饰符的编码。地址模式修饰符至少包括单个前导字节(AddrModeByte),可能后跟附加信息。AddrModeByte中的高位用于指示这是否是最后一个地址模式修饰符(该位被清零),或者后面有更多地址模式修饰符(该位被置位)。较低的7位被如下编码:
·01h:(AM_FIELD)意味着这是对字段的引用——后跟字段令牌(编码在“字段令牌编码”一章中解释)。
·02h:(AM_INDEX)意味着这是对数组元素的引用——后跟详细描述该数组的元素类型的所谓的索引字节,以及索引寄存器(细节在下文解释)。
·03h:(AM_OFFSET)意味着显式偏移量被添加到地址模式——该偏移量跟在后面,并且被编码为MDIL即值(在关于即值编码的一章中解释)。
·04h:(AM_CONSTPOOL)意味着这是对例程中的CONST_DATA节的引用。基址寄存器应是ESP/RSP且被忽略。
·05h:(AM_LOCALVAR)意味着这是对局部变量的引用。局部变量号跟在后面,被编码为即值。局部变量的类型变为地址模式的类型。
·06h:(AM_REGISTER)意味着这是对基址寄存器的直接引用,而非其指向的存储器。
·09h-0fh:为将来的扩展所保留
·10h-17h:对数组和串的长度字段的引用:
ο10h:(AM_BYTEARRAY_LEN)意味着这是对字节数组的长度的引用
ο11h:(AM_WORDARRAY_LEN)意味着这是对字数组的长度的引用
ο12h:(AM_DWORDARRAY_LEN)意味着这是对双字数组的长度的引用
ο13h:(AM QWORDARRAY_LEN)意味着这是对四字数组的长度的引用
ο14h:(AM_REFARRAY_LEN)意味着这是对引用数组的长度的引用
ο15h:(AM_STRUCTARRAY_LEN)意味着这是对结构体数组的长度的引用——后面跟着指定该数组的元素类型的类型令牌
ο16h:(AM_STRING_LEN)意味着这是对串的长度的引用
ο17h:(AM_MDIM_ARRAY_LEN)意味着这是对多维数组的长度之一的引用后面跟着进一步描述该多维数组的MdimArrayByte。在此之后,跟着指定想要的边界的维度的MdimBoundByte,以及指的是该维度中的下界还是长度。
·18h-1eh:地址模式的显式类型化:
ο18h:(AM_EXPLICIT_BYTE):类型地址模式作为字节大小
ο19h:(AM_EXPLICIT_WORD):类型地址模式作为字大小
ο1Ah:(AM_EXPLICIT_DWORD):类型地址模式作为双字大小
ο1Bh:(AM_EXPLICIT_QWORD):类型地址模式作为四字大小
ο1Ch:(AM_EXPLICIT_DQWORD):类型地址模式作为16字节大小
ο1Dh:(AM_EXPLICIT_TOKEN):由后面的类型令牌给出的类型
ο1Eh:(AM EXPLICIT_REF):类型地址模式作为引用
·20h-3Fh:(AM_SMALL_OFFSET):AM_OFFSET的空间节省变体——由低的5位给出的0到31的偏移量。
·40h-7Fh:(AM_LOCAL_FIELD):AM_FIELD的空间节省变体——低5位给出了封装当前方法的类型中的相对字段号。
·可能的将来改变:减少用于AM_LOCAL_FIELD的编码的数量来获得用于局部变量引用的较短的编码。
x86上的短编码。如上所述,AddrRegsByte包含用于指示流行的寻址模式的高位中的附加标志。这些替换编码的目的是在以上列出的更一般的编码上节省一字节的MDIL代码空间。标志具有以下含义:
·00b:(AF_FIELD):指示这是对字段的引用——后面跟着字段令牌
·01b:(AF_INDEX):指示这是对数组元素的引用——后面跟着详细描述数组元素和索引寄存器的类型的索引字节
·10b:(AF_INDIR):指示这是简单间接——后面没有信息且访问大小是双字。这可以在某一时刻被重新打算来改变该用于对局部变量的较短引用的编码。
·11:(AF_AMODE):这是指示后面跟着如上所述的一个或多个地址模式修饰符的一般模式。
索引字节编码。索引字节让人想起本机x86/x64寻址模式的SIB字节。它们指示索引寄存器、缩放、以及什么种类的数据对象正被索引。
低的4位指示索引寄存器:
·0h:索引寄存器是寄存器eax(或64位代码中的rax)
·1h:索引寄存器是寄存器ecx/rcx
·2h:索引寄存器是寄存器edx/rdx
·2h:索引寄存器是寄存器ebx/rbx
·4h:没有索引寄存器
·5h:索引寄存器是寄存器ebp/rbp
·6h:索引寄存器是寄存器esi/rsi
·7h:索引寄存器是寄存器edi/rdi
·8h:索引寄存器是寄存器r8
·0fh:索引寄存器是寄存器r15
高的4位指示什么种类的数据对象要被索引:
·00h:(IB_BYTE_PTR):从字节指针索引,即,缩放=1,无附加偏移量
·10h:(IB_WORD_PTR):
·20h:(IB_DWORD_PTR):
·30h:(IB_QWORD_PTR):
·40h:(IB_BYTE_ARRAY):索引到托管字节数组——缩放因子=1,数组头部大小要作为附加偏移量来添加
·50h:(IB_WORD_ARRAY):
·60h:(IB_DWORD_ARRAY):
·70h:(IB_QWORD_ARRAY):
·80h:(IB_STRUCT_ARRAY):索引到用户定义的结构体的数组。后面跟着类型令牌。这要用于ELEM_SCALE伪指令来实现部分缩放,索引将在适当时实现其余缩放。例如,如果用户定义的结构体的大小为12字节,则ELEM_SCALE可以乘以3,并且实际数组访问将按照4来缩放索引。
·0A0h:(IB_REF_ARRAY):索引到引用的数组。
·0B0h:(IB_STRING):索引到串的数组。
·0C0h:(IB_MDIM_ARRAY):索引到多维数组。后面跟着指示多维数组的秩和元素类型的MdimArrayByte。
·0E0h:(IB_EXPLICIT_SCALE):要被索引到的数据对象以及缩放因子在下一字节中单独给出。该字节的低4位给出了要被索引到的对象的种类(与索引字节的高4位相同的编码),并且高4位给出了缩放因子的二进制算法(在0..3的范围内)。
MdimArrayByte编码。MdimArrayBytes用于描述多维数组的秩和元素类型。它们或者用于索引到多维数组中,或者用于访问多维数组的边界(在这一情况下它们后面跟着MdimBoundByte)。
编码将数组的秩放在该字节的低5位中(将秩限于31或更少)。较高的3位包含元素类型的编码:
·00h(MAB_KIND_BYTE):元素类型是字节(有符号或无符号)
·20h(MAB_KIND_WORD):元素类型是字
·40h(MAB_KIND_DWORD):元素类型是双字
·60h(MAB_KIND_QWORD):元素类型是四字
·80h(MAB_KIND_REF):元素类型是引用
·0A0h(MAB_KIND_STRUCT):元素类型是结构体(后面跟着指定元素类型的类型令牌)
MdimBoundByte编码。这指定了正在引用多维数组的哪一维,以及正在引用该维中的下界还是长度。
类似于MdimArrayBytes,维度被放在低5字节中。较高的3位在下界和长度之间进行区分:
·00h(MBB_LOW_BOUND):正在引用该维中的下界
·20h(MBB_LENGTH):正在引用该维中的长度
简单地址模式示例:
·递增类型短整型的字段,假定0400000e是类型短整型的字段的字段定义令牌:
2f 02 00 0e 分解:INC[edx].0400000e
·初始化(符号寻址的)局部变量:
30 c4 05 00 01分解:STORE_IMM[esp].var#0,0x1
·对照寄存器esi所指向的数组的上界来检查寄存器edi中的索引:
1d fe 12 CMP edi,dword ptr[esi].DWordArrayLength
·从寄存器esi中的双精度数组加载值——索引在寄存器edi中:
37 46 77 FLD qword ptr[esi.QWordArray+edi*8]
复杂地址模式示例:
·从对象中的字段加载长整型(即,64位有符号整数值)。这包括两个指令:
13 c1 81 00 0f 1a LOAD eax,dword ptr[ecx].0400000f
13 d1 81 00 0f a4 1a LOAD edx,dword ptr[ecx+0x4].0400000f
注意第一个指令中的显式类型覆盖(尾部的1ah字节)。
注意第二个指令中的显式偏移量4(0a4h字节)来对长整型值的较高的一半双字进行寻址。
·从例程的常量池中的偏移量8加载文字双精度值:
37 c4 84 a8 1b FLD qword ptr[ConstPoo]+0x8]
此处,有三个地址模式修饰符。第一个(084h)意味着寻址是相对于常量池的(丢弃AddrRegsByte所暗示的基址寄存器esp),第二个(0a8h)意味着存在附加偏移量08h,第三个(1bh)意味着访问的大小是四字。
·在双精度数组中的索引3处存储双精度值(未示出范围检查):
37 d9 82 74 38 FSTP qword ptr[ecx.QWordArray+0x18]
在该示例中,AddrRegsByte(0d9h)将两个最高有效位置位,即,后面跟着完全地址模式。接下来三个位是用于FSTP的子操作码的一部分。最低有效3位是001b,其是用于ecx的寄存器代码。接着后面跟的是地址修饰符082h(或AM_CONTINUE|AM_INDEX),其表示后面跟着更多地址模式修饰符,并且还跟着索引字节。索引字节是74h(IB_QWORD_ARRAY|MDIL_REGISTER_ESP),意味着正在索引到四字的数组中,并且索引寄存器是esp,即,没有索引。最后,地址模式修饰符038h表示附加偏移量018h。
直接转换成机器指令的MDIL指令。此处是与LOAD伪指令表现非常相似的MDIL伪指令的列表(除了其中的一些就像本机指令那样影响cpu标志以外):
·LOAD(操作码13h):从由地址修饰符描述的托管存储器位置加载寄存器-转换成mov reg,mem本机指令
·STORE(操作码14h):正如加载一样,转换成mov mem,reg本机指令
·LOAD_SX(操作码16h):用符号扩展来加载,转换成movsx reg,mem本机指令。存储器访问的大小由地址模式来暗示(比如字段的类型)。
·LOAD_ZX(操作码17h):用零扩展来加载,转换成movzx reg,mem本机指令。存储器访问的大小再次由地址模式来暗示。
·LOAD_X(操作码18h):以”自然扩展”来加载,即,根据地址模式的大小和有无符号。对这一指令的主要使用是在类属代码中。
·LOAD_ADDR(操作码19h):加载有效地址,转换成lea reg,mem本机指令
·ADD(操作码1Ah):从存储器加到寄存器,转换成add reg,mem本机指令。条件代码因此如在本机指令中那样受到影响。
·ADC(操作码1Bh):带有进位从存储器加到寄存器
·AND(操作码1Ch):将寄存器与存储器进行“与”
·CMP(操作码1Dh):将寄存器与存储器进行比较
·OR(操作码1Eh):将寄存器与存储器进行“或”
·SUB(操作码1Fh):从寄存器中减去存储器
·SBB(操作码20h):带有借位来相减
·XOR(操作码21h):将寄存器与存储器进行“异或”
这些MDIL指令中的某一些具有其中目的地操作数是存储器而非寄存器的“反向”形式:
·ADD_TO(操作码22h):将寄存器加到存储器
·ADC_TO(操作码23h):带有进位将寄存器加到存储器
·AND_TO(操作码24h):将寄存器与存储器进行“与”
·CMP_TO(操作码25h):将寄存器与存储器进行比较
·OR_TO(操作码26h):将寄存器与存储器进行“或”
·SUB_TO(操作码27h):从存储器中减去寄存器
·SBB_TO(操作码28h):带有借位从存储器中减去寄存器
·XOR_TO(操作码29h):将寄存器与存储器进行“异或”
还存在其中源操作数是即值常量而非寄存器的变体。操作码是OP_IMM(操作码31h)。地址模式中的AddrRegsByte中的寄存器字段(见以下关于地址模式的一章)用作表达以下操作的子操作码:
·ISO_ADD(0h):将即值加到存储器
·ISO_OR(1h):将即值与存储器进行“或”
·ISO_ADC(2h):带有进位将即值加到存储器
·ISO_SBB(3h):带有借位从存储器中减去即值
·ISO_AND(4h):将即值与存储器进行“与”
·ISO_SUB(5h):从存储器中减去即值
·ISO_XOR(6h):将即值与存储器进行“异或”
·ISO_CMP(7h):将即值与存储器进行比较
这些指令的编码具有跟在地址模式描述后面的即值。即值被编码为MDIL即值——这具有用于小常量的小形式,以及表达符号常量的能力(在编译时不是常量但在绑定时是常量)。参见“即值编码”下的更长的描述。
存在更多的引用存储器的MDIL指令,对于大部分,它们很直接地转换成本机指令。所有这些都包含地址模式。有时候,在操作码和地址模式之间存在子操作码,并且有时在最后的后面存在即值(总是按照标准MDIL方式来编码——参见“即值编码”)。在许多情况下,地址模式的寄存器字段用于子操作码,在其他情况下,不使用寄存器字段且其应为0(全0位)。
引用存储器的一些附加MDIL指令包括:
·TEST(操作码2Ah):test寄存器,存储器
·MUL_DIV_EAX(操作码2Bh):not/neg/mul/imul/div/idiv存储器指令组,子操作码由地址模式中的寄存器字段给出:
οMDSO_NOT(2h):not存储器
οMDSO_NEG(3h):neg存储器
οMDSO_MUL_EAX(4h):mul eax/ax/al,存储器(大小由地址模式暗示)
οMDSO_IMUL_EAX(5h):imul eax/ax/al,存储器
οMDSO_DIV_EAX(6h):div eax:edx/ax:dx/ax,存储器
οMDSO_IDIV_EAX(7h):idiv eax:edx/ax:dx/ax,存储器
·IMUL(操作码2Ch):imul寄存器,存储器
·IMUL_IMM(操作码2Dh):imul寄存器,存储器,即值
·INC_DEC_PUSH(操作码2Fh):inc/dec/push存储器指令组,子操作码由地址模式中的寄存器字段给出:
οIDP_INC(0h):inc存储器
οIDP_DEC(1h):dec存储器
οIDP_PUSH(6h):push存储器(存储器大小必须是字或双字)
·STORE_IMM:(操作码30h):mov存储器,即值。地址模式的寄存器字段在这一指令中未使用且必须为0。
·TEST_IMM(操作码32h):test存储器,即值。地址模式的寄存器字段未使用且必须为0。
·SHIFT_1(操作码33h):rol/ror/rcl/rcr/shl/shr/sar存储器,1。该操作在地址模式的寄存器字段中如下编码:
οSSO_ROL(0h):循环左移
οSSO_ROR(1h):循环右移
οSSO_RCL(2h):进位循环左移
οSSO_RCR(3h):进位循环右移
οSSO_SHL(4h):逻辑左移
οSSO_SHR(5h):逻辑右移
οSSO_SAR(7h):算术右移
·SHIFT_IMM(操作码34h):rol/ror/rcl/rcr/shl/sar存储器,即值。该操作在地址模式的寄存器字段中如上对于SHIFT_1一样编码。
·SHIFT_CL(操作码35h):rol/ror/rcl/rcr/shl/shr/sar存储器,cl。该操作在地址模式的寄存器字段中如上对于SHIFT_1一样编码。
·OP_XMM(操作码36h):xmm指令——下一字节包含如下子操作码:
οXSO_LOAD(0h):movss/movsd xmm寄存器,存储器
οXSO_STORE(1h):movss/movsd存储器,xmm寄存器
οXSO_ADD(2h):addss/addsd xmm寄存器,存储器
οXSO_SUB(3h):subss/subsd xmm寄存器,存储器
οXSO_MUL(4h):mulss/mulsd xmm寄存器,存储器
οXSO_DIV(5h):divss/divsd xmm寄存器,存储器
οXSO_CMP(6h):ucomiss/ucomisd xmm寄存器,存储器
οXSO_F2D(7h):cvtss2sd xmm寄存器,存储器
οXSO_F2I(8h):cvtss2si/cvtsd2si寄存器,存储器
οXSO_FT2I(9h):cvttss2si/cvttsd2si寄存器,存储器--截断版本
οXS_F2L(0Ah):cvtss2si/cvtsd2si寄存器,存储器
οXSO_FT2L(0Bh):cvttss2si/cvttsd2si寄存器,存储器--截断版本
οXSO_F2S(0Ch):cvtsd2ss xmm寄存器,存储器
οXSO_I2D(0Dh):cvtsi2sd xmm寄存器,存储器
οXSO_I2S(0Eh):cvtsi2ss xmm寄存器,存储器
οXSO_LOAD_16(0Fh):movdqa xmm寄存器,存储器
οXSO_STORE_16(10h):movdqa存储器,xmm寄存器
ο...将来可添加更多子操作码
·LD_ST_FPU(opcode 37h):fld/fst/fstp存储器指令组。该操作在地址模式的寄存器字段中如下编码:
οLSO_FLD(0h):fld存储器
οLSO_FST(2h):fst存储器
οLSO_FSTP(3h):fstp存储器
·OP_FPU(opcode 38h):fadd/fmul/fcom/fcomp/fsub/fsubr/fdiv/fdivr存储器指令组。该操作在地址模式的寄存器字段中如下编码:
οFSO_FADD(0h):fadd存储器
οFSO_FMUL(1h):fmul存储器
οFSO_FCOM(2h):fcom存储器
οFSO_FCOMP(3h):fcomp存储器
οFSO_FSUB(4h):fsub存储器
οFSO_FSUBR(5h):fsubr存储器
οFSO_FDIV(6h):fdiv存储器
οFSO_FDIVR(7h):fdivr存储器
·ILOAD_FPU(opcode 39h):fild存储器。地址模式的寄存器字段未使用且必须为0。
·ISTORE_FPU(opcode 3Ah):fistp存储器。地址模式的寄存器字段未使用且必须为0。
·SET_CC(opcode 3Bh):setcc存储器——下一字节包含条件代码(这遵循用于条件代码的普通x86编码):
οSET_O(0h):在溢出时set(设置)
οSET_NO(1h):在无溢出时set
οSET_C(2h):在进位时set(无符号<)
οSET_NC(3h):在无进位时set(无符号>=)
οSET_Z(4h):在零时set(==)
οSET_NZ(5h):在非零时set(!=)
οSET_BE(6h):在小于或等于时set(无符号<=)
οSET_A(7h):在大于时set(无符号>)
οSET_S(8h):在为负时set(<0)
οSET_NS(9h):在为正或零时set(>=0)
οSET_PE(0Ah):在奇偶性为偶时set
οSET_PO(0Bh):在奇偶性为奇时set
οSET_L(0Ch):在小于时set(有符号<)
οSET_GE(0Dh):在大于等于时set(有符号>=)
οSET_LE(0Eh):在小于等于时set(有符号<=)
οSET_G(0Fh):在大于时set(有符号>)
·XADD(opcode 3Ch):lock xadd存储器,寄存器
·XCHG(opcode 3Dh):xchg存储器,寄存器
·CMPXCHG(opcode 3Eh):lock cmpxchg存储器,寄存器
ELEM_SCALE伪指令的操作。ELEM_SCALE(操作码2Eh)指令取目的地寄存器、地址模式、以及引用值类型的类型令牌。其用于预缩放对寄存器的数组索引,以使其能用于索引结构体的数组。假设是结构体的精确大小对于编译器是未知的(因为例如可能在版本化时改变)。
绑定器的操作是在结构体被用作数组元素时首先计算结构体的大小。由于本机地址模式可包括缩放因子1、2、4或8,因此绑定器将结构体大小除以作为其除数的这些因子中的最大者。例如,如果结构体大小是12,则绑定器将其除以4并得到3、最后,绑定器将生成按照该最终因子来缩放地址模式的指令。
由于move、shift、lea和imul指令将在适当时由绑定器使用,因此cpu条件代码寄存器必须被该指令认为是作废的。否则,仅目的地寄存器被设为预缩放的值。该值当然可由多个指令用作引用结构体数组的地址模式中的经缩放的数组索引。
无用信息收集(GC)信息
介绍。无用信息收集器将停止托管线程并检查每一线程的栈上的方法的栈帧。在某些实现中,必须使无用信息收集器可能找到包含指向无用信息收集器的堆的指针的所有局部变量(即,寄存器和栈位置)。该组所谓的“gc根”当然可取决于执行在方法内确切地到达了何处,即,指令指针或返回地址在方法内指向何处。
作为第一近似,因此可以将用于方法的gc信息认为是映射(方法内的偏移量)->(包含托管指针的寄存器和栈位置的组)。存在对该描绘的某些复杂化,接着将进一步考虑。
指针的风格。CLR和像C#这样的在其上运行的语言允许被无用信息收集器不同地对待的不同指针风格:
·最简单且最常见类型的指针是指向托管对象的指针。它必须始终指向对象的开头,否则它必须为空。
·另一种类型的指针(所谓的内部指针)可以指向对象的中间,或者栈位置或非托管存储器,或者它可以为空。这些经常由C#的ref参数或其在其他CLR语言中的等效物来生成。它们也可以通过优化编译器来生成,例如高效地走查托管数组。
·CLR还允许销住指针(pinning pointer)。具有指向(或指入)对象的销住指针告知无用信息收集器将该对象暂时保持在原处,例如,它不应出于堆压缩而被移到别处。
·在某些上下文中,CLR要求托管代码专门地标记方法的this指针。
不可中断位置。CLR是多线程环境。这意味着有时候线程将不得不被停止来进行无用信息收集,因为某一其他线程进行了分配并且用完了无用信息收集堆上的空间。由此,在某些实现中,当前正在执行托管代码的每一线程必须能够在短的时间量内停止,并且它必须能够在当前gc根可被无用信息收集器发现的位置处停止。
CLR中的一般规则是线程可在例程将要返回到其调用者时停止。由此,(JIT)编译器必须能够至少在每一调用位置描述gc根。
对于没有调用的紧凑的长期运行循环,使用一附加规则来保证线程能够在不将整个程序的执行延迟过长的情况下被停止。在CLR的上下文中使用两种技术来实现这一点:
·完全可中断代码在每处都具有准确的gc根信息。CLR对于x86上的整个方法支持完全可中断代码,它还支持x64上的完全可中断区域。两个伪指令START_FULLY_INTERRUPTIBLE/END_FULLY_INTERRUPTIBLE(开始完全可中断/结束完全可中断)以MDIL表达了这一概念,其中在x86上,完全可中断区域必须是方法的除了前序处理和结尾处理之外的整个主体。
·GC探测是被插入到紧凑循环中以便检查待决的无用信息收集的显式指令。存在以MDIL表达这一概念的伪指令GC_PROBE(GC探测)——其确切是如何实现的是留给绑定器的细节。很清楚,在GC_RPOBE伪指令中,GC信息必须是准确的。
在运行
Windows操作系统的系统上,常用的技术是完全可中断代码,因此它给出了最佳代码性能(没有额外指令来检查待决的无用信息收集)。不利方面是gc信息的大小要大得多。
GC信息如何以MDIL来表达。MDIL使用专门的指令来传达哪些寄存器或栈位置在方法中的什么位置包含什么种类的指针:
·REF_BIRTH_REG(操作码73h)。这意味着寄存器开始包含在当前代码偏移量处开始的gc指针。之后是在高5位中包含寄存器号和在低3位中包含标志的一附加字节。标志的指派如下:
ο如果指针是内部指针,则位0为1,否则为0
ο如果指针是销住指针,则位1为1,否则为0
ο如果指针是this指针,则位2为1,否则为0
·REF_DEATH_REG(操作码7Bh):这意味着寄存器不再包含在当前代码偏移量处开始的gc指针。之后跟着在高5位中包含寄存器号的一附加字节。低3位必须全为0。
·REF_BIRTH_EAX(操作码6Fh):用于寄存器号为0且标志为全0的REF_BIRTH_REG的短编码。注意,在x64代码中,这涉及全寄存器RAX。
·REF_BIRTH_ECX(操作码70H):相同概念...
·REF_BIRTH_EDX(操作码71h):同上
·REF_BIRTH_EBX(操作码72h):同上
·REF_BIRTH_EBP(操作码74h):同上
·REF_BIRTH_ESI(操作码75h):同上
·REF_BIRTH_EDI(操作码76h):同上
·REF_DEATH_EAX(操作码77h):用于寄存器号为0的REF_DEATH_REG的短编码。
·REF_DEATH_ECX(操作码78h):相同概念...
·REF_DEATH_EDX(操作码79h):
·同上
·REF_DEATH_EBX(操作码7Ah):同上
·REF_DEATH_EBP(操作码7Ch):同上
·REF_DEATH_ESI(操作码7Dh):同上
·REF_DEATH_EDI(操作码7Eh):同上
·REF_BIRTH_EBP_V(操作码7Fh):经由EBP/RBP寻址的变量变为活的。之后是在低3位中指示标志(值在REF_BIRTH_REG下详述)且在高位中指示EBP/RBP相对偏移量的MDIL即值。存在偏移量在x86上4字节对齐且在x64上8字节对齐的要求。由此不表示要求为0的位,即,包含在MDIL即值常量中的偏移量在x86上右移2位,在x64上右移3位。
·REF_DEATH_EBP_V(操作码80h):经由EBP/RBP寻址的变量不再包含gc指针。如同REF_BIRTH_EBP_V一样,之后是MDIL即值,除了低3位中的标志必须全为0以外。
·REF_BIRTH_ESP_V(操作码81h):对于经由ESP/RSP寻址的变量是类似的。
·REF_DEATH_ESP_V(操作码82h):同上
·REF_BIRTH_LOCAL(操作码83h):对于经由变量号通过符号寻址的变量是相似的。之后是在低3位中包含标志且在高位中包含变量号的MDIL即值。该指令的语义取决于变量仅仅被给予了局部大小还是被给予了显式类型:
ο变量仅仅被给予了大小:该变量被认为是gc指针。大小必须是指针的大小(x86上的4字节,x64上的8字节)。
ο变量被给予了显式类型且该类型是gc引用(类或数组)——该指令开始意味着变量所引用的栈位置包含在当前偏移量开始的gc指针。
ο变量是不包含任何gc引用的值类型:该指令完全被忽略。
ο变量是包含一个或多个gc引用的值类型:所包含的gc引用被认为是从当前偏移量开始有效。
·REF_DEATH_LOCAL(操作码84h):同上
·REF_BIRTH_LCLFLD(操作码85h):之后是引用局部结构体或块中的字段的地址模式。通常标志被包含在AddrRegsByte的reg字段中。
·REF_DEATH_LCLFLD(操作码86h):同上
·REF_UNTR_EBP_V(操作码87h):这意味着在方法的整个主体(不包括前序处理和结尾处理)期间EBP相对变量包含gc引用。这意味着该变量的活性不被MDIL编译器跟踪。之后如同REF_BIRTH_EBP_V一样是MDIL即值。
·REF_UNTR_ESP_V(操作码88h):相似的概念...
·REF_UNTR_EBP_VS(操作码89h):多个未跟踪的EBP变量。之后是包含起始偏移量和标志的MDIL即值,后面是包含变量号的另一MDIL即值。
·REF_UNTR_ESP_VS(操作码8Ah):相似的概念...
·REF_UNTR_LOCAL(操作码8Bh):通过符号寻址的局部变量未被跟踪。
·REF_UNTR_LCLFLD(操作码8Ch):通过符号寻址的局部字段未被跟踪。
涉及完全可中断性和gc探测的一些MDIL指令:
·START_FULLY_INTERRUPTIBLE(操作码8Dh):该代码从当前偏移量开始完全可中断。
·END_FULLY_INTERRUPTIBLE(操作码8Eh):该代码从当前偏移量开始不再完全可中断。
·GC_PROBE(操作码8Fh):插入对待决无用信息收集的检查。
当在x86代码中一些参数被压到栈上时,在该架构上有附加指令来允许跟踪当前ESP级别和压入了什么:
·NONREF_PUSH(操作码92h):不包含gc指针的双字被压到栈上。
·GCREF_PUSH(操作码93h):包含普通gc指针的双字被压到栈上。
·BYREF_PUSH(操作码94h):包含内部gc指针的双字被压到栈上。
·REF_PUSH(操作码95h):包含gc指针的双字被压到栈上。之后是包含通常的标志的字节。
·REF_POP_1(操作码96h):一个双字从栈弹出,例如通过所调用的方法中的参数清理。
·REF_POP_N(操作码97h):之后是指示从栈中弹出了多少双字(包含gc指针和非gc指针)的MDIL即值。
·REF_INV_N(操作码98h):之后是指示先前被压到栈上的多少双字不再有效的MDIL即值。这通常在对使用cdecl或varargs调用约定的方法的调用之后发生。
·REF_DEATH_REGS_POP_N(操作码99h):通常在完全可中断代码中的调用之后使用的节省空间的变体。较低的6位是指示哪些寄存器不再包含gc引用的寄存器掩码:
ο如果EAX不再包含gc引用则位0为1,否则为0(即,没有改变)
ο如果EDX不再包含gc引用则位1为1,否则为0
ο如果ECX不再包含gc引用则位2为1,否则为0
ο如果EBX不再包含gc引用则位3为1,否则为0
ο如果ESI不再包含gc引用则位4为1,否则为0
ο如果EDI不再包含gc引用则位5为1,否则为0
较高的2位是从栈弹出的双字的数量(0..3)。如果弹出了更多双字,则这必须用附加的REF_POP_N指令来表达。
由于在部分可中断代码中,gc信息仅在调用位置被报告,并且压入的自变量通常是由所调用的方法来使用的,因此报告栈改变的伪指令在部分可中断代码中将是相对稀少的。有两种情况将需要这些伪指令:
·没有帧指针的方法。在这一情况下,对栈指针改变的跟踪是必要的,即使是在非调用位置。否则,CLR无法找到栈上的返回地址的位置,这是需要的,使得它能够在无用信息收集开始时停止线程。
·其中对外部调用的自变量在压入对内部调用的自变量之前被压到栈上的嵌套调用。在这一情况下,对外部调用的自变量需要被报告,因为它们尚未被外部调用消耗。
具有GC信息指令的MDIL代码示例。这里是从以下的C#代码示例编译的一个示例,该示例取串数组并使用“,”作为分隔符来串接它们。
用于该示例的MDIL代码看上去如下(MDIL分解器对伪指令使用大写,对本机指令使用小写):
注意以下几点:
·这是部分可中断代码,即,关于活跃gc指针的信息仅在调用位置有效(ALLOC_OBJECT和极少的其他伪指令视为调用位置)。
·原型编译器尽可能晚地发出寄存器的活性信息——实际上,它在其有效的调用位置之后才发出。返回地址也指向调用位置之后,并且gc信息指令不是“真实”指令,因此可以争辩这是可接受的。但是可能会有混淆,因此可能将这些指令改为移至调用位置之前的某一处。
·相反,栈局部变量的活性信息就在栈位置变得对栈局部变量的产生有效之后,或者在其被最后一次用于局部变量的死亡之后立即由原型编译器急切地发出。
·尽管这与gc信息无关,但是可以注意到,当具有数组长度的显式比较时,一个编译器未正确地跟踪数组类型——这就是为什么偏移量0053处的for循环最终变为“CMP_TOdword ptr[edi].ByteArrayLength,ebx”,即使是通过串数组来迭代的。但是这在CLR的相关版本的最终机器代码的正确性方面是可接受的。
当向该示例添加没有调用的紧凑循环时,获得不同的gc信息,因为现在要求的是完全可中断代码。以下示例被改为预先计算最终串的总长度,以避免StringBuffer必须增长:
所得的MDIL代码看上去如下,此处分成四个部分来更方便参考;这里是第一部分:
现在是第二部分:
这是第三部分:
最后,这里是第四部分:
在某些实现中,可以注意以下关于完全可中断gc信息的几点:
·编译器应在一旦在寄存器或局部变量中存储了gc指针就将其标记为包含这一值。换言之,关于在哪里发出产生没有任何容忍。
·相反,关于在哪里发出死亡有一些容忍。可以发出它们的最早场所是在最后一次使用变量之后(由各种活变量分析计算)可以发出它们的最后的场所是在用非gc指针(或者具有一组不同的标志的gc指针)盖写了gc指针的指令之后。
·如果一个寄存器或局部变量被复制到另一个,则两者都应被适当地标记为包含gc指针。当一个寄存器或局部变量足以保持堆对象被无用信息收集器作为无用信息来对待时,无用信息收集器应了解引用对象的所有位置,使得它能够在决定移动对象时适当地更新这些位置。
·对于寄存器和局部变量在整个前序处理和结尾处理期间被精确跟踪没有要求。然而,在对完全可中断代码的转换位置或完全可中断代码之外,该组gc根应是准确的。这是REF_BIRTH_EBX恰好在START_FULLY_INTERRUPTIBLE指令之前,且REF_BIRTH_EAX恰好在EPILOG_RET指令之前的原因。
前序处理和结尾处理。在方法的开头,编译器一般将生成前序处理序列,该前序处理序列可建立帧指针、保存某些寄存器、将参数值从其传入位置复制到其他位置、初始化局部变量、等等。相反,在方法的结束,一般存在结尾处理序列,该结尾处理序列概念上撤消前序处理所完成的大多数事情,因此还原调用者的上下文并准备好返回给它。
在像CLR这样的托管代码环境中,前序处理序列是高度结构化的,并且存在各种限制以确保执行引擎可以解开栈、找到寄存器值被保存在何处、有多少字节的参数被压到栈上、等等。在MDIL中,这被反映为典型的前序处理中的大多数指令是具有双重目的的伪指令:一方面,它们被转换成执行帧建立的实际机器指令,另一方面,它们产生向执行引擎告知帧是如何确切布局的副表。这也给予绑定器足够的信息,以使其能够在给定前序处理的情况下产生有效的结尾处理。为了节省MDIL代码空间,结尾处理因而在通常情况下由单个指令来表示。
与其中帧布局是完全在编译器的控制下的机器代码形成对比,在MDIL中,部分帧可在所有值类型的大小已知时在绑定时布局。换言之,帧的某些部分可以是其大小直到绑定时才已知的变量或参数。代替能够向它们指派固定偏移量并使用其偏移量来引用它们,编译器向它们指派变量号并使用这些号来引用它们。
前序处理示例。看一下一个简单的前序处理示例,并讨论其中出现的伪指令:
在该示例中:
·EBP_FRAME伪指令指示要使用ebp帧。它生成简单序列“push ebp;mov ebp,esp”,并且它还在副表中生成将该方法标记为具有ebp帧的信息。
·PUSH_REGS伪随机指令将被调用者被保存的寄存器压到栈上,并且它还在副表中生成反映哪些寄存器被保存在哪里的信息。
·两个LOCAL_BLOCK指令在栈帧中为局部变量保留空间。在该具体情况下,编译器可以自己计算偏移量,因为所有局部变量是固定的已知大小。
·FRAME_SIZE指令为编译器(而非绑定器)所分配的局部变量保留空间。存在所有LOCAL_BLOCK/LOCAL_STRUCT指令必须在FRAME_SIZE之前出现的限制,因此当遇到FRAME_SIZE时,绑定器知道它能够分配所有局部变量并计算最终栈帧大小。它用这一机会来将esp递减适当的量,并在副表中反映最终栈帧大小。
·mov ebx,edx指令简单地将传入复制到被调用者被保存的寄存器。
·REF_BIRTH_EBX指令将该寄存器标记为包含gc引用(参见关于gc信息的一节)。
·最后,START_FULLY_INTERRUPTIBLE指令用于标记前序处理的结束。还存在END_PROLOG指令以便在部分可中断代码中使用。
前序处理和结尾处理指令。关于符号栈布局:
·LOCAL_BLOCK(操作码0A7h)<MDIL即值>:这保留了局部存储块并向其指派变量号。即值仅仅是要保留的字节数。编译器一般不能假设连续的局部块是邻接的(即,没有居间的填充),或甚至必定是以所给出的次序来分配的。
·LOCAL_STRUCT(操作码0A8h)<MDIL类型令牌>:这为类型令牌给出的值类型保留局部存储块。绑定器将计算类型的大小并保留适当大小的局部块。再一次,指派变量号,通过变量号,编译器可引用该局部块。
·PARAM_BLOCK(操作码0AAh)<MDIL即值>:这保留参数空间的块。同样,指派了变量号。
·PARAM_STRUCT(操作码0ABh)<MDIL类型令牌>:这为类型令牌给出的类型的参数保留参数空间块。同样,指派了变量号。
·PRESERVE_REGISTER_ACROSS_PROLOG(操作码0D0h)<字节寄存器>分配局部变量并将指定寄存器的值存储到该局部变量中作为该函数的前序处理的一部分。
关于与帧有关的信息:
·ARG_COUNT(操作码0B1h)<MDIL即值>:这给出了编译器分配的(即,不是由PARAM_BLOCK或PARAM_STRUCT伪指令声明的)自变量双字(或在x64的情况下是四字)的数量。
·EBP_FRAME(操作码0B2h):建立ebp帧。
·DOUBLE_ALIGN_ESP(操作码0B3h):将esp向下对齐到8字节的倍数。这意味着局部变量将基于esp来寻址,而参数将经由ebp来寻址。这意味着EBP_FRAME必然在之前出现过。该指令对于具有类型为double的被大量使用的局部变量的浮点代码是有用的。
·PUSH_REGS(操作码0B4h)<字节寄存器掩码>:压入被调用者被保存的寄存器。字节掩码中的位如下分配:
οBit 0:如果该位为1则压入EBX/RBX
οBit 1:ESI/RSI
οBit 2:EDI/RDI
οBit 3:EBP/RBP
οBit 4:R12
οBit 5:R13
οBit 6:R14
οBit 7:R15
如果EBP_FRAME伪指令在以前出现过,则位3不应在寄存器掩码中置位。
·SAVE_REG(操作码0B5h)<字节寄存器><MDIL即值>:将被调用者被保存的寄存器保存到栈帧中的偏移量。这仅在x64代码中使用。
·SAVE_XMMREG(操作码0B6h)<字节寄存器><MDIL即值>:将被调用者被保存的xmm寄存器保存到栈帧中的偏移量。这仅在x64代码中使用。
·FRAME_PTR(操作码0B7h)<字节>:在x64代码中建立帧指针。之后是在低4位中对帧指针寄存器编码且在高4位中对从rsp的偏移量编码的字节(以16字节为单位)。
·FRAME_SIZE(操作码0B8h)<MDIL即值>:该即值的大小是编译器分配的局部帧的一部分的大小(以字节为单位),即,不包括任何经由LOCAL_BLOCK或LOCAL_STRUCT的保留。
·SECURITY_OBJECT(操作码0BDh)<MDIL即值>:帧包含指针大小的“安全对象”,且即值是其偏移量。可能的改变:偏移量实际上由CLR约束,且因此偏移量是冗余的且可被消除。
·GS_COOKIE_OFFSET(操作码0BEh)<MDIL即值>:帧包含“gs cookie”(针对栈盖写恶意利用的安全措施)。即值给出其偏移量。
·LOCALLOC_USED(操作码0BFh):该例程利用栈上的分配(localloc,或C用法中的_alloca)。
·VAR_ARGS(操作码0C0h):该例程具有可变自变量列表。
·PROFILER_CALLBACKS(操作码0C1h):该例程包含剖析器回调。
·EDIT_AND_CONTINUE(操作码0C2h):该例程是对编辑并继续(edit-and-continue)编译的。
·SYNC_START(操作码0C3h):对于同步的例程,是监视器在何处进入。
·SYNC_END(操作码0C4h):对于同步的例程,是监视器在何处退出。
·END_PROLOG(操作码0B9h):标记前序处理的结束。此时,栈必须可由执行引擎来走查。
·PINVOKE_RESERVE_FRAME(操作码0CCh)<函数中未使用的、保存在ebp帧中的寄存器的掩码>只能在加EBP帧的方法中使用。
·PINVOKE_RESERVE_FRAME_WITH_CURRENTMETHOD_DESCRIPTOR(操作码0CFh)<函数中未使用的、保存在ebp帧中的寄存器的掩码><包含方法描述符值的变量的双字局部变量索引>只能在加EBP帧的方法中使用。指定的局部变量必须在允许栈走查器执行之前被初始化。
关于结尾处理,存在生成结尾处理或传达关于结尾处理的信息的几个指令:
·EPILOG_RET(操作码0BBh):这是最常见形式的结尾处理。它生成解除分配栈帧、还原被调用者被保存的寄存器并返回到调用例程、弹出适当量的参数空间的指令。
·EPILOG(操作码0BAh):这是用于尾调用的一种形式的结尾处理。它仅仅解除分配栈帧,并还原被调用者被保存的寄存器,但是不返回。
·END_EPILOG(操作码0BCh):这对由EPILOG伪指令开始的结尾处理标记该结尾处理的结束。对于由EPILOG_RET指令生成的结尾处理,不需要或不允许该指令。
对静态字段的访问
在CLR中对静态字段的访问出于以下两个主要原因而是复杂的:类构造函数可能需要在访问被作出之前运行,并且对每一应用程序域存在静态字段的一个单独的副本。为了允许对静态访问的某种编译器优化,该访问被拆分成两部分。首先,存在返回对于类型令牌指定的类的静态量的基址的伪指令(取类型令牌)。其次,可以用将用于引用实例字段的普通寻址模式来引用个别的静态字段,但使用静态量的基址作为基址寄存器。
第一步对于一个类只需完成一次,即,它可被编译器提升。CLR通过将包含gc指针的静态字段与包含普通旧数据的字段分离来优化无用信息收集器性能。由此,对每一个类实际上有两个静态区域,且因此一种方法使用两个不同的伪指令来获得静态基址:
·GET_STATIC_BASE(操作码45h)<类型令牌>:该指令获得不包含gc指针的静态量(即,int、double、bool等)的基址。
·GET_STATIC_BASE_GC(操作码46h)<类型令牌>:这获得包含gc指针的静态量的基址。
这还包括用户定义的结构体类型。
两个伪指令都转换成助手调用,且因此必须被认为是将通常的调用者被保存的寄存器作废。出于gc报告的目的,它们必须被认为是返回eax中的内部指针。
静态字段访问示例。这里是初始化静态字段的某种简单的C#代码:
编译器产生的对应的MDIL代码可能看上去为:
注意可以如何将GET_STATIC_BASE伪指令的结果用于两个整型指派。
线程静态字段。CLR还实现线程静态字段。对它们的访问通过两个伪指令来支持:
·GET_THREADSTATIC_BASE(操作码0CCh)<类型令牌>:获得不包含关于<类型令牌>指定的类型的gc指针的线程静态字段的基址。
·GET_THREADSTATIC_BASE_GC(操作码0CDh)<类型令牌>:获得包含关于<类型令牌>指定的类型的gc指针的线程静态字段的基址。
这些伪指令在其功能和使用方面类似于GET_STATIC_BASE和GET_STATIC_BASIC_GC。
RVA静态字段。CLR支持映像内的绝对地址处的数据字段,称为RVA静态字段。对它们的访问通过两个MDIL伪指令来支持:
·LOAD_RVA_FIELD_ADDR(操作码0A3h)<编码寄存器的字节><字段令牌>:将RVA字段的地址加载到寄存器中。下一字节编码寄存器,后面跟字段令牌。
·LOAD_RVA_FIELD(操作码0A4h)<编码寄存器的字节><字段令牌>:将RVA字段加载到寄存器中。下一字节编码寄存器,后面跟字段令牌。
这些伪指令在其功能和使用方面类似于GET_STATIC_BASE和GET_STATIC_BASIC_GC。
调用。MDIL代码具有表达对其他方法或运行时系统的调用的能力。这里是支持调用的MDIL伪指令的列表:
·CALL(操作码47h)<方法令牌>:调用<方法令牌>指定的方法(这按照小尾序字节次序被表达为双字,并且可以是方法定义、成员定义或方法规范令牌)。参数被假定为要被加载到寄存器中或已经存储在栈上,如调用约定所要求的。
·CALL_VIRT(操作码48h)<方法令牌>:做出对<方法令牌>指定的方法的虚调用。寄存器eax/rax被假定为作为临时值可用——这是很好的,因为没有可用的调用约定使用eax/rax作为参数寄存器。注意,实际虚调用机制通过该伪指令来抽象,因此实现不限于使用虚表实现。
·CALL_INDIRECT(操作码49h)<地址模式>:经由地址模式指定的指针做出间接调用。该地址模式的寄存器字段用于标志:
οCIF_NORMAL(0h):普通操作
οCIF_METHOD_TOKEN(1h):后面跟着方法令牌。该变体用于共享的类属代码,其中方法令牌指示调用的预期目标。
·TAIL_CALL(操作码4Ah)<方法令牌>:跳转到<方法令牌>指定的方法。
·HELPER_CALL(操作码4Bh)<MDIL即值>:调用运行时助手。MDIL即值是助手号。要指定助手号列表。
·CONSTRAINT(操作码4Ch)<类型令牌>:当编译器能够得出this指针的类型的精确运行时类型时,这是对虚调用的前缀。该类型由类型令牌指定。在值类型的情况下,this指针引用该类型的未装箱表示,且因此绑定器需要生成对最终的目标方法的直接调用而非虚调用(没有间接穿过的虚表)。这大部分在类属代码中使用,因为在其他情况下,编译器通常能够直接表达对最终目标的调用。
·CALL_DEF(操作码4Dh)<方法rid字>:这是CALL的更紧凑编码。之后是对方法令牌的低16位编码的字(小尾序字节次序)。较高的16位被假定为0600h。因此,这是调用同一模块中的方法的紧凑方式,只要所调用的方法定义令牌在范围06000000h到0600FFFFh内。
·CALL_REF(操作码4Eh)<方法rid字>:这是CALL的更紧凑编码。之后是对方法令牌的低16位编码的字(小尾序字节次序)。较高的16位被假定为0A00h。这是调用其他模块中的方法或类属实例化中的方法的紧凑方式,只要所调用的成员引用令牌在范围0A000000h到0A00FFFFh内。
·CALL_VIRT_DEF(操作码4Fh)<方法rid字>:CALL_VIRT的更紧凑编码。参见对CALL_DEF的解释。
·CALL_VIRT_REF(操作码50h)<方法rid字>:CALL_VIRT的更紧凑编码。参见对CALL_REF的解释。
调用示例。这里是完成某些虚和非虚调用的小型C#示例:
这里是以上示例可被编译成的MDIL代码:
注意编译器如何能够使用偏移量0010处的非虚调用,但是不能在偏移量002d处这样做——较智能的编译器能够跟踪装箱的整数的类型,可能甚至消除装箱。
托管到本机调用。MDIL代码具有生成从托管代码到本机代码的调用的能力。这要求以特定顺序发出MDIL指令。
1.PINVOKE_LEAVE_RUNTIME指令。
2.调用指令,或REMOVEME_CALL_INDIRECT_STACK_ARGUMENT_SIZE指令后跟CALL_INDIRECT指令。
3.MDIL或不依赖于处于托管状态的文字机器指令。建议开发者将其自身限制为弹出指令。
4.PINVOKE_ENTER_RUNTIME指令。
这里是专用于托管到本机调用支持的伪指令的列表。这些函数可只在前序处理中包含PINVOKE_RESERVE_FRAME或PINVOKE_RESERVE_FRAME_WITH_CURRENTMETHOD_DESCRIPTOR指令的函数中使用。
·PINVOKE_LEAVE_RUNTIME(操作码0CDh)<寄存器的字节掩码>寄存器掩码是此时未被MDIL代码使用的寄存器的集合。该掩码可包含预留的和非预留的寄存器。
·PINVOKE_ENTIRE_RUNTIME(操作码0CEh)<寄存器的字节掩码>寄存器掩码是此时未被MDIL代码使用的非预留寄存器的集合。来自先前的PINVOKE_LEAVE_RUNTIME指令的预留的寄存器被假定为仍可供使用。
·REMOVEME_CALL_INDIRECT_STACK_ARGUMENT_SIZE(操作码0D2h)<双字栈自变量大小>这是在当前MDIL格式中用于描述由通过CALL_INDIRECT做出的本机调用逻辑上弹出的栈空间的量的指令。希望在MDIL规范被最终化之前通过去除对该指令的需求来去除该指令。
·CALL_PINVOKE(操作码0D1h)<字节寄存器>通过寄存器中找到的方法描述符来调用本机方法。
pinvoke机制修改的指令如下示出。
·CALL_DEF当用作pinvoke调用的一部分时,该调用对该函数所表示的本机函数而非托管函数做出。这只能用于DllImport方法。
·CALL_REF当用作pinvoke调用的一部分时,该调用对该函数所表示的本机函数而非托管函数做出。这只能用于DllImport方法。
·CALL当用作pinvoke调用的一部分时,该调用对该函数所表示的本机函数而非托管函数做出。这只能用于DllImport方法。
·CALL_INDIRECT当用作pinvoke调用的一部分时,该函数前面必须有REMOVEME_CALL_INDIRECT_STACK_ARGUMENT_SIZE指令。
这里是一个示例:
这里是另一个示例:
跳转
MDIL具有完整的一组条件和非条件跳转,就像本机机器代码一样。对此存在伪指令的原因是所得本机代码中的跳转距离一般不同于它们在MDIL代码中的距离。由此,跳转需要由绑定器来处理,并且其距离被调整而非逐字复制到本机代码输出。
另一方面,在MDIL代码中具有本机代码跳转是完全合法的,但是只有跳转距离保证不改变时才合法。如在本机机器代码中一样,跳转目标由从下一指令的第一个字节测得的有符号的跳转距离来指示。如有可能,该距离通过有符号字节来表达(例外是JUMP_LONG,其中始终使用双字)。否则,该字节为-1(或0FFh),以指示后面是双字距离(按照小尾序)。
MDIL跳转仅在方法内才被允许——对另一方法的跳转由TAIL_CALL伪指令(参见关于调用的一章)来表达。
这里是MDIL提供的跳转伪指令:
·JUMP(操作码051h):无条件跳转
·JUMP_LONG(操作码052h):具有32位距离的无条件跳转
·JUMP_O(操作码053h):溢出位被置位情况下的条件跳转
·JUMP_NO(操作码054h):溢出位未被置位情况下的条件跳转
·JUMP_ULT(操作码055h):无符号<时的条件跳转
·JUMP_UGE(操作码056h):无符号>=时的条件跳转
·JUMP_EQ(操作码057h):==时的条件跳转
·JUMP_NE(操作码058h):!=时的条件跳转
·JUMP_ULE(操作码059h):无符号<=时的条件跳转
·JUMP_UGT(操作码05Ah):无符号>=时的条件跳转
·JUMP_S(操作码05Bh):符号位被置位情况下的条件跳转
·JUMP_NS(操作码05Ch):符号位未被置位情况下的条件跳转
·JUMP_PE(操作码05Dh):奇偶性为偶时的条件跳转
·JUMP_PO(操作码05Eh):奇偶性为奇时的条件跳转
·JUMP_LT(操作码05Fh):有符号<时的条件跳转
·JUMP_GE(操作码060h):有符号>=时的条件跳转
·JUMP_LE(操作码061h):有符号<=时的条件跳转
·JUMP_GT(操作码062h):有符号>时的条件跳转
跳转示例。这里是某个简单的C#代码:
这里是利用了条件跳转的所得的MDIL代码:
注意对于偏移量0005h处的JUMP_NE指令如何将距离给出为04h字节,这被理解为从位于偏移量0007h处的下一指令的开始起的四字节。由此,该条件跳转的目标是04h+0007h,即000bh。这也是分解器示为跳转目标的内容。
加载令牌、串文字、函数指针、RVA字段等。
存在一组允许托管代码引用执行引擎所维护的数据结构的MDIL伪指令:
·LOAD_TOKEN(操作码9Ch)<目的地寄存器字节><双字令牌>:这将对表示令牌的运行时数据结构的句柄加载到寄存器中。
·PUSH_TOKEN(操作码9Dh)<双字令牌>:这将对表示令牌的运行时数据结构的句柄压到栈上(仅x86)。
·LOAD_STRING(操作码9Eh)<目的地寄存器字节><串令牌>:这将对串文字的引用加载到目的地寄存器中。可能的将来变化:使用<目的地寄存器字节>的较高位来表示标志,因此编译器能够指示代码路径是否可能被频繁地采取(在这一情况下对串文字的急切加载是有意义的),或者完全不可能被采取(例如,错误路径)。在后一情况下,希望延迟串文字的加载。
·PUSH_STRING(操作码9Fh)<临时寄存器字节><串令牌>:将对串文字串的引用压到栈上——用于自变量传递(仅x86)。在<临时寄存器字节>中必须指示可作废的临时寄存器。
·LOAD_FUNCTION(操作码0A0h)<标志和目的地寄存器字节><双字方法令牌>:将<方法令牌>指定的方法的代码的地址加载到目的地寄存器中。<标志和目的地寄存器字节>的较高半字节包含仅在某些共享类属代码情形中才相关的标志:
οLFF_NORMAL(0h):普通操作。在涉及共享类属代码的某些情况下,这将在前进到共享类属代码之前返回加载实例化参数的“实例化占位程序”的地址。
οLFF_SHARED_CODE(1h):这将提供代码的实际地址,这在某些共享类属情况下可需要附加的实例化参数来正确工作。
·LOAD_VIRT_FUNCTION(操作码0A1h)<目的地寄存器字节><双字方法令牌>:将虚或接口方法的地址加载到寄存器中。其虚方法被加载的对象实例应被传递到寄存器ecx中。当前,这始终经由助手调用来实现,且因此将目的地寄存器约束为eax,且进一步,edx和ecx寄存器(加上x64上的r8-r11)被作废。
·PUSH_FUNCTION(操作码0A2h)<双字方法令牌>:类似于LOAD_FUNCTION,除了方法地址被压到栈上且没有标志以外。
·LOAD_GS_COOKIE(操作码0A5h)<目的地寄存器字节>:将全局gs cookie加载到寄存器中。
·LOAD_STATIC_SYNC_OBJ(操作码0A6h)<目的地寄存器字节>:加载对用于同步的静态方法的监视对象的句柄
·LOAD_VARARGS_COOKIE(操作码0C9h)<目的地寄存器字节><双字成员引用令牌>:这加载描述对于像printf的函数的调用位置的实际参数的varargs cookie。
·PUSH_VARARGS_COOKIE(操作码0CAh)<双字成员引用令牌>:这将描述对于像printf的函数的调用位置的实际参数的varargs cookie压到栈上(仅x86)。
这些伪指令中的某一些接受令牌的较高位中的标志——参见类属的讨论来获得细节。
异常处理
异常处理在MDIL中通过在本质上类似于MSIL中使用的异常表的异常表来被支持。然而,编码是不同的——见下文。异常支持也由包含try子句的任何方法的栈帧中的指针大小的变量的局部数组来提供(对每一try子句嵌套级有一个元素)。此处列出了与异常相关的某些MDIL伪指令。
异常表的编码。异常子句的数量由MDIL方法头部给出。每一异常子句由6个压缩的双字构成:
·包含标志的双字(与MSIL异常子句中的相同的标志)
·包含try子句的MDIL偏移量的双字
·包含try子句的长度的双字
·包含句柄的MDIL偏移量的双字
·包含句柄的长度的双字
·包含类型令牌或过滤器偏移量的双字
每一双字在其被给出为按照大尾序次序的字节序列的意义上来压缩,每一字节给出了要被压缩的双字的7位,并且最高有效位用作双字仍继续的标志。由此:
·12h被编码为单个字节12h
·123h被编码为两个字节82h 23h
支持异常处理的MDIL伪指令:
·THROW(操作码63h):抛出传入寄存器ecx/rcx中的异常对象。
·RETHROW(操作码64h):重新抛出当前异常(必须出现在catch子句中)。
·BEGIN_FINALLY(操作码65h)<距离双字>:将标签的绝对地址压到栈上(仅x86)
·END_FINALLY(操作码66h):从栈弹出地址并跳转到那里(使ezx作废)(仅x86)。
对象分配。MDIL具有用于对象分配的伪指令,该伪指令被转换成对适当的助手的调用:
·ALLOC_OBJECT(操作码6Dh)<类型令牌>:分配具有<类型令牌>指定的类型的对象。构造函数经由编码器生成的对其的显式调用来运行。
·ALLOC_ARRAY(操作码6Eh)<类型令牌>:分配<类型令牌>指定的类型的数组。元素数量传入edx/rdx中。
这些指令使通常的调用者被保存的(caller-saved)寄存器作废。
类型强制转换。MDIL具有用于转换成对适当的助手的调用或转换成内联代码或转换成两者的混合的类型强制转换的伪指令。这些指令都使通常的调用者保存的寄存器作废。
·ISINST(操作码69h)<类型令牌>:寄存器edx/rdx中的对象能被强制转换成<类型令牌 >指定的类型吗?如果是,则将eax/rax设置为输入参数edx/rdx,如果否则设置为空。
·CASTCLASS(操作码6Ah)<类型令牌>:检查寄存器edx/rdx中的对象能否被强制转换成<类型令牌>指定的类型。如果否则抛出异常。
·BOX(操作码6Bh)<类型令牌>:为<类型令牌>指定的类型分配装箱的表示。将寄存器edx/rdx引用的值复制到它。返回eax/rax中的结果。
·UNBOX(操作码6Ch)<类型令牌>:检查edx/rdx中的对象引用是否实际上是<类型令牌>指定的值类型的装箱表示。如果是,则返回对eax/rax中的内容的引用,否则抛出异常。
开关。本机代码编译器可向开关语句应用充分的优化——要使用的最佳代码序列主要取决于情况(case)数量以及情况标签值有多密集地被聚类。如果它们是相当稀疏的,或者只有非常少,则通常最佳地经由比较和跳转(可能与几个加或减组合)的序列来实现开关。如果是密集的,并且有许多,则通常最佳地经由跳转表来使用间接跳转。
MDIL具有对表跳转的专门支持:
·SWITCH(操作码09Ah)伪指令经由跳转表来实现间接跳转。它后面跟着在低4位中指示寄存器要被用作索引的字节。在x64上,要被用作临时值的另一寄存器在高4位中编码。在此之后跟着一个双字(小尾序字节次序),其是该表所在的常量池(参见下文的常量池一章)中的偏移量。对照表的边界来检查索引寄存器是编译器的责任。
·SWITCH_TABLE(操作码09Bh)伪指令自己实现跳转表。它后面跟着一个双字(小尾序字节次序),其是跳转表假定要具有的条目的数量。在此之后跟着跳转表本身的内容——每一条目是其中该条目假设要跳转到的当前方法内的MDIL偏移量。
绑定器将小心地生成用于SWITCH的适当的机器代码,以及将SWITCH_TABLE中的条目转换成适当的绝对地址或偏移量。在x64上,MDIL代码将给出SWTCH_TABLE中的条目仍为表示MDIL偏移量的双字,但是它们将被转换成表示绝对地址的四字。编译器在计算常量池偏移量时应将其考虑在内。
开关示例。这里是使用C#中的开关语句的方法的示例,以及所得的MDIL:
从原型编译器输出的MDIL代码看上去为:
注意,该编译器采用了混合策略——它经由跳转表实现情况1到4,但使用显式比较来检查情况8和16。开关表因此具有4个条目。在通过跳转表跳转(使用eax作为索引寄存器)之前,编译器使用对照4的无符号比较来确保索引在范围0..3内。
开关表中的条目0是针对情况i==1的。它包含值020h,这是要跳转到的MDIL偏移量。该偏移量处的列表的确包含将eax置零然后跳转到偏移量049h(这是结尾处理序列)的代码。
开关表中的条目1是针对情况i==2的。它包含值025h。该偏移量的确包含用值1来加载eax然后跳转到结尾处理序列的代码。
开关表中的条目2是针对情况i==3的。它包含值045h,其是该开关的“默认”情况的偏移量。因此,此处的代码将值-1加载到eax中(使用节省代码空间的指令),然后直接落到结尾处理序列。
开关表中的条目3是针对情况i==4的。由此,其跳转到偏移量2dh,其将值2加载到eax中然后跳转到结尾处理序列。
由于该方法是非常简单的,因此开关表在该方法的常量池的很开头处,由此开关表的偏移量碰巧为0。
常量池。MDIL具有在方法中声明只读数据的区域的规定。这不仅用于存储浮点文字,而且还用于存储在开关语句中使用的跳转表(参见关于开关的一章)。它也可用于存储附加的常量查找表,例如,用于实现可被表示为表查找的开关语句。
对于对常量数据的MDIL支持存在两个分量:
·存在CONST_DATA(操作码0C8h)伪指令来引入常量数据。它之后跟着一个双字(小尾序字节次序),其是后面跟着的实际数据字节的计数。
·存在特殊地址模式修饰符(AM_CONSTPOOL),该修饰符允许MDIL引用常量数据。这引用常量池的开头,因此一般而言,将需要附加的AM_SMALLOFFSET或AM_OFFSET修饰符来引用常量池内的适当偏移量。
编译器对常量池进行布局,并且使用其常量池相对偏移量来引用常量数据项。由于开关表也进入常量池,因此其大小对于该布局也被考虑在内。
常量池示例。这里是进行浮点算术的某一简单C#代码:
并且,这里是编译器可能将以上代码转变成的MDIL代码:
在该示例中,双精度常量1.0/120.0=0.008333作为第一项被放在常量池中(以偏移量0来寻址),且双精度常量1.0/6.0=0.16666作为第二项来放置(以偏移量8来寻址)。编译器足够智能来认识到常量1.0可由本机机器指令(fldl指令)来加载,且因此不需要被放入常量池中。
写屏障。具有世代无用信息收集器的系统一般使用某种风格的写屏障来跟踪世代间的指针。MDIL具有伪指令STORE_REF(操作码15h)来表达写屏障的概念。它取源寄存器和目的地地址模式,就像常规的STORE伪指令一样。然而,由于该伪指令在后台被转变成助手调用,因此寄存器约定是不寻常的:
·在x86上,STORE_REF使edx作废。还存在所存储的源寄存器不可以是edx的限制。
·在x64上,STORE_REF使RAX、RCX、RDX、R8作废。
另外,不需要写屏障来存储空指针。换言之,如果所存储的值为空,则可使用常规的STORE或STORE_IMM伪指令。并且,如果存储的目的地可被编译器证明为在无用信息收集堆外部(即,如果目的地在栈上或在非托管存储器中),则不需要写屏障。在写时,实验编译器尚不生成STORE_REF——取而代之的是,它直接生成STORE_REF将转变成的助手调用。
可版本化结构体
MDIL代码可能必须要处理其大小和布局在编译器生成了使用它们的代码之后改变的值类型。换言之,生成MDIL代码的编译器可不作出关于结构体的大小和布局的假设。严格而言,这仅对于其他模块中定义的结构体是真实的——对于当前模块中的结构体,编译器可以计算并使用大小和布局,只要它通过作出结构体显式布局并将字段偏移量和结构体大小持久保存在CTL中来传递该布局决策。
在一般的情况下,编译器应当生成针对结构体类型的大小和布局的改变为稳健的MDIL代码。这还完全支持用于类属的代码生成,其中类属参数的大小和布局对于编译器是未知的。
某些特征支持这一点:
·MDIL即值具有表达绑定时常量的概念,绑定时常量在编译时不是常量,例如结构体的大小。这主要对于进行地址算术是有用的,例如,用于在调用之后或在使用内部指针走查结构体数组之后清理栈。
·前序处理具有向栈帧添加结构体类型的变量的能力,从而在某种程度上将栈帧布局的负担加在绑定器上。
·地址模式具有按照变量号而非显式偏移量来对变量寻址的能力。
·GC信息具有使得“整个变量”活跃,即完全定义且有效的能力,绑定器得出哪些嵌入的字段包含需要被跟踪的gc指针。
在MDIL中还存在被显式提供来支持可版本化结构的某些伪指令:
·COPY_STRUCT(操作码03Fh)<目的地地址模式><源地址模式>:这是复制(结构体)类型的指令。它将某一结构体值从源地址模式给出的位置复制到目的地地址模式给出的位置。目的地地址模式的寄存器字段包含某些标志:
ο如果位0是1,则这意味着用于结构体复制的绑定器生成的代码可使ecx/rcx寄存器作废。换言之,这向绑定器表示ecx/rcx作为临时寄存器可用。如果位0是0,则这意味着ecx/rcx不可以被作废。
ο如果位1是1,则这意味着对于嵌入的gc指针不需要写屏障。这在例如目的地可被编译器证明为不驻留在无用信息收集堆上(例如,如果目的地已知在栈上)的情况下是真实的。
寄存器eax/rax和edx/rax可总是由绑定器生成的结构体复制代码来作废。
PUSH_STRUCT(操作码040h)<地址模式>:将地址模式给出的位置处的结构体压到栈上。这也将使得绑定器生成适当的gc信息来跟踪esp的变化并压入嵌入的gc指针。可能的将来变化:在地址模式的寄存器字段中添加传递可被使用的临时寄存器的标志。该指令仅在x86代码中有效。
·INIT_VAR(操作码0AFh)<MDIL即值变量号>:这初始化由变量号给出的局部变量。可能的将来变化:添加传递哪些寄存器可被作废、eax(或某一其他寄存器)是否已经包含0、多个变量是否需要被初始化等等的标志字节。
·INIT_STRUCT(操作码0B0h)<地址模式>:初始化地址模式给出的位置处的结构体。可能的将来变化:在地址模式的寄存器字段中添加向绑定器传递哪些临时寄存器可被使用的标志。
可版本化结构体也可被如下看待。首先,可版本化结构体是其布局可以在版本化时改变的值类型,例如,其大小可以改变,字段可以移位(并且GC引用的位置一起移位),可添加新字段,并且可移除非公有字段。通常,可版本化结构体来自其他模块(或其他版本化泡),但是来自同一模块的结构体也可变为可版本化,例如,因为它嵌入了来自其他模块的可版本化结构体。在决定哪些结构体将要对特定实施例可版本化时可以有某种灵活性。例如,另一模块中的结构体可由程序员标记为不可版本化,或者该实现可出于某种原因决定即使在同一模块中也保持结构体可版本化。
它们出现在哪里以及需要支持什么操作。可版本化结构体可出现为:
·传入参数
·局部变量
·传出参数
·返回值
·数组元素
·结构体或类中的实例字段
·静态字段
·可具有引用它们的byref
·可具有指向它们的指针
可能需要或甚至必需的能力可包括:
·访问可版本化结构体中的字段,不论其存储在何处
·访问整个结构体——复制它,将其作为参数来传递
·对结构体装箱和出箱
·报告GC信息
·在缺少结构体的大小的编译器知识的情况下进行指针算术
·进行可版本化结构体的栈打包,即,编译器应该能够向绑定器指示哪些结构体能够占据栈帧布局中的相同字节。
·适当地初始化局部可版本化结构体。
传入参数。传入参数可以是可版本化结构体。使用通常的分配方案,仅对于第一个这样的参数的偏移量可由编译器来确定,后续参数可以移位。出于这一原因,当可版本化结构体的大小已知时,参数和局部变量的栈布局是在绑定阶段完成的。注意,对于参数的这一推理仅真正适用于其中结构体值参数被压到栈上的调用约定(例如,用于x86的寻常约定),对于其中结构体值参数通过引用来传递的调用约定,没有对参数的这一难题。
如果调用约定传递寄存器中的小结构体,则会遇到问题。如果在版本化时这一结构体增长得更大,它可能不再在寄存器中传递,因此这会影响传入自变量的寄存器号。看上去在版本化时允许这种改变是复杂的,因此可以或者禁止寄存器大小的结构体增长,或者改变调用约定个因此可版本化结构体从不在寄存器中传递。
局部变量。类似的考虑应用于可版本化结构体类型的局部变量。由于其大小可以改变,因此后续的局部变量将在存储器中移位。解决这一问题的两种方式是或者经由间接来对这些变量进行寻址,并且在代码密度和速度方面付出代价,或者将物理栈布局推迟到绑定阶段,并且使编译器发出符号栈布局而非物理栈布局。
传出参数。这主要对于类似x86的调用约定是有问题的,其中值类型物理地被压到栈上。由于参数的大小可以变化,因此参数传递代码必须被虚拟化,即,实际机器代码必须由绑定器生成。不仅如此,而且与参数传递相关联的gc信息(栈深度,gc引用在哪里等)也必须被虚拟化。另一方面,当值类型通过引用来传递时,编译器需要能将结构体复制到临时值(见下文),但是没有关于gc信息的附加需求。
返回值。假定可版本化结构体类型的返回值始终作为“大值类型”来对待,其中调用者传递指向返回值的指针,则这里没有另外的问题。
数组元素。可版本化结构体的数组是有问题的,因为结构体的大小可能改变,并且因此用于索引到数组中的最优代码序列也可能改变。优化编译器也可能希望在走查这些数组时使用强度减小。这意味着它们必须能够发出按照在绑定时已知但在编译时未知的即值来递增或递减指针的指令。
结构体或类中的实例字段。给定在MDIL中表达包括多个字段令牌的完整访问路径的能力,这些不表示另外的问题。
静态字段。由于编译器不能知道可版本化结构体在将来可如何改变,因此如果所生成的用于访问可版本化结构体类型的静态字段的MDIL代码依赖于结构体的布局,则是有问题的。例如,如果仅仅包装了整型的结构体以一种方式来被访问,但是包含两个整型的结构体以另一方式来被访问,则具有版本化漏洞。
引用可版本化结构体的byref或指针。这里没有其他问题——仅仅是能够告知是正在引用装箱的结构体(在这一情况下需要将v表大小添加到字段偏移量),还是在引用未装箱结构体。
传入参数和局部变量。一种方法以符号方式描述了栈布局。另一目标是例如从符号中确定物理布局应仅需要简单的、线性时间算法,而非图着色。
描述参数和局部变量的伪指令。以下伪指令用于描述参数和局部变量:
PARAM_STRUCT<类型令牌>描述了传入参数。<类型令牌>可以是typedef、typeref或typespec令牌。它能够(但不一定)引用可版本化结构体。
·PARAM_BLOCK<以字节计的大小>描述了不包含任何gc引用的栈参数块。
·LOCAL_STRUCT<类型令牌>描述了局部变量。对<类型令牌>的使用与PARAM_STRUCT相同。
·LOCAL_BLOCK<以字节计的大小>描述了不包含任何gc引用的局部存储块。
这些伪指令中的每一个分配一局部存储块。参数按照距离当前栈指针的递增距离来指定,即,最后被调用者压入的参数必须被首先指定。向存储块指派变量号来允许后续的MDIL代码引用它们——第一个块被给予变量号0,第二个块被给予变量号1,以此类推。或者,可以在自变量出现时对它们进行编号,并以绑定器复杂度的略微增加来对局部变量使用不同的编号空间。为了允许绑定器在一遍(one pass)中完成代码扩张,所有局部变量或参数存储块必须在引用它们中的任一个之前被指定。可以考虑为EBP帧放松这一规则。
并且,跟踪因除了PUSH_STRUCT(见下文)之外的任何压入而导致的ESP改变以及添加因对ESP相对局部变量访问的压入而导致的附加偏移量是编译器的责任。另外,局部空间可以使用FRAME_SIZE来分配——该空间将始终最接近EBP或ESP,因此其偏移量对编译器是已知的。这允许编译器将已知大小/布局的局部变量一起分配,因此可生成用于初始化这些局部变量等的高效代码。作出其他规定来描述栈打包——见下文。
对参数和局部变量的引用。可以添加地址模式元素来允许指定局部变量号。绑定器将查找它指派给该局部变量的偏移量,并将其添加到任何附加的字段偏移量或指令中指定的显式偏移量。
示例MDIL代码:
用于地址模式的编码方案可表达任意数量的字段令牌,例如访问封装结构体内部的结构体类型的字段中嵌套的字段。
指定参数和局部变量的活性。一些伪指令跟踪可版本化参数和局部变量的活性:
·REF_BIRTH_LOCAL<变量号>规定参数或局部变量在MDIL代码流的这一指令边界变为活跃。绑定器将查看参数或局部变量的内部布局,并且确定哪些gc引用现在在栈帧中是活跃的(如果有的话)。
·REF_DEATH_LOCAL<变量号>规定参数或局部变量变为死亡。
·REF_UNTR_LOCAL<变量号>规定参数或局部变量在方法的整个持续时间是活跃的。
在这些活性指令、完全可中断代码、以及COPY_STRUCT伪指令一般扩展到多个机器指令的事实之间存在相互作用。如果在以局部变量为目标的COPY_STRUCT之前该局部变量死亡,并且之后活跃,则本机gc信息必须在gc引用分量出现时反映它们的指派——否则如果在副本的中间中断将会有gc漏洞。仅有的另一方式是在指派结构体之前对其进行零初始化,这是浪费的。还存在相反的情况,其中局部变量是COPY_STRUCT的源,局部变量在复制之前是活跃的,在复制之后死亡。在这一情况下,正确性不是问题,但是仍期望只要gc引用被复制就将其标记为死亡。还存在其中可能需要用于gc活性跟踪的更细粒度的情形——参见下文的“初始化局部结构体”。
复制可版本化结构体。将存在其中结构体必须被复制的情况。由于编译器不知道大小或布局,因此它利用一个新的伪指令:
·COPY_STRUCT<目的地地址模式>,<源地址模式>
所复制的类型由目的地或源地址模式来暗示(如果两者都指定了类型,则它们必须一致)。绑定器将COPY_STRUCT扩展成机器指令序列,在适当时插入写屏障和对gc信息的更新。存在由该伪指令来作废的一组架构相关的临时寄存器。
传递可版本化结构体作为(值)参数。采用寻常的x86调用约定,这涉及将可版本化结构体压到栈上。同样,存在关注细节的新的伪指令:
·PUSH_STRUCT<地址模式>
这不仅将发出完成压入的必要的机器代码,而且还将发出跟踪栈深度的gc信息,并且可能发出跟踪源的分量的活性的gc信息。
还可表达调用消耗了多少栈空间——为此,扩展REF_POP_N和REF_INV_N伪指令来取MDIL即值,该即值也可包含像类型的大小这样的绑定时常量——参见以下的指针算术来获得细节。
可版本化结构体作为数组元素。此处的一个难点是元素的大小对编译器是未知的,因此不能发出缩放索引的本机代码。另一方面,可能期望使用本机地址模式的缩放能力,因此可避免不必要的乘法。例如,如果数组元素的大小是12,则索引应按照3来缩放,并且数组访问应使用附加的缩放因子4。
为了支持这一缩放,支持以下伪指令:
·STRUCT_ELEM_SCALE<寄存器>,<地址模式>,<类型令牌>被用作索引中的初始步骤。由<类型令牌>指定的类型的以字节计的大小被拆分成两个因子,其中第一个因子是1、2、4或8。STRUCT_ELEM_SCALE将<地址模式>的内容乘以第二个因子并将结果放在<寄存器>中。
·索引的地址模式支持指定元素类型令牌,并且然后在适当时按照1、2、4或8来缩放访问,假定索引已使用STRUCT_ELEM_SCALE来缩放。
示例:
·假定元素的大小在绑定时变为4。因此,STRUCT_ELEM_SCALE进行简单的移动到目的地寄存器(实际上乘以1),并且使用该类型的索引的地址模式将索引按照4来缩放。
·假定元素的大小为10。STRUCT_ELEM_SCALE乘以5(使用IMUL、LEA或shift/add),并且索引的地址模式将索引按照2来缩放。
由于STRUCT_ELEM_SCALE取决于可版本化结构体的实际大小来生成不同指令,因此在此指令之后的机器标志的状态未被指定。实际上,STRUCT_ELEM_SCALE可能完全无法生成任何指令。然而,编译器可以假定具有相同索引值、相同类型令牌和相同源值的STRUCT_ELEM_SCALE指令的多个副本返回相同值,即,STRUCT_ELEM_SCALE指令可进行CSE、移出循环、等等。
指针算术。某些实现支持“绑定时即值”,其可以是<即值常量0+<sizeof(类型1)>*<即值常量1>+<sizeof(类型2)>*<即值常量2>...这些可特别地在ADD和SUB伪指令中使用。为了支持加到寄存器,还可支持寄存器作为伪指令中的地址模式。因此,例如,可以为以下循环生成高效的代码:
可以生成该伪指令来将在数组中前进指针:
ADD reg,ElementSizeOf(MyStruct)*2
即值可以由绑定器来计算。注意,使用“ElementSizeof(MyStruct)”而非“Sizeof(MyStruct)”来使正在关于两个相邻数组元素之间的距离所讨论的变得清楚——MyStruct的大小在作为字段嵌入在类或结构体中时可以更小。当调用向其传递了可版本化结构体作为参数的varargs方法时,有指针算术的细致情况-在该情况中,栈需要按不能被编译器确定的量被清除。。然而,该量还可被表达为可以在绑定时计算的MDIL即值。然而,对齐规则略微不同——数组元素的大小必须被上舍入到其对齐的倍数,而栈自变量的大小被上舍入到4字节或其对齐的倍数,取其中较大者。由此,由1、2或3字节构成的结构体被上舍入到栈上的4字节,但是在它们是数组元素时则不这样做。意图是将指针算术限于数组走查、栈指针调整和在调用之后更新栈gc信息(使用上述的REF_POP_N和REF_INV_N)。使用指针算术来从结构体或类中的一个字段到下一字段并不是有效的。这是因为(如上文已经提到的)相同结构体在其是类中的字段时要比其是数组的元素时更紧密地打包。
可版本化结构体的指针或Byref不需要任何附加的内容来引用来自结构体的字段——字段令牌如常被指定。为了复制或压入整个结构体,需要一种方式来指定正在引用什么结构体类型。一种方式是添加取类型令牌的大小覆盖地址模式分量。
栈打包。这可以与在C/C++中使用struct和union关键字来指定结构体布局相似地完成。为了指定局部变量#1和#2要一起活跃,但是#3可以重复使用相同的存储,基本上要写成:
可能的伪指令包括:
·struct{ (″SEQUENTIAL_GROUP″)
·union{ (″PARALLEL_GROUP″)
·} (″END_GROUP″)
绑定器将跟踪顺序组内的嵌套和当前偏移量,或者并行组的最大偏移量。仍将预先指定所有这些区域——栈打包与gc活性完全无关,这与所指派的栈偏移量不同。
初始化局部结构体。编译器知道局部结构体何时变活跃,因此它能够发出初始化结构体的伪指令——比如INIT_STRUCT<变量号>。它还可知道某些字段被初始化,因此此处有优化的可能。然而,可能有新的字段被添加,因此绑定器将负责这些。并且,对于完全可中断代码,编译器将指定具体字段何时变活跃,因此在这一情况下,在字段粒度级上跟踪gc活性可是合乎需要的。
示例。为了示出伪指令可以如何用于可版本化结构体,这里是另一个示例。假定以下声明在某一其他模块中:
还假定使用该声明的以下代码:
编译器将为SetPixel生成以下MDIL代码:
这将由绑定器变换为:
以下MDIL代码将为TestSetPixel生成:
这将由绑定器变换为:
类属
以上在别处关于可版本化结构体讨论的特征也有助于生成类属MDIL代码。并非为每一实例化生成专门的MDIL代码,编译器将对每一类型自变量生成几个不同风格的代码——比如,对于类型自变量为某种风格的整型类型生成一种风格的代码,对类型自变量为引用类型生成另一种风格的代码,对类型自变量为结构体类型生成再一种风格的代码,等等。对于一具体实例化,绑定器然后选取正确风格的MDIL代码(使用类属实例节中的信息),插入类型自变量,并生成最终机器代码。
这里是类属C#代码的一个短示例:
这里是从中为适合整数寄存器的整型类型生成的MDIL代码:
这里是该代码的“类属”特征:
·ELEM_SCALE伪指令对索引进行适当的预缩放。在这一情况下向其提供typespec令牌1b000001,该令牌仅仅代表形式(formal)自变量类型T。插入T并确定需要什么缩放是绑定器的事情。严格而言,ELEM_SCALE在这里并不是必需的,因为可用整型类型的大小为1、2、4或8字节,且因此CPU缩放能力始终是足够的。
·LOAD_X伪指令被定义为完成对该元素类型适当的“自然”扩展。数组的元素类型被给出为1B000001,其仅仅意味着“T”。因此,如果自变量类型是例如“short”,则LOAD_X伪指令将被转换成movsx机器指令。如果自变量类型改为是“byte”,则获得movzx指令,且如果是“int”,则获得mov指令。
因此,一般而言,对于类属代码,MDIL代码对于一组自变量类型是相同的,但是所生成的本机代码是不同的。
共享类属代码
对于引用类型,所生成的本机代码将非常相似,以致于甚至共享本机代码是有意义的。因此,并非对比如“对象”、“串”以及自己的引用类型“FooBar”具有本机代码的三个单独的副本,将只有该本机代码的一个副本,该副本对于所有引用类型都起作用。
例如,这里是用于以上类属C#示例的引用类型的MDIL代码:
这实际上几乎与早先用于整型类型的代码完全相同。唯一区别是附加(但未使用的)参数——注释将其写作“inst.parameter”。其原因是存在其中本机代码需要知道能够做正确事情的确切的实例化的情形。例如,如果代码希望分配T或T[]等,或者强制转换到这一类型(其中T是形式自变量类型),这将是本机代码需要找出T的精确类型的情形。为了允许它这样做,存在附加到this指针的(在类属引用类型上的非类属实例方法的情况下)或显式传递的(在结构体类型上的方法、类属方法(即,其中方法本身具有类型自变量,而不仅仅是封装类型)、或静态方法的情况下)所谓的字典。这是实例化参数的一个示例。
为了查找具体类型、字段或方法,本机代码执行字典查找。这由MDIL伪指令GENERIC_LOOKUP(操作码0C5h)来抽象,后面跟着令牌。作为一个附加自变量,传递实例化参数,或者在类上的非类属实例方法的情况下,传递this指针所包含的方法表。GENERIC_LOOKUP与普通的助手调用一样运作,这表现在它使寻常的调用者被保存的寄存器作废并在eax/rax中传递其结果。但是它是被优化的,使得对同一令牌的重复查找通常将不会执行调用,而是仅执行几个间接。
这里是一个简单的C#示例:
这转换成以下MDIL代码(用于共享引用类型):
以下观察可能是有帮助的:
·实例化参数在GENERIC_LOOKUP之前被加载到ecx中。
·GENERIC_LOOKUP指令取令牌5b000001。这仅仅是代表形式自变量类型T的1b000001加上在修改来改为意味着T[]的高位中设置的标志。
·导致ALLOC_ARRAY伪指令的代码将GENERIC_LOOKUP的结果加载到ecx中。ALLOC_ARRAY将仅仅被转换成对绑定器的助手调用,且GENERIC_LOOKUP的结果是对助手调用的参数之一。
·ALLOC_ARRAY伪指令将相同的令牌传递给它。然而,这里,附加的标志意味着“类型已经从类属查找中获得——不要试图加载它”。
存在可与GENERIC_LOOKUP一起使用的其他MDIL伪指令——这里是一个列表:
·ALLOC_OBJECT
·ALLOC_ARRAY
·ISINST
·CASTCLASS
·BOX
·UNBOX
·GET_STATIC_BASE
·GET_STATIC_BASE_GC
·GET_THREADSTATIC_BASE
·GET_THREADSTATIC_BASE_GC
·CALL_VIRT(用于对接口方法的调用)
·LOAD_TOKEN
·PUSH_TOKEN
·LOAD_FUNCTION
·PUSH_FUNCTION
如上可以看见的,在GENERIC_LOOKUP之后的令牌有时候在该令牌的较高位中具有附加标志。这里是一个列表:
·类型令牌的较高位中的40000000h意味着(如上看见的):给我该类型的数组,不是该类型本身。
·方法令牌的较高位中的0C000000h意味着:给我我能够通过其来调用的间接单元(用于调用类属接口中的方法),而非到表示该方法的数据结构的句柄。
在使用GENERIC_LOOKUP的结果的指令的令牌上还可能有更多标志:
·较高位中的40000000h指示绑定器通过字典查找(如上见到的)已经加载了该令牌所代表的任何内容。
·较高位中的80000000h具有相反的含义——即使这是涉及通常在运行时需要字典查找的类型参数的令牌,也使用静态查找。这主要用于获得表示共享类属代码中的当前方法的句柄。
类属支持也可被如下看待。如同在IL中一样,类属类型和方法,以及相关联的方法主体被保持在定义模块中,并且使用模块仅仅引用它们。对于放置本机代码和支持用于绑定器所生成的类属实例化的数据结构,一种方法遵循与当今ngen所遵循的相似的策略。
然后,某些东西对于MDIL是不同的——由于编译器处理调用约定和寄存器分配,实际的类属类型自变量是像int或float这样的原语类型还是引用类型还是结构体对于所生成的代码是有区别的。因此,一般而言,编译器将为MDIL中的类属方法生成多个方法主体。以下的讨论描述了这多个方法主体是如何表示的,以及绑定器如何找到适当的方法主体来为给定的一组类属类型自变量扩展。
由于MDIL可被表征为在比本机代码略高的级别,因此在MDIL中可能隐藏不同实例化之间的某些差别。换言之,在实例化之间共享比本机代码更多的MDIL代码。另一方面,
紧凑类型布局(CTL)中的类属类型和方法的表示与其在元数据中的对应物非常相似,某些细节在CTL中略微改变。CTL是描述类型的一种方式——它列出了字段和方法、基类型是什么、什么虚方法覆盖基类型中的方法、等等。
CTL附加和改变。需要对CTL的几个改变和附加来表示类属类型和方法,及其实例化。可以一般遵循类属IL引用类属类型和方法的方式。因此可以使用:
1.类属类型实例化的表示。在IL元数据中,存在引用表示类属实例化的IL签名的TypeSpec令牌。CTL已经具有了表示数组类型的TypeSpec。可以使合理紧凑的表示类属类型实例化的IL方式继续下去。改变对CTL表示的类型引用的表示,但是仅改变很少。
示例:一个程序可能使用类型规范令牌1b000002来引用List<int>。CTL类型规范表中在索引2处的条目将引用像以下字节序列的内容:
15 //ELEMENT_TYPE_GENERICINST
12 //ELEMENT_TYPE_CLASS
0E //类型引用01000003,即List的压缩表示
//给出类型自变量数量的字节,1
08 //ELEMENT_TYPE_I4
2.IL中的MemberRef令牌可以引用类属类型实例化内的方法。允许CTL中的相同内容。可以使用外部类型索引来指示该类型索引是引用外部类型还是类型规范。
3.MethodSpec令牌是IL能够引用类属方法实例化的方式。方法规范令牌表示被引用的方法(作为方法定义或成员引用令牌)以及类型自变量。类似于类型规范,这被实现为包含到MDIL代码池中的字节序列的偏移量的新表。字节序列表示方法定义或成员定义令牌(寻常的CTL编码),之后是类型自变量的数量以及类型自变量本身。
示例:代码希望引用Array.Sort<int>(int[]a)。这由(比如)方法规范令牌2b00003来表达。在新方法规范表中的索引3处,将找到以下字节序列的偏移量:
12 //成员引用0a000004(Array.Sort)的压缩表示
01 //给出类型自变量数的字节,1
08 //ELEMENT_TYPE_I4
可能需要供类属类型或方法指示类型自变量数的方式。这可主要用于对CTL进行差错检查和转储,因此严格而言其不是必需的。
表示类属方法主体。如上所述,对于同一IL方法主体可以有多个MDIL方法主体。为了对每一MDIL方法主体表达它应应用于什么种类的类型自变量,可以对运行时类型进行归类。一种方式是使用CorElementType。这假定用于比如ELEMENT_TYPE_I1的MDIL代码可以与用于ELEMENT_TYPE_U2的MDIL代码有很大的不同,但是用于所有ELEMENT_TYPE_VALUETYPE实例的代码是相同的(对此存在以下描述的某些复杂化)。
所提出的归类精细地拆分可能的自变量类型的空间——一般对表现相似的类型(它们可被存储在相同的寄存器中、以相同方式传递等等)生成相同的MDIL主体。由此,并非对每一类属类型自变量对每一CorElementType具有一个MDIL方法主体,允许编译器对给定MDIL方法主体对其有效的每一类属类型自变量指定一组CorElementType。需要支持的CorElementType编码在数值上都小于32,因此一组CorElementType可用单个DWORD来方便地表示。使用该组,还可高效地支持其中MDIL主体完全不依赖于一个或多个类型自变量的情形。
总而言之,可以为每一类属方法使用包含以下内容的数据结构:
·类型自变量的数量
·该具体方法的不同MDIL风格的数量
·对于每一风格,想要
ο对每一类型自变量,有效的CorElementType组
οMDIL代码偏移量
因此,在类C的伪代码中,定义如下数据结构:
将方法定义令牌映射到其实现的MDIL代码偏移量的表改为指向MdilGenericMethodDesc的偏移量。将该偏移量的高位设为指示该方法主体是类属的,即,该偏移量引用MdilGenericMethodDesc而非直接引用MDIL方法主体。
注意,允许m_flavorTable中的两个条目具有完全相同的m_dilCodeOff。这实际上经常在编译器足够智能来合并相同的MDIL方法主体且具有相同主体的区域不具有完美的“矩形”形状的情况下发生。还要注意,可对m_flavorTable进行顺序搜索并选取匹配的第一个条目。这使得可能首先具有高度专门化且优化的主体,之后是较慢的“catch-all”主体。
示例——用于某一具体方法的该数据结构的转储可能看上去如下(每一行给出了类属方法主体的偏移量和大小,之后是其适用的CorElementTypes的集合——这是从原型实现输出的):
用于类属方法06001a39的7个实例-7个独特主体,总共223字节
注意,位掩码80000000、40000000和00800000不真正对应于此处有意义的CorElementType。这是因为此时结构体上的主体被进一步拆分来处理Nullable<T>的特殊情况和包含gc引用且因此需要共享实现的类属结构体的特殊情况。可以消除这些区别。
类属MDIL代码。某些MDIL指令在许多情形下允许从特定的实例化中抽象出类型自变量。这允许编译器缩减它需要生成的方法主体风格的数量。当然,存在折中——生成更多具体代码有时候将允许更多优化。
这里是帮助支持类属代码的某些MDIL指令的列表:
1.与结构体和符号栈布局相关:
·LOCAL_BLOCK<字节数>//定义局部变量
·LOCAL_STRUCT<类型令牌>
·PARAM_BLOCK<字节数>//定义栈参数
·PARAM_STRUCT<类型令牌>
·COPY_STRUCT<目的地地址模式>,<源地址模式>[,标志]
·PUSH_STRUCT<地址模式>[,临时寄存器]
·REF_BIRTH_LOCAL var#
·REF_DEATH_LOCAL var#
·REF_UNTR_LOCAL var#
·INIT_LOCAL var#
·INIT_STRUCT<地址模式>
·REF_BIRTH_REG<类型令牌>
2.用于原语类型的抽象
·LOAD_X reg,<地址模式>//零,符号扩展或仅仅加载
·LOAD_RESULT<地址模式>//将值加载到eax,(eax,edx),或st(0)
·STORE_RESULT<地址模式>
·PUSH_RESULT<地址模式>
·DISCARD_RESULT<地址模式>//弹出fpu栈
·REF_BIRTH_REG<类型令牌>//活性信息-如果<类型令牌>是引用类型则仅生成gc信息
·STORE_REF<地址模式>,寄存器//存储,如有必要则插入wb
3.用于共享/非共享代码的抽象
·INST_ARG<寄存器或栈>,<寄存器或栈>//指示将实例化自变量传递到何处,并且对其要做什么-对于非共享代码则无操作
·LOAD_INST寄存器,<方法令牌>//传递实例化自变量,对非共享代码为无操作
·PUSH_INST<方法令牌>
4.用于可空类型的抽象
·UNBOX<临时变量#>,<类型令牌>//对ecx中的实例出箱,使用<临时变量#>作为存储,将引用留给eax中的结果
·COND_LOCAL<类型令牌>//有条件地保留局部空间(比如,仅在<类型令牌>为Nullable<T>时)
类属MDIL代码的一些示例。这里是类属MDIL代码的一些简单示例。首先,假定以下C#代码:
用于构造函数的MDIL代码可看上去为(其中T是某一整型类型,U是任何类型):
该风格仅在T在寄存器中传递而U不在寄存器中传递时才适用。因此,在某些实现中将有其他风格,例如在T在栈上传递而U在EDX中传递的情况下:
在两个参数都在栈上传递的情况下还有另外一种风格:
另一方面,一些方法完全不依赖于一些类型自变量——例如,特性获取器get_First获得用于整型或引用类型的T的该MDIL主体:
b3 END_PROLOG
18 01 c0 04 LOAD_X eax,[ecx].0a000004
b5 EPILOG_RET
还可以利用上述LOAD_RESULT,并且还包含long/ulong和float/double类型自变量。在以上所有情况下,U的类型是无关的。
这里是一个类属栈示例:
构造函数和IsEmpty都完全不依赖于类型自变量的特定CorElementType,且因此不那么有趣。这里是用于Push的方法主体,如果A是整型类型则是适当的:
注意,这里有可被消除的某些低效性/特质:
·两个LOAD_X指令应真正是LOAD(但是这不是真正的问题)。
·ELEM_ SCALE指令可被消除(如果该参数在EDX中传递,这意味着其大小是1、2或4)。
在参数在栈上传递的情况下,将具有如下:
多维数组
创建多维数组。即使用于创建多维数组的C#句法是直截了当且类似于单维数组,但编译器为其创建的MSIL是相当不同的。例如,该C#代码片段:
double[,]m=new double[10,20];
将生成以下MSIL代码:
因此,这看上去更像新建一个常规对象而不是分配单维数组。
在MDIL代码中,助手应用边界作为自变量以及引用所需类型和构造函数方法的句柄来被调用。助手具有可变自变量列表,因此调用者应清理栈。这给出了以下代码序列(这在它所来自的较大的方法中是完全可中断代码):
访问多维数组的边界。在CLR中,多维数组在每一维都具有下界和长度。如在地址模式的讨论中所注意到的,存在允许访问每一维中的下界和长度的地址模式修饰符AM_MDIM_ARRAY_LEN。之后的MdimArrayByte指定数组的秩和元素类型,且MdimBoundByte指定了哪一维以及是否要访问该维中的下界或长度。
访问多维数组的元素。在MDIL中,这涉及就像计算“展平索引”,即,其中数组是真正多维的事实消失的索引。每一维中的索引因此乘以该维中的子元素的数量,且结果,合计所有这些缩放的索引。例如,以上示例中的语句分配具有10行和20列的矩形矩阵。如果索引到该数组中,则行索引乘以列数,并且加上列索引。
多维数组示例
这里是通过以上分配的矩阵来运行的两个嵌套for循环的示例——C#代码看上去为:
所生成的MDIL可能看上去为(没有太多优化)——以矩阵分配开始:
接着是循环:
最后是返回序列以及范围检查:
由于原型编译器不生成用于访问多维数组下界和长度的MDIL,因此该MDIL分解列表是大量手动地编辑的。由此,一些细节中可能有错误。此处的其他示例还可以部分地被验证,并且不必由现有的代码生成器来生成。
这种循环服从许多编译器优化。例如,范围检查不是在该循环中真正必需的,可以使用指令变量来消除索引计算,可以在某种程度上解开内部循环,等等。
代码示例
现在考虑示出如何声明并访问字段和方法的一个示例。以下面的C#源代码开始:
该源代码120被一个编译系统编译成二进制中间代码202。以下是中间代码的部分分解,用注释来注解。注释由//来标记。在此处的代码清单中也修改了空白部分,以便于遵从专利文献格式指南。
可以注意到以下几点。编译系统中的所有模块具有对最低级库(mscorlib)的编译器生成的引用。该示例中的类型系统在引用类型(直接或间接从System.Object派生)和值类型(从System.ValueType中派生)进行区分。本文别处也讨论了可能对于回复力和其他话题的更大的兴趣,本示例中的系统中有若干种类的符号令牌。以01...开头的令牌被称为类型引用令牌,并且引用来自其他模块的类型。这是经由包含其他模块的号码,后跟该模块中的类型令牌的表来完成的。以02...开头的令牌被称为类型定义令牌,并且引用该模块中的类型(类型被编号,从02000002开始,类型02000001被保留)。以04...开头的令牌被称为字段定义令牌,且引用该模块中的字段(所有字段被编号,从04000001开始)。以06...开头的令牌被称为方法定义令牌,且引用该模块中的方法(所有方法被编号,从06000001开始)。以0A...开头的令牌被称为成员引用令牌,并且引用来自其他模块的字段或方法。这是经由包括包含类型(通常是类型引用令牌)、指示是字段还是方法被引用的位、以及相对于类型中包含的最低字段或方法令牌对所引用的字段或方法令牌的编号进行编码的序数的表来完成的。
这里是引用来自第一模块的类型和方法的另一模块的C#源代码:
以下示出了上述源代码编译成的中间代码202的部分分解。本示例中的代码202以模块、类型和成员引用开始:
接着,本示例中的代码202描述了DerivedClass(派生类),包括覆盖:
描述DeribedClass的代码202继续,具有对Main()的描述:
可以注意到以下几点。用于Main的中间代码202未指定局部变量的大小。这些大小由绑定器来确定604,绑定器还将确定606相对于寄存器ebp的栈帧偏移量。并且,还存在初始化(INIT_STRUCT)和复制(COPY_STRUCT)这些变量的伪指令212;这些伪指令由绑定器转换成机器代码序列。MDIL还具有将这些变量作为参数来传递的PUSH_STRUCT指令(以上未示出)。还要注意如何通过给出局部变量号和对字段的符号引用248来访问局部变量。
代码修补
为了进一步示出以上内容的各方面,现在考虑展示熟悉的对象代码修补如何是不足够的一个示例。采用用于诸如以下的类型的源代码;在该示例中,编程语言是C#,但是各实施例不必限于C#环境。
为方法GetI产生的MDIL代码202可以看上去如下。在该分解(以及此处的其他分解)中,大写指示伪指令212,而小写指示机器指令134。在该分解中,注释由;;来标记。
绑定器214可以通过转换每一伪指令212,通过复制本机机器指令134,通过查找对于字段令牌04000001指定的字段的字段偏移量,以及通过确保跳转距离将做出到正确地方的跳转,来将该中间代码202转换成机器代码136。假定字段偏移量具有足够低的值(比如为8),这将导致如下的机器代码:
如果出于某种原因字段偏移量变得更大(比如现在是0xb4),则将在偏移量4处使用更大的指令——机器代码因而看上去如下:
注意,除了调整字段偏移量之外,两个项目被改变。首先,绑定器需要更大的指令来引用该字段。其次,作为结果,条件跳转中的距离需要被调整。该示例示出了伪指令到不同长度的变换602。旧样式的目标代码修补只能处理将新值插入到指令的字段中;它们不能使得指令更大或者调整分支距离来做出到正确地方的跳转。在这一意义上,绑定器214具有链接器中不存在的能力。
结论
尽管此处将具体实施例明确地图示并描述为进程、配置的介质或系统,但可以理解,对一种类型的实施例的讨论一般也可扩展到其他实施例类型。例如,结合图3到7的过程的描述也帮助描述了配置的介质,且帮助描述了如结合其他附图讨论的系统和产品的操作。并不能得出一个实施例中的限制一定要加进另一实施例中。具体而言,各进程不一定要限于在讨论诸如配置的存储器等系统或产品时提出的数据结构和安排。
并非附图中所示的每一项目都需要存在于每一实施例中。相反,一实施例可以包含附图中未明确示出的项目。尽管此处在文字和附图中作为具体示例示出了某些可能性,但各实施例可以脱离这些示例。例如,一示例的具体特征可被省略、重命名、不同地分组、重复、以硬件和/或软件不同地实例化、或是出现在两个或更多示例中的特征的混合。在某些实施例中,在一个位置处示出的功能也可在不同位置提供。
通过附图标记对全部附图作出了引用。在附图或文字中与给定的附图标记相关联的措辞中的任何明显的不一致性应被理解为仅仅是拓宽了该标记所引用的内容的范围。
如此处所使用的,诸如“一”和“该”等术语包括了所指示的项目或步骤中的一个或多个。具体而言,在权利要求书中,对一个项目的引用一般意味着存在至少一个这样的项目,且对一个步骤的引用意味着执行该步骤的至少一个实例。
标题是仅出于方便起见的;关于给定话题的信息可在其标题指示该话题的节之外找到。
所提交的所有权利要求和摘要是该说明书的一部分。
尽管在附图中示出并在以上描述了各示例性实施方式,但对本领域技术人员显而易见的是,可以作出不背离权利要求书中阐明的原理和概念的多种修改。尽管用结构特征和/或进程动作专用的语言描述了本主题,但可以理解,所附权利要求书中定义的主题不必限于上述权利要求中的具体特征或动作。给定定义或示例中标识的每一装置或方面不一定要存在于每一实施例中,也不一定要在每一实施例中都加以利用。相反,所描述的具体特征和动作是作为供在实现权利要求时考虑的示例而公开的。
落入权利要求书的等效方案的含义和范围内的所有改变应被权利要求书的范围所涵盖。