admin管理员组

文章数量:1656956

目录

1、概述

2、dll动态库的隐式加载与动态加载

2.1、dll库的隐式加载

2.2、dll库的显式加载

3、为什么要使用动态加载dll动态库的方式?什么时候需要使用动态加载?

3.1、调用系统dll库中未公开的接口

3.2、调用控件库中的注册接口向系统中注册该控件

3.3、底层的业务模块做成动态启动方式,上层产品可以根据自己的业务需要选择想启动的业务模块

4、LoadLibrary动态加载dll动态库失败的场景

4.1、自制的安装包程序中遇到的LoadLibrary加载dll库失败问题

4.2、主程序底层模块调用LoadLibrary加载dll库失败问题

5、 LoadLibaray加载失败的可能原因

6、参考开源操作系统ReactOS中的regsvr32.exe程序的实现源码,找到了解决LoadLibrary加载dll库失败的办法

6.1、ReactOS开源操作系统简介

6.2、使用Source Insight打开ReactOS源码,找到regsvr32.exe程序的代码

7、到微软MSDN上查看LOAD_WITH_ALTERED_SEARCH_PATH参数的含义

8、dll动态库加载失败的其他原因

9、最后


C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn/chenlycly/article/details/125529931C/C++实战专栏(专栏文章已更新460多篇,持续更新中...)https://blog.csdn/chenlycly/article/details/140824370C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn/chenlycly/category_2276111.html       有时我们需要在代码中调用LoadLibrary去动态加载某个dll库,然后从dll中获取某些函数地址去调用这些函数,但个别情况下会出现LoadLibrary加载dll库失败的问题。今天我们就来讲一讲动态库动态加载的相关内容。

1、概述

       有时我们会将一些功能封装成一个dll动态库,实现对代码的模块化封装,既方便程序按模块去管理,也方便日后调试与维护。此外,为了方便第三方厂商接入我们系统进行二次开发,也会将相关业务封装成SDK动态库供他们使用。

       一般C++程序中会使用到多个dll库,程序在发布时要将这些dll库打包到安装包中一起发布,安装时会将这些dll放置到程序的安装目录中(有个别库可能要放到系统路径中)。这些dll主要包括程序依赖的业务库,第三方开源库以及一些C/C++运行时库以及UCRT系统通用运行时库等。关于C/C++运行时库以及UCRT系统通用运行时库的详细说明,可以查看我之前的文章:

C/C++运行时库和UCRT系统通用运行时库总结及问题实例分享(程序打包时要带上这些运行时库)https://blog.csdn/chenlycly/article/details/139094024

此外,程序在运行时除了会加载其依赖的业务库、C/C++运行时库以及UCRT系统通用运行时库,还会加载其依赖的系统库,这些系统库是操作系统提供的,操作系统会保证其兼容性。

2、dll动态库的隐式加载与动态加载

       程序启动时,系统会给程序进程分配指定大小的进程空间(虚拟内存空间),系统先将exe主程序依赖的多个dll库加载到进程空间中,然后再将exe主程序文件加载到进程空间中,然后进入main函数,程序开始运行。如果启动过程有dll库加载失败(非动态加载,会弹出报错提示框),则程序启动终止,程序启动失败!

      程序启动时主要会加载以下3类dll动态库:

1)程序依赖的dll业务库:包括上层UI库、业务组件库、音视频编解码库、协议库、网络库以及第三方开源库等。

2)运行时库:包含msvcp100.dll、msvcr100.dll、msvcp140.dll、vcruntime140.dll等C/C++运行时库,api-ms-win-core-file-l1-2-0.dll、api-ms-win-core-handle-l1-1-0.dll等UCRT系统通用运行时库。使用不同版本Visual Studio开发的程序,依赖的运行时库的版本是不同的。如果多个模块使用了多个版本的Visual Studio开发,则要将这些版本对应的运行时库都带上。比如Visual Studio 2010对应的运行时库是msvcp100.dll和msvcr100.dll,Visual Studio 2017对应的运行时库则是msvcp140.dll、vcruntime140.dll。

