您现在的位置是:首页 >技术交流 >使用Process Explorer和Dependency Walker排查C++程序中dll库动态加载失败问题网站首页技术交流

使用Process Explorer和Dependency Walker排查C++程序中dll库动态加载失败问题

dvlinker 2023-05-19 08:00:03
简介使用Process Explorer和Dependency Walker排查C++程序中dll库动态加载失败问题

目录

1、exe主程序启动时的库加载流程说明

2、加载dll库两种方式

2.1、dll库的隐式引用

2.2、dll库的动态加载

3、本案例中的问题描述

4、使用Process Explorer和Dependency Walker分析dll库加载失败的原因

4.1、Process Explorer工具介绍

4.2、使用Process Explorer工具查看目标dll库有没有加载起来

4.3、使用Dependency Walker查看xxxpdll.dll库的依赖关系

5、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(正在更新中)https://blog.csdn.net/chenlycly/category_12279968.html?spm=1001.2014.3001.5482        有时在代码中会调用LoadLibrary或者LoadLibraryEx接口去动态加载dll库,可能会因为库的版本不一致导致dll库动态加载失败,我们可以使用Process Explorer和Dependency Walker两个工具去排查库加载失败的原因。本文将详细讲述这类问题的完整排查过程。

1、exe主程序启动时的库加载流程说明

       我们先来大概地看一下exe主程序启动时的库加载流程。

       当我们启动exe主程序时,操作系统根据exe程序的位数,给程序进程分配对应大小的虚拟内存空间。如果是32位exe程序,会分配4GB的虚拟内存。在Windows系统中,默认的用户态内存和内核态各占2GB;在Linux系统中,默认的用户态内存占3GB,内核态内存占1GB。

       接着,操作系统会把exe主程序依赖的dll库都加载到当前程序的进程空间中,待所有dll库加载成功后,才会将exe主程序启动起来,然后代码运行到exe主程序的main函数中,exe主程序开始运行。如果依赖的dll库加载失败,程序启动过程会被终止,exe程序会启动失败。比如exe主程序依赖libcurl.dll库,但exe主程序所在的路径中缺少这个库,则启动exe主程序时会报找不到libcurl.dll库:

程序启动失败。

       找不到依赖的dll库是一种场景,还有一种场景是调用的某个接口在目标dll库中找不到,会报类似如下的错误:

出现这种找不到接口的情况,可能是两种原因:

1)如果在产品业务库中找不到接口,可能是业务库的版本有问题,使用了老版本的dll库,需要使用新版本的库;
2)如果在操作系统的系统库中找不到接口,可能调用的接口是新接口,高版本的操作系统才支持,而当前低版本的操作系统不支持。

       dll和exe二进制文件加载到进程的进程空间中,二进制文件中存放的都是二进制代码(二进制代码与汇编代码是等价的,汇编代码是二进制代码的助记符,所以也可以看成是汇编代码),这些二进制代码是存放在代码段内存中,代码段内存也隶属于所在进程的虚拟内存,是从进程虚拟内存划拨出来的。

2、加载dll库两种方式

         加载dll库有两种方式,一种是隐式引用,一种是调用LoadLibrary或LoadLibraryEx去动态加载。

2.1、dll库的隐式引用

        所谓的隐式引用,就是包含dll库的API头文件,引入dll库对应的.lib库(链接时会用到):

#include "netdll_api.h"
#pragma comment(lib, "netdll.lib")

然后在代码中直接调用dll库的导出接口。对于这种隐式引用的方式,之所以叫隐式,是相对于显式调用LoadLibrary或LoadLibraryEx接口去动态记载的。隐式加载的dll库,在exe程序启动时系统会优先将这些dll加载到进程空间中。

2.2、dll库的动态加载

       有时代码中会调用LoadLibrary或LoadLibraryEx动态加载dll库,然后再调用GetProcAddress函数获取dll库导出函数的地址(函数名称就是函数代码段的首地址),然后再去call这个函数地址,完成函数调用。比如我们通过动态加载shell32.dll库,去动态地获取该库中的API导出接口SHOpenFolderAndSelectItems,相关代码如下所示:

