调试器的实现
来源:互联网 发布:淘宝联盟怎么操作步骤 编辑:程序博客网 时间:2024/06/06 01:29
[Win32]一个调试器的实现(十一)显示函数调用栈
本文讲解如何在调试器中显示函数调用栈,如下图所示:
原理
首先我们来看一下显示调用栈所依据的原理。每个线程都有一个栈结构,用来记录函数的调用过程,这个栈是由高地址向低地址增长的,即栈底的地址比栈顶的地址大。ESP寄存器的值是栈顶的地址,通过增加或减小ESP的值可以缩减或扩大栈的大小。上一篇文章已经简略地介绍过在调用函数时线程栈上会发生什么事情,现在我们再来详细地看看这个过程:
①在栈上压入参数。
②执行CALL指令,在栈上压入函数的返回地址。
③压入EBP寄存器的值。
④将ESP寄存器的值赋给EBP寄存器。
⑤减小ESP寄存器的值,为局部变量分配空间。
⑥执行函数代码。
⑦将EBP寄存器的值赋给ESP寄存器,等于回收了局部变量的空间。
⑧弹出栈顶的值,赋给EBP,即将第③步中压入的值重新赋给EBP。
⑨执行RET指令,弹出栈顶的返回地址。如果被调用函数负责回收参数的空间,则需要增加ESP的值。
完成第③步的指令是push ebp,它是所有函数的第一条指令,因此每个函数在栈上都会保存有一个EBP值,标志了一个函数调用的开始,这就像分界线一样,将每个函数调用区分开来。从一个分界线开始,到下一个分界线之间的部分称作“栈帧”,一个栈帧代表一个函数调用。
在压入了EBP的值之后,第④步立即将ESP的值赋给了EBP,此时ESP和EBP的值都是刚刚压入的值的地址。从此之后,ESP的值随着指令的执行不断变化,而EBP的值在当前栈帧中永远不会改变,一直指向当前栈帧的起始地址,所以EBP也被称为“栈帧指针”。
函数返回的时候,第⑦步将EBP的值赋给ESP,此时ESP指向第③步压入的值,然后第⑧步弹出这个值,赋给EBP,恢复EBP在上一个函数调用中的值。
函数调用过程的第③步和第④步使得各个压入的EBP值形成了一个链表的结构,而EBP寄存器是链表的表头,如下图所示:
正是这种链表结构的存在,使得获取函数调用栈成为可能。只要从EBP寄存器开始,沿着链表层层往上,就可以得到函数调用的轨迹。
由于EBP在当前函数调用中的不变性,调试版的程序都使用EBP作为变量和参数的基址,将EBP的值与一个偏移值相加就可以得到变量或参数的地址。有些发行版的程序会对函数的调用过程进行优化,省略了压入EBP的步骤,因此不能再使用EBP作为变量和参数的基址,也不能使用EBP链表来获取函数调用栈。
StackWalk64
在DbgHelp中主要使用StackWalk64函数来获取函数调用栈,该函数的声明如下:
2 DWORD MachineType,
3 HANDLE hProcess,
4 HANDLE hThread,
5 LPSTACKFRAME64 StackFrame,
6 PVOID ContextRecord,
7 PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine,
8 PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,
9 PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine,
10 PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress
11 );
该函数的参数比较多,这意味着灵活性,同时也意味着复杂性。事实上StackWalk64有多种不同的使用方式,使用何种方式由传入的参数决定。在这里我只介绍一种最简单的方式,这种方式已经足够了。如果想了解更多有关StackWalk64的信息,请参考MSDN。
MachineType参数指定CPU的类型,它的取值范围以及意义如下表所示(摘自MSDN):
Value
Meaning
IMAGE_FILE_MACHINE_I386
Intel x86
IMAGE_FILE_MACHINE_IA64
Intel Itanium Processor Family (IPF)
IMAGE_FILE_MACHINE_AMD64
x64 (AMD64 or EM64T)
调试器需要根据CPU的类型来设置该参数的值。在目前,大部分情况下都是设置为IMAGE_FILE_MACHINE_I386。
hProcess和hThread分别指定被调试进程的进程句柄以及线程句柄。而且在当前所使用的方式下,hProcess必须是符号处理器的标识符。如果在调用SymInitialize创建符号处理器时使用的就是进程的句柄,那么在这里不会有任何问题;如果不是使用进程句柄,那么就必须用另一种方式调用StackWalk64了。
StackFrame参数是一个STACKFRAME64结构体的指针,在调用StackWalk64之前需要初始化这个结构体,函数调用成功后,前一个栈帧的信息会保存到该结构体中;然后用这些信息再次调用StackWalk64,以获取再前一个栈帧的信息……由此看出,StackWalk64的工作就是获取指定栈帧的前一个栈帧,所以,必须要在循环中获取所有栈帧。STACKFRAME64结构体中需要初始化的字段有三个:AddrPC,AddrStack和AddrFrame,它们分别表示程序计数器,线程栈顶以及栈帧指针,也是EIP,ESP和EBP的用途。这三个字段又分别是一个ADDRESS64结构体,这个结构体可以表示多种不同类型的地址,但Windows应用程序只会使用虚拟地址,所以Mode字段应设为AddrModeFlat,Offset字段设为上述寄存器的值。STACKFRAME64结构体的初始化代码如下所示(context为CONTEXT结构):
2 stackFrame.AddrPC.Mode = AddrModeFlat;
3 stackFrame.AddrPC.Offset = context.Eip;
4 stackFrame.AddrStack.Mode = AddrModeFlat;
5 stackFrame.AddrStack.Offset = context.Esp;
6 stackFrame.AddrFrame.Mode = AddrModeFlat;
7 stackFrame.AddrFrame.Offset = context.Ebp;
第一次成功调用StackWalk64后,STACKFRAME64结构体的其它字段会被设置为适当的值,而上述的三个字段不会改变。从第二次调用开始才会真正获取前一个栈帧,这三个字段才会改变。
ContextRecord参数是指向CONTEXT结构体的指针,调用之前需要使用GetThreadContext初始化该结构体,StackWalk64函数会使用里面的值,并有可能会修改它。
ReadMemoryRoutine是一个回调函数的指针,当StackWalk64函数需要读取被调试进程的内存时会调用该函数。如果不想提供这样的函数,最简单的方法就是设置该参数为NULL,这样StackWalk64就会使用默认的函数,此时hProcess必须是一个有效的进程句柄。
FunctionTableAccessRoutine也是一个回调函数的指针,当StackWalk64需要访问函数表时会调用该函数。简单来说,函数表保存了每一个函数的信息,比如起始地址,长度等。这个参数不能为NULL,但是我们可以将它设置为一个已有的函数,这个函数就是SymFunctionTableAccess64。此时hProcess必须是符号处理器的标识符。
GetModuleBaseRoutine又是一个回调函数的指针,当StackWalk64需要获取模块的基地址时会调用该函数。将该参数设置为GetModuleBase64函数即可,此时hProcess也必须是符号处理器的标识符。
最后的参数TranslateAddress仍然是回调函数的指针,不过该参数只用于16位地址的转换,几乎不会用到,设置为NULL即可。
可以看到,选择StackWalk64的何种使用方式由后面的四个参数决定。最简单的使用方式就是直接使用NULL或DbgHelp提供的函数作为这几个参数的值,不过此时对hProcess和hThread的限制最大。我们也可以自己提供这几个回调参数,此时hProcess和hThread几乎没有什么限制,它们只是作为唯一标识符。
StackWalk64调用成功后,STACKFRAME64结构体被赋值,在众多的字段中,我们只需要关心AddrPC,它表示栈帧的返回地址(除了第一次调用StackWalk64之外),即CALL指令下一条指令的地址,本文开头的图片中显示的地址就是AddrPC.Offset的值。由于返回地址是指向前一个栈帧的,所以每次调用StackWalk64都会使STACKFRAME64结构体填充前一个栈帧的信息。StackWalk64只能获取用户模式下的栈帧,如果栈帧遍历完毕,它会返回FALSE。
获取函数名称
STACKFRAME64结构体的AddrPC字段的值肯定是某个函数内的地址,所以可以用这个字段的值来调用SymFromAddr获取函数的信息,包括函数名称。关于SymFromAddr函数的用法,前面的文章已经介绍过了,这里不再重复。
获取模块名称
显示函数调用栈时最好同时显示函数所在的模块,这样可以方便知道每个函数位于哪个模块。有一个SymGetModuleInfo64函数可以获取模块的信息,但是却不可以获取模块的名称,而另一个SymEnumerateModules64函数可以做到这点,虽然它的使用方式比较麻烦。SymEnumerateModules64用于枚举所有已经加载到符号处理器中的模块,它的声明如下:
2 HANDLE hProcess,
3 PSYM_ENUMMODULES_CALLBACK64 EnumModulesCallback,
4 PVOID UserContext
5 );
第一个参数是符号处理器的标识符。第二个参数是一个回调函数的指针,对于每个模块都会调用这个函数。该回调函数的声明如下:
2 PCSTR ModuleName,
3 DWORD64 BaseOfDll,
4 PVOID UserContext
5 );
ModuleName是模块文件的绝对路径;BaseOfDll是模块的基地址;UserContext就是SymEnumerateModules64的第三个参数,可以通过这个参数给回调函数传递更多信息。
可以这样使用SymEnumerateModules64函数:使用STL的map创建模块的基址-名称映射表,在回调函数中往这个表中添加记录。然后使用SymGetModuleBase64函数获取模块的基地址,使用这个基地址从表中查找模块的名称。具体的做法请参考示例代码。
示例代码
MiniDebugger中新增了一个命令:
w
显示函数调用栈。
http://files.cnblogs.com/zplutor/MiniDebugger11.rar
----------
本文是《一个调试器的实现》系列文章的最后一篇。实现一个调试器不是简单的事情,毕竟这不是主流的应用,相关的文档非常匮乏,只能靠自己不断地摸索前进。我在实现MiniDebugger的过程中遇到了非常多的困难,在解决这些困难的过程中有很多心得体会,于是将它们写成了这一系列文章,与大家分享我的经验,希望能让大家少走弯路,节省宝贵的时间。虽然最终实现的MiniDebugger非常丑陋,但是透过它所表达出来的技术原理,一定能帮助大家实现一个更优秀的调试器。对技术的追求,是我不断前进的动力。
作者:Zplutor
出处:http://www.cnblogs.com/zplutor/
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
» 博主后一篇:[Win32]IP数据报的首部如何定义
评论:
在此请教解决方法。你的程序好像是在VS2010上编译的运行的。这个我也实验了,也不行。我用的是VS2008.
还有就是能不能不依靠dbghelp.dll,而编写一个控制台的调试器呢。请赐教?谢谢
你好,出现这个问题的原因可能是系统中的dbghelp.dll版本不正确或者损坏,你可以尝试一下将Visual Studio使用的dbghelp.dll复制到项目文件夹下再运行看看。获取dbghelp.dll的方法可以参考《一个调试器的实现(五)》。
另,不依赖dbghelp.dll而写出调试器是可能的,但是这个难度非常大,因为你必须对符号文件的格式非常了解,并自己写程序去解析这些符号文件。
另外,如果我要是编写数据库的调试器的话,即SQL语言的调试器。那么dbghelp.dll还可以使用吗,调试符号又是什么呢,还是要借助pdb文件吗。还是要自己编写调试符号。有没有可以借用的调试符号文件呢。
谢谢!
DbgHelp是不能用来调试SQL的,因为SQL需要由存储引擎来执行,它的执行方式与Windows应用程序有很大不同。据我所知Visual Studio可以调试SQL的存储过程,但是我不知道它是如何做到的,所以我回答不了你的问题,很抱歉!
· 科学家宣称人类20年内将实现长生不老
· Windows 8触控虚拟键盘诞生记!
· 通过界面拖拽实现交互,FlixMaster要做交互视频领域的WordPress
· 诺基亚嘴硬?称Lumia 900“寿命还很长”
· 扎克伯格竟然赢得乔布斯生前的敬佩!
» 更多新闻...
· 说说JSON和JSONP,也许你会豁然开朗
· 写更少的代码
· 一次谷歌面试趣事
· 程序员究竟该如何提高效率
China-Pub 低价书精选
China-Pub 计算机绝版图书按需印刷服务
园龄:2年11个月
粉丝:25
关注:1
搜索
常用链接
- 我的随笔
- 我的评论
- 我的参与
- 最新评论
- 我的标签
随笔分类
- ASP.NET(3)
- C#
- C/C++(5)
- Others(5)
- SQL Server(5)
- Win32(14)
- Windows(3)
随笔档案
- 2012年3月 (1)
- 2012年2月 (1)
- 2011年9月 (2)
- 2011年8月 (1)
- 2011年7月 (1)
- 2011年4月 (5)
- 2011年3月 (6)
- 2011年2月 (2)
- 2010年11月 (1)
- 2010年10月 (1)
- 2010年8月 (1)
- 2010年5月 (3)
- 2010年4月 (1)
- 2010年3月 (1)
- 2010年1月 (1)
- 2009年10月 (2)
- 2009年9月 (1)
- 2009年8月 (3)
- 2009年7月 (1)
积分与排名
- 积分 - 13917
- 排名 - 7314
最新评论
- 1. Re:[Win32]一个调试器的实现(二)调试事件的处理
- LOAD_DLL_DEBUG_EVENT的处理:
我也一样遇到了这个问题,299,
CreateToolhelp32Snapshot
Module32First
Module32Next
做毕业设计在,先握个手,有空再请教! - --Mr.elegent
- 2. Re:[Win32]一个调试器的实现(十)显示变量
- 谢谢!
- --LuckyBool
- 3. Re:[Win32]一个调试器的实现(十)显示变量
- @LuckyBool可以先使用SymEnumSymbols枚举符号,然后在回调函数中通过判断pSymInfo->Tag是否SymTagFunction来确定当前符号是否函数。接下来使用SymGetLineFromAddr64获取函数地址所在CPP文件的路径。没有直接的方法可以知道哪个函数有没有被调用,为了达到这个目的要使用特别的技巧,例如找出所有的call指令,然后看看目的地址是否该函数的地址。
- --Zplutor
- 4. Re:[Win32]一个调试器的实现(十)显示变量
- 如果要获取到函数,仅仅是显示出.cpp文件中用到的函数,那需要用什么函数呢?
- --LuckyBool
- 5. Re:[Win32]一个调试器的实现(五)调试符号
- @LuckyBool
是一个简单的测试性的控制台程序,也是用Visual Studio 2010写的。 - --Zplutor
阅读排行榜
- 1. [Windows]在Win7游戏管理器中添加游戏(2415)
- 2. [C/C++]函数如何返回struct或class对象(2187)
- 3. [C++]实现委托模型(2138)
- 4. [Win32]一个调试器的实现(三)异常(2030)
- 5. [ASP.NET]自动发送邮件功能的实现(1898)
评论排行榜
- 1. [C++]实现委托模型(20)
- 2. [Win32]一个调试器的实现(四)读取寄存器和内存(12)
- 3. [C/C++]函数如何返回struct或class对象(11)
- 4. [Windows]在Win7游戏管理器中添加游戏(9)
- 5. [Win32]一个调试器的实现(三)异常(5)
推荐排行榜
- 1. [C++]实现委托模型(4)
- 2. [Win32]一个调试器的实现(一)调试事件与调试循环(3)
- 3. [Windows]关于映像劫持(2)
- 4. [C++]将标准IO库应用于套接字(2)
- 5. [Win32]一个调试器的实现(四)读取寄存器和内存(2)
- 调试器的实现
- 以太网调试器的实现
- 简易调试器的实现
- [Win32]一个调试器的实现(五)调试符号
- [Win32]一个调试器的实现(五)调试符号
- 浅析Lua调试器的实现
- 浅析Lua调试器的实现
- linux调试器的实现---主要框架
- 一个调试器的实现(一)调试事件与调试循环
- [Win32]一个调试器的实现(一)调试事件与调试循环
- [Win32]一个调试器的实现(一)调试事件与调试循环
- [Win32]一个调试器的实现(一)调试事件与调试循环
- [Win32]一个调试器的实现(一)调试事件与调试循环
- [Win32]一个调试器的实现(一)调试事件与调试循环
- linux调试器的实现---有关寄存器操作的实现
- linux调试器的实现---断点的实现
- SEH反调试的实现与调试
- 调试断点的实现原理
- org.apache.catalina.core.StandardContext listenerStart(Error creating bean with name 'sessionFactory
- 0点到现在的秒数(北京时间)
- 陀螺仪相关
- 什么时候用Convert.ToInt32 ?
- lftp 使用方法
- 调试器的实现
- Webview---播放网页中的flash
- JavaScript设计模式
- 华为Ascend D1开箱:全新商务风格,颠覆P1外观
- Quartz 2D编程指南(1)
- 从数据库生成PD 并且设置数据库字段说明对应PD的NAME属性
- ANT详解及运行小例子
- screen展现前输出
- Android 布局 android:XXXXX