自定义Rotor: 给Rotor添加一个CIL指令
快速指南
下载我修改的文件,找一份新的Rotor代码,覆盖,而后Build,运行本文的例子即可
引言:
CIL = Common Intermediate Language, 在.Net Framework中,我们称他为MSIL
还不知道Rotor? 看看Wiki的解释 (注:我们这里的Rotor如果没有特别注明都是指SSCLI 2.0)
关于添加CIL的文章已经不少了这里,还有这里. 然而中文的,很可惜,无,虽然有前辈(Flier's Sky - (Flier Lu), NeoRAGEx2002's Weblog)做过一些探索,然而终于是没有给俺们看到,于是有了俺的不断探索,虽频遭头痛,偶有心得,分享之。:)
思路概述:
我们先那一个指令来搞清楚CIL的执行过程, 而后在对应的地方作相应的操作,模仿即可. : )
线索:
- IL指令涉及到Verify, Compile, 和运行期验证的过程. 他是JIT一部分.
- 在rotor目录中有 clr/src/fjit 这个目录
从拿一个指令开刀说起
很多地方的例子都是乘法,我们也拿他开始. 例子程序
// Sample Entrypoint and a sample function here,
// Please add your own code in your environment
public static void Main()
{
double x = 2;
double y = 5;
double z = x * y;
Console.Read();
}
按照我之前的如何调试SSCLI的步骤, 到PAL_Initialize的位置。步骤如下
cd E:/Research/Rotor // 切换到SSCLI所在目录
env // 使用环境设置env.bat
csc /debug E:/Practices/cil/MULTest.cs // 编译
devenv /debugexe clix MulTest.exe // 运行调试
我们在Visual Studio的类试图中搜索MUL指令, 而且我们知道他是JIT的一部分,于是找到了如下图这个指令,
我们给这个指令加上一个断点,按F5执行。哈哈,抓到断点了 Bingo!
查看Call Stack, 就可以轻易的看到执行过程了,双击 / 按右键 Go to source 也可以轻易的跳转到对应代码了 :D
而后根据call stack跳转到compileCEE_MUL方法之前执行的步骤
有case, 哈哈,那就有switch. :)
看到CEE_PREFIX1, CEE_LDARG_0这种东西你是不是和我一样兴奋,意味着可以在某个地方添加我们的CEE_MYCUSTOM_IL_OPCODE
接着看看MUL的实现代码
聪明的你应该看明白了,至少代码的解释应该是清楚的, 得到栈上(topOp)上第0个和第一个参数,进行运算而后进栈,
BINARY_NUMBERIC_RESULT: 查看两个参数是否为同一类型
TYPE_SWITCH_ARITH: 得到指令的详细格式,例如ldloc.i4 或者 ldloc.i8 等,这里这是emit_mul_i4
关于emit_mul或者CEE_Prefix1等等操作指令定义在fjitdef.h中,我们查看MUL_I4的操作指令
/************* MUL ************************/
#ifndef emit_MUL_I4
#define emit_MUL_I4() \
{ \
callInfo.reset(); \
emit_tos_arg( 1, INTERNAL_CALL ); \
emit_tos_arg( 2, INTERNAL_CALL ); \
emit_callhelper_I4I4_I4(MUL_I4_helper); \
emit_pushresult_I4(); \
}
#ifdef DECLARE_HELPERS
int HELPER_CALL MUL_I4_helper(int i, int j) {
return j * i;
}
#endif
#endif
到这里,我们对一个指令的执行过程有了一个认识,于是,开始我们的新指令之旅
添加一门新武器 – EXPON
修改分为如下步骤
- 写一个测试程序
- 添加新指令到Rotor源码
- 编译运行
测试程序
.assembly expi4demo {}
.method static void main() {
.entrypoint
ldc.i4 3
ldc.i4 3
expon
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
添加指令到Rotor
给clr\src\inc\opcode.def 模仿CEE_MUL添加上EXPON.
并在Reflection.Emit添加这个指令 clr\src\bcl\system\reflection\emit\opcodes.cs
StackBehaviour.Push1, OperandType.InlineNone, OpCodeType.Primitive, 1, (byte)0xff, (byte)0xa6,
FlowControl.Next, false, -1);
接着要进行JIT指令验证的过程了. 验证的代码在clr\src\vm\validator.cpp中,我们只需要添加指令到vertable.h
VEROPCODE(CEE_EXPON, "N=:-")
所谓”N=:-” N – Number, = - 接着另一个Number : - 结果, - - 入栈.
关于格式的详细解释,在vertable.h的注释中有详尽的解释,有兴趣的朋友不妨一看。
接下来则要给他添加执行过程了, 在clr/src/vm/fjit/fjit.cpp 中的jitCompile方法添加一个switch判断
case CEE_EXPON:
JitResult = compileCEE_EXPON();
break;
接着在本类中和头文件中添加对应的实现代码
fjit.cpp
FJitResult FJit::compileCEE_EXPON()
{
OpType result_xf;
BINARY_NUMERIC_RESULT(topOp(), topOp(1), CEE_EXPON, result_xf);
TYPE_SWITCH_ARITH(topOp(), emit_EXPON, ());
POP_STACK(2);
pushOp(result_xf);
return FJIT_OK;
}
fjit.h
FJitResult compileCEE_EXPON();
TYPE_SWITCH_ARITH中定义了对应的实现方法,于是我们在clr\src\jit\fjit.def 中添加对应的定义
#ifndef emit_EXPON_I4
#define emit_EXPON_I4() \
{ \
callInfo.reset(); \
emit_tos_arg( 1, INTERNAL_CALL ); \
emit_tos_arg( 2, INTERNAL_CALL ); \
emit_callhelper_I4I4_I4(EXPON_I4_helper); \
emit_pushresult_I4(); \
}
#ifdef DECLARE_HELPERS
int HELPER_CALL EXPON_I4_helper(int i, int j) {
return j * i;
}
#endif
#endif
#ifndef emit_EXPON_I8
#define emit_EXPON_I8() \
{ \
callInfo.reset(); \
int NumRegUsed = 0; \
emit_tos_fixedsize_arg( 1, 8, NumRegUsed, INTERNAL_CALL ); \
emit_tos_fixedsize_arg( 1 + NumRegUsed, 8, NumRegUsed, INTERNAL_CALL ); \
emit_callhelper_I8I8_I8(EXPON_I8_helper); \
emit_pushresult_I8(); \
}
#ifdef DECLARE_HELPERS
__int64 HELPER_CALL EXPON_I8_helper(__int64 i, __int64 j) {
return j * i;
}
#endif
#endif
#ifndef emit_EXPON_R4
#define emit_EXPON_R4() \
{ \
callInfo.reset(); \
emit_tos_arg( 1, INTERNAL_CALL ); \
emit_tos_arg( 2, INTERNAL_CALL ); \
emit_callhelper_I4I4_I4(EXPON_R4_helper); \
emit_pushresult_I4(); \
}
#ifdef DECLARE_HELPERS
unsigned int HELPER_CALL EXPON_R4_helper(int i, int j) {
float result = (*(float *)&j) * (*(float *)&i);
return *(unsigned int*)&result;
}
#endif
#endif
#ifndef emit_EXPON_R8
#define emit_EXPON_R8() \
{ \
callInfo.reset(); \
int NumRegUsed = 0; \
emit_tos_fixedsize_arg( 1, 8, NumRegUsed, INTERNAL_CALL ); \
emit_tos_fixedsize_arg( 1 + NumRegUsed, 8, NumRegUsed, INTERNAL_CALL ); \
emit_callhelper_R8R8_R8(EXPON_R8_helper); \
emit_pushresult_I8(); \
}
#ifdef DECLARE_HELPERS
unsigned __int64 HELPER_CALL EXPON_R8_helper(__int64 i, __int64 j) {
double result = (*(double *)&j) * (*(double *)&i);
return *(unsigned __int64*)&result;
}
#endif
#endif
重新编译Rotor, 切换到开始的IL所在目录并运行
ILAsm expon.il
clix expon
编译结果:
在编译的过程中,碰到一些问题,需要注意如下:
- 文件编码要正确
- 如果编译错误,删除原来编译结果,而后重新编译
在这里,我把我所修改的文件共享下,覆盖原来的代码文件即可
结语:
修改本来是件Easy的事情,不幸运的是俺第一次编译失败,而后面对一堆的错误,不知道哪里入手,检查一次又一次代码之后,确定代码无错误,尝试着删除某个编译结果后错误不一样了, 俺兴奋了,俺强烈的兴奋了。 在经过几次尝试和漫长的编译等待之后,
env
buildall
等待…… Zzzz Orz…
编译错误, 查看clr/src/build.err
env
buildall
修改过程虽然简单,可惜俺那稳定性不高的编码速度实在害人,总结下,发发牢骚而已 :)
Have Fun :)