欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【windbg】用WinDbg探索ruby的奥秘

程序员文章站 2022-05-25 16:26:09
...
写这篇文章是受从main.c开始走进Ruby-登上调试Ruby之旅》的启发,不同的是该文章用的是GDB,GDB虽然很强大,但是毕竟是命令行,在调试的时候,可能同时需要查看许多信息,比如call statck,汇编代码,源代码等等,命令行就有点力不从心,所以续写一篇,改GDB为同样强大的windbg,以便更方便的探索Ruby的内部奥秘。

这里的主旨不是ruby,而是针对Windbg的调试技巧,所用的方法同样适用于其他C/C++应用,ruby在这里只是做实验用的小白鼠。

从编译开始

要进行源码调试,必须让编译器或链接器在构建二进制文件时生成符号文件(.pdb文件)。这些符号文件保存了二进制指令和源码行之间的对应关系。

另外,调试器必须能够访问源码文件,因为符号文件中并不包含实际的源代码文本。

如果这些都满足,编译器和链接器还不能对代码进行优化。如果代码经过优化,在源码调试时访问局部变量会变得很困难,有时候几乎是不可能的。如果使用Build 实用程序作为编译器和链接器,可以将MSC_OPTIMIZATION 宏设置为/Od /Oi 来避免优化。

ruby编译过程可以参考:这篇文章
不同的是需要修改下ruby的makefile以便输出debug信息。
CFLAGS = /Od -MD $(DEBUGFLAGS) $(OPTFLAGS) $(PROCESSOR_FLAG) /DEBUG /Zi
LDFLAGS = $(LDFLAGS) -manifest /DEBUG


指定 /ZI 或 /Zi 而不指定 /Fd 时,VC++最终将生成两个 PDB 文件:
  • VCx0.PDB (其中 x 表示 Visual C++ 的版本。)该文件存储各个 OBJ 文件的所有调试信息并与项目生成文件驻留在同一个目录中。
  • project.PDB   该文件存储 .exe 文件的所有调试信息,它包含了调试中需要用到的各种数据,例如:全局变量、本地变量、函数名、函数类型、源代码行、程序入口地址.....,这些所有的东西都叫做Symbol。


PDB文件说明
PDB文件全称是程序数据库 (PDB) 文件,它保存着调试和项目状态信息,使用这些信息可以对程序的调试配置进行增量链接。

每当创建 OBJ 文件时,C/C++ 编译器都将调试信息合并到 VCx0.PDB 中。插入的信息包括类型信息,但不包括函数定义等符号信息。

链接器将创建 project.PDB,它包含项目的 EXE 文件的调试信息。project.PDB 文件包含完整的调试信息(包括函数原型),而不仅仅是在 VCx0.PDB 中找到的类型信息。这两个 PDB 文件都允许增量更新。

我们知道当应用程序被链接以后,代码被逐一地翻译为一个个的地址,优化以后的代码可能初看起来更是面目全非,如果调试时,充斥着类似NTDLL! 774fe4b6() NTDLL! 774fe489()之类的调用堆栈无疑会增加调试的难度。有了PDB文件之后, 每当我们使用vs或者windbg等微软的调试工具进行调试的时候,我们可以方便地使用变量名来查看内存、可以使用函数名称来下断点、甚至可以指定某个文件的某一行来下断点。

设置WinDbg的符号文件

启动WinDbg,选择菜单的file -> symbols file path,或者按ctrl+s 然后输入
srv*c:\symbols*http://msdl.microsoft.com/download/symbols

按照这样设置,WinDbg将先从本地文件夹c:\symbols中查找Symbol,如果找不到,则自动从MS的Symbol Server上下载Symbols。

你也可以自己去下载MS提供的Windows符号文件,那么symbols file path就直接指向符号文件安装的目录就可以了。

调试器是如何来判别EXE、DLL等是否和一个pdb文件匹配呢?每次我们链接EXE或者DLL或者SYS的时候,链接器都将产生一个唯一的GUID,然后将其写入到PDB和可执行文件。调试器加载的时候将检查两者的GUID,如果一致就表示他们匹配。

【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 

如果出现无法加载符号文件的现象,通常是你的符号文件和你调试的二进制不匹配。

由于链接器在其创建的 .exe 或 .dll 文件中嵌入 .pdb 文件的路径,这里不需要在设置ruby的符号文件路径。

我们只需要为WinDbg设置源代码路径,这样可以很方便地查看ruby的源代码。

常见的符号操作
查看符号路径:.sympath
列出加载模块: lm
加载指定模块: ld <module>
重新加载模块: .reload <module>
重新加载所有模块: .reload /n
列出模块详细信息: !lmi <module>!db <module>
显示模块的符号信息: x <module>!<symbols>
查看模块的数据结构: dt <module>!<symbols>

启动应用
Windbg提供了两种方式来启动应用,这里以ruby的irb为例:
open executable:ruby -x "<path>\irb.bat",这时候我们可以很方便地在任何地方设置断点,包括main函数。

也可以运行irb之后,在WinDbg里选择attach to a process, 在列出的进程中选择ruby.exe。

【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 

要探索ruby的内部奥秘,最重要的技巧是设置断点,而其中最经常使用的是未定断点。

如果一个断点是设置在某个还未加载的函数名上,则称为延迟、虚拟或未定断点。 (这些术语可交替使用。) 未定断点没有被关联到任何具体被加载的模块上。每当一个新的模块被加载时,会检查该函数名。如果这个函数出现,调试器计算虚拟断点的实际位置并启用它。

在WinDbg里设置断点的几个方法:

bu设置的断点自动被认为是未定断点。如果断点在一个已加载模块中,则会启用并正常生效。但是,如果模块之后被卸载并重新加载,这个断点不会消失。而使用bp设置的断点会立即绑定到某个地址。 如果bp的断点地址在某个已加载模块中找到,并且该模块之后被卸载,则该断点会从断点列表中移除。bm断点用法如同bu,但是bm可以支持正则表达式,用于设置多个断点。

在WinDbg中设置断点的格式有如下几种:
1. 虚拟地址:即给出直接地址,如 12345678
2. 函数偏移量:如DriverEntry+5c.
3. 源代码+行数 :`[[Module!]Filename][:LineNumber]`
4. 可以对模块中的某个类的方法设置断点。


查看ruby入口点
ruby是个c编写的应用,通常入口点就是main函数,用open executable的方式启动irb
在arguments里填写上 -x "<path>\irb.bat"

bu main
g


windbg会捕获main这个函数的调用,并同时显示出main的源代码:

【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 



  • 【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 
  • 大小: 41.6 KB
  • 【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 
  • 大小: 101.2 KB
  • 【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 
  • 大小: 105.6 KB
  • 【windbg】用WinDbg探索ruby的奥秘
            
    
    博客分类: 调试与测试 RubyCC++C#正则表达式 
  • 大小: 18.6 KB