Procedure Call and Stack

2018-05-18 12:22:55 浏览数 (1)

欢迎访问作者博客原文,更好的阅读体验: 陈同学 | Procedure Call and Stack

文章简介

最近查资料时,偶然在youtobe看到了华盛顿大学自然科学与工程一位老师 关于 Procedure & Stacks 的课程,深入讲解了基于Stack的过程调用,展示了应用级别和寄存器级别的处理过程,演示非常形象,受益良多。以下是课程重点及视频链接,可以自行访问外国网站观看。

  • 1-Stacks
  • 2-Procedure Calls and Returns
  • 3-Stack-based languages
  • 4-Linux stack frame
  • 5-Registers and variables
  • 6-x86-64 Procedure Calling Convention

文本作为学习笔记,仅先记录过程调用时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返回的结果
  • 由于CallerCallee 运行在同一个CPU上,它们共享寄存器,因此它们需要自行存储寄存器上的数据。
  • CallerCallee 之间需要一定的约定,例如:Callee约定将返回值存到某个寄存器,Caller去某个寄存器读取数据即可,这是一种通过约定共享信息的方式。这种约定成为 Procedure call linkage

Procedure Control Flow

通过 Stack 来支持 procedure call 和 return.

先假设几个概念,方法main调用方法B,假设方法main的代码如下:

代码语言:javascript复制
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、�x等都是通用寄存器 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,应该还可以获取到栈帧中的各种数据。

0 人点赞