目录
1、动态加载dll库去调用库中的函数
1.1、调用系统dll库中未公开的接口
1.2、调用控件库中的注册接口向系统中注册该控件
2、LoadLibrary动态加载dll库失败的场景
2.1、自制安装包中遇到的LoadLibrary加载dll库失败问题
2.2、主程序底层模块调用LoadLibrary加载dll库失败问题
3、受系统regsvr32的启发,试图寻找LoadLibrary加载dll库失败的解决办法
4、到ReactOS开源代码中去查看regsvr32的实现,找到解决问题的线索
4.1、ReactOS开源操作系统简介
4.2、使用Source Insight打开ReactOS源码,找到regsvr32.exe程序的代码
5、到微软MSDN上查看LOAD_WITH_ALTERED_SEARCH_PATH参数的含义
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931 有时我们在代码中需要调用LoadLibrary去动态加载某个dll库,然后从dll中获取某些函数地址去调用这些函数,但个别情况下会出现LoadLibrary加载dll库失败的问题。今天我们就来讲一讲如何解决这类dll库加载失败问题。
一般我们在调用dll库中的接口时,只需要包含dll库的API头文件并将dll库对应的lib库引入到工程中,就可以在代码中直接调用dll库的导出接口或者使用dll库的导出类了。这就是在编译期就确定的静态引入的方式。除了这种方式,还支持动态加载dll库去调用库中接口的方式。在某些个别的场景下,我们确实需要在代码中使用动态加载dll库的方式。
对于静态调用dll库接口的方式,dll库在主程序启动时就被加载进主程序的进程空间中了(主程序在启动时会先将其依赖的底层dll库先加载到进程空间中,然后exe主程序才会启动起来)。
对于动态加载的dll库,只有代码执行到LoadLibrary或者LoadLibarayEx函数调用时才会将dll库加载到进程空间中,可能在函数调用完成后,就将dll库从当前进程空间中卸载掉(调用FreeLibrary)。
有时我们可以需要调用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函数的声明,所以后来决定使用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.net/chenlycly/article/details/123591092
对于控件库的注册,我们可以手动在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 );
}
以前我们遇到过自制安装包在注册控件库时出现动态加载控件库dll失败的问题。我们是调用LoadLibrary去加载的,传入的dll库的路径也是要加载的dll库的全路径,比如D:\Program Files\XXXXXX\ImageOle.dll,这个路径在出问题的电脑上确实是存在的,并且dll文件也是在这个路径下面的,为什么LoadLibrary还会加载失败呢?
于是修改代码,在调用LoadLibrary之后调用GetLastError去获取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.net/chenlycly/article/details/125216591C++软件开发值得推荐的十大高效软件分析工具https://blog.csdn.net/chenlycly/article/details/127608247
接着上面说到的问题,自制安装包程序中注册控件dll失败,于是在出问题的电脑上尝试手动使用regsvr32命令去注册,以注册ImageOle.dll控件为例,注册命令如下:
regsvr32 "C:\Program Files\feiq\GifDll\ImageOle.dll"
regsvr32命令在注册控件时,应该也会去加载控件dll库的,为啥该命令可以将dll库加载起来呢?该命令是调用哪个API函数去加载dll库的呢?
regsvr32命令的相关代码是在Windows系统库中的,是没法看到Windows系统的源码的!突然想到,可以查看仿Windows的ReactOS开源操作系统的源码,ReactOS桌面操作系统的源码是开源的,并且是和Windows是类似的,可以查看ReactOS中regsvr32命令的相关实现代码,看看能否模仿相关的代码去处理。
之前下载过开源操作系统ReactOS的源码,ReactOS中的系统库内部实现和Windows是很相像的,提供的系统API接口基本是一模一样的。我们时常会去查看ReactOS中API函数及底层库的内部实现,去了解Windows系统的内部实现,以辅助排查我们在开发过程中遇到的问题。
ReactOS是一款基于 Windows NT 架构的类似于Windows XP系统的免费开源操作系统,旨在实现和Windows操作系统二进制下的完全应用程序和驱动设备的兼容性,通过使用类似构架和提供完全公共接口。ReactOS一直在持续维护中,可以到reactos官网上找到ReactOS源码的下载地址,使用svn将ReactOS源码下载下来。
ReactOS开源代码对于我们Windows软件开发人员来说非常有用,我们可以去查看API函数的内部实现,可以去查看系统exe的内部实现,可以去查看ReactOS系统内部任意模块的实现代码。ReactOS是比较接近Windows系统的,可以通过查看ReactOS的代码去大概地了解Windows系统的内部实现,对我们排查Windows软件的问题是很有好处的!
ReactOS源码中没有Visual Studio工程文件,无法使用Visual Studio打开查看源代码,可以Source Insight去查看源码。至于怎么使用Source Insight,可以参看我之前写的一篇关于Source Insight的文章:
使用Source Insight查看编辑源代码https://blog.csdn.net/chenlycly/article/details/124347857 因为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 );
}
改成上述代码后,就没再出现库加载失败的问题了,上述代码果然很有用啊!
后来其他模块中也遇到库加载失败的问题,也更换成上述代码,结果后面再也没有出过问题了。这个通过库的绝对路径去加载库失败的问题,不是必现的,只是在个别机器上才会出问题。
为了搞清楚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库。