陈同学
微服务
Accelerator
About
# Procedure Call and Stack ## 文章简介 最近查资料时,偶然在youtobe看到了华盛顿大学自然科学与工程一位老师 关于 **Procedure & Stacks** 的课程,深入讲解了基于Stack的过程调用,展示了应用级别和寄存器级别的处理过程,演示非常形象,受益良多。以下是课程重点及视频链接,可以自行翻墙观看。 * [1-Stacks](https://www.youtube.com/watch?v=7dLZRMDcY6c) * [2-Procedure Calls and Returns](https://www.youtube.com/watch?v=45g5c43wO4g) * [3-Stack-based languages](https://www.youtube.com/watch?v=4zBG7ZAvURE) * [4-Linux stack frame](https://www.youtube.com/watch?v=PrDsGldP1Q0) * [5-Registers and variables](https://www.youtube.com/watch?v=FdHj4NeOkF0) * [6-x86-64 Procedures and Stack](https://www.youtube.com/watch?v=uS4KO-rpvsU) 文本作为学习笔记,仅先记录过程调用时Stack和寄存器的变化. ## 课程笔记 ### Procedure Call Overview 下图为Caller(调用方) 调用 Callee(被调用方)的示例. Caller需要保存它在寄存器上的数据,因为Callee会覆盖;Caller需要设置参数,调用Callee,然后清理参数,将数据重新存储到寄存器,然后找到返回值。 Callee需要保存局部变量,存储返回值,将一些数据存储到寄存器,再返回到Caller  > save regs 表示保存寄存器数据;args 表示参数;local vars 表示局部变量; return val 表示返回值 为了实现上述过程,需要解决以下问题。 * **Callee** 需知道去哪儿找参数(机器没有传参之说,它只知道去哪儿读取数据,然后做何种计算) * **Callee** 需知道去哪儿找 "return address", 即Callee执行结束后如何返回到上图中Caller部分的`call`代码处,并继续执行**Caller**中的指令 * **Caller** 需要去哪儿找**Callee**返回的结果 * 由于**Caller** 和 **Callee** 运行在同一个CPU上,它们共享寄存器,因此它们需要自行存储寄存器上的数据。 * **Caller** 和 **Callee** 之间需要一定的约定,例如:**Callee**约定将返回值存到某个寄存器,**Caller**去某个寄存器读取数据即可,这是一种通过约定共享信息的方式。这种约定成为 **Procedure call linkage** ### Procedure Control Flow > 通过 Stack 来支持 procedure call 和 return. 先假设几个概念,方法main调用方法B,假设方法main的代码如下: ```java B(123); // call println("123"); // return address ``` 我们暂且称调用B()方法的指令为`call`指令,称`call`之后需要执行的指令(`println("123")`)的地址为 **return address(返回地址)** 那么调用时执行的指令可以用下图来表示:  * call 8048b90: 表示调用方法B()的指令 * 8048553: 表示返回地址,即执行`call`之后需要返回到Caller处继续执行(`println("123")`)的指令,需要把这条指令push到栈顶,这样B()执行完后可以返回回来,那么 **return address** 的值就是8048553 * 当B() return时,将 **return address** 从stack 中pop出来,这样就拿到了下一条需要执行的指令。然后再读取**return address**上存储的指令并执行即可(这条指令做的事情就是`println(result)`) ### Procedure Call Example > 说明:%eip、%esp、%eax等都是通用寄存器 > > esp专门作为存放当前线程的栈顶指针; > eip用于存放下一个待执行的CPU指令的内存地址,当CPU执行完当前指令后会从eip寄存器读取下一个指令的地址并继续执行 > eax是累加器,例如:add eax,-2 可以表示给给变量eax的当前值加上2 下图有2条待执行的指令: 804854e:这条指令中的call表示在main方法中调用下一个方法 8048553 :这条指令表示main方法中执行call之后待执行的下一条指令  此时,栈顶存储的**123** 是某个参数值;esp寄存器指向栈顶**0x108**,eip寄存器存储了下一条准备执行的指令 **804854e** 在准备执行`call 8048b90` **之前**. 为了在call之后能正常返回到Caller而且正确执行Caller的下一条指令,需先把return address即下一条要执行的指令(8048553)push到栈顶, 变化如下图:  此时,栈顶变为 **0x8048553**,同时esp存储新的栈顶元素0x104,eip存储了下一条待执行的地址0x8048553 接下来,准备执行 `call 8048b90 `,所eip寄存器存储了`8048b90 `作为待执行的指令  在`call`调用的方法执行结束后,需要返回到Caller继续执行Caller的后续指令。如下图: 8048591: 表示return到caller,结束当前方法的调用  因为马上要执行`ret`命令,因此将`8048591 `指令存到了eip寄存器,表示下一条待执行的指令是`0x8048591 `  执行`ret`之后,我们从栈顶去读取返回地址,读取的`8048553 `就是下一条需要执行的指令。 然后我们将`8048553`从栈顶pop出来,此时esp指向0x108(即存储123的位置), 0x104上的值虽然存在,但是没有任何意义。 eip指向了下一条待执行的指令`8048553`. 而`8048553`是返回地址,也就是`call`之后需要执行的下一条指令,这样就结束了Callee的方法调用,正常回到了Caller中. ## JVM Stack 通过上述学习,对于JVM Stack的理解就不再浮于表面的理解,类似于这种苍白的阐述:JMM包含虚拟机栈,栈包含栈帧,栈帧有局部变量表、操作数栈、返回链接, blablabla…... JMM之所以有Stack,是基于Stack数据结构来实现方法调用,保存方法调用轨迹(是不是用LinkedList也可以实现呢?)。 栈帧(Stack Frame):执行一个方法时会创建栈帧,用来存储局部变量(参数、方法内变量等)、返回地址(Caller call之后的下一条指令,提供给CPU来执行下一条指令)、指向上一个栈帧的指针等。 Stack中一个个栈帧的入栈/出栈就表示一个方法调用的开始与结束。栈中连续的栈帧可以体现出方法调用链,所以在发生异常时,我们才能获取到stacktrace(就是调用链轨迹,抓取栈中的所有栈帧即可)。同时,每个栈帧都存储了调用某个方法时的状态(即各种数据,如参数、变量等),因此除了获取到stacktrace,应该还可以获取到栈帧中的各种数据。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Procedure Call and Stack
文章链接:
https://chenyongjun.vip/articles/32
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力