如何通过CLR访问C#堆栈?

时间:2012-07-31 13:37:52

标签: c# stack clr heap cil

这可能是一个非常简单的问题,但我在SO上找不到答案,也不知道有人问我答案:

我可以这样写一个简单的c#方法:

private void foo()
{
   int a = 1;
   int b = 5;
}

如果CIL-Code(由编译器创建)由公共语言运行时执行,它将在堆栈顶部创建以下字段,而执行控件在方法内:

b = 5
a = 1

但是现在,我扩展了Method来访问名为“a”的字段:

private void foo()
{
   int a = 1;
   int b = 5;
   Console.WriteLine(a);
}

现在CLR必须访问不在堆栈顶部的字段,但根据FILO(先进先出)原则,它必须在访问之前处理请求字段之上的所有字段。

在请求字段“a”上方的堆栈上名为“b”的字段会发生什么?

CLR无法删除它,因为之后执行方法可能会使用它,那么会发生什么呢?

AFAIK,只有两种方法可以存储字段,堆栈或堆。将它移到堆中将没有多大意义,因为这将从CLR堆叠中获得所有好处。 CLR是否会创建类似第二个堆栈的东西?

这是如何运作的?

-edit -

也许我没有清楚地解释我的意图。

如果我写一个像这样的方法:

private void foo()
{
   int a = 1;
   int b = 5;
   Console.WriteLine(a);
   Console.WriteLine(b);
}

CLR首先在堆栈上写入2个字段,然后以相反的顺序访问它们。

首先,它必须访问字段“a”,但为了达到它,CLR必须处理位于堆栈上字段“a”之上的字段“b”。它不能从堆栈中删除字段“b”,因为它必须在之后访问它。

这是如何工作的?

6 个答案:

答案 0 :(得分:5)

变量不是单独堆叠的;堆栈包含“框架”。每个帧包含当前方法调用所需的所有变量(本地,参数等)。因此,在您的示例中,ab在同一帧中彼此并存,并且无需删除其中任何一个。当方法foo完成时,整个堆栈帧从堆栈中弹出,将调用方法的框架保留在顶部。

The wikpedia article可能会提供一些启示。

答案 1 :(得分:3)

调用堆栈不是严格意义上的“纯”堆栈,您只能与顶层元素进行交互。在调用堆栈中,您正在堆叠整个函数调用和/或整个变量范围,而不是变量。

例如,如果调用了一个新函数,比如foo(),它会将两个变量ab置于堆栈顶部,并具有对它们的完全访问权限。它(通常)不知道堆栈上那些变量以下的任何东西。

我们来看看这段代码:

void foo() { // << Space is allocated on the stack for a and b.
             // << Anything in this scope has full access to a and b.
             // << But you cannot (normally) access anything from the
             // << calling function.
    var a = 1;
    var b = 2;

    if (a == 1) {  // << Another variable scope is placed on the stack.
                   // << From here you can access a, b and c.
        var c = 3;
    } // << c is removed from the stack.
} // << a, b and anything else in foo() is removed from the stack.

答案 2 :(得分:2)

也许以下 简化 表示可以清理事物。在调用Console.WriteLine之前,堆栈的顶部看起来像这样:

|5| // b
|1| // a

Console.WriteLine内,为其参数添加了一个额外的堆栈框架(称为value,它获取变量a的副本):

|1| // value = a
|5| // b
|1| // a

一旦Console.WriteLine返回,弹出顶部框架并再次成为堆栈:

|5| // b
|1| // a

答案 3 :(得分:1)

你有关于堆栈的错误心理形象,它只是在方法调用之间的堆栈。在方法中,堆栈帧的作用类似于局部变量数组。托管代码的堆栈框架也没有什么特别之处,它与本机C或C ++代码中使用的堆栈框架一样完全

局部变量与EBP寄存器(堆栈帧指针)有固定的偏移量。该偏移量由JIT编译器确定。

您发布的代码的具体结果是,即时编译器中内置的优化器将消除未使用的局部变量。最后一个示例中的a变量很可能最终出现在cpu寄存器中,而不会出现在堆栈中。标准优化。

答案 4 :(得分:1)

当涉及到CLR时,最好将局部变量视为编号为“slot”的邮箱。存储在那些“槽”中的值是否在方法的堆栈帧中结束(其他人已经涵盖了这个概念),存储在CPU寄存器中甚至完全优化的是抖动细节。有关更多信息,请参阅IL Stloc指令。

最好考虑运行执行堆栈的CLR,并根据正在执行的指令弹出和推送值。管理代码如何在CPU上执行和执行的基本细节是一个单独的问题,这是传统的堆栈帧,寄存器和指针解除引用重新发挥作用的地方。然而,从IL级别的CLR来看,这些事情(大部分)并不重要。

答案 5 :(得分:0)

有四个相关但不同的概念:C#中的局部变量,CIL中的局部变量,CIL中的堆栈和本机堆栈。

请注意C#locals如何映射到CIL以及CIL本地和堆栈映射到本机内存是如何实现定义的,因此您不应该依赖于任何此类。

你知道C#locals是什么。它们可以表示为CIL本地,但它们通常不会继续使用CIL堆栈(C#编译器中可能会进行一些优化)。但是也有很少的其他选择:本地可以完全优化掉,如果不需要,或者可以编译为具有不可言名的类的类中的字段(lambdas的闭包变量,yield方法中的变量或async方法)。 此外,即使一些C#本地编译为CIL本地,它们也不必映射1:1,因为如果编译器知道这样做是安全的,那么一个CIL本地可用于更多C#本地。

在CIL中,有局部变量并且有堆栈。局部变量与堆栈完全分离,并且有不同的CIL指令用于处理它们中的每一个。 局部变量用于保持较长时间所需的值,并且可以随时访问每个本地。 CIL堆栈主要包含正在使用的值:指令的参数及其返回值。在堆栈中,只能访问最高值。

CIL本地和CIL堆栈实际上都放在本机堆栈上,但如果它们适合,它们通常只在寄存器中。当然,JIT编译器可以进行任何其他优化。正如其他人所说,当前方法堆栈中的任何值都可以随时访问,而不仅仅是顶层。