BOOL OpenFolderAndSelFile( CString strFilePath )
{
    LPITEMIDLIST pidl = NULL;
    LPCITEMIDLIST cpidl = NULL;
    LPSHELLFOLDER pDesktopFolder = NULL;
    WCHAR wfilePath[MAX_PATH+1] = { 0 };

    // 主工程已经初始化了COM库,此处不用在初始化了
    //::CoInitialize( NULL );

    if ( SUCCEEDED( SHGetDesktopFolder( &pDesktopFolder ) ) )
    {
        // IShellFolder::ParseDisplayName要传入宽字节
        LPWSTR lpWStr = NULL;
#ifdef _UNICODE
        _tcscpy( wfilePath, strFilePath );
        lpWStr = wfilePath;
#else
        MultiByteToWideChar( CP_ACP, 0, strFilePath.GetData(), -1, wfilePath, MAX_PATH ); 
        lpWStr = wfilePath;
#endif

        HRESULT hr = pDesktopFolder->ParseDisplayName( NULL, 0, lpWStr, NULL, &pidl, NULL );
        if ( FAILED( hr ) )
        {
            pDesktopFolder->Release();
            //::CoUninitialize();
            return FALSE;
        }

        cpidl = pidl;

        // SHOpenFolderAndSelectItems是非公开的API函数,需要从shell32.dll获取
        // 该函数只有XP及以上的系统才支持,Win2000和98是不支持的,考虑到Win2000
        // 和98已经基本不用了,所以就不考虑了,如果后面要支持上述老的系统,则要
        // 添加额外的处理代码
        HMODULE hShell32DLL = ::LoadLibrary( _T("shell32.dll") );
        ASSERT( hShell32DLL != NULL );
        if( hShell32DLL != NULL )
        {
            typedef HRESULT (WINAPI *pSelFun)( LPCITEMIDLIST pidlFolder, UINT cidl, LPCITEMIDLIST  *apidl, DWORD dwFlags );
            pSelFun pFun = (pSelFun)::GetProcAddress( hShell32DLL, "SHOpenFolderAndSelectItems" );
            ASSERT( pFun != NULL );   
            if( pFun != NULL )
            {
                hr = pFun( cpidl, 0, NULL, 0 ); // 第二个参数cidl置为0,表示是选中文件
                if ( FAILED( hr ) )
                {
                    ::FreeLibrary( hShell32DLL );
                    pDesktopFolder->Release();
                    //::CoUninitialize();
                    return FALSE;
                }
            }
            else
            {
                FreeLibrary( hShell32DLL );
                pDesktopFolder->Release();
                return FALSE;
            }

            ::FreeLibrary( hShell32DLL );
        }
        else
        {
            pDesktopFolder->Release();
            //::CoUninitialize();
            return FALSE;
        }

        // 释放pDesktopFolder
        pDesktopFolder->Release();
    }
    else
    {
        //::CoUninitialize();
        return FALSE;
    }

    //::CoUninitialize();
    return TRUE;
}

       以前我们遇到过调用LoadLibrary去动态加载dll库加载失败的问题,而且这个问题还遇到过几次,一次是在自制的安装包程序中加载失败,一次是底层库加载库失败。在这些问题场景中,调用LoadLibrary时传入的是dll库的绝对路径,绝对路径中也有目标dll文件,但就是加载失败。后来为了记录这类问题,专门写了一篇文章,对问题的解决过程做了一个完整的总结!如果由朋友遇到类似的问题,可以参看我写的这篇文章:

查看开源操作系统ReactOS源码,解决dll库动态库加载失败问题(调用LoadLibrary加载失败)https://blog.csdn.net/chenlycly/article/details/129200442

3、本案例中的问题描述

       在某天测试时发现,软件UI层执行了某些业务子系统的操作时,一点反应都没有。正常情况下,服务器会给出一些回应,UI层会显示操作的结果。软件在业务和设计上,一般都是分层的,我们的软件也不例外,软件底层多个业务模块是动态启动的,上层软件可以根据自己的需要去选择要启动的模块。上层将要启动的业务模块信息告诉底层,底层去调用LoadLibrary或者LoadLibraryEx去启动对应的dll库。

       以前我们遇到过类似的问题,估计这次又是因为底层的业务库加载失败了,库没有加载起来,导致UI层发出的操作请求到底层后没有处理,所以出现了上述问题。

4、使用Process Explorer和Dependency Walker分析dll库加载失败的原因

       类似的问题,我们以前遇到过,已经有相关的处理经验了,这类问题排查起来比较简单,直接拿Process Explorer和Dependency Walker两工具来分析一下就能知道原因了。

