【问题标题】:Why does `OpCode.Value` have the "wrong" endianness?为什么 `OpCode.Value` 有“错误”的字节序?
【发布时间】:2012-08-14 10:11:22
【问题描述】:

事实:

  1. CIL指令rethrow的操作码的正确编码是两字节序列FE 1A

  2. OpCodes.Rethrow.Value(类型为 short)在我的 little-endian 机器上具有值 0xFE1A

  3. BitConverter 在与字节序列进行转换时遵循机器的字节序。

  4. 在我的 little-endian 机器上,BitConverter.GetBytes(OpCodes.Rethrow.Value) 生成字节序列 1A FE

这意味着,使用BitConverter 在little-endian 机器上序列化OpCode.Value 不会产生正确的操作码编码;字节顺序颠倒了。

问题:

  • OpCode.Value 的字节顺序是否记录在案(如果有,在哪里?),还是“实施细节”?

  • 上面的步骤 4 在大端机器上是否也会导致错误的字节顺序?也就是说,OpCodes.Rethrow.Value 在大端机器上会是 0x1AFE 吗?

【问题讨论】:

  • 你不能这样组合操作码。元数据阅读器会轻咬内容,并在找到 FE 时知道它是一个长版本。如果它代表相反的方式,那么您将很难阅读它。
  • @leppie:我知道FE 必须首先出现在指令字节流中。我的问题是关于存储在OpCode.Value 中的操作码编码的字节顺序。
  • 可能是为了阅读方便:)

标签: .net endianness cil reflection.emit opcode


【解决方案1】:

试试:

var yourStream = MemoryStream();
var writer = new System.IO.BinaryWriter(yourStream);
writer.Write(OpCodes.Rethrow.Value);

您无需担心字节顺序,因为 BinaryWriter(或读取器)将为您处理实现细节。我怀疑你得到“错误”字节顺序的原因是你在 OpCode 值已经被解码为小端时应用 BitConverter,并且再次应用 BitConverter.GetShort() 调用将反转字节订单,给你“错误”的结果。

【讨论】:

  • 除非您尝试实施,否则您不会知道。我实际上每天都在使用这些东西,因为我目前正在从头开始重写 System.Reflection.*——所以是的,我知道。 :)
  • 请不要误解我的意思:我相信你知道你的东西,我相信你的代码可能会正常工作。但我要求提供文档(如果存在),因为我希望我的代码(这是一个扩展 System.Reflection btw 的库)实际上不依赖于 CLI 标准的一种实现的未记录行为。
  • 我刚刚试用了您的代码。不幸的是,它没有做正确的事情。 documentation for BinaryReader.Write(short) 说:“BinaryWriter 以小端格式存储此数据类型。”。我已经尝试过了,确实,您最终会在FE 之前将1A 写入流中。
【解决方案2】:

我已经得出结论,基于OpCode.Value 属性序列化操作码表示,即:

OpCode someOpCode = …;
byte[] someOpCodeEncoding = BitConverter.GetBytes(someOpCode.Value);

是个坏主意,但不是因为使用了 BitConverter.GetBytes(short) ,它的行为有据可查。罪魁祸首是OpCode.Value属性,whose documentation在两个方面含糊:

  1. 它声明该属性包含“立即操作数的值”,它可能指也可能不指操作码的编码;该术语没有出现在 CLI 规范中的任何地方。

  2. 即使我们假设它确实实际上包含操作码的编码,文档也没有说明字节顺序。 (在byte[]short 之间转换时,字节顺序会起作用。)

为什么我的论点基于 MSDN 文档,而不是 CLI 标准?因为System.Reflection.Emit 不是 CLI 标准定义的反射库的一部分。出于这个原因,我认为可以相当肯定地说the MSDN reference documentation for this namespace 与官方规范一样接近。 (但与@Hans Passant 的回答不同,我不会更进一步声称参考来源在任何方面都是一种规范。)

结论:

有两种方法可以输出给定OpCode 对象的操作码编码:

  • 继续使用System.Reflection.Emit 功能并使用ILGenerator.Emit(someOpCode)。这在某些情况下可能过于严格。

  • 在操作码编码(即byte[] 序列)和各种OpCode 对象之间创建自己的映射。

【讨论】:

  • 替代方案:如果不使用反射,不要使用OpCode 对象?如果您只是喜欢使用OpCodes.Rethrow.Value 来提高可读性,您可以使用Mono Cecil's OpCodes 类来实现相同的目的;它的OpCode 结构有明确的Op1Op2 属性。
【解决方案3】:

参考源中的 Value 属性如下所示:

public short Value
{
    get
    {
        if (m_size == 2)
            return (short) (m_s1 << 8 | m_s2);
        return (short) m_s2;
    }
}

当然,这看起来完全正常,m_s2 始终是最低有效字节。查看 ILGenerator:

    internal void InternalEmit(OpCode opcode)
    {
        if (opcode.m_size == 1)
        {
            m_ILStream[m_length++] = opcode.m_s2;
        }
        else
        {
            m_ILStream[m_length++] = opcode.m_s1;
            m_ILStream[m_length++] = opcode.m_s2;
        }

        UpdateStackSize(opcode, opcode.StackChange());

    }

这是您所期望的,首先发出 0xfe 字节。

因此,框架代码小心地避免依赖字节序。 CIL 没有字节序依赖性,也没有可变长度数据。真适用于文本文件、utf-8 编码、x86 核心机器码指令。一个 CIL。因此,如果您将可变长度数据转换为 single 值,就像 Value 属性 getter 所做的那样,那么该代码不可避免地 确实 会从非字节序数据转换为字节序性数据。这不可避免地让世界上的一半人感到不安,因为他们认为这是错误的方式。以及 100% 遇到它的所有程序员。

可能最好的方法是像框架一样使用您自己的 Opcode 类型版本尽快恢复 m_s1 和 m_s2。操作简单:

foo.m_s1 = opc.Value >> 8;
foo.m_s2 = opc.Value & 0xff;
foo.m_size = opc.Size;

没有字节序依赖性。

【讨论】:

  • “它不像 InternalEmit() 那样做”:你是说OpCode.Value 中的字节顺序是一个(内部)实现细节吗?或者.NET reference source 是否真的具有公共规范的特征?
  • Opcode.Value 属性不是内部实现细节。它只是可以使用的公共属性,实现不使用它。
  • 请重新阅读我的问题:我问的不是OpCode.Value 本身,而是它的字节顺序
  • "which has no endian-ness dependency": 你用执行位操作的代码证明自己是错误的。您基于您对参考源的了解,恕我直言,它不能替代公共规范或文档。即便如此:对于诸如“像框架那样做” 之类的陈述,您实际上帮助我得出了我自己的结论,即我们正在谈论的东西实际上是无证的。
  • 旁注:您的答案有一点不准确:与操作码不同,CIL 指令字节流中有 其他数据具有一定的字节顺序(总是很少-endian AFAIK):例如嵌入的元数据标记,或表示分支目标的相对偏移量。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多