前言
在上一篇《前端魔法堂——异常不仅仅是try/catch》中我们描述出一副异常及如何捕获异常的画像,但仅仅如此而已。试想一下,我们穷尽一切捕获异常实例,然后仅仅为告诉用户,运维和开发人员页面报了一个哪个哪个类型的错误吗?答案是否定的。我们的目的是收集刚刚足够的现场证据,好让我们能马上重现问题,快速修复,提供更优质的用户体验。那么问题就落在“收集足够的现场证据”,那么我们又需要哪些现场证据呢?那就是异常信息,调用栈和栈帧局部状态。(异常信息我们已经获取了)
本文将围绕上调用栈和栈帧局部状态叙述,准开开车^_^
概要
本篇将叙述如下内容:
一.什么是调用栈?
既然我们要获取调用栈信息,那么起码要弄清楚什么是调用栈吧!下面我们分别从两个层次来理解~
印象派
倘若主要工作内容为应用开发,那么我们对调用栈的印象如下就差不多了:
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程序,透过其对应的汇编指令来讲解了。我会尽我所能用通俗易懂的语言描述这一切的,若有错误请各位指正!!
前提知识
- Intel X86架构中调用栈的栈底位于高位地址,而栈顶位于低位地址。(和印象派中示意图的方向刚好相反)
- 调用栈涉及的寄存器有
ESP/RSP, 暂存栈顶地址
EBP/RBP, 暂存栈帧起始地址
EIP, 暂存下一个CPU指令的内存地址,当CPU执行完当前指令后,从EIP读取下一条指令的内存地址,然后继续执行
- 操作指令
PUSH <OPRD>,将ESP向低位地址移动操作数所需的空间,然后将操作数压入调用栈中
POP <OPRD>,从调用栈中读取数据暂存到操作数指定的寄存器或内存空间中,然后向高位地址移动操作数对应的空间字节数
MOV <SRC>,<DST>,数据传送指令。用于将一个数据从源地址传送到目标地址,且不破坏源地址的内容
ADD <OPRD1>,<OPRD2>,两数相加不带进位,然后将结果保存到目标地址上
RET,相当于POP EIP。就是从堆栈中出栈,然后将值保存到EIP寄存器中
LEAVE,相当于MOV EBP ESP,然后再POP EBP。就是将栈顶指向当前栈帧地址,然后将调用者的栈帧地址暂存到EBP中
- 每个函数调用前汇编器都会加入以下前言(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