通过Python ctypes调用C函数:为什么将uint传递给期望size_t的函数有效?

时间:2018-08-17 23:24:26

标签: python ctypes

我有一些简单的代码将两个size_t加在一起:

#include <stdlib.h>

extern "C" __declspec(dllexport) size_t _cdecl add(size_t x, size_t y)
{
    return x + y;
}

(注意:此代码已编译并在64位系统上运行。)

通过Python的ctypes调用该函数并将其传递为c_uint类型的参数(大小为32位而不是64位)时,该函数将按预期工作:

import ctypes

lib = ctypes.cdll['./ctypetest.dll']

add = lib.add

add.restype = ctypes.c_uint
add.argtypes = [ctypes.c_uint, ctypes.c_uint]

add(1, 2) # = 3

作为健全性检查,我验证了uintsize_t的大小是否不同:

>>> ctypes.sizeof(ctypes.c_size_t)
8
>>> ctypes.sizeof(ctypes.c_uint)
4

在给定不同大小的参数的情况下,ctypes如何成功调用此函数?

1 个答案:

答案 0 :(得分:1)

答案取决于用于编译Python的C编译器的ABI调用约定。


听起来好像您在x86-64 Windows上。 1 如果是,则您的系统是基于Microsoft x64 ABI构建的。如果不是,那仍然是一个很好的例子,所以让我们假装你是。稍为简化, 2 该ABI的调用约定如下所示:

  • 前四个参数存储在寄存器RCX,RDX,R8和R9中。
  • 任何其他参数都被压入堆栈。

因此,您的c_uint参数分别存储在RCX和RDX的低32位中,而每个寄存器的高32位被清除为0。

add函数将RCX和RDX添加为无符号64位整数,其结果恰好是您期望的结果;一切正常。 3


但是,假设您使用的是不同的平台,并且使用了不同的ABI。实际上,您的想象力不必走得太远。如果在同一Windows计算机上运行32位程序,则将获得Microsoft IA-32 ABI而不是Microsoft x64。该ABI具有三种不同的调用约定,并且您声明中的_cdecl现在选择这三种之一,

  • 将所有内容推送到堆栈上。

好的,现在c_uintsize_t都是32位,但是我们用c_ushort做同样的事情。

您的Python代码将两个16位值压入堆栈。

add尝试将两个值(如x | (y<<32)中一样)用作其x参数,然后将堆栈上与其相邻的所有内容用作其{{ 1}}参数。所以,您得到的是垃圾。


它甚至会变得更糟。

如果您使用过y,该怎么办?在不执行任何操作的Microsoft x64 ABI中,但在Windows IA-32 ABI中,它指定与_stdcall相同的参数传递顺序,但由被调用方而不是调用方进行堆栈清除。

因此,_cdecl为您生成垃圾后,便清理了堆栈,并期望它的大小不同于您提供的大小,并且……实际上,我认为在这种情况下,您会得到删除它是因为堆栈的参数区域对齐到16个字节的页面,所以清除16个字节而不是8个字节并不重要。但这只是愚蠢的运气。


也有一些平台在 partial 寄存器中传递值。例如,add的Win32s版本IIRC就是这样的:

  • 如果是32位,则为EAX中的第一个参数;如果是16位,则为AX;如果是8位,则为AL。
  • 如果是32位,则为EDX中的第二个参数;如果是16位,则为DX;如果是8位,则为DL。
  • 如果是32位,则为EBX中的第三个参数;如果是16位,则为BX;如果是8位,则为BL。
  • 堆栈上的所有其他内容。

AL只是AX的下半部分。并且将一个字节加载到AL中不会清除高半部分。因此,如果调用一个_fastcall函数想添加两个16位数字,但又想添加两个8位数字又会怎样呢?您将得到_fastcallxyz*256的总和,其中w*256z就是剩下的东西在wAH中通过先前的说明操作。


我所有奇怪的示例都来自32位或更小的ABI是有原因的。大多数64位ABI的设计都是较新的,并且不那么随意,并且专门用于使POSIX / C代码和/或Win64 / C代码良好运行,因此它们往往非常相似。例如,System V AMD64 ABI(x86_64上除Windows以外几乎用于所有其他功能),AArch64 ABI(ARM64上几乎用于所有功能)和PowerPC64(PowerPC64上所有功能使用)都具有与Microsoft x64 ABI,除了一组不同的整数参数寄存器和稍微不同的浮点数和填充规则外。但这并不意味着您可以依靠它来安全地获取错误的参数。这只是意味着您很难找到测试系统来检测和调试错误……


1。您没有说,但是DH__declspec通常只出现在Windows代码中。而且您说的是“ 64位系统”,我怀疑您使用的是Itanium还是其他64位平台。

2。浮点数,SSE向量,大于64位的结构,varargs都有一些额外的复杂性。

3。您可能会对_cdecl0xffffffff + 0xffffffff而不是0x00000001fffffffe感到有些惊讶……但是由于您也误解了0xfffffffe,因此将其截断为32位(而且您使用的是Little-endian系统,并且该系统返回寄存器中的值-如果两个都不正确,则答案为1。),由于这些都是无符号的int,因此会被截断并翻转看起来相同,因此两个错误被抵消,您会看到预期的restype