# 5.5 Stack

接下来我们讨论一下栈，stack。栈之所以很重要的原因是，它使得我们的函数变得有组织，且能够正常返回。

下面是一个非常简单的栈的结构图，其中每一个区域都是一个Stack Frame，每执行一次函数调用就会产生一个Stack Frame。

![](/files/-MM4D2J3t3ajqkngxRPC)

每一次我们调用一个函数，函数都会为自己创建一个Stack Frame，并且只给自己用。函数通过移动Stack Pointer来完成Stack Frame的空间分配。

对于Stack来说，是从高地址开始向低地址使用。所以栈总是向下增长。当我们想要创建一个新的Stack Frame的时候，总是对当前的Stack Pointer做减法。一个函数的Stack Frame包含了保存的寄存器，本地变量，并且，如果函数的参数多于8个，额外的参数会出现在Stack中。所以Stack Frame大小并不总是一样，即使在这个图里面看起来是一样大的。不同的函数有不同数量的本地变量，不同的寄存器，所以Stack Frame的大小是不一样的。但是有关Stack Frame有两件事情是确定的：

* Return address总是会出现在Stack Frame的第一位
* 指向前一个Stack Frame的指针也会出现在栈中的固定位置

有关Stack Frame中有两个重要的寄存器，第一个是SP（Stack Pointer），它指向Stack的底部并代表了当前Stack Frame的位置。第二个是FP（Frame Pointer），它指向当前Stack Frame的顶部。因为Return address和指向前一个Stack Frame的的指针都在当前Stack Frame的固定位置，所以可以通过当前的FP寄存器寻址到这两个数据。

我们保存前一个Stack Frame的指针的原因是为了让我们能跳转回去。所以当前函数返回时，我们可以将前一个Frame Pointer存储到FP寄存器中。所以我们使用Frame Pointer来操纵我们的Stack Frames，并确保我们总是指向正确的函数。

Stack Frame必须要被汇编代码创建，所以是编译器生成了汇编代码，进而创建了Stack Frame。所以通常，在汇编代码中，函数的最开始你们可以看到Function prologue，之后是函数的本体，最后是Epilogue。这就是一个汇编函数通常的样子。

![](/files/-MM51g2yien9HyEOP6mn)

我们从汇编代码中来看一下这里的操作。

![](/files/-MM55NSH1I42_YJ8PUnO)

在我们之前的sum\_to函数中，只有函数主体，并没有Stack Frame的内容。它这里能正常工作的原因是它足够简单，并且它是一个leaf函数。leaf函数是指不调用别的函数的函数，它的特别之处在于它不用担心保存自己的Return address或者任何其他的Caller Saved寄存器，因为它不会调用别的函数。

而另一个函数sum\_then\_double就不是一个leaf函数了，这里你可以看到它调用了sum\_to。

![](/files/-MM57BrzXVm-ij0-dwh-)

所以在这个函数中，需要包含prologue。

![](/files/-MM57e54ANIFdXpVubXH)

这里我们对Stack Pointer减16，这样我们为新的Stack Frame创建了16字节的空间。之后我们将Return address保存在Stack Pointer位置。

之后就是调用sum\_to并对结果乘以2。最后是Epilogue，

![](/files/-MM58TOs_9kQtfl4bVPw)

这里首先将Return address加载回ra寄存器，通过对Stack Pointer加16来删除刚刚创建的Stack Frame，最后ret从函数中退出。

这里我替大家问一个问题，如果我们删除掉Prologue和Epilogue，然后只剩下函数主体会发生什么？有人可以猜一下吗？

> 学生回答：sum\_then\_double将不知道它应该返回的Return address。所以调用sum\_to的时候，Return address被覆盖了，最终sum\_to函数不能返回到它原本的调用位置。

是的，完全正确，我们可以看一下具体会发生什么。先在修改过的sum\_then\_double设置断点，然后执行sum\_then\_double。

![](/files/-MM5FKgM14gj9VrgebcG)

我们可以看到现在的ra寄存器是0x80006392，它指向demo2函数，也就是sum\_then\_double的调用函数。之后我们执行代码，调用了sum\_to。

![](/files/-MM5Fs1G_y-4BlwL3RXf)

我们可以看到ra寄存器的值被sum\_to重写成了0x800065f4，指向sum\_then\_double，这也合理，符合我们的预期。我们在函数sum\_then\_double中调用了sum\_to，那么sum\_to就应该要返回到sum\_then\_double。

之后执行代码直到sum\_then\_double返回。如前面那位同学说的，因为没有恢复sum\_then\_double自己的Return address，现在的Return address仍然是sum\_to对应的值，现在我们就会进入到一个无限循环中。

我认为这是一个很好的例子用来展示为什么跟踪Caller和Callee寄存器是重要的。

> 学生提问，为什在最开始要对sp寄存器减16？
>
> TA：是为了Stack Frame创建空间。减16相当于内存地址向前移16，这样对于我们自己的Stack Frame就有了空间，我们可以在那个空间存数据。我们并不想覆盖原来在Stack Pointer位置的数据。
>
> 学生提问：为什么不减4呢？
>
> TA：我认为我们不需要减16那么多，但是4个也太少了，你至少需要减8，因为接下来要存的ra寄存器是64bit（8字节）。这里的习惯是用16字节，因为我们要存Return address和指向上一个Stack Frame的地址，只不过我们这里没有存指向上一个Stack Frame的地址。如果你看kernel.asm，你可以发现16个字节通常就是编译器的给的值。

接下来我们来看一些C代码。

![](/files/-MM8AM8rqcOaYozyza3b)

demo4函数里面调用了dummymain函数。我们在dummymain函数中设置一个断点，

![](/files/-MM8B3-AbW3Onhnzb77l)

现在我们在dummymain函数中。如果我们在gdb中输入info frame，可以看到有关当前Stack Frame许多有用的信息。

![](/files/-MM8BtVIjSM5xZ3lL4fn)

* Stack level 0，表明这是调用栈的最底层
* pc，当前的程序计数器
* saved pc，demo4的位置，表明当前函数要返回的位置
* source language c，表明这是C代码
* Arglist at，表明参数的起始地址。当前的参数都在寄存器中，可以看到argc=3，argv是一个地址

如果输入backtrace（简写bt）可以看到从当前调用栈开始的所有Stack Frame。

![](/files/-MM8FCpKDwxRqZQDjua0)

如果对某一个Stack Frame感兴趣，可以先定位到那个frame再输入info frame，假设对syscall的Stack Frame感兴趣。

![](/files/-MM8GApvnQ0OqMJGrX_2)

在这个Stack Frame中有更多的信息，有一堆的Saved Registers，有一些本地变量等等。这些信息对于调试代码来说超级重要。

> 学生提问：为什么有的时候编译器会优化掉argc或者argv？这个以前发生过。
>
> TA：这意味着编译器发现了一种更有效的方法，不使用这些变量，而是通过寄存器来完成所有的操作。如果一个变量不是百分百必要的话，这种优化还是很有常见的。我们并没有给你编译器的控制能力，但是在你们的日常使用中，你可以尝试设置编译器的optimization flag为0，不过就算这样，编译器也会做某些程度的优化。

（1:04:08 - 1:09:46 在介绍一些gdb技巧，conditional breakpoint，watchpoint等，与课程内容无关，故跳过）


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec05-calling-conventions-and-stack-frames-risc-v/5.5-stack.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
