关于动态加载 dll 的排错经历

文章目录

  1. 1. 问题
  2. 2. 查找问题
  3. 3. 解决
    1. 3.1. 解决 GetDynamicTimeZoneInformation 定位问题
    2. 3.2. 解决 TLS 问题
  4. 4. 总结

问题

使用 VS2013 开发的程序在 Win10,Win7 上运行都没有问题,但在 XP 下无法运行,提示未找到 Controller 插件(即 dll,程序使用 QT 开发,使用了动 态加载插件机制)

查找问题

首先想到的解决思路是去查找这个插件有没有问题,这个 Controller 插件是别人开发的,并使用了我开发 的 dal 动态库,由于在 XP 上不能进行调试,所以使用了写日志的方式进行调试,通过日志的跟踪, 发现了问题出在 LoadLibrary 上,加载 dll 失败,这个其实是最初就知道的结果,只是对程序 中的动态加载机制不甚了解,所以这次调试只是熟悉了这个机制,并没有找到问题所在。

为什么是这个 dll 加载失败,而其它的 dll 没有问题呢。 那就写一个测试程序,使用 LoadLibrary 动态加载 Controller 动态库,程序在 XP 上一运行就出错,错误信息是

  无法定位程序输入点 GetDynamicTimeZoneInformation 于动态链接库 KERNEL32.dll 上

开始我以为是这个测试程序有问题,就寻找 VS2013 开发 XP 兼容程序的解决方法,在多次尝试 之后,仍无法解决。GetDynamicTimeZoneInformation 这个信息一直没有引起我的注意,因 为我在程序中没有使用过这个函数。在经过一番努力之后,仍然没有解决问题,就转换一下 方法。

从网上得知,使用 process monitor 可以查看 dll 的加载情况,process monitor 是一个实时 进程监控软件,能监控系统中的文件操作和注册表的读写过程。通过监控跟踪,发现 dll 都 进行了加载,并没有发现问题,再换种方法。

使用 depends 查看 Controller 动态库的依赖关系,开始时怀疑是路径有问题,但其它 dll 没有问题,应该不是这个原因。然后仔细检查依赖项, 在依赖树中,发现 Controller 依 赖 dal,dal 又依赖 kernel32,而其中使用的函数就包括 GetDynamicTimeZoneInformation,但这个函数在 XP 下的 kernerl32 中找不到。于是查了一下 MSDN,发现这个函数支持的最低版本是 VISTA, 原来是这个函数在捣鬼,但是 dal 中没有使用 到这个函数,开始时我并没有怀疑 dal 中使用的开源库,因为那些库都是跨平台的。在找遍所有 的代码也没有找到与此相关的东西。那就用最笨的办法,把工程中的文件依次从工程中剔除, 编译后使用 depends 看是否使用了这个函数,到最后,整个工程只剩基本框架了,还是对这个函数有依赖, 此时只使用了 spdlog 这个开源库,那就从这个库中搜搜这个函数,没想到竟然找到了,原来 这个日志库获取时间信息要用到这个函数,而且源码中也加了宏判断,在只有系统版本高 于 VISTA,才使用这个函数,这个函数在 details/os.h 中。

inline int utc_minutes_offset(const std::tm& tm = details::os::localtime())

{

#ifdef _WIN32
#if _WIN32_WINNT < _WIN32_WINNT_WS08
TIME_ZONE_INFORMATION tzinfo;
auto rv = GetTimeZoneInformation(&tzinfo);
#else
DYNAMIC_TIME_ZONE_INFORMATION tzinfo;
auto rv = GetDynamicTimeZoneInformation(&tzinfo);
#endif
if (rv == TIME_ZONE_ID_INVALID)
throw spdlog::spdlog_ex("Failed getting timezone info. Last error: " + GetLastError());

int offset = -tzinfo.Bias;
if (tm.tm_isdst)
offset -= tzinfo.DaylightBias;
else
offset -= tzinfo.StandardBias;
return offset;
#else
return static_cast<int>(tm.tm_gmtoff / 60);
#endif
}

于是在预处理器定义中加上

WINVER=_WIN32_WINNT_WINXP

_WIN32_WINNT=_WIN32_WINNT_WINXP

编译后的程序在 XP 上不出现找不到插件的错误,而是直接报出异常信息,异常的位置还是出 现在 dal 动态库中。由于无法定位这个异常的位置,没办法在 XP 中安装了 windbg,使用 winbdg 调试,很快定位到内部函数 XXXGetLastError 上,这个函数是用于获取错误信息的, 使用唯一的技术就是 TLS(Thread Local Storage),不过使用的是静态 TLS:

__declspec(thread) DWORD g_error;

查看 MSDN,发现延迟加载 DLL 的约束中有说明

如果在延迟加载的 DLL 的入口点发生每个进程初始化,则 DLL 的延迟加载可能不会造成进程行为相同。其他情况包括静态 TLS(线程本地存储区),它使用通过 LoadLibrary 加载 DLL 时不处理的 __declspec(thread) 来声明。使用 TlsAlloc、TlsFree、TlsGetValue 和 TlsSetValue 的动态 TLS 仍可在静态或者延迟加载的 DLL 中使用。

原因PRB: Calling LoadLibrary() to Load a DLL That Has Static TLS在中有说明。

解决

解决 GetDynamicTimeZoneInformation 定位问题

于是在预处理器定义中加上

WINVER=_WIN32_WINNT_WINXP

_WIN32_WINNT=_WIN32_WINNT_WINXP

解决 TLS 问题

使用动态 TLS,即 TlsAlloc、TlsFree、TlsGetValue 和 TlsSetValue,替换静态 TLS。

DWORD g_tls_index;


inline void XXXSetLastError(int error) {
TlsSetValue(g_tls_index, (LPVOID)error);
}
XXX_API int XXXLastError() {
return (int)TlsGetValue(g_tls_index);
}
XXX_API int XXXInit() {
if (g_init) {
return XXX_OK;
}
if ((g_tls_index = TlsAlloc()) == TLS_OUT_OF_INDEXES)
return XXX_INIT_ERROR;
g_init = true;
return XXX_OK;
}

XXX_API void XXXDestroy() {
if (g_init) {
TlsFree(g_tls_index);
}
}

总结

程序在 XP 上不能运行的问题实际是由两个问题引起的,这里面牵扯了一些我不清楚的知识, 对于 Windows 下的动态库加载有了进一步理解。在查错排错的过程中一定要冷静和多思 考,这次查错经历就因为鲁莽走了不少弯路,浪费了不少时间。


Render by hexo-renderer-org with Emacs 25.0.50.1 (Org mode 8.2.10)

版权声明

本博客 采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可