⚙️ 第一章 对程序员来说,CPU到底是什么
📄 一句话概括

函数调用只多了两个动作——把返回地址压进栈、跳过去;返回就是把它弹出来。

📝 本节和前面几节的关系

1.4 讲 PC 被改写,这一节讲被改写时怎么还能回得来。

一、调用一个函数,机器要做几件事

跳过去是必须的(改 PC)——但光跳过去不行:回来的时候不知道回哪。 所以调用之前,得先把【下一条指令的地址】存起来。存哪儿?栈。 教材的表述很简洁:call 指令会把下一条指令的地址存入栈。 所以函数调用在硬件上比普通跳转只多了一件事:多存一个地址。

二、栈:一摞只能从上头取的盘子

栈是一段内存,但它有个特殊规矩:只有顶端一个开口,放也从顶上、取也从顶上。 结果就是后放的先取出(LIFO,后进先出)。 这个规矩看着绕,用在函数调用上却严丝合缝——最后调用的那个函数,本来就该最先返回。 栈寄存器(esp)的作用就是记住【当前顶端在哪】——它每压一次就动一点。

三、嵌套调用看着最清楚

A 调 B、B 调 C,栈里依次压进:A 的下一条地址、B 的下一条地址。 C 执行完 return → 弹出 B 的下一条地址,回到 B。 B 执行完 return → 弹出 A 的下一条地址,回到 A。 一层层原路退回,顺序天然正确——不需要谁去做额外的记录。 这也是为什么栈被叫做【函数调用的账本】。

四、局部变量也在栈上

函数里的局部变量,通常也放在栈上——跟着这一次调用一起生、一起灭。 所以【函数一结束,局部变量就没了】不是被谁清掉了,而是栈顶回退,那块地方被收回。 顺带解释了另一件事:递归太深会把栈压满,程序崩的常见原因就是这个。

五、动手 Demo:函数调用——把返回地址压进栈

call 只做两件事:把下一条指令的地址压进栈,然后跳过去。 return 只做一件事:从栈里弹出地址,跳回那里。

stack = []
print("执行到 0x0100 的 call -> 压入返回地址,PC 跳到 0x0300")
stack.append("0x0105")
print("  栈里的内容:", stack)
print("在函数里干活……")
ret = stack.pop()
print("遇到 return -> 弹出 %s,PC 跳回" % ret)
print("  栈里的内容:", stack)

实跑输出:

--------------------------------------------------------------------
函数调用——把返回地址压进栈
--------------------------------------------------------------------

**call 只做两件事:把下一条指令的地址压进栈,然后跳过去。**
**return 只做一件事:从栈里弹出地址,跳回那里。**

  情形:主程序在 0x0100 处调用位于 0x0300 的函数

  步骤     CPU 在做什么               PC         栈里的内容
  ------------------------------------------------------------------
  1        执行到 0x0100 的 call      0x0300     0x0105(返回地址)
  2        在函数里干活……             0x0301     0x0105
  3        遇到 return                0x0105     (弹空了)
  4        继续从 0x0105 往下执行     0x0105     (空)

  两个关键点:
    压进栈的是**调用点的下一条地址**(0x0105),不是调用点本身
    函数嵌套时,栈里会叠起一层层返回地址——最后调用的最先返回

  **所谓【函数调用】,不过是把 PC 存起来、跳走、再取回来。**
  这也是为什么栈寄存器(esp)和 PC 一起,成了程序里最忙的两个寄存器。

压进栈的是【调用点的下一条地址】,不是调用点本身。 所谓函数调用,不过是把 PC 存起来、跳走、再取回来。

30 秒实验

  1. 写两个互相调用的函数,在纸上画出栈里依次压进了什么。
  2. 把递归的层数一直加大,观察程序什么时候出错——那就是栈被压满了。

常见误区

  1. 以为函数调用是【把那段代码复制过去执行】。代码只有一份,压进栈的是返回地址。
  2. 以为递归崩掉是算力不够。绝大多数情况是栈被压满了。
自测三问(点开对答案) 1. call 指令做了哪两件事? —— 把下一条指令的地址压进栈,然后跳转到函数入口。 2. 为什么嵌套调用能正确返回? —— 栈是后进先出,返回地址一层层弹出,顺序天然对应【后调用的先返回】。 3. 局部变量为什么跟着函数调用一起消失? —— 它们放在栈上;调用结束后栈顶回退,那块内存被回收。

相关笔记