"前端魔法堂——调用栈,异常实例中的宝藏 "

本文深入探讨了调用栈的概念,通过具体示例说明了调用栈的工作原理及其在前端异常处理中的作用。

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

点击有惊喜


前言

 在上一篇《前端魔法堂——异常不仅仅是try/catch》中我们描述出一副异常及如何捕获异常的画像,但仅仅如此而已。试想一下,我们穷尽一切捕获异常实例,然后仅仅为告诉用户,运维和开发人员页面报了一个哪个哪个类型的错误吗?答案是否定的。我们的目的是收集刚刚足够的现场证据,好让我们能马上重现问题,快速修复,提供更优质的用户体验。那么问题就落在“收集足够的现场证据”,那么我们又需要哪些现场证据呢?那就是异常信息调用栈栈帧局部状态。(异常信息我们已经获取了)
 本文将围绕上调用栈栈帧局部状态叙述,准开开车^_^

概要

 本篇将叙述如下内容:

  1. 什么是调用栈?
  2. 如何获取调用栈?
  3. 什么是栈帧局部状态?又如何获取呢?

一.什么是调用栈?

 既然我们要获取调用栈信息,那么起码要弄清楚什么是调用栈吧!下面我们分别从两个层次来理解~

印象派

 倘若主要工作内容为应用开发,那么我们对调用栈的印象如下就差不多了:

function funcA (a, b){
  return a + b
}

function funcB (a){
  let b = 3
  return funcA(a, b)
}

function main(){
  let a = 5
  funcB(a)
}

main()

 那么每次调用函数时就会生成一个栈帧,并压入调用栈,栈帧中存储对应函数的局部变量;当该函数执行完成后,其对应的栈帧就会弹出调用栈。
 因此调用main()时,调用栈如下

----------------<--栈顶
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

 调用funcB()时,调用栈如下

----------------<--栈顶
|function:funcB|
|let b = 3     |
|return funcA()|
----------------
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

 调用funcA()时,调用栈如下

----------------<--栈顶
|function:funcA|
|return a + b  |
----------------
|function:funcB|
|let b = 3     |
|return funcA()|
----------------
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

funcA()执行完成后,调用栈如下

----------------<--栈顶
|function:funcB|
|let b = 3     |
|return funcA()|
----------------
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

funcB()执行完成后,调用栈如下

----------------<--栈顶
|function: main|
|let a = 5     |
|return void(0)|
----------------<--栈底

main()执行完成后,调用栈如下

----------------<--栈顶
----------------<--栈底

 现在我们对调用栈有了大概的印象了,但大家有没有留意上面记录"栈帧中存储对应函数的局部变量",栈帧中仅仅存储对应函数的局部变量,那么入参呢?难道会作为局部变量吗?这个我们要从理论的层面才能得到解答呢。

理论派

 这里我们要引入一个简单的C程序,透过其对应的汇编指令来讲解了。我会尽我所能用通俗易懂的语言描述这一切的,若有错误请各位指正!!

前提知识
  1. Intel X86架构中调用栈的栈底位于高位地址,而栈顶位于低位地址。(和印象派中示意图的方向刚好相反)
  2. 调用栈涉及的寄存器有
ESP/RSP, 暂存栈顶地址
EBP/RBP, 暂存栈帧起始地址
EIP, 暂存下一个CPU指令的内存地址,当CPU执行完当前指令后,从EIP读取下一条指令的内存地址,然后继续执行
  1. 操作指令
PUSH <OPRD>,将ESP向低位地址移动操作数所需的空间,然后将操作数压入调用栈中
POP <OPRD>,从调用栈中读取数据暂存到操作数指定的寄存器或内存空间中,然后向高位地址移动操作数对应的空间字节数
MOV <SRC>,<DST>,数据传送指令。用于将一个数据从源地址传送到目标地址,且不破坏源地址的内容
ADD <OPRD1>,<OPRD2>,两数相加不带进位,然后将结果保存到目标地址上
RET,相当于POP EIP。就是从堆栈中出栈,然后将值保存到EIP寄存器中
LEAVE,相当于MOV EBP ESP,然后再POP EBP。就是将栈顶指向当前栈帧地址,然后将调用者的栈帧地址暂存到EBP
  1. 每个函数调用前汇编器都会加入以下前言(Prolog),用于保存栈帧和返回地址
push   %rbp      ;将调用者的栈帧指针压入调用栈
mov    %rsp,%rbp ;现在栈顶指向刚入栈的RBP内容,要将其设置为栈帧的起始位置

 现在们结合实例来理解吧!
C语言

#include <stdio.h>

int add(int a, int b){
  return a + b;
}
int add2(int a){
  int sum = add(0, a);
  return sum + 2;
}

void main(){
  add2(2);
}

然后执行以下命令编译带调试信息的可执行文件,和dump文件

$ gcc -g -o main main.c
$ objdump -d main > main.dump

下面我们截取main、add2和add对应的汇编指令来讲解

