我的简单MSIL有什么问题?

4

我正在尝试生成以下类:

public class MyType
{
    public string MyMethod() { return "Hi"; }
}

我的 Emit 代码如下:

var assemblyBuilder = GetAssemblyBuilder("MyAssembly");
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule");
var typeBuilder = moduleBuilder.DefineType("MyType", TypeAttributes.Public);
var methodBuilder = typeBuilder.DefineMethod("MyMethod", MethodAttributes.Public, typeof(string), new Type[] { });
var ilBuilder = methodBuilder.GetILGenerator();
ilBuilder.Emit(OpCodes.Nop);
ilBuilder.Emit(OpCodes.Ldstr, "Hi");
ilBuilder.Emit(OpCodes.Stloc_0);
ilBuilder.Emit(OpCodes.Br_S);
ilBuilder.Emit(OpCodes.Ldloc_0);
ilBuilder.Emit(OpCodes.Ret);
var type = typeBuilder.CreateType();

但是,当我在MyType实例上调用MyMethod时,我会收到一个InvalidProgramException:Common Language Runtime检测到一个无效程序。

我尝试将返回类型更改为void,并仅使用EmitWriteLineEmit(OpCodes.Ret),它可以正常运行,所以问题肯定出在我写的IL上。

我是否漏掉了什么显而易见的东西?因为我刚开始使用Emit,所以需要一个清晰的解释。

来自评论的其他信息

“原始”IL是从LINQ-pad中的IL生成中获取的。


@Selman22 我相信这正是他所做的,当编译成IL时看起来就是这样。 - flindeberg
一些提示:1)像 Sigil 这样的工具将使错误变得更加困难;2)如果不确定,请将您想要执行的内容编译为常规 C# 代码,然后在 ILDASM、反编译器或类似工具中进行 IL 反向工程。 - Marc Gravell
1
在IL中出现“nop”是你实际上已经完成了第二步,但查看了调试版本的输出。不要这样做;在执行第二步时,始终使用发布版本。 - Marc Gravell
@aigouzhuren Comment a) 这实际上是我根据Kendall答案中的评论所做的。Comment b) 我现在明白了 - 发布构建产生了Andrew的后半部分。 - dav_i
@MarcGravell 出于好奇,调试代码中的 Nop 的目的是什么? - dav_i
3
对于那些对 nop 和 branch 指令的来源感兴趣的人(在调试版本中,它们会被优化掉以提高发布版的性能):请参考以下链接以了解基于反汇编简单 C# 代码所编写的 IL 的相关问题:https://dev59.com/D2nWa4cB1Zd3GeqPwgwB - flindeberg
2个回答

5
除了像 @Kendall 指出的那样删除 OpCodes.Br_S,你还需要声明一个本地变量来存储 "Hi"
ilBuilder.DeclareLocal(typeof(string));
ilBuilder.Emit(OpCodes.Nop);
ilBuilder.Emit(OpCodes.Ldstr, "Hi");
ilBuilder.Emit(OpCodes.Stloc_0);
ilBuilder.Emit(OpCodes.Ldloc_0);
ilBuilder.Emit(OpCodes.Ret);

此外,我认为整个过程可以缩短到(如果您使用编译器优化构建类,则会得到以下结果):
ilBuilder.Emit(OpCodes.Ldstr, "Hi");
ilBuilder.Emit(OpCodes.Ret);

只需将字符串加载到计算堆栈的顶部,然后返回该字符串即可。

如果您想复制关闭编译器优化版本(我认为这是您最初尝试做的):

Label returnLabel = ilBuilder.DefineLabel();

ilBuilder.DeclareLocal(typeof(string));
ilBuilder.Emit(OpCodes.Nop);
/* Load the string "HI" and put it on the evaluation stack. */
ilBuilder.Emit(OpCodes.Ldstr, "Hi");

/* Store "Hi" in the local variable we declared. */
ilBuilder.Emit(OpCodes.Stloc_0);

/* Jump to the return label, which is, err the next line anyway: */
ilBuilder.Emit(OpCodes.Br_S, returnLabel);

/* Mark "returnLabel" here so we can jump to it: */
ilBuilder.MarkLabel(returnLabel);    

/* Load the contents of our local variable, put it on the evaluation stack: */
ilBuilder.Emit(OpCodes.Ldloc_0);

/* Return "Hi", which is now back on top of the evaluation stack: */
ilBuilder.Emit(OpCodes.Ret);

您需要定义一个标签以进行分支(使用ILGenerator.DefineLabel),然后使用MarkLabel,并在发出OpCodes.Br_S时将该标签传递给Emit

但是,从这段代码中可以看出,对于这么短的方法来说,分支有点毫无意义。


你关于缩短版本是完全正确的。 - Marc Gravell
我看不到任何与DeclareLocal等效的生成IL代码,它的等效是什么? - dav_i
@dav_i:你在使用LINQPad吗?如果是的话,该程序的“IL”选项卡将不会显示已声明的本地变量。 - Andrew Whitaker
@AndrewWhitaker 抓住我了!谢谢你的信息 :) - dav_i
@dav_i:没问题。我知道的唯一原因是因为我也遇到了同样的问题。很高兴能帮忙。 - Andrew Whitaker

3
ilBuilder.Emit(OpCodes.Br_S);

你原本想要分支到哪里?br.s需要一个带符号字节参数,指定要跳转的偏移量。

谢谢Kendall,我只是在复制我的示例类生成的IL时...显然,错过了这一点。您如何定义要将br.s跳转到哪个位置,因为生成的代码定义了“行号”。 - dav_i
您可以使用Emit(OpCodes.Br_S, theLabel)将其传递给标签。请参阅Label以获取有关如何使用它们的代码示例。 - Kendall Frey
@dav_i 通常不需要;你可以使用 DefineLabel 并传入标签 - 同时记得在跳转点使用 MarkLabel - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接