⚙️ 第十章 通过汇编语言认识程序的真面目
📄 一句话概括

栈是自动管理的一块内存,用 pushl 和 popl 两条指令操作;它的规矩是后进先出——函数调用、局部变量、返回地址全靠这条规矩活着。

📝 本节和前面几节的关系

10.5 说的是在寄存器和内存之间搬,这一节专门讲其中最有规矩的那块内存。

一、栈:一块很守规矩的内存

普通的内存是这样的:你想写哪儿就写哪儿,随心所欲。 栈不一样,它只允许你从一头进出——这个头叫栈顶。 放进去叫压栈(push),取出来叫出栈(pop)。 因为只有一头,所以顺序被天然定死了:最后放进去的,最先被取出来。

二、两条指令,一个规矩

pushl 值——把一个值压到栈顶。 popl 目标——把栈顶的值取出来,放到目标里。 栈顶在哪儿,由一个叫 esp 的寄存器记着(1.5 讲过,它每压一次就动一点)。 拿一串 push 和 pop 跑一遍,栈怎么长、值怎么出来,全都看得见。

三、实测:先取出 2,再取出 1

先把 1 压进去,再把 2 压进去——栈里从底到顶是 [1, 2]。 接着弹出两次:第一次弹出的是 2,第二次才是 1。 这就是后进先出(LIFO)——最后进去的,最先出来。 有人说栈是【一摞盘子】——只能从最上面拿,这个比喻在这里很准。

四、为什么函数离不开它

因为函数调用需要一个【能记住来路】的地方,而且天生就是层层嵌套的。 A 调 B、B 又调 C——每层都把返回地址押在栈上。 C 先返回、再轮到 B、最后回到 A——正好是后进先出的顺序,一点不用额外安排。 所以后进先出不是巧合,它是【嵌套调用】这件事在内存上最自然的形状。

五、动手 Demo:将数据存入栈中

栈是自动管理的一块内存,用 pushl 和 popl 操作。 跑一串 push 与 pop,看栈是怎么长的。

prog = [
    ("movl", "$1", "%eax"),
    ("movl", "$2", "%ebx"),
    ("pushl", "%eax"),
    ("pushl", "%ebx"),
    ("popl", "%ecx"),
    ("popl", "%edx"),
]
vm = VM(prog)
while vm.pc < len(prog):
    shown, before, after = vm.step()
    print(shown, "栈=", vm.stack, "ecx=", after["ecx"], "edx=", after["edx"])

实跑输出:

--------------------------------------------------------------------
将数据存入栈中
--------------------------------------------------------------------

**栈是自动管理的一块内存**,用两条指令操作:

    pushl 把一个值压到栈顶
    popl  把栈顶的值取出来

  跑一串 push 与 pop,看栈是怎么长的:

  指令              eax     ebx     ecx     edx     栈(底→顶)
  --------------------------------------------------------------
  movl $1, %eax     1       0       0       0       (空)
  movl $2, %ebx     1       2       0       0       (空)
  pushl %eax        1       2       0       0       [1]
  pushl %ebx        1       2       0       0       [1, 2]
  popl %ecx         1       2       2       0       [1]
  popl %edx         1       2       2       1       (空)

  **注意最后两步取出来的顺序:先取出 2,再取出 1。**
  这就是**后进先出**——**函数调用、局部变量、返回地址,全都靠这条规矩活着。**
  1.5 讲过的【把返回地址压进栈、跳过去;返回就是把它弹出来】,靠的就是这条规矩。

实测:压入 1、2 之后栈是 [1, 2];弹出两次先得到 2,再得到 1。 后进先出——函数调用、局部变量、返回地址,全靠这条规矩活着。

30 秒实验

  1. 在纸上推一遍:再压一个 3,然后连弹三次,顺序是什么。
  2. 想一想:如果栈不是后进先出,函数嵌套调用还能不能成立。

常见误区

  1. 以为栈和普通内存一样可以随便读写。它只允许从栈顶进出。
  2. 以为后进先出是人为规定的。它是嵌套调用最自然的形状。
自测三问(点开对答案) 1. 栈的两条操作指令是什么? —— pushl 压栈、popl 出栈,都只作用于栈顶。 2. 后进先出是什么意思? —— 最后压进去的值最先被弹出来,栈里从底到顶是 [1, 2],弹出顺序就是 2、1。 3. 为什么函数调用离不开栈? —— 因为要记住返回地址,而嵌套调用天然是后进先出的顺序,栈正好匹配。

相关笔记