前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Procedure Call and Stack

Procedure Call and Stack

原创
作者头像
码代码的陈同学
发布2018-05-18 12:22:55
1.5K0
发布2018-05-18 12:22:55
举报

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

文章简介

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

文本作为学习笔记,仅先记录过程调用时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的代码如下:

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,应该还可以获取到栈帧中的各种数据。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章简介
  • 课程笔记
    • Procedure Call Overview
      • Procedure Control Flow
        • Procedure Call Example
        • JVM Stack
        相关产品与服务
        数据保险箱
        数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档