/******************************************************************************************
* 版权声明
* 本文为本人原创,本人拥有此文的版权。鉴于本人持续受益于开源软件社区,
* 本人声明:任何个人及团体均可不受限制的转载和复制本文,无论是否用于盈利
* 之目的,但不得修改文章内容,并必须在转载及复制时同时保留本版权声明,否
* 则为侵权行为,本人保留追究相应主体法律责任之权利。
* speng2005@gmail.com
* 2019-7
******************************************************************************************/
近日在win10上配置python 3.6开发环境,经常需要安装各种第三方包。由于未使用anconda等环境管理工具进行安装,所以经常碰到一类非常恼人的错误信息:
ImportError: DLL load failed: 找不到指定的程序。
这么一个没什么有用信息的报错,令人非常恼火。其实,这并非是python为了省事,简单报个错,而是python也只能得到一个简单的错误代码和一句干巴巴的错误信息,再也没有其他有用信息了。
难道真没有详细错误信息了吗?
还真有,不过不是一般人能想到使用的。这就是Gflags工具(看来又要用它了)。
Gflags是微软为windows平台调试应用程序错误的利器。它来自windows sdk开发工具包。
Gflags 官方参考:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-commands
Gflags可以设置一个特殊参数,使得目标程序下次启动后,windows的dll加载器ldr会打印加载过程中的详细调试信息。不过,这个调试信息需要在vistual studio工具attach到目标程序之后,才能在其output窗口中看到。
此处Gflags用法:
假设目标程序是testProject1.exe
cmd中执行命令:
gflags.exe -i testProject1.exe +sls
可实现下次启动testProject1.exe后,用vistual studio连接到testProject1.exe,即可在output窗口看到windows的ldr打印的一大堆调试信息。仔细看这些输出,你肯定会找到为什么dll加载失败了。
那么脑洞再开一下,为什么Gflags设置以后就有ldr的详细信息了?经过一番探究,原来是windows的dll加载器ldr核心代码就在ntdll.dll中,在该dll的LdrLoadDll()函数就是实现具体dll加载过程的入口函数。该dll中有一个未公开的ShowSnap全局变量,该变量不同的bit位可以提供不同的debug功能支持。只要ShowSnap的最低位为1,就可以实现打印ldr的详细调试信息。gflags.exe上述命令设置的目的也就是用来影响目标程序testProject1.exe启动后,系统自动设置ShowSnap全局变量的值,使其最低位为1。
那为什么ldr打印的调试信息必须要在vistual studio中才能看到?因为ldr打印调试信息不是普通的printf,而是以异常信息来实现的,具体来说就是通过调用ntdll.dll中的RtlRaiseException()来实现的,在异常信息中设置异常代码为DBG_PRINTEXCEPTION_C,然后vistual studio以调试器的身份捕获该异常并显示ldr抛出的调试信息。
如果对目标程序调试完毕,最好使用命令:
gflags.exe -i testProject1.exe -sls
来关闭ldr打印调试信息,以提高性能。
以笔者遇到的情况,在搭建基于pytorch的人工智能项目环境时,在import torch时就失败了,出现了上面的无用错误提示。为了找到问题原因,就需要使用上述方法。为方便调试,写了如下测试脚本test_torch.py:
import torch
print(torch.__version__)
然后执行如下命令开启ldr打印调试信息功能:
gflags.exe -i python.exe +sls
接着运行py脚本:
python.exe d:\test\test_torch.py
然后打算用vistual studio来attach时,发现上面代码迅速执行结束,根本没有机会attach。所以这时需要启用python的debugger来让py脚本中断执行:
python.exe -m pdb d:\test\test_torch.py
命令启动后,python的debugger让脚本停留在第1行,也就是import torch语句处,这时可以有充足的时间使用vistual studio来attach目标程序python.exe,然后在python的debugger中输入"n"命令就可以单步执行了,最后输入"exit"命令退出执行。与此同时,可以看到vistual studio的输出窗口中打印出了大量的调试信息。
一般来说,dll加载不成功就两个原因:一是该dll所依赖的其他dll在ldr的所有加载搜索路径中都找不到;二是dll所依赖的其他dll的函数或变量的符号找不到,也就是所依赖的其他dll的版本不匹配导致。
根据上述思路去查看vistual studio的输出窗口中的调试信息,就可以找到相关线索并解决之。在笔者的案例中,出现了两条重要的信息:
LdrpNameToOrdinal - WARNING: Procedure "PySlice_Unpack" could not be located in DLL at base 0x00000000725A0000.
LdrpReportError - ERROR: Locating export "PySlice_Unpack" for DLL "C:\Users\Administrator\AppData\Local\Programs\Python\Python36\lib\site-packages\torch\lib\torch_python.dll" failed with status: 0xc0000139.
原来import torch失败的原因是加载torch_python.dll时其所依赖的"PySlice_Unpack"函数找不到,而这个函数应该在基地址为"0x00000000725A0000"的dll中。正好在vistual studio工具的模块列表中可以查到,这个基地址为"0x00000000725A0000"的dll就是python36.dll。
进一步网络搜索得知,"PySlice_Unpack"这个函数要到python3.6.2才有,而笔者的环境是python3.6.0。所以,问题的根本原因还是所安装的torch与其依赖的python环境版本失配问题。但是通过上述方法,我们最终能够解决未知问题。
那么有人说了,上面的方法使用起来也挺麻烦啊,需要安装windows sdk开发工具包(为使用Gflags),vistual studio开发环境,还需要比较懂c/c++调试程序的技巧,对于不熟悉这些的人,还是很不友好的。
所以笔者专门开发了方便使用的工具loadDllDumper,这里也做个广告传送门:https://download.youkuaiyun.com/download/test201105/11393930
此工具很小,绿色免安装,解压后即可使用,不需要安装别的工具包了,是不是很方便啊!
使用loadDllDumper来跟踪上面的pytorch问题的话,命令如下:
LoadDllDumper.exe python36 -m pdb d:\test\test_torch.py
执行过程中就会打印部分重要信息到屏幕上,同时也在当前目录下生成loadDllDumper_log.txt记录详细的ldr调试信息,仔细查看就会找到问题原因。
注意,loadDllDumper仅支持Win7,Win10的64位应用程序的跟踪。除了解决python环境问题,也可用于诊断其他任意程序的dll加载问题。