.NET IL / MSIL 评估堆栈基础

Posted

技术标签:

【中文标题】.NET IL / MSIL 评估堆栈基础【英文标题】:.NET IL / MSIL Evaluation Stack fundamentals 【发布时间】:2019-07-23 22:40:33 【问题描述】:

似乎无法为这些问题找到一个好的答案。

以下是我认为我知道的和我不清楚的。

评估堆栈是类似于 C 样式堆栈的内存缓冲区(它是原生 int / size_t 的堆栈)吗? 评估堆栈元素可以是 32 位或 64 位(这些元素如何混合在一个堆栈中?) Ldloc_0 将局部变量存储在评估堆栈上,但是如果它大于 64 位怎么办? Ldloc_0 是否只是将 ptrs 存储到评估堆栈上的局部变量中? 评估堆栈中存储的对象是否总是指针或原始值? 如果 .maxsize 为 8 是否意味着 (8 * size_t)?如果是这样,如果我阅读说明其 32 位或 64 位的文档,该怎么办

以下面的例子为例。此局部变量是否通过 ptr 引用存储在评估堆栈中?

public struct MyStruct

    public long x, y, z;

    public static MyStruct Foo()
    
        MyStruct c;
        c.x = 1;
        c.y = 2;
        c.z = 3;
        return c;   
    

“ldloc.0”清楚地将结构存储到评估堆栈中,但它也远大于 64 位。是否存储了引用?

.class public sequential ansi sealed beforefieldinit MyStruct
    extends [mscorlib]System.ValueType

    // Fields
    .field public int64 x
    .field public int64 y
    .field public int64 z

    // Methods
    .method public hidebysig static 
        valuetype MyStruct Foo () cil managed 
    
        // Method begins at RVA 0x2050
        // Code size 34 (0x22)
        .maxstack 2
        .locals init (
            [0] valuetype MyStruct,
            [1] valuetype MyStruct
        )

        IL_0000: nop
        IL_0001: ldloca.s 0
        IL_0003: ldc.i4.1
        IL_0004: conv.i8
        IL_0005: stfld int64 MyStruct::x
        IL_000a: ldloca.s 0
        IL_000c: ldc.i4.2
        IL_000d: conv.i8
        IL_000e: stfld int64 MyStruct::y
        IL_0013: ldloca.s 0
        IL_0015: ldc.i4.3
        IL_0016: conv.i8
        IL_0017: stfld int64 MyStruct::z
        IL_001c: ldloc.0// What is actually stored here?
        IL_001d: stloc.1
        IL_001e: br.s IL_0020

        IL_0020: ldloc.1
        IL_0021: ret
     // end of method MyStruct::Foo

 // end of class MyStruct

【问题讨论】:

不要忘记评估堆栈是抽象的。 CIL 是 JITted,所以当代码实际执行时,值可能会存储在寄存器或内存位置。 @llidanS4 明白了。制作一个 IL 到 C 的翻译器并只向前分支预测将设置哪些本地或字段变量,然后修改“Br”/goto 位置。这样我就得到了 C 级优化。 【参考方案1】:

如果 .maxsize 为 8,这是否意味着 (8 * size_t)?

.maxstack 指令与运行时评估堆栈的实际大小无关。相反,它会提示分析工具同时有多少项驻留在堆栈中。错误地设置.maxstack(如,太小),该方法被认为是不可验证的,这可能会导致低信任情况下出现问题(但这对你来说应该不是问题,因为你正在阅读CIL,而不是写作)。

例如,让我们考虑一个简单的 Add 方法,它接受 int 参数,将这些参数相加,将结果存储在名为 sum 的类字段中并返回该字段的值。

.method private hidebysig instance
    int32 Add (
        int32 value1,
        int32 value2
    ) cil managed

    .maxstack 3 // At most, there are three elements on the stack.

    ldarg.0                   // 1 item on the stack
    ldarg.1                   // 2 items on the stack
    ldarg.2                   // 3 items on the stack
    add                       // 2 items on the stack
    stfld    int32 Foo::sum   // 0 items on the stack
    ldarg.0                   // 1 item on the stack
    ldfld    int32 Foo::sum   // 1 item on the stack
    ret

方法的评估堆栈上同时存在的项目不超过 3 个。


来源:

ECMA-335, Section III.1.7.4

【讨论】:

【参考方案2】:

堆栈的元素大小不一,可以包含任意大小的值类型 (structs)。 来自 ECMA-335,第 I.12.3.2.1 节:

评估堆栈由可以容纳任何数据类型的槽组成,包括值类型的未装箱实例

[...]

虽然一些 JIT 编译器可能会更详细地跟踪堆栈上的类型,但 CLI 只要求值是以下之一:

int64,8字节有符号整数 int32,4字节有符号整数 native int,4 或 8 字节的有符号整数,以更方便目标架构为准 F,浮点值(float32float64,或底层硬件支持的其他表示形式) &,托管指针 O,一个对象引用 *,一个“瞬态指针”,只能在单个方法的主体中使用,它指向一个已知位于非托管内存中的值(有关更多详细信息,请参阅 CIL 指令集规范。* 类型是在 CLI 内部生成;它们不是由用户创建的)。 用户定义的值类型

稍早一点,在第 I.12.1 节中:

用户定义的值类型可以出现在内存位置或堆栈上,并且没有大小限制

因此,在您的情况下,ldloc.0 指令将整个值类型实例(及其三个数据字段)加载到堆栈中。

感谢 this answer 将我指向这些 ECMA 部分。 这个问题和其他答案表明为什么堆栈可以在槽而不是字节中测量:因为 JIT 编译器已经在评估如何将 MSIL 转换为本机指令,所以它必须知道每条指令栈上的值的类型。

【讨论】:

我已经阅读了您从 ECMA-335 发布的一些内容,但我想困惑来自于不了解评估堆栈是如何被否定的? JIT 肯定不会做所有这些额外的副本?

以上是关于.NET IL / MSIL 评估堆栈基础的主要内容,如果未能解决你的问题,请参考以下文章

.NET Framework基础

虚拟机和无虚拟机 - 堆栈和跟踪

浅析C#中的托管非托管堆栈与垃圾回收

浅析C#中的托管非托管堆栈与垃圾回收

MSIL实用指南-返回结果

MSIL 静态类在 IL 定义上和非静态类的差别