3)系统动态库:包括多个操作系统的动态库(系统库),比如user32.dll、kernel32.dll、ntdll.dll、ws2_32.dll、shell32等。

       程序中引用dll动态库(调用dll中的接口),在dll动态库加载时可分隐式加载和动态显式加载。

2.1、dll库的隐式加载

       所谓隐式加载就是在程序中使用#pragma预编译指令引入dll库对应的.lib导入库:

#pragma comment( lib, "libcurl.lib")

或者在VS的工程配置中配置.lib导入库:

       对于隐式加载的库,在引入lib导入库之后,并包含dll库的头文件,就可以直接在代码中调用dll库的API接口了。

代码编译时,会用到动态库API头文件,如果找不到调用的函数定义,则会报错;链接时,会链接.lib库中的符号,如果找不到函数符号,也会报错。

       隐式加载的dll库,在程序启动时就加载了(上面已经讲了程序启动时dll库的加载流程,主程序启动之前会将依赖的dll库先加载到程序的进程空间中),如果dll库加载失败,则立即终止exe主程序的启动流程,程序启动失败。

2.2、dll库的显式加载

       dll库的显式加载,是调用LoadLibrary或者LoadLibraryEx去动态地加载dll库。

       对于显式加载的库,需要调用GetProcAddress接口去获取dll库中API接口的函数地址(代码段地址),然后通过该地址去调用API函数。比如如下的一段代码:

BOOL AutoRegsvr32( LPCTSTR lpszDllPath )
{
	if ( lpszDllPath == NULL )
	{
		return;
	}

	// 1、使用LoadLibraryEx加载动态库
	HINSTANCE hInstLib = LoadLibraryEx( lpszDllPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH );
	if ( NULL == hInstLib )
	{
		return FALSE;
	}

    // 2、调用GetProcAddress获取要调用的函数地址
	typedef HRESULT (*DllRegisterServer)(void);
	DllRegisterServer dllRegisterServer = (DllRegisterServer)GetProcAddress( hInstLib, "DllRegisterServer" );
	if ( dllRegisterServer != NULL )
	{
        // 3、使用获取的函数地址去调用函数
		HRESULT hr = dllRegisterServer();
	}
	else
	{
		FreeLibrary(hInstLib);
		return FALSE;
	}

	FreeLibrary(hInstLib);
	return TRUE;
}

       显式加载的dll库,不会在程序启动时加载,而是在代码执行到LoadLibrary或者LoadLibraryEx函数的调用时才会动态的加载。如果dll库加载失败,也不会导致程序启动失败。本文主要讨论动态加载动态库的相关内容。


        在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)

专栏1:该精品技术专栏的订阅量已达到530多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)

C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn/chenlycly/article/details/125529931

本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!

考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!

专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!

专栏2:(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,订阅量已达170多个,专栏文章已经更新到460多篇,持续更新中...)

C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn/chenlycly/category_11931267.html

