问题描述
似乎这个问题仅存在于Windows下Python x64中,且Python 2和Python 3中的表现不一致;并且一般是由于调用的C-API包含有指针传递,出现类似如下错误
OSError: exception: access violation reading 0x0000000025F0FA60
示例
C++代码:
#include
struct Foo
{
char* child = "a child";
};
extern "C"
{
__declspec(dllexport) char* bar(char*, char*);
__declspec(dllexport) Foo* getFoo(void);
__declspec(dllexport) char* getChild(Foo*);
}
char* bar(char* a, char* b)
{
char* out = new char[strlen(a) + strlen(b) + 1];
strcpy(out, a);
strcat(out, b);
return out;
}
Foo* getFoo()
{
return new Foo();
}
char* getChild(Foo* foo)
{
return foo->child;
}
函数bar()
期望完成的是两个字符串的连接。getFoo()
返回一个类指针,而getChild()
接受一个类指针,返回字符串。
测试一
正常的Python代码:
import ctypes
DLL_PATH = 'C:/Test/x64/Release/Test.dll'
lib = ctypes.cdll.LoadLibrary(DLL_PATH)
a = b'wow'
b = b'python'
out = lib.bar(a, b)
text_out = ctypes.string_at(out)
print(text_out)
在Python 2.7.12下执行正确,返回
wowpython
但在Python 3.5.2下执行会提示错误:
Traceback (most recent call last):
File "C:\Desktop\test_x64.py", line 9, in
text_out = ctypes.string_at(out)
File "C:\Python35\lib\ctypes\__init__.py", line 491, in string_at
return _string_at(ptr, size)
OSError: exception: access violation reading 0x000000002258AF60
ctypes.string_at(out)
出现了非法的内存地址访问。
测试二
正常的Python代码:
import ctypes
DLL_PATH = 'C:/Test/x64/Release/Test.dll'
lib = ctypes.cdll.LoadLibrary(DLL_PATH)
foo = lib.getFoo()
out = lib.getChild(foo)
text_out = ctypes.string_at(out)
print(text_out)
在Python 2和Python 3中都会出现非法的内存访问,但出错的语句不一样。
在Python 2.7.12下执行提示错误:
Traceback (most recent call last):
File "C:\Desktop\test_x64_2.py", line 8, in
text_out = ctypes.string_at(out)
File "C:\Python27\lib\ctypes\__init__.py", line 506, in string_at
return _string_at(ptr, size)
WindowsError: exception: access violation reading 0xFFFFFFFF83823230
ctypes.string_at(out)
出现了非法的内存地址访问。
在Python 3.5.2下执行提示错误:
Traceback (most recent call last):
File "C:\Desktop\test_x64_2.py", line 7, in
out = lib.getChild(foo)
OSError: exception: access violation reading 0xFFFFFFFFDCCDD220
lib.getChild(foo)
出现了非法的内存地址访问。
原因分析
先来看看ctypes.string_at()
,在官方文档里给出的描述是
ctypes.string_at(address, size=-1)
This function returns the C string starting at memory address address as a bytes object. If size is specified, it is used as size, otherwise the string is assumed to be zero-terminated.
string_at()
接受的参数是内存地址,似乎隐约知道出错的原因了:给string_at()
传递了一个错误的内存地址,导致程序执行时试图访问非法的内存地址,提示错误。
但传入string_at()
的参数是由API直接返回的,怎么会出错呢?继续看官方文档,官方文档中提到在加载动态链接库时
Functions in these libraries use the standard C calling convention, and are assumed to return int.
意思就是C-API中任何类型的返回值,在Python中统一都是int,这样也容易理解:返回int类型就是int值,返回指针就是内存地址(毕竟指针实际就是记录内存地址的)。那是不是返回的指针地址就是错的?我们来试试看。
现在打印出返回的内存地址
import ctypes
DLL_PATH = 'C:/Test/x64/Release/Test.dll'
lib = ctypes.cdll.LoadLibrary(DLL_PATH)
a = b'wow'
b = b'python'
out = lib.bar(a, b)
print('address:', out)
text_out = ctypes.string_at(out)
print(text_out)
在Python 3.5.2运行几次,看看返回的内存地址和错误提示的关系
address: -1736381920
OSError: exception: access violation reading 0xFFFFFFFF9880EA20
address: -797043632
OSError: exception: access violation reading 0xFFFFFFFFD07E1450
address: 851060880
OSError: exception: access violation reading 0x0000000032BA2890
返回的内存地址和非法内存地址访问是一致的!而且内存地址甚至出现了负数,怎么可能!问题就是出在返回指针(即返回内存地址)上面了,无论是char*
还是Foo*
都有这种问题!
通过测试32位版本的动态链接库,Python 2和Python 3均未出现问题,可以确定是32位和64位兼容性问题,类似于著名的C语言x86和x64下int的长度问题。
注意:限于自己目前的水平,更深层次的原因还不得而知,但需要注意Python2和Python3下出错的表现不一致,Python2下似乎可以克服一些指针传递问题。
解决方法
通过多次尝试,我发现可以通过设置API的restype
来获得正确的内存地址。
restype
可以重载API的返回值类型(记得上面说的默认值为int吧),因为64位下内存地址为64位无符号型整数,因此设置为ctypes.c_uint64
。由于修改了返回值类型,在指针作为函数调用的输入参数时,也可能要作出对应的类型转换。
修正后的测试一
import ctypes
DLL_PATH = 'C:/Test/x64/Release/Test.dll'
lib = ctypes.cdll.LoadLibrary(DLL_PATH)
a = b'wow'
b = b'python'
lib.bar.restype = ctypes.c_uint64 # 修改lib.bar返回类型
out = lib.bar(a, b)
text_out = ctypes.string_at(out)
print(text_out)
Python 2.7.12和Python 3.5.2均能得到正确结果。
修正后的测试一
import ctypes
DLL_PATH = 'C:/Test/x64/Release/Test.dll'
lib = ctypes.cdll.LoadLibrary(DLL_PATH)
lib.getFoo.restype = ctypes.c_uint64 # 修改lib.getFoo返回类型
foo = lib.getFoo()
lib.getChild.restype = ctypes.c_uint64 # 修改lib.getChild返回类型
out = lib.getChild(ctypes.c_uint64(foo)) # 指针作为输入也要进行类型转换
text_out = ctypes.string_at(out)
print(text_out)
Python 2.7.12和Python 3.5.2均能得到正确结果。
小结
如上提到的解决方法只能算作临时的修补,我更倾向于认为这是Python自身的一个bug:没有解决64位环境下的C指针类型问题。