4.1、Process Explorer工具介绍

       Process Explorer是Winternals公司(该公司已经被微软收购)开发的增强版任务管理器软件工具(procexp.exe),功能要比Windows系统自带的任务管理器的功能多很多。使用该工具可以查看服务、进程、线程、模块、句柄、磁盘使用状态、网络使用状态等多类信息。Process Explorer和下面要讲到的Process Monitor都是Winternals公司开发的免费工具,被放置在Sysinternals Suite工具包中。

       Process Explorer的主界面如下所示:

        在日常工作中,我们主要用该工具来查看进程的相关信息,以排查程序运行过程中出现的问题。平时用到的主要功能有:

1)查看进程启动时传入的命令行启动参数;
2)查看进程加载了哪些dll库(通过该功能确定目标dll有没有启动起来),以及这些库的详细信息;
3)查看进程的磁盘和网络使用统计信息;
4)查看进程的各个线程的信息,可以看到各线程的CPU占用比例和线程的函数调用堆栈(在排查进程CPU占用高时会用到,找到占用CPU高的那个线程);
5)查看进程的GPU占用情况(判断程序是否使用GPU硬件编解码);
6)查看进程的虚拟内存的占用情况。
7)查看进程占用的句柄信息,比如文件句柄、线程句柄、注册表句柄等。

4.2、使用Process Explorer工具查看目标dll库有没有加载起来

       要查看进程的库占用情况,如果是第一次使用该工具查看进程加载的dll库列表,需要点击工具栏中的“View DLLs”图标按钮:
 

才会显示目标进程加载的dll库列表。默认情况下显示的是句柄信息。

        在进程列表中,点击选择目标进程,下方就会显示加载了哪些库,然后去查看这些库的信息,比如占用库的路径,占用库的版本。通过库的路径或版本,确定是否加载了正确版本的库。通过查看进程加载的dll库列表得知,底层的xxxpdll.dll不在列表中,所以确定该库没加载起来,应该是加载失败了。

4.3、使用Dependency Walker查看xxxpdll.dll库的依赖关系

        为啥xxxmpdll.dll会加载失败呢?根据经验得知,肯定是xxxpdll依赖的底层库有问题,有可能是该库依赖的底层库找不到,有可能是调用到的接口在底层库中找不到。具体是什么问题,使用Dependency Walker工具打开xxxpdll库文件,查看依赖的库信息便知道了。

       Dependency Walker工具主要用来查看exe或dll文件的库依赖关系,如果依赖的库有问题,在库节点前会议红色(红色一般表示接口缺失或者接口参数不对)或者黄色问号(黄色一般表示对当前系统中找不到该库)标记出来。

       在出问题的电脑上,打开Dependency Walker工具,然后将xxxpdll拖入到Dependency Walker中查看dll库的依赖关系。果然看到了问题:

如果你机器用的是Win10系统,Dependency Walker工具对win10的兼容性不太好,打开exe程序时非常慢,有时会卡好几分钟甚至需要更长的时间,需要耐心等一下。

从上图可以看到,当前的xxxpdll.dll依赖的xxxControl.dll库有问题(xxxControl.dll库节点前面的图标显示红色),具体的问题为:xxxpdll.dll调用xxxControl.dll库中的RegisterRtcLogCallback接口,但该接口在xxxControl.dll中找不到(函数前面显示红色图标),这就是原因所在了!

       于是和维护xxxControl.dll库的同事沟通,RegisterRtcLogCallback是xxxControl.dll中新增的接口,新版本的xxxControl.dll库已经包含了,估计当前产品中使用的还是老版本的xxxControl.dll库。

       于是使用PE查看工具查看xxxControl.dll的时间戳,果然当时用的是老版本,然后将新版本取过来覆盖一下重新运行程序就号了。关于如何查看二进制文件的时间戳,可以参看我之前写的文章:

查看exe和dll等二进制文件时间戳(生成时间)的工具与方法介绍https://blog.csdn.net/chenlycly/article/details/130085431

5、最后

       本文详细讲述了使用Process Explorer和Dependency Walker两工具分析dll库动态加载失败的完整分析过程,给大家提供一个借鉴和参考。我们很有必要去掌握一些诸如Process Explorer、Dependency Walker这些常用的分析工具,使用这些工具去有效地分析软件运行过程中遇到的问题。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。