以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、数据结构与算法、C++11及以上新特性(不仅看开源代码会用到,日常编码中也会用到部分新特性,面试时也会涉及到)、常用C++开源库的介绍与使用、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(排查软件异常的手段与方法、分析C++软件异常的基础知识、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与网络问题分析进阶内容等。

专栏3:  

C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn/chenlycly/article/details/131405795

常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!

专栏4:   

VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn/chenlycly/article/details/124272585

将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。

专栏5: 

C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn/chenlycly/category_12695902.html

根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。


3、为什么要使用动态加载dll动态库的方式?什么时候需要使用动态加载?

       为什么要动态加载dll动态库以及什么时候使用动态加载方式,我们通过几个场景来说明一下。

3.1、调用系统dll库中未公开的接口

       有时我们可以需要调用Windows系统库中未公开的接口,有可能这个接口Windows官方未公开,也有可能这个接口在Windows某个版本之后才支持,即某个版本之后才提供该接口。

比如最近有个CSDN上一个刚毕业的小伙找到我,问了一个类似的问题,他们项目使用的IDE开发环境是VS2015,他为了实现UI界面对触摸屏消息的支持,需要调用触摸屏相关的某个API函数,但这个函数在VS2017以上的版本才支持,在VS2015中找不到这个函数的定义,没法使用。他最开始想把项目从VS2015升级到VS2019,但升级后出现了很多编译错误,应该是新版本的编译器和老版本的有差异导致的。因为刚毕业,没多少开发经验,很难去处理升级后的那些编译错误。

       于是他找到我,问在这种情况下是否有办法使用到这个新API函数。后来建议他采用动态加载dll库的方式,因为这个API函数在较新的操作系统上才支持,动态加载时正好可以判断一下目标dll库是否有这个函数。因为程序要支持所有版本的操作系统,在一些老的操作系统中比如XP系统(当前估计基本没人用了,但我们这边有个测试同事的测试机器上还在用XP系统)是不支持一些新的API函数的。

       我们再举一个具体的例子,我们10多年前在开发一个打开文件所在文件夹的功能,要实现打开所在文件夹并选中该文件的效果:

​经搜索得知,已经有API函数SHOpenFolderAndSelectItems直接支持这个功能了,但这个函数只有Windows XP系统才支持,当时还有部分用户在用Windows 2000系统,这个系统是不支持这个函数的。

        如何知道目标函数支持的最低操作系统版本?如何知道目标函数位于哪个系统dll库中呢?其实很简单,以“函数名 msdn”为关键字到网页上搜索,找到微软MSDN上对该函数的说明,将页面滚动到页面底部,就可以看到函数的相关信息。以SHOpenFolderAndSelectItems函数为例,该函数的信息描述如下:

​首先是该函数最低支持操作系统是Windows XP,即之前的版本是不支持的,比如Windows 2000系统中是不支持的,即Windows 2000系统的系统dll库中没有这个接口。接着可以看到,SHOpenFolderAndSelectItems函数的声明位于shlobj_core.h头文件中,函数实现位于Shell32.dll库中。

作为一名Windows开发人员,要学会使用微软的MSDN,是基本的要求,在MSDN上可以看到微软官方对窗口消息、系统API函数等的详细说明。遇到问题时,我们应该第一时间到MSDN上查看相关的说明。

       当时我们使用的还是Visual Studio 6.0(VC6),在VC6中也找不到SHOpenFolderAndSelectItems函数的声明(将SHOpenFolderAndSelectItems输入到代码中,会找不到该函数的声明,编译会报错),所以后来决定使用LoadLibrary动态加载系统库Shell32.dll,然后再调用GetProcAddress接口到dll中去获取SHOpenFolderAndSelectItems函数的函数指针(函数首地址),然后去调用该函数地址即可。

如果代码运行在Windows 2000的系统上,系统库Shell32.dll中是找不到SHOpenFolderAndSelectItems函数的,调用GetProcAddress会返回NULL,这样也就没法使用SHOpenFolderAndSelectItems函数了,此时可以简单的调用ShellExecute API函数打开所在文件夹,但不能选中目标文件了。

         动态加载库Shell32.dll去调用接口SHOpenFolderAndSelectItems的代码如下:

// 打开文件夹并选中对应的文件
BOOL OpenFolderAndSelectFile(CString strFilePath)
{
    LPITEMIDLIST pidl;
    LPCITEMIDLIST cpidl;
    LPSHELLFOLDER pDesktopFolder;
    ULONG chEaten;
    HRESULT hr;
    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, (LPCSTR)strFilePath, -1, wfilePath, MAX_PATH);
        lpWStr = wfilePath;
#endif

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

        cpidl = pidl;

        // SHOpenFolderAndSelectItems是非公开的API函数,需要从shell32.dll获取
        // 该函数只有XP及以上的系统才支持,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;
                }
            }

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

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

    ::CoUninitialize();
    return TRUE;
}

       对系统函数SHOpenFolderAndSelectItems的调用封装到函数OpenFolderAndSelectFile中,可以根据OpenFolderAndSelectFile函数的返回值判断有没有成功执行到系统函数SHOpenFolderAndSelectItems,如果返回FALSE,则表示没成功调用SHOpenFolderAndSelectItems,则调用ShellExecute去打开所在文件夹,但不会选中目标文件。

