1. 基础知识
静态库和动态库
首先说一下,库的导出分为静态库和动态库。
导出静态库的话,会生成一个.lib文件,其实就是.obj的集合,.obj又是.cpp编译得到的,所以,静态库里有函数的实现。不过这些实现是二进制的,是不带源码的。使用静态库的时候,需要链接.lib,包含.h。
导出动态库的话,会生成.dll文件和.lib文件,动态库生成的.lib文件会很小,因为这里的.lib只需要指明函数在.dll中的位置就可以了。使用的时候,需要把.dll和程序将要生成的.exe放在一个位置(双击exe运行),或者放在“调试 - 工程目录”指定的位置(在IDE中运行)。当然,如果你把“调试 - 工程目录“设置成$(OutDir),就和exe的输出位置一致了,调试也能运行,双击exe也能运行。
符号文件.pdb
.pdb文件就是所谓的符号文件。在库的编译过程中一同产生,pdb的生成位置和文件名由“链接器 - 调试 - 生成程序数据库文件”决定。这个名字是会写进.dll的,也可以和.dll的名字不一样。例如,dll的名字是a.dll,生成时的符号文件是b.pdb,你在调试时把b.pdb强行改成a.pdb,调试器是不认的。所以,为了调试方便不混淆,我建议把.pdb的文件名设置成和.dll一样。否则debug版和release版的pdb都一个名字,就无法区分了。
.pdb文件里面记载了函数名称和对应的.cpp文件名称,.cpp文件的md5信息。所谓“符号文件”,就是在你调试时用来查找函数对应于哪个.cpp文件哪一行的,好告知调试器跳转过去。
2. 加载符号
现在假设你已经有一个工程了,可以编译运行。你调用了在.lib中的函数,下了断点,按F11想要步入。
这时有两种情况,第一种是你直接跳转到库的源码了。这里的原理很简单,因为你的pdb已经加载(你把pdb放在调试目录了),pdb里记载了函数所在的cpp,而这个库是你在本机编译的,绝对路径没有改动,调试器直接就找到了对应的cpp,所以就直接跳转了。
而第二种情况,按F11并不能进入到源码,而是直接步过了。如果想进入.lib的源码,需要怎么做呢?
首先需要准备.pdb文件,还有库的源码。
之后要检查符号是否加载。下断点,F5调试。然后“<font color=red>调试 - 窗口 - 模块</font>”打开模块窗口。
找到第三方dll的名字,我这里是DuiLib_ud.dll,看“符号文件”一栏是空的。说明这个dll的符号文件没有加载。
把.pdb和.dll放在一起。
再次调试。可以看到符号文件已经加载了。
其实不和dll放一起也可以。“工具 - 选项 - 符号”,在符号文件位置下面增加路径也可以。不过我不喜欢改动没有移植性的环境选项,更喜欢凡事都用相对路径。顺带一提,通过上面的模块界面,可以加载系统dll的符号文件,调试时可以进入系统dll的汇编。
3. 加载源码
现在符号文件已经加载了。再次开始调试,在断点处按F11步入。弹出了查找源码的选项。
在这里你就可以手动指定源码的位置了,不用担心版本出错,有md5校验的,你指定的.cpp如果不是这个库当初生成时的.cpp是通不过的。
不过我不喜欢这种硬编码绝对路径的做法,不优雅。我们点取消,就会看见未找到XXX.cpp界面。展开“源搜索信息”,可以看到调试器都从哪些地方在寻找这个.cpp文件。记住这个界面,如果我们之后指定.cpp路径时不正确,可以在这里来查看。
之后,把库的源码复制到我们自己的工程目录里。工程文件什么的不用带,只需要.cpp就可以了,理论上.h都不需要。
之后,在解决方案上右键“属性”,注意是解决方案上右键,不是工程。选择“通用属性 - 调试源文件”,在“包含源代码的目录”里增加我们刚拷进来的库的源代码目录。
微软说这里的查找源文件功能是不能识别子目录的,所有子目录都要手动加进来。
这里修改了之后,我实验发现要重启VS才生效。再次调试,可以进到库的源码了。在库的源码上悬停,我这里是在UIBase.cpp上悬停,可以看到它找到的源码的文件路径。确认是找到了我们刚放进去的源码。
本来我还想实验一下。我改了lib_src文件夹的名字,发现它还是能找到源码;我删掉debug文件夹下的内容,它还是能找到;我删掉和exe一块儿生成的.ilk和.pdb,它还是能找到;我删掉.vs隐藏文件夹,它还是能找到;我改动工程路径,它还是能找到。
这就很迷茫了。总之,也许它找到一次之后,查找文件功能就突然逆天了,知道寻找解决方案下面的所有cpp文件吧。
参考
如何调试静态库的代码(libeay32.lib和ssleay32.lib)
在 Visual Studio 调试器(C#、C++、Visual Basic、F#)中指定符号 (.pdb) 和源文件