CLI Calling Conventions [in ECMA-335]
CLI函数调用协定精确地描述了托管代码函数调用方Caller与被调用方Callee之间是如何利用堆栈来传递调用参数的。由于CLI采用了完全基于堆栈的简易虚拟机模型,因此,托管函数调用中所有的实参均通过CLI堆栈来传递。下面给出了在调用call指令之前,托管堆栈帧的构造步骤及示例:
1. | this引用压栈,如果是方法调用的话 |
2. | 函数调用实参从左到右进栈 |
3. | 如果最后一个实参是可变数量参数的话,仅需其数组引用进栈即可 |
4. | call/calli/callvirt等call指令返回之后,返回值或引用处于栈顶,函数调用参数全部退栈 |
JIT Calling Conventions of x86 [in Rotor 1.x]
JIT函数调用协定主要适用于JIT生成的本地代码(Jitted Native Code)之间、及其与执行引擎内部函数之间的参数传递过程。事实上,在真实的本地代码执行过程中并不存在着一个优美而又简单的CLI虚拟堆栈。在进行即时编译时,JIT Compiler需要将抽象的CLI函数调用协议转换成为具体的面向x86本地代码的JIT函数调用方式。在Rotor 1.x与Rotor 2.0中采用了不同的JIT调用协定,后者比前者更为简洁、高效。Rotor 1.x中的JIT调用协定不仅饶舌,而且让人看起来非常费解,其设计者David Stutz在公开演讲中曾多次提到了这一点,他指出:之所以采用这种令人困惑的参数传递方式,主要是为了尽可能避免和减少对执行引擎中已有的非托管代码部分(如Stub/JIT Prestub等处)的修改,貌似有偷懒之虞...
Rotor 1.x中的JIT函数调用协定主要分为两种类型,其步骤与示例分别如下:
A.无可变数量参数时: | ||
1. | 函数调用实参从左到右进栈 | |
2. | 将栈中的实参进行重排(Reordering) | |
2.1 | 挑选出调用参数列表中最左边的两个可放置在4字节寄存器中的实参(如类型为byte/int的变量等) | |
2.2 | 将其值分别存进ECX和EDX寄存器 | |
2.3 | 从堆栈中删除挑选出的实参,并压缩堆栈 | |
2.4 | EDX进栈,然后ECX进栈 | |
3. | 函数返回缓冲区指针(Return buffer ptr)进栈,如果需要的话 | |
B.存在可变数量参数时: | ||
1. | 函数调用实参从左到右进栈 | |
2. | 栈中实参无需进行Reordering | |
3. | 将可变数量实参的实际参数数量压栈 | |
4. | 将可变数量实参的元数据类型Token压栈 | |
5. | Return buffer ptr进栈,如果需要有的话 | |
6. | this指针进栈,如果是对象方法调用的话 |
JIT Calling Conventions of x86 [in Rotor 2.0]
Rotor 2.0对JIT函数调用协定进行了再次的整理,其参数传递方式类似于__fastcall的改版,同时还利用了寄存器和浮点堆栈来实现整型及浮点类型的返回。相对整洁的流程及寄存器的使用令整个JIT函数调用过程得到了进一步的优化。其基本流程和2个实例如下:
1. | this指针进栈(如果是方法调用的话),然后Return buffer ptr进栈(如果需要的话) |
2. | 所有非可变实参以从左至右的次序进栈 |
3. | 如果存在可变数量参数,则所有可变实参从左至右进栈,然后再将可变实参的类型Token压栈 |
4. | 从上述压栈次序中挑选出最前面两个可放置在4字节寄存器中的参数,分别存进ECX、EDX并将它们从栈中删除,然后压缩堆栈 |
5. | 如果返回值为浮点数,该返回值将放置在FP堆栈的栈顶返回(非调用栈栈顶!) |
6. | 如果返回值为32位整数(或者是可扩充为32bit的类型),则通过EAX寄存器返回 |
7. | 如果返回值为64位整数,则通过EAX:EDX寄存器返回 |
8. | 其他返回类型一律通过Return buffer ptr返回 |