栈回溯简单实现(x86)
0x01 栈简介
首先局部变量的分配释放是通过调整栈指针实现的,栈为函数调用和定义局部变量提供了一块简单易用的空间,定义在栈上的变量不必考虑内存申请和释放。只要调整栈指针就可以分配和释放内存。
每个函数在栈中使用的区域叫做栈帧Stack Frame,在X86中,通常使用EBP寄存器作为帧指针使用,EBP寄存器所指向的栈单元中保存的是前一个EBP寄存器的值,通常也就是父函数的EBP值。帧指针不仅对函数中的代码起到定位变量和参数的参照物作用,而且将栈中的一个个栈帧串联在一起,形成了一个可以遍历所有栈帧的链条,这也就是栈回溯的基本原理。值得注意的是必须要保证函数返回时栈指针的值与进入函数时一致,即保证栈平衡。
0x02 栈帧
百度百科的经典解释:“栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
可以理解为:栈帧就是存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元。栈帧表示程序的函数调用记录,而栈帧又是记录在栈上面,很明显栈上保持了N个栈帧的实体
0x03 栈回溯简单实现
通过帧指针ebp来实现函数返回地址,调用地址的获取。
对于这里的call指令,研究的是E8类型的call指令机器码,其他类型的call指令并未采取相关的操作。
机器码e8后面的四字节是一个相对偏移,即当前指令指针中(EIP)(即下一条指令的地址)的值与目的地址(被调函数首地址)的差值。所以目的地址(被调函数首地址)的计算方法为: 目的地址 = 返回地址 + 相对偏移(四字节机器码)
下一条指令地址:0x011c1722 + 5 = 011c1727
如上图:0x011c1727 + 0xFFFFF933 = 0x011c105a目的地址(被调函数首地址)
所以可以根据call机器指令的特征码E8来找到函数返回地址和被调用函数地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void __stdcall StackTraceFunction( int StackBase, int ebp, int esp) { LONG_PTR LimitCount = 30; LONG_PTR RetAddress = 0; LONG_PTR CalleeFunctionAddress = 0; printf ( "ebp RetAddress CalleeFunctionAddress\n" ); while ((ebp > esp) && (ebp < StackBase) && (LimitCount--)) { int v1 = ebp; int v2 = esp; RetAddress = *( LONG_PTR *)(ebp + 4); CalleeFunctionAddress = 0; if (*(unsigned char *)(RetAddress - 5) == 0xe8) { CalleeFunctionAddress = *( LONG_PTR *)(RetAddress - 4) + RetAddress; } printf ( "%08x %08x %08x\n" , ebp, RetAddress, CalleeFunctionAddress); ebp = *( LONG_PTR *)ebp; } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