05年的时候我写过两篇有关Visual Studio调试器的小文,这是第三篇。仅作抛砖引玉之用。
前两篇请见:
看过前两篇的朋友一定知道,Visual Studio在其安装目录下的Common7PackagesDebugger目录下,有一个叫做autoexp.dat的文件,这个文件中描述了Visual Studio Debugger在调试中求值显示的规则。
熟悉Win32调试器的原理的朋友肯定知道,就Native调试来说,当调试器中断(Int3)目标进程以后,调试器是通过ReadProcessMemory来读取目标进程数据的,那么读取之后呢?阅读过如何编写 Visual C++ 的表达式分析插件后我们可以猜到,其实调试器也作了类似的工作,就是用已有的symbol来解析目标对象的内存结构,并且显示在变量和监视窗口中,这个解析存在一个复杂性:对于简单数据类型来说,可以格式化之后直接显示;对于复杂类型来说,调试器并不知道程序员关心的是结构或者类或者模板实例化之后的哪一个数据成员。于是需求就出来了:如何正确的解析目标进程中对象的值。
对于这个问题,微软在Visual Studio中定义了Visual Studio Debugger Language,作为一种脚本语言实现了表达式求值操作的自定义。autoexp.dat就是这个脚本的承载体。
autoexp.dat分为几段,每一段负责不同的行为和表现,介绍如下:
[AutoExpand]:自动求值
所谓的自动求值也就是在调试过程中自动评估变量的值并显示,语法规则如下:
type=[text]<member[,format]>...
type 类型名,对于模板可以后跟随<*>来表示
text 任意文本,可以用来显示变量名等
member 成员变量的名字
format 数据的格式化规则,解释如下
字符 | 含义 | 例子 | 显示 |
d,i | 十进制符号整数 | 0xF000F065,d | -268373915 |
u | 十进制无符号整数 | 0x0065,u | 101 |
o | 八进制无符号整数 | 0xF065,o | 0170145 |
x,X | 十六进制整数 | 61541,X | 0X0000F065 |
l,h | 对d, i, u, o, x, X的长度修饰 | 00406042,hx | 0x0c22 |
f | 有符号浮点数 | 3./2.,f | 1.500000 |
e | 有符号科学记数法 | 3./2.,e | 1.500000e+000 |
g | e和f的简短记法 | 3./2.,g | 1.5 |
c | 一个字符 | 0x0065,c | 'e' |
s | 以0结尾的字符串 | pVar,s | "Hello world" |
su | unicode字符串 | pVar,su | "Hello world" |
下面以CPoint为例:
CPoint =x=<x> y=<y>
对于CPoint类实例来说,我们显然主要关心的不是它的this,而是x和y,于是上面的表达式达到了我们的需要。
另外一种特殊的就是我在如何编写 Visual C++ 的表达式分析插件中写的,用自定义插件来评估特定类型:
_SYSTEMTIME=$ADDIN(EEAddIn.dll,AddIn_SystemTime)
[Visualizer]:数据可视化
对于复杂数据类型,比如std::string,调试的时候我们主要关心的是它的内容,但是我们也关心它的一些其他成员,比如字符串的长度等,特别是对于继承的类来说,我们有时候还想看它的父类的一些结构,这个时候我们就需要定义可视化规则了。
在这方面,我们有很好的例子可以参考,因为微软已经把VC自带的STL的数据可视化规则写在了autoexp.dat,我们只需要根据自己的需要编写自己的规则就可以了。
下面,我以STLport的wstring为例来介绍数据可视化编写。假设我用的是STL port5.1.x,下面是默认wstring在vc++调试器中的样子:
可以看到,strName被解析为{...},也就是调试器不知道如何解析这个数据类型。在调试中,我们只能把这个变量的结构展开来看自己感兴趣的数据,这样会极大的影响我们调试的效率和心情。
为了告诉调试器如何解析这个变量,我们来编写一个规则:
stlp_std::basic_string<unsigned short,*>|stlp_std::basic_string<wchar_t,*>|stlpx_std::basic_string<unsigned short,*>|stlpx_std::basic_string<wchar_t,*>|stlpmtx_std::basic_string<unsigned short,*>|stlpmtx_std::basic_string<wchar_t,*>|stlpxmtx_std::basic_string<unsigned short,*>|stlpxmtx_std::basic_string<wchar_t,*>|stlpd_std::priv::_NonDbg_str<unsigned short,*>|stlpd_std::priv::_NonDbg_str<wchar_t,*>|stlpdx_std::priv::_NonDbg_str<unsigned short,*>|stlpdx_std::priv::_NonDbg_str<wchar_t,*>|stlpdmtx_std::priv::_NonDbg_str<unsigned short,*>|stlpdmtx_std::priv::_NonDbg_str<wchar_t,*>|stlpdxmtx_std::priv::_NonDbg_str<unsigned short,*>|stlpdxmtx_std::priv::_NonDbg_str<wchar_t,*>{
preview
(
#(
#if($c._M_end_of_storage._M_data < $c._M_buffers._M_static_buf)
(
[$c._M_buffers._M_dynamic_buf, su]
)
#else
(
[$c._M_buffers._M_static_buf, su]
),
)
)
stringview
(
#(
#if($c._M_end_of_storage._M_data < $c._M_buffers._M_static_buf)
(
[$c._M_buffers._M_dynamic_buf, su]
)
#else
(
[$c._M_buffers._M_static_buf, su]
),
)
)
children
(
#(
[raw view]: [$c,!],
buffer: #if($c._M_end_of_storage._M_data < $c._M_buffers._M_static_buf)
(
[(unsigned int)$c._M_buffers._M_dynamic_buf, x]
)
#else
(
[(unsigned int)$c._M_buffers._M_static_buf, x]
),
length: #if($c._M_end_of_storage._M_data < $c._M_buffers._M_static_buf)
(
[(unsigned int)($c._M_finish - $c._M_buffers._M_dynamic_buf), u]
)
#else
(
[(unsigned int)($c._M_finish - $c._M_buffers._M_static_buf), u]
)
)
)
}
一开始的一长串定义告诉了编译器我们关心的模版实例化参数,也就是WCHAR_T。
preview,stringview,children三节分别描述了数据在特定环境下的可视化规则,一般来说,preview,stringview可以定义成一样。下面就是效果了:
补充一下,STL port已经在最新的svn中提供了规则,除了略有bug外,还是很好用的,上面这段其实就是我改过bug的版本。
[hresult]:自定义错误代码
这个比较容易理解。如微软的例子一样1234=my custom error code。这样我们就可以通过调试器的伪寄存器变量err,hr来方便的查看了。下次给大家介绍一下Visual Studio Debugger的伪变量。