目录
1、问题描述
2、如何调试Release版本的代码?
3、使用Process Explorer查看exe主程序加载的dll库列表,发现mediaplay.dll没有加载起来
4、使用Dependency Walker查看rtcmpdll.dll的库依赖关系和接口调用情况,定位问题
4.1、使用Dependency Walker工具查看到rtcmpdll.dll依赖的mediaplay.dll库有问题
4.2、为啥rtcmpdll.dll调用的接口在mediaplay.dll中找不到时没有弹出报错提示框呢?
4.3、动态库的入口函数DllMain什么时候会执行到?
4.4、解决办法
5、关于调试底层dll库
5.1、附加到进程调试
5.2、配置exe主程序启动dll调试
6、最后
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础入门与实战进阶(专栏文章已更新280多篇,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.htmlWindows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html 音视频开发组的同事要调试他们维护的动态库代码,但始终没有命中断点,于是找到我,让我帮忙看看是什么问题导致的。本文记录一下这个问题的排查过程。
1、问题描述
音视频开发组的同事为了排查某个问题,需要直接在Visual Studio中调试用来播放音视频的媒体处理动态库mediaplay.dll(直接调试Release版本的库)。
同事在其机器上安装了exe软件,并将本地Visual Studio编译出来的mediaplay.dll文件拷贝到exe主程序的安装目录中,将原先的mediaplay.dll库覆盖掉。
之所以覆盖,因为本地编译的mediaplay.dll指向的pdb文件在本地可以找到,可以直接借助pdb中的符号信息调试mediaplay.dll库的源码。
然后在打开mediaplay.dll工程的Visual Studio中,打开mediaplay工程的属性窗口,在调试节点下,配置当前dll的启动程序为exe主程序:
然后启动VS调试,当代码执行到dll库中,就可以调试当前dll的源码了。
因为要调试当前dll的初始化过程的代码,直接将断点设置在dll的入口函数DllMain中,但当exe主程序运行起来后,并没有命中断点。同事搞不懂为啥没有命中断点,不知道是不是哪里配置的有问题,于是找到我,让我帮忙排查一下,看看到底是怎么回事。
2、如何调试Release版本的代码?
同事说他要调试Release下的代码,当前没有命中断点,不能调试mediaplay.dll库的源码,是不是哪里配置的有问题。
其实,调试Release下的代码,只要做两点操作就行了。首先,要关闭工程属性中的优化选项:(到C/C++ --> 优化,将优化关闭掉)
因为不关闭优化,很多行的代码可能会被优化掉,这样调试源码时会很麻烦,且很奇怪。
其次,是将本地编译生成的dll文件拷贝到exe主程序目录中。这样保证exe主程序跑起来后,调用的是本地带调试信息的mediaplay.dll库。其实调试信息主要是存放在本地mediaplay.dll对应的pdb文件mediaplay.pdb中。那调试mediaplay.dll的源码时,是怎么找到其对应的pdb文件呢?是这样的,mediaplay.dll编译时会将对应的pdb文件的完整路径写到mediaplay.dll二进制文件中,类似下面的:
这样调试器可以根据mediaplay.dll中写入的pdb文件的完整路径找到对应的pdb文件,就能将其加载起来,就能使用到pdb文件中的符号信息了,就能进行代码调试了。
有人可能会问,release下生成pdb文件是否需要手动配置?其实,默认情况下,是自动配置了生成pdb文件的,如下所示:(到链接器 --> 调试 中查看)
当然在进行release调试之前,也可以到上述页面确认一下有没有打开生成pdb的选项。
3、使用Process Explorer查看exe主程序加载的dll库列表,发现mediaplay.dll没有加载起来
exe主程序下有一层是业务组件层,业务组件层包装了当前的音视频播放库。包装当前mediaplay.dll库的业务组件库是rtcmpdll.dll,这个rtcmpdll.dll是动态加载的(调用LoadLibraryEx接口动态加载的)。因为我这边排查过多个与业务组件库动态加载有关的多个问题,所以猜测可能是rtcmpdll.dll没有动态加载起来,导致更底层的mediaplay.dll没有加载起来。
要确认dll库有没有加载起来,很简单,直接使用Process Explorer工具查看exe主程序加载的dll列表即可。看看加载的dll列表中是否有mediaplay.dll和rtcmpdll.dll。具体的做法是,打开Process Explorer,找到我们的exe主程序,选中之,就可以看到exe主程序加载的dll列表:
列表中果然没有mediaplay.dll和rtcmpdll.dll两个库。
4、使用Dependency Walker查看rtcmpdll.dll的库依赖关系和接口调用情况,定位问题
业务组件库rtcmpdll.dll没有动态加载起来,应该是其依赖的库有问题导致的,使用Dependency Walker工具打开rtcmpdll.dll查看一下库的依赖关系和接口状况就可以确定问题了。
4.1、使用Dependency Walker工具查看到rtcmpdll.dll依赖的mediaplay.dll库有问题
打开Dependency Walker,将rtcmpdll.dll文件拖进来,打开rtcmpdll.dll文件。Dependency Walker是2006年的版本,没有更新的版本了,可能是对后来上市的Win10、Win11兼容不好,打开文件比较慢,可能要等上好几分钟才能打开。打开后,一眼就看到了问题,如下所示:
rtcmpdll.dll依赖的mediaplay.dll库有问题,mediaplay.dll库节点之前显示淡红色,选中mediaplay.dll节点,可以看到rtcmpdll.dll调用的Draw接口(接口所在行显示行色图标)在mediaplay.dll中找不到。往下看,mediaplay.dll库的导出接口列表中是有Draw接口,再仔细一看,两个接口的参数不一样:
mediaplay.dll导出的Draw接口中使用的是tagVIDEO_FRAME结构体,而rtcmpdll.dll中调用的Draw接口中使用的是VIDEO_FRAME结构体,结构体名称之前是没有tag字样的。
于是问同事有没有修改结构体名称,他说确实修改了,原先的结构体如下所示:
同事看到这个地方定义的有问题,应该在typedef时的原始结构体名称前加一个tag字样,修改后的如下:
这就是问题所在了。当前要调试代码的mediaplay.dll库本地修改了头文件中的结构体名称,但没有拿到业务组件的代码中去编译,即当前业务组件库rtcmpdll.dll使用的还是之前未修改的结构体名称,而当前本地的mediaplay.dll已经使用了修改后的tagVIDEO_FRAME,所以导致rtcmpdll.dll中调用的Draw接口在mediaplay.dll中找不到了(Draw接口的参数不一样了),所以rtcmpdll.dll加载失败了。
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到500多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有PE工具、Dependency Walker、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3: (本专栏文章已经更新到280多篇,还在持续更新中,欢迎订阅)
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点、C++11新特性、C++开源库介绍与使用、代码分享(调用系统API、使用开源库)、编程技术(动态库、多线程、多进程和网络编程等)、软件UI编程(duilib/QT)、C++软件调试技术(排查软件异常的手段与方法、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与进阶内容等。
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了Windows C++ 应用软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
4.2、为啥rtcmpdll.dll调用的接口在mediaplay.dll中找不到时没有弹出报错提示框呢?
一般情况下,如果程序中调用的接口在对应的dll中找不到时,启动exe主程序时会弹出如下的类似提示框:
程序是怎么知道该到哪个dll库中去检查调用的接口是否存在呢?这是代码编译链接时确定的,调用的接口会在链接时确定关系,到哪个库中去链接接口。然后在生成dll二进制文件时,会将调用的其他dll中的接口(从其他dll导入的接口)写到二进制文件的PE头中,这样程序启动时在加载二进制文件时会从PE头信息找到其调用哪些库中的哪些接口,就可以直接去校验了。
弹出上述报错提示框,是使用#pragma comment(lib, "xxx.lib")或者在工程配置中引入lib库(dll对应的lib库,用于链接使用),隐式调用dll接口的场景。而类似于本例中的使用LoadLibrary或LoadLibraryEx动态启动的dll库,如果其依赖的库有问题,是不会弹出报错提示框的。
4.3、动态库的入口函数DllMain什么时候会执行到?
在排查问题的过程中,同事问我,当前要调试的mediaplay.dll库的DllMain函数在什么时候回执行到?程序启动时,是先进入到mediaplay.dll库的DllMain函数,还是先进入exe主程序的main函数呢?
其实这个过程很简单,双击启动exe主程序时,会优先把exe依赖的dll库加载到进程空间里来,要加载的dll库,也需要将其依赖的dll库先加载起来,等exe主程序依赖的dll库及底层的dll库都加载起来后,才会将exe主程序文件加载到进程空间中,然后exe主程序开始运行,进入main函数。在加载dll库时,会进入DllMain函数,所以DllMain函数应该是先于exe主程序的main函数被执行到。
4.4、解决办法
解决办法很简单,将对结构体VIDEO_FRAME的名称修改回退掉即可,先能将代码调试起来排查问题。
至于结构体VIDEO_FRAME名称定义的不规范,后面再修改,修改后发布到业务组件那边,让他们重新编译包装mediaplay.dll的rtcmpdll.dll库的代码即可。
5、关于调试底层dll库
比如本案例中的底层库mediaplay.dll,和上层的exe主程序及中间的业务组件层,是不同的开发组负责的,这些模块的代码都不在一起的,没法放到一起直接调试的。
如果要调试底层的dll库,则需要依赖上层的exe主程序,因为dll库是不能独立运行的,是需要依附在exe主程序中才能跑起来的。需要拷贝Debug版本的exe程序(包含其依赖的底层的库),或者安装release版本的exe程序。然后使用依附到exe主程序的方式去调试底层的库。
当然有的dll库,为了测试dll库的接口及内部的功能,会手写一个python exe主程序,将dll依附在该python exe主程序上测试。但这种测试相对有限,不能集成到具体的产品业务中去测试,所以仅仅依赖这种单元测试是远远不够的。而依附到产品的exe主程序中去测试才是全面的。
依附exe主程序去调试代码,主要有两种方式,一种是附加到进程调试,一种是配置exe主程序其启动dll调试。两种方式都需要将本地编译出来的dll库拷贝到exe主程序的路径中。
5.1、附加到进程调试
Visual Studio有一个附加到进程调试的功能。先将exe主程序运行起来,然后在打开dll库源码的Visual Studio中,点击菜单栏中的调试 -> 附加到进程调试,打开附加到进程调试的对话框:
在窗口的进程列表中找到exe主程序的进程,选中之,点击下方的附加按钮,可以开启附加到exe主程序的调试。
5.2、配置exe主程序启动dll调试
可以到dll工程的属性中,在调试节点下,设置当前dll的启动exe程序的完整路径,如下所示:
这样在Visual Studio开启调试时,会优先根据设置的exe主程序的路径去把exe主程序启动起来,然后就可以调试dll代码了。
这种方式,便于调试dll库中的初始化代码,如果使用附加到进程调试,要exe主程序启动起来后,才能附加到进程,等exe主程序启动起来后,初始化的代码可能已经执行完了。
其实为了使用附加到进程调试,可以尝试在初始化的代码中弹出一个MessageBox对话框阻塞一下:
::MessageBox( NULL, _T("将代码阻塞住,给附加进程创造时机!"), _T("Tip"), MB_OK );
给附加到进程创造时机。这个代码会弹出如下的这个提示框:
先不要点确定,将VS附加到进程上,等附加上后再点这个窗口中的确定按钮,把窗口关闭掉,让程序向下跑,就可以调试初始化的代码了。
6、最后
虽然process Explorer和Dependency Walker都是小工具,但这些小工具在排查小问题时确实很好用、很实用,所以很有必要把这些工具用起来。
关于日常开发工作中常用的十大软件分析工具,之前专门写了介绍的文章,可以查看:(对应的文章专栏中还撰写了这些工具的实战使用案例,有较大的实战参考价值)
C++软件开发值得推荐的十大高效软件分析工具https://blog.csdn.net/chenlycly/article/details/127608247