ShellExecute( NULL, _T("open"), _T("explorer.exe"), strFilePath, NULL, SW_SHOWNORMAL );

        关于打开文件所在文件夹的详细细节说明,可以参见我之前写的文章:

VC++实现打开文件和打开所在文件夹的功能(附源码)https://blog.csdn/chenlycly/article/details/123591092

3.2、调用控件库中的注册接口向系统中注册该控件

        对于控件库的注册,我们可以手动在cmd窗口中使用regsvr32命令去注册,如下所示:

regsvr32 "D:\Program Files\feiq\GifDll\ImageOle.dll"

​但有时需要在代码中去自动注册控件库,比如在用代码实现的程序Setup安装包程序中,需要通过代码去注册程序中包含的控件库。

该安装包是自己写代码实现的,不是使用InstallShield、InnoSetup打包工具制作的,自己通过代码实现安装包要灵活很多,可以定制安装包的UI界面,可以定制安装包执行的操作。

        一般对于需要向系统注册的控件库,内部都会实现DllRegisterServer接口,我们只要获取到这个接口,就可以向系统注册了。在代码中调用LoadLibrary将控件库加载起来,然后调用GetProcAddress函数去获取DllRegisterServer函数地址,然后去call这个函数即可。注册控件库的代码如下

void RegCtrl( LPCTSTR lpszDllPath )
{
    if ( lpszDllPath == NULL )
    {
        return;
    }
 
    CString strLog;
    strLog.Format( _T("[RegCtrl] lpszDllPath: %s."), lpszDllPath );
    WriteLog( strLog );
 
    // 1、先将库动态加载起来
    HINSTANCE hInstance = LoadLibrary( lpszDllPath )
    if ( NULL == hInstance )
    {
        strLog.Format( _T("[RegCtrl] load dll failed, GetLastError: %d."), GetLastError() );
        WriteLog( strLog );
    }
 
    // 2、获取库中的DllRegisterServer函数接口,调用该接口去完成控件的注册
    typedef HRESULT (*DllRegisterServerFunc)(void);
    DllRegisterServerFunc dllRegisterServerFun = (DllRegisterServerFunc)GetProcAddress( hInstance, "DllRegisterServer" );
    if ( dllRegisterServerFun != NULL )
    {
        HRESULT hr = dllRegisterServerFun();
        strLog.Format( _T("[RegCtrl] DllRegisterServer return: %d"), hr );
        WriteLog( strLog );
    }
    else
    {
        strLog.Format( _T("[RegCtrl] Get DllRegisterServer address failed. GetLastError: %d"), GetLastError() );
        WriteLog( strLog );
    }
 
    FreeLibrary( hInstance );
}

3.3、底层的业务模块做成动态启动方式,上层产品可以根据自己的业务需要选择想启动的业务模块

       对于底层的业务组件层,在设计时可以按业务类型划分成多个dll去实现,在启动时使用动态启动的形式。上层产品可以根据自己需要的功能去启动对应的dll业务模块,不需要的模块则可以不启动(通过调用接口告诉底层要启动哪些模块),这样程序就不用一上来就加载所有的模块,更加灵活,更节省资源(较少程序对内存等资源的占用)。

程序在加载dll库时,将dll库的二进制文件加载到程序的进程空间中,dll库二进制文件会占用程序的虚拟地址空间。二进制文件中存放的二进制代码,所以二进制文件占用的是程序代码段内存。此外,加载起来的模块,模块中的部分变量可能在加载或者初始化时就分配内存了,会占用数据段内存。所以尽量加载少的模块,可以节约内存空间。