main函数对应的汇编指令

0x40050f <main>                 push   %rbp
0x400510 <main+1>               mov    %rsp,%rbp
;将2暂存到寄存器EDI中
0x400513 <main+4>               mov    $0x2,%edi
;执行call指令前,EIP寄存器已经存储下一条指令的地址0x40051d了
;首先将EIP寄存器的值入栈,当函数返回时用于恢复之前的执行序列
;然后才是执行JUMP指令跳转到add2函数中开始执行其第一条指令
0x400518 <main+9>               callq  0x4004ea <add2>
;什么都不做
0x40051d <main+14>              nop
;设置RBP为指向main函数调用方的栈帧地址
0x40051e <main+15>              pop    %rbp
;设置EIP指向main函数返回后将要执行的指令的地址
0x40051f <main+16>              retq

下面是执行add2函数第一条指令前的调用栈快照

+++++++++++++++++ 高位地址
99 |   110    | -- 存放main函数调用方的栈帧地址 <-- EBP
+++++++++++++++++
98 | 0x40051d | -- EIP的值,存放add2返回后将执行的指令的地址 <-- ESP
+++++++++++++++++ 低位地址

add2函数对应的汇编指令

0x4004ea <add2>                 push   %rbp
0x4004eb <add2+1>               mov    %rsp,%rbp
0x4004ee <add2+4>               sub    $0x18,%rsp      ;栈顶向低位移动24个字节,为后续操作预留堆栈空间
0x4004f2 <add2+8>               mov    %edi,-0x14(%rbp);从EDI寄存器中读取参数,并存放到堆栈空间中
0x4004f5 <add2+11>              mov    -0x14(%rbp),%eax;从堆栈空间中读取参数,放进EAX寄存器中
0x4004f8 <add2+14>              mov    %eax,%esi       ;从EAX寄存器中读取参数,存放到ESI寄存器中
0x4004fa <add2+16>              mov    $0x0,%edi       ;将0存放到EDI寄存器中
;执行call指令前,EIP寄存器已经存储下一条指令的地址0x400504了
;首先将EIP寄存器的值入栈,当函数返回时用于恢复之前的执行序列
;然后才是执行JUMP指令跳转到add函数中开始执行其第一条指令
0x4004ff <add2+21>              callq  0x4004d6 <add>
0x400504 <add2+26>              mov    %eax,-0x4(%rbp) ;读取add的返回值(存储在EAX寄存器中),存放到堆栈空间中
0x400507 <add2+29>              mov    -0x4(%rbp),%eax ;又将add的返回值存放到EAX寄存器中(这是有多无聊啊~~)
0x40050a <add2+32>              add    $0x2,%eax       ;读取EAX寄存器的值与2相加,结果存放到EAX寄存器中
0x40050d <add2+35>              leaveq                 ;让栈顶指针指向main函数的栈帧地址,然后让EBP指向main函数的栈帧地址
0x40050e <add2+36>              retq                   ;让EIP指向add2返回后将执行的指令的地址

下面是执行完add2函数中mov %rsp,%rbp的调用栈快照

+++++++++++++++++ 高位地址
99 |    110   | -- 存放main函数调用方的栈帧地址
+++++++++++++++++
98 | 0x40051d | -- 存放EIP的值,add2返回后将执行的指令的地址
+++++++++++++++++
97 |    99    | -- 存放add2函数调用方(即main函数)的栈帧地址<-- ESP,EBP
+++++++++++++++++ 低位地址

下面是执行add函数第一条指令前的调用栈快照

+++++++++++++++++ 高位地址
99 |    110   | -- 存放main函数调用方的栈帧地址
+++++++++++++++++
98 | 0x40051d | -- 存放EIP的值,add2返回后将执行的指令的地址
+++++++++++++++++
97 |    99    | -- 存放add2函数调用方(即main函数)的栈帧地址<-- EBP
+++++++++++++++++
96 |   0xXX   |
+++++++++++++++++
.................
76 |   0x02   | -- 这是`mov %edi,-0x14(%rbp)`的执行结果
+++++++++++++++++
.................
+++++++++++++++++
73 |   0xXX   |
+++++++++++++++++
72 | 0x400504 | -- EIP的值,存放add返回后将执行的指令的地址 <-- ESP
+++++++++++++++++ 低位地址

add函数对应的汇编指令

0x4004d6 <add>                  push   %rbp
0x4004d7 <add+1>                mov    %rsp,%rbp
0x4004da <add+4>                mov    %edi,-0x4(%rbp)
0x4004dd <add+7>                mov    %esi,-0x8(%rbp)
0x4004e0 <add+10>               mov    -0x4(%rbp),%edx
0x4004e3 <add+13>               mov    -0x8(%rbp),%eax
0x4004e6 <add+16>               add    %edx,%eax
0x4004e8 <add+18>               pop    %rbp
0x4004e9 <add+19>               retq

点击有惊喜


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值