我们知道符号文件对我们调试是非常重要的,如果没有符号文件,我们在调试器里看到的要么是偏移地址,要么看到的是错误符号,这会导致我们定不了位或错误定位,如果有了匹配的符号文件,这一切都不是问题了。
一、调试器寻找符号文件
首先在我们编译我们的程序时,如果设置了符号选项,那么在编译连接时,除了产生我们需要的符号文件外,还会在我们的PE文件(exe,dll)里记录下对应的符号文件信息。
这里的路径其实是开发编译机上生成的路径,当我们在这台机上调试时,就会到这个目录下寻找符号文件。当在别的机器上调试时,调试器就会会拿路径里的文件名去查找。
当调试器加载一个PE模块时,第一个搜索的路径就是这个模块所在的路径,如果不在模块所在的路径,则查找模块中记录的build目录,就时上图里的路径, 如果以上两个路径都没有找到PDB,则根据symbol server的设置,在本地的symbol server的cache中查找,如果在本地的symbol server的cache中没有对应的PDB,则最后才到远程的symbol server中查找。
当在某个路径找到了文件名匹配的符号文件,就要看符合文件根模块是否匹配。根据上图,我们知道编译器在生成PE模块时,调试信息里面会生成一个GUID值,同时符号文件里也会记录这个GUID值,每一次编译连接,这个值都会发生变化。
当两个文件里的GUID值相等时,符号文件才跟模块文件匹配,这时才能进行正常的调试,获得正确的符号信息。所以我们在调试时,一定要把正确的符号文件跟模块文件放在同一目录下才行。
二、调试器定位
当有了正确的符号文件后,调试器是如何定位源代码、行号等信息的呢?我们以VS来大概讲一下,默认情况下,在pdb文件中,保存了可执行文件中所有的符号(函数名、变量名等)所在源文件、行号、OFFSET(文件中的偏移)等信息。但是这些信息,是在编译阶段得到的,编译器在编译每个cpp的过程中,就可以把这些符号的相关信息收集起来,存放在各个cpp所生成的obj文件中,然后在链接的时候,提取每个obj中的这些信息,生成一个单独的pdb文件。这样,以后调试程序的时候,调试器只要找得到这个pdb,就可以知道可执行文件中,所有符号所在的源文件、行号和OFFSET了。反过来说,当给出一个源文件和行号,就可以拿到对应的OFFSET了,所以在还没有启动调试的时候,我们下的断点,实际上调试器是知道这个断点应该在哪个OFFSET上了,等启动调试的时候,用这个OFFSET加上这个模块所加载到的基地址值,就可以得到这个断点所在的VA(程序加载到内存后的一个虚拟地址)了,然后在这个VA处强行写上int 3指令,并继续执行,当执行到这里,便中断下来给我们一个调试机会了。当我们在VS里鼠标放在某个变量上时,调试器可以拿到这个变量的名称,根据我们前面说的,用这个名称去pdb中查找,自然就可以找到pdb文件中保存的OFFSET了,加上这个模块的基地址,就找到了这个变量所在内存的VA,剩下的就是读一下这个VA内存中的内容了。这样也就实现了观察变量值得功能。