4、LoadLibrary动态加载dll动态库失败的场景

       我们几年前在项目中遇到过好几次LoadLibrary动态加载库失败的情况了(虽然加载失败的概率很小,是偶发的,但一旦出现,会明显影响产品的使用),明明指定的绝对路径中的dll文件是存在的,文件也是正常的,但就是会加载失败。我们尝试调用GetLastError接口去获取LastError值也没搞清楚加载失败的原因。下面列举几个项目中遇到的问题场景。

4.1、自制的安装包程序中遇到的LoadLibrary加载dll库失败问题

       我们在自制的安装包程序中需要向系统注册控件(控件做成了dll),相关代码如下:(传入要注册控件的完整路径)

void RegCtrl( LPCTSTR lpszDllPath )
{
    if ( lpszDllPath == NULL )
    {
        return;
    }
 
    CString strLog;
    strLog.Format( _T("[RegCtrl] lpszDllPath: %s."), lpszDllPath );
    WriteLog( strLog );
 
    // 1、先将库动态加载起来
    HINSTANCE hInstance = LoadLibrary( lpszDllPath )
    if ( NULL == hInstance )
    {
        strLog.Format( _T("[RegCtrl] load dll failed, GetLastError: %d."), GetLastError() );
        WriteLog( strLog );
    }
 
    // 2、获取库中的DllRegisterServer函数接口,调用该接口去完成控件的注册
    typedef HRESULT (*DllRegisterServerFunc)(void);
    DllRegisterServerFunc dllRegisterServerFun = (DllRegisterServerFunc)GetProcAddress( hInstance, "DllRegisterServer" );
    if ( dllRegisterServerFun != NULL )
    {
        HRESULT hr = dllRegisterServerFun();
        strLog.Format( _T("[RegCtrl] DllRegisterServer return: %d"), hr );
        WriteLog( strLog );
    }
    else
    {
        strLog.Format( _T("[RegCtrl] Get DllRegisterServer address failed. GetLastError: %d"), GetLastError() );
        WriteLog( strLog );
    }
 
    FreeLibrary( hInstance );
}

控件内部会实现DllRegisterServer接口,我们只要获取到这个接口,就可以向系统注册了。在代码中调用LoadLibrary将控件库加载起来,然后调用GetProcAddress函数去获取DllRegisterServer函数地址,然后去call这个函数即可。

       调用系统API函数LoadLibrary时,传入的控件文件的完整路径,比如D:\Program Files\XXXXXX\ImageOle.dll,这个路径和文件都存在的,但为什么LoadLibrary还会加载失败呢?

       于是修改代码,在调用LoadLibrary之后调用GetLastError去获取LoadLibrary失败的错误码,根据错误码(错误码为2)的含义得知,是系统找不到指定的文件。传入的dll全路径是对的,文件也是有的,为啥还找不到文件呢?这就有点诡异了!这个问题只在个别的电脑上会出现。

4.2、主程序底层模块调用LoadLibrary加载dll库失败问题

       主程序的底层模块实现了组件化启动的方式,上层可以根据其需要的模块功能,可以有选择性的启动一些需要的底层业务模块,不用启动所有的业务模块,这样显得更加灵活,更节约资源。

       某天在某台电脑上出现业务不响应的问题,从软件的运行日志中找不到某个业务dll模块输出的日志,于是用Process Explorer查看我们程序加载的dll库列表:

​发现底层的某个动态加载的dll库没有加载到进程空间中。

       后来我们在LoadLibrary动态加载库的代码处添加了打印,将要加载的dll库的路径打印出来,并将LoadLibrary的LastError值打印出来。运行添加打印的程序后,复现了现象,取出日志看到,要加载的dll文件是全路径,LoadLibrary执行完后的LastError值显示找不到文件。这和上面的例子类似的,要加载的dll文件的路径都是正确的,但就是加载失败。

       关于如何使用Process Explorer工具,可以参见我之前写的文章:
使用Process Explorer和Dependency Walker定位dll库动态启动失败的问题https://blog.csdn/chenlycly/article/details/125216591C++软件开发值得推荐的十大高效软件分析工具https://blog.csdn/chenlycly/article/details/127608247

