我们在使用VS调试源代码或使用Windbg调试exe程序时,遇到异常,调试器就会中断下来,然后就能查看到此刻的函数调用堆栈。软件是执行到某一句机器代码产生了异常,可以看成执行了某一句汇编代码产生了异常,通过一句汇编代码,是如何将所在线程此刻完整的函数调用堆栈给回溯进来的呢?下面我们就来讲讲栈回溯的原理。
要搞清楚栈回溯的原理,需要对照着函数调用时的栈分布情况来看:
1、函数入口处的汇编代码
对照着上图,看一下函数入口的ebp和esp寄存器操作。
ebp - 函数栈基址寄存器,esp - 函数栈顶地址寄存器。函数占用的栈空间(地址范围)就在esp中的栈顶地址到ebp中的栈基址之间,函数的栈空间在函数入口处就进行分配了。
在每个函数的入口处都会有下面两句代码:
push ebp
mov ebp,esp
push的是主调函数的ebp,当前主调函数的栈顶地址esp,给被调函数ebp,即主调函数在调用函数时的栈顶地址就是被调函数的栈基址ebp。从栈内存分布来看,被调函数的栈基址,就是主调函数的栈基址值存放在栈内存上的内存地址。
2、函数退出处的汇编代码
每个函数退出处都会有下面两句汇编代码:(return之前的汇编代码)
mov esp,ebp
pop ebp
被调函数即将退出时,将自己(被调函数)的栈基址给主调函数的esp,即作为主调函数当前的栈顶地址。然后从栈中将主调函数的栈基址值拿出来,放到ebp寄存器中。
3、栈回溯的过程
首先通过当前发生异常的那句汇编代码的地址(代码段地址),通过遍历当前程序中所有函数的地址范围,即可得知当前发生异常的汇编指令位于哪个函数中。
就是以当前ebp寄存器中的值作为栈内存地址,该地址指向的栈内存中的4字节内容就是主调函数的栈基址值ebp(先记录着,为下一轮推算做准备),向下偏移4个字节的内存中存放着主调函数的返回地址,根据返回地址遍历当前程序中的函数地址范围,确定主调函数是哪个函数。
上面得到了主调函数的栈基址ebp,依次向上推算,确定更上一层的主调函数,这样就将函数调用栈给回溯出来了。