Procedure Call and Stack

本文深入探讨了基于栈的过程调用机制,包括调用者与被调用者之间的交互流程、寄存器使用规范、栈帧结构以及x86-64过程调用约定等内容。通过具体实例和图解,帮助读者理解程序执行过程中栈和寄存器的变化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章来源:陈同学 | 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,应该还可以获取到栈帧中的各种数据。

Sure, here's a possible implementation of the ShowParams procedure in x86 assembly language: ``` ShowParams PROC paramCount:DWORD push ebp mov ebp, esp sub esp, 8 ; reserve space for local variables mov eax, [ebp + 8] ; get the number of parameters from the stack mov ebx, ebp ; initialize ebx to the base pointer add ebx, 12 ; skip the return address and the base pointer ; loop over the parameters and display their addresses and values mov ecx, 0 ; initialize a loop counter LoopParams: cmp ecx, eax ; have we displayed all the parameters? jge EndLoopParams push ebx ; save the current parameter address on the stack push dword ptr [ebx] ; push the parameter value on the stack push offset ParamFormat ; push the format string on the stack call printf ; call the C standard library printf function add esp, 12 ; clean up the stack add ebx, 4 ; move to the next parameter inc ecx ; increment the loop counter jmp LoopParams EndLoopParams: mov esp, ebp ; restore the stack pointer pop ebp ret ParamFormat db "Param %d: address=%p, value=%08Xh", 0 ShowParams ENDP ``` The procedure first saves the base pointer and sets up a new stack frame. It then subtracts 8 bytes from the stack pointer to reserve space for local variables. The loop over the parameters starts by getting the number of parameters from the stack and initializing the ebx register to the address of the first parameter. The loop counter is initialized to 0, and we jump to the label LoopParams. Inside the loop, we push the current parameter address, value, and a format string on the stack, and call the C standard library function printf to display them. We then clean up the stack and move to the next parameter. The loop counter is incremented, and we jump back to LoopParams if we haven't displayed all the parameters yet. Once the loop is done, we restore the stack pointer, pop the base pointer, and return. Note that this implementation assumes that the printf function is available and properly linked with the program. Also, the format string uses the %p and %08X format specifiers to display the parameter address and value in hexadecimal format, respectively. You may need to adjust the format string if your platform or compiler uses different conventions.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值