5、 LoadLibaray加载失败的可能原因

       好像当时出问题的机器都是Win7系统,据技术群里的大佬说,Win7纯净版系统的LoadLibaray内部实现有bug,有概率会出现加载失败的情况,需要安装一个补丁才能解决!

       但这个方法不太可行,因为不能到客户机器上去安装补丁,也没有精力到所有的win7系统中去安装补丁。这个问题比较严重,严重影响软件的使用,我们还是要找到解决问题的根本办法才行,从代码层去解决,而不是让用户去介入!

       我们之前接触过仿Windows XP系统的开源操作系统ReactOS,当时在研究Windows系统异常的处理流程时详细地看过相关的源码,这次也想尝试到ReactOS的源码中找到解决的办法!

6、参考开源操作系统ReactOS中的regsvr32.exe程序的实现源码,找到了解决LoadLibrary加载dll库失败的办法

        之前下载过开源操作系统ReactOS的源码,ReactOS中的系统库内部实现和Windows是很相像的,提供的系统API接口几乎是一模一样的。我们时常会去查看ReactOS中API函数及底层库的内部实现,去了解Windows系统的内部实现,以辅助我们排查遇到的问题。

6.1、ReactOS开源操作系统简介

​       ReactOS是一款基于 Windows NT 架构的类似于Windows XP系统的免费开源操作系统,旨在实现和Windows操作系统二进制下的完全应用程序和驱动设备的兼容性,通过使用类似构架和提供完全公共接口。ReactOS一直在持续维护中,可以到reactos官网上找到ReactOS源码的下载地址,使用svn将ReactOS源码下载下来。

       ReactOS开源代码对于我们Windows软件开发人员来说非常有用,我们可以去查看API函数的内部实现,可以去查看系统exe的内部实现,可以去查看ReactOS系统内部任意模块的实现代码ReactOS是比较接近Windows系统的,可以通过查看ReactOS的代码去大概地了解Windows系统的内部实现,对我们排查Windows软件的问题是很有好处的!

6.2、使用Source Insight打开ReactOS源码,找到regsvr32.exe程序的代码

        ReactOS源码中没有Visual Studio工程文件,无法使用Visual Studio打开查看源代码,可以Source Insight去查看源码。至于如何使用Source Insight查看编辑源码,可以参看我之前写的一篇关于Source Insight的文章:
使用Source Insight查看编辑源代码https://blog.csdn/chenlycly/article/details/124347857       为什么会想到去查看ReactOS中的regsvr32.exe程序的源码实现呢?因为上面我们说到的一个问题场景,在安装包中去动态加载dll控件库去注册控件,LoadLibrary加载失败,但我们在cmd命令行中使用regsvr32程序去手动注册,是可以加载起来的:(此处以注册飞秋软件中的图片插入控件ImageOle.dll为例

​于是想去看看regsvr32.exe程序的内部实现,看看它加载dll库的代码是什么样子的!正好我们看过ReactOS开源系统的源码,其实现和Windows系统很类似的,于是去ReactOS中查看regsvr32.exe源码实现。

       因为regsvr32是一个独立的exe,不是一个函数,所以需要找到该程序对应的.c源文件于是尝试到文件列表中以regsvr32为关键字进行搜索,找到了regsvr32.c文件。在该文件中找到_tWinMain函数,在该main函数中看到了加载dll库文件的代码,如下所示:

​​代码中是调用LoadLibraryEx接口去加载dll库的,传入的参数为LOAD_WITH_ALTERED_SEARCH_PATH,并且也是获取dll控件库中的DllRegisterServer去进行注册的。

       regsvr32.exe使用这种方式去加载库文件应该是有它的道理的,于是我们也参照它的做法,把加载dll库的代码改成调用LoadLibraryEx,传入LOAD_WITH_ALTERED_SEARCH_PATH,即如下所示:

HINSTANCE hInstance = LoadLibraryEx( lpszDllPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH )
if ( NULL == hInstance )
{
    strLog.Format( _T("[RegCtrl] load dll failed, GetLastError: %d."), GetLastError() );
    WriteLog( strLog );
}

