带回调的 ctypes:退出时访问冲突
ctypes with callback: access violation on exit
我以前问过这个问题,但没有人回答。我再问一次,这次要简单得多。
我有一个由 Python ctypes 调用的 dll,带有回调函数。回调一直正常工作(如果我在 Visual Studio 中单步执行程序,我可以看到它在运行),但在退出时 Visual Studio 抛出一个 "access violation" 异常。但是,如果我从 dll 中删除对回调的调用,它会正常退出而不会出现访问冲突。
我还必须执行其他操作才能从带有回调的 dll 中退出吗?我已经研究了几个小时,但我还没有在网上找到任何可以解决这个问题的方法。
这是 ctypes 代码。我省略了 dll 代码以保持简短(它写在 NASM 中)但如果需要它我也可以 post 它。
def SimpleTestFunction_asm(X):
Input_Length_Array = []
Input_Length_Array.append(len(X)*8)
CA_X = (ctypes.c_double * len(X))(*X)
length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)
hDLL = ctypes.WinDLL("C:/Test_Projects/SimpleTestFunction/SimpleTestFunction.dll")
CallName = hDLL.Main_Entry_fn
CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)]
CallName.restype = ctypes.POINTER(ctypes.c_int64)
#__________
#The callback function
LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
def LibraryCall(ax):
bx = math.ceil(ax)
return (bx)
lib_call = LibraryCB(LibraryCall)
lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))
#__________
ret_ptr = CallName(CA_X,length_array_out,lib_call)
我真的非常感谢任何关于如何解决这个问题的想法。我希望这个简化的 post 会有所帮助。
非常感谢。
我对你的代码做了一些小改动,实际上是 运行(导入)并添加了打印以查看传递的对象的地址和 return 值,另外还创建了一个等效的 C DLL 以确保指针正确传递并且回调有效。
Python:
import ctypes
import math
def SimpleTestFunction_asm(X):
Input_Length_Array = []
Input_Length_Array.append(len(X)*8)
CA_X = (ctypes.c_double * len(X))(*X)
length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)
hDLL = ctypes.WinDLL('test')
CallName = hDLL.Main_Entry_fn
CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)]
CallName.restype = ctypes.POINTER(ctypes.c_int64)
LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
def LibraryCall(ax):
bx = math.ceil(ax)
return (bx)
lib_call = LibraryCB(LibraryCall)
lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))
ret_ptr = CallName(CA_X,length_array_out,lib_call)
print('{:016X} {:016X} {:016X} {}'.format(ctypes.addressof(CA_X),ctypes.addressof(length_array_out),ctypes.addressof(lib_call.contents),ret_ptr.contents))
SimpleTestFunction_asm([1.1,2.2,3.3])
Test.DLL 来源:
#include <inttypes.h>
#include <stdio.h>
typedef double (*CB)(double);
__declspec(dllexport) int64_t* __stdcall Main_Entry_fn(double* p1, double* p2, long long* p3)
{
static int64_t x = 123;
double out = ((CB)p3)(1.1);
printf("%p %p %p %lf\n",p1,p2,p3,out);
return &x;
}
输出:
0000021CC99B23A8 0000021CCBADAC10 0000021CCBC90FC0 2.000000
0000021CC99B23A8 0000021CCBADAC10 0000021CCBC90FC0 c_longlong(123)
您可以看到指针相同,回调 return 值和函数 return 值正确。
很可能您的 NASM 代码没有正确实现调用约定或破坏了访问数组的堆栈。我只是做了最少的工作以使您的 Python 代码正常工作。我确实认为 length_array_out
始终是一个长度为 1 的双精度数组,其值是输入数组 X
长度的 8 倍,这确实很奇怪。 NASM 代码如何知道数组的长度?
您可以更正确地输入并声明以下内容,而不是将回调强制转换为 long long *
:
CALLBACK = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),CALLBACK]
CallName.restype = ctypes.POINTER(ctypes.c_int64)
@CALLBACK
def LibraryCall(ax):
bx = math.ceil(ax)
return (bx)
ret_ptr = CallName(CA_X,length_array_out,LibraryCall)
@Mark Tolonen,非常感谢您的详细分析。我将此作为答案发布,因为代码的格式不会在评论中正确显示——但我选择了您的答案作为最佳答案。
我怀疑堆栈对齐可能是问题所在,并且您消除了 ctypes 作为源,所以我专注于堆栈。这是我为使其工作所做的工作。
在 NASM 代码中,我在进入时推送 rbp 和 rdi,然后在退出时恢复它们。在这里,在调用之前,我通过从堆栈中弹出 rbp 和 rdi 来设置堆栈状态。然后我从 rsp 中减去 32 个字节(不是 40 个)。调用完成后,我恢复堆栈状态:
pop rbp
pop rdi
sub rsp,32
call [CB_Pointer] ; The call to the callback function
add rsp,32
push rdi
push rbp
对于外部函数调用(比如 C 库函数),我必须减去 40 个字节,但是对于这个回调我只需要 32 个字节。在您回答之前,我已经尝试过使用 40 个字节,但没有成功。我想原因是因为它没有调用外部库,它是对首先调用 dll 的 ctypes 代码的回调。
还有一件事。该调用发送一个浮点值 (xmm0) 和 returns 一个整数值,但整数值是 returned 在 xmm0 寄存器中,而不是 rax。将 ctypes 中的原型设置为整数 return 不会这样做。它必须保持这样:
LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
再次感谢您的回复。你告诉我在哪里看。
P.S。 length_array_out 将输入数组的长度传递给 NASM。如果我传递多个数组,length_array_out 会更长,每个长度有一个 qword;目前我在输入时将 qword 转换为整数。
我以前问过这个问题,但没有人回答。我再问一次,这次要简单得多。
我有一个由 Python ctypes 调用的 dll,带有回调函数。回调一直正常工作(如果我在 Visual Studio 中单步执行程序,我可以看到它在运行),但在退出时 Visual Studio 抛出一个 "access violation" 异常。但是,如果我从 dll 中删除对回调的调用,它会正常退出而不会出现访问冲突。
我还必须执行其他操作才能从带有回调的 dll 中退出吗?我已经研究了几个小时,但我还没有在网上找到任何可以解决这个问题的方法。
这是 ctypes 代码。我省略了 dll 代码以保持简短(它写在 NASM 中)但如果需要它我也可以 post 它。
def SimpleTestFunction_asm(X):
Input_Length_Array = []
Input_Length_Array.append(len(X)*8)
CA_X = (ctypes.c_double * len(X))(*X)
length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)
hDLL = ctypes.WinDLL("C:/Test_Projects/SimpleTestFunction/SimpleTestFunction.dll")
CallName = hDLL.Main_Entry_fn
CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)]
CallName.restype = ctypes.POINTER(ctypes.c_int64)
#__________
#The callback function
LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
def LibraryCall(ax):
bx = math.ceil(ax)
return (bx)
lib_call = LibraryCB(LibraryCall)
lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))
#__________
ret_ptr = CallName(CA_X,length_array_out,lib_call)
我真的非常感谢任何关于如何解决这个问题的想法。我希望这个简化的 post 会有所帮助。
非常感谢。
我对你的代码做了一些小改动,实际上是 运行(导入)并添加了打印以查看传递的对象的地址和 return 值,另外还创建了一个等效的 C DLL 以确保指针正确传递并且回调有效。
Python:
import ctypes
import math
def SimpleTestFunction_asm(X):
Input_Length_Array = []
Input_Length_Array.append(len(X)*8)
CA_X = (ctypes.c_double * len(X))(*X)
length_array_out = (ctypes.c_double * len(Input_Length_Array))(*Input_Length_Array)
hDLL = ctypes.WinDLL('test')
CallName = hDLL.Main_Entry_fn
CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_longlong)]
CallName.restype = ctypes.POINTER(ctypes.c_int64)
LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
def LibraryCall(ax):
bx = math.ceil(ax)
return (bx)
lib_call = LibraryCB(LibraryCall)
lib_call = ctypes.cast(lib_call,ctypes.POINTER(ctypes.c_longlong))
ret_ptr = CallName(CA_X,length_array_out,lib_call)
print('{:016X} {:016X} {:016X} {}'.format(ctypes.addressof(CA_X),ctypes.addressof(length_array_out),ctypes.addressof(lib_call.contents),ret_ptr.contents))
SimpleTestFunction_asm([1.1,2.2,3.3])
Test.DLL 来源:
#include <inttypes.h>
#include <stdio.h>
typedef double (*CB)(double);
__declspec(dllexport) int64_t* __stdcall Main_Entry_fn(double* p1, double* p2, long long* p3)
{
static int64_t x = 123;
double out = ((CB)p3)(1.1);
printf("%p %p %p %lf\n",p1,p2,p3,out);
return &x;
}
输出:
0000021CC99B23A8 0000021CCBADAC10 0000021CCBC90FC0 2.000000
0000021CC99B23A8 0000021CCBADAC10 0000021CCBC90FC0 c_longlong(123)
您可以看到指针相同,回调 return 值和函数 return 值正确。
很可能您的 NASM 代码没有正确实现调用约定或破坏了访问数组的堆栈。我只是做了最少的工作以使您的 Python 代码正常工作。我确实认为 length_array_out
始终是一个长度为 1 的双精度数组,其值是输入数组 X
长度的 8 倍,这确实很奇怪。 NASM 代码如何知道数组的长度?
您可以更正确地输入并声明以下内容,而不是将回调强制转换为 long long *
:
CALLBACK = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
CallName.argtypes = [ctypes.POINTER(ctypes.c_double),ctypes.POINTER(ctypes.c_double),CALLBACK]
CallName.restype = ctypes.POINTER(ctypes.c_int64)
@CALLBACK
def LibraryCall(ax):
bx = math.ceil(ax)
return (bx)
ret_ptr = CallName(CA_X,length_array_out,LibraryCall)
@Mark Tolonen,非常感谢您的详细分析。我将此作为答案发布,因为代码的格式不会在评论中正确显示——但我选择了您的答案作为最佳答案。
我怀疑堆栈对齐可能是问题所在,并且您消除了 ctypes 作为源,所以我专注于堆栈。这是我为使其工作所做的工作。
在 NASM 代码中,我在进入时推送 rbp 和 rdi,然后在退出时恢复它们。在这里,在调用之前,我通过从堆栈中弹出 rbp 和 rdi 来设置堆栈状态。然后我从 rsp 中减去 32 个字节(不是 40 个)。调用完成后,我恢复堆栈状态:
pop rbp
pop rdi
sub rsp,32
call [CB_Pointer] ; The call to the callback function
add rsp,32
push rdi
push rbp
对于外部函数调用(比如 C 库函数),我必须减去 40 个字节,但是对于这个回调我只需要 32 个字节。在您回答之前,我已经尝试过使用 40 个字节,但没有成功。我想原因是因为它没有调用外部库,它是对首先调用 dll 的 ctypes 代码的回调。
还有一件事。该调用发送一个浮点值 (xmm0) 和 returns 一个整数值,但整数值是 returned 在 xmm0 寄存器中,而不是 rax。将 ctypes 中的原型设置为整数 return 不会这样做。它必须保持这样:
LibraryCB = ctypes.WINFUNCTYPE(ctypes.c_double, ctypes.c_double)
再次感谢您的回复。你告诉我在哪里看。
P.S。 length_array_out 将输入数组的长度传递给 NASM。如果我传递多个数组,length_array_out 会更长,每个长度有一个 qword;目前我在输入时将 qword 转换为整数。