【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信息。
指定 /ZI 或 /Zi 而不指定 /Fd 时,VC++最终将生成两个 PDB 文件:
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 然后输入
按照这样设置,WinDbg将先从本地文件夹c:\symbols中查找Symbol,如果找不到,则自动从MS的Symbol Server上下载Symbols。
你也可以自己去下载MS提供的Windows符号文件,那么symbols file path就直接指向符号文件安装的目录就可以了。
调试器是如何来判别EXE、DLL等是否和一个pdb文件匹配呢?每次我们链接EXE或者DLL或者SYS的时候,链接器都将产生一个唯一的GUID,然后将其写入到PDB和可执行文件。调试器加载的时候将检查两者的GUID,如果一致就表示他们匹配。
如果出现无法加载符号文件的现象,通常是你的符号文件和你调试的二进制不匹配。
由于链接器在其创建的 .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。
要探索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"
windbg会捕获main这个函数的调用,并同时显示出main的源代码:
这里的主旨不是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,如果一致就表示他们匹配。
如果出现无法加载符号文件的现象,通常是你的符号文件和你调试的二进制不匹配。
由于链接器在其创建的 .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。
要探索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的源代码: