函数调用栈的混淆

10

根据维基百科的说法:

调用方将返回地址推入堆栈,当被调用的子程序完成时,它会从调用堆栈中弹出返回地址,并将控制转移到该地址。

维基图片:

enter image description here

我不太理解这个。 假设我有一个 C 程序如下:

#include <stdio.h>

int foo(int x)
{
    return x+1;
}

void spam()
{
    int a = 1;  //local variable
    int b = foo(a);  //subroutine called
    int c = b;  //local variable
}

int main()
{
    spam();
    return 0;
}

我认为调用栈应该像下面的图示一样:

<None> means none local variables or params

      _| parameters for foo() <int x>  |_
top    | local of spam() <int c>       |
^      | return address of foo()       |<---foo() called, when finishes, return here?
|      | local of spam() <int b>       |
bot    | local of spam() <int a>       |
      _| parameters for spam() <None>  |_
       | locals of main() <None>       | 
       | return address of spam()      |<---spam() called, when finishes, return here?
       | parameters for main() <None>  |

问题:

根据从Wiki引用的话,

被调用的子程序在结束时会从调用栈中弹出返回地址,并将控制权转移至该地址。

1.我的图示正确吗?

2.如果我的图示正确,那么当foo()结束时,它将会

从调用栈中弹出返回地址并将控制权转移到该地址

,但是它如何能够弹出返回地址呢? 因为当foo结束时,当前的堆栈指针指向spam的本地变量,对吗?

更新:

如果main()看起来像这样:

int main()
{ 
    spam();
    foo();
}

那么调用栈应该是什么样子的?

1个回答

15

你的绘图不正确。一个函数的本地堆栈变量都在任何返回地址下面。否则,正如你所观察到的,当你调用一个函数时局部变量会丢失。

应该像这样:

| parameters for foo() <int x>  |
| return address of foo()       |
| local of spam() <int c>       |
| local of spam() <int b>       |
| local of spam() <int a>       |
| parameters for spam() <None>  |
| return address of spam()      |
| locals of main() <None>       | 
| parameters for main() <None>  |

我认为混淆的原因在于您认为变量声明被视为语句并按顺序执行。实际上,编译器通常会分析函数以确定需要多少堆栈空间用于存储所有局部变量。然后它会发出代码来相应地调整堆栈指针,并在进入函数时进行该调整。任何对其他函数的调用都可以将其推送到堆栈上,而不会干扰该函数的堆栈帧。


是的,先生,我同意您的观点。但在“spam()”中,“foo()”在“<int c = b>”之前被调用,对吗?那么我应该把“int c”放在调用栈的哪里呢?还是在返回地址下面? - Alcott
3
通常在函数执行开始时,局部变量的栈空间分配一次性完成。因此,在调用foo函数之前,a、b和c的栈空间都已经被预先分配好了。 - David Heffernan
明白了,先生。如果我在主函数中调用spam()和foo()(见更新),调用栈会是什么样子?您的意思是一个函数的堆栈帧大小是其本地变量大小的总和吗? - Alcott
David说得对,似乎你误解了变量声明是按顺序执行的语句。任何一个函数调用只会在调用栈上推入一个堆栈帧。这个堆栈帧将已经拥有函数所需的所有空间(由编译器定义)。每个堆栈帧都有一个返回地址,用于函数返回,并且有一定数量的空间用于所有参数和局部变量。 - Lncn
1
我想我已经回答过这个问题了。我不想在修改问题并提出后续问题的漫长过程中陷入。然而,在你的更新中,你通过推送返回值、调用函数、弹出返回值和跳转到那里来称之为垃圾邮件。然后你以完全相同的方式调用foo。最后,你的更新问题没有完全指定,因为调用堆栈在执行的不同阶段将会有所不同。 - David Heffernan

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