改成上述代码后,就没再出现库加载失败的问题了,参考ReactOS中regsvr32.exe源码确实很有用

后来在其他模块代码中也遇到dll库加载失败的问题,也更换成上述代码,后面再也没有出过问题了。

7、到微软MSDN上查看LOAD_WITH_ALTERED_SEARCH_PATH参数的含义

        为了搞清楚LOAD_WITH_ALTERED_SEARCH_PATH参数的含义,我们到微软MSDN上查看LoadLibraryEx API函数的说明页面,找到了LOAD_WITH_ALTERED_SEARCH_PATH参数的说明:

LOAD_WITH_ALTERED_SEARCH_PATH:(0x00000008)

If this value is used and lpFileName specifies an absolute path, the system uses the alternate file search strategy discussed in the Remarks section to find associated executable modules that the specified module causes to be loaded. If this value is used and lpFileName specifies a relative path, the behavior is undefined.
If this value is not used, or if lpFileName does not specify a path, the system uses the standard search strategy discussed in the Remarks section to find associated executable modules that the specified module causes to be loaded.

This value cannot be combined with any LOAD_LIBRARY_SEARCH flag.

从上述描述文字得知,如果设置了LOAD_WITH_ALTERED_SEARCH_PATH参数,则系统会使用the alternate file search strategy搜索策略,那这个搜索策略到底是什么样的呢?

        在LoadLibraryEx函数的说明页面继续向下看,看到了"Dynamic-Link Library Search Order"超链接,这是动态连接库加载顺序的详细说明页面。从页面中我们看到了,如果没设置LOAD_WITH_ALTERED_SEARCH_PATH参数,则使用Standard Search Order for Desktop Applications标准搜索顺序:

1、The directory from which the application loaded.
2、The system directory. Use the GetSystemDirectory function to get the path of this directory.
3、The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
4、The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
5、The current directory.
6、The directories that are listed in the PATH environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.

        如果设置了LOAD_WITH_ALTERED_SEARCH_PATH参数,则系统会使用Alternate Search Order for Desktop Applications搜索顺序:

1、The directory specified by lpFileName.
2、The system directory. Use the GetSystemDirectory function to get the path of this directory.
3、The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
4、The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
5、The current directory.
6、The directories that are listed in the PATH environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.    

所以我们最终找到了答案,当我们设置LOAD_WITH_ALTERED_SEARCH_PATH参数时,就会使用Alternate Search Order for Desktop Applications,会优先使用设置下来的完整路径去加载dll库。

8、dll动态库加载失败的其他原因

       本文主要讲使用LoadLibrary加载dll动态库失败问题,dll动态库是没问题的,在问题场景中也是能启动的,比如手动在cmd窗口中使用regsvr32去注册控件时加载dll库都是能加载来的。

      dll动态库加载失败还有其他的原因,比如当前dll依赖的其他动态库在当前系统中找不到(比如安装目录中缺少业务库或者C/C++运行时库):

或者当前dll接口调用下层的dll库的接口在下层dll中找不到(下层dll库的版本不对):

​这两种原因在日常项目中也比较常见,限于篇幅,我就不详细展开了,可以去查看我之前写的关于dll动态库加载失败导致程序启动报错以及dll库加载失败的常见原因分析的专题文章

dll动态库加载失败导致程序启动报错以及dll库加载失败的常见原因分析与总结https://blog.csdn/chenlycly/article/details/142714236

9、最后

       本文详细讲解了C++动态库编程相关的内容,包括动态库隐式与显式加载、为什么要动态加载动态库、LoadLibrary加载失败、用LoadLibraryEx替代LoadLibrary、参考开源操作系统ReactOS源码等,有一定的参考价值。

       此外,建议大家去了解一下ReactOS开源操作系统,通过查看ReactOS的代码去大概地了解Windows系统的内部实现,对排查Windows软件问题是很有好处的。

本文标签: 动态加载开源源码操作系统