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

Calling C functions via Python ctypes: why does passing uints to a function expecting size_t work?

我有一些简单的代码可以将两个 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如何成功调用此函数?

答案取决于用于编译您的 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 参数,然后将堆栈中它旁边的任何值作为其参数y 参数。所以,你得到的是垃圾。


而且情况可能会变得更糟。

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

因此,在为您生成垃圾后,add 开始清理堆栈,它期望的大小与您给它的大小不同,而且……好吧,实际上,我认为在这种特定情况下因为堆栈的参数区域与 16 字节页面对齐,所以您可以摆脱它,因此清理 16 个字节而不是 8 个字节并不重要。但这只是运气。


也有一些平台在部分寄存器中传递值。例如,IIRC,_fastcall 的 Win32s 版本做了这样的事情:

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

AL只是AX的下半部分。将一个字节加载到 AL 不会清除高半部分。那么,如果您调用 _fastcall 函数想要添加两个 16 位数字,但您认为它想要添加两个 8 位数字,会发生什么?您会得到 xyz*256w*256 的总和,其中 zw 就是碰巧剩下的任何东西在 AHDH 之前的一些指令中。


我所有奇怪的示例都来自 32 位和更小的 ABI,这是有原因的。大多数 64 位 ABI 都是最近才设计的,不那么随意,特别是为了使 POSIX/C 代码 and/or Win64/C 代码 运行 很好,所以它们往往非常相似。例如,System V AMD64 ABI(除了 x86_64 上的 Windows 之外几乎所有东西都使用),AArch64 ABI(ARM64 上几乎所有东西都使用)和 PowerPC64(PowerPC64 上所有东西都使用)具有与 Microsoft x64 ABI 基本相同的调用约定,除了一组不同的整数参数寄存器和略有不同的 float-and-stuff 规则。但这并不意味着您可以放心地将参数弄错。这只是意味着你很难找到测试系统来检测和调试你的错误……


1.你没说,但是 __declspec_cdecl 通常只出现在 Windows 代码中。你说 "a 64-bit system",我怀疑你使用的是 Itanium 还是其他一些 64 位平台。

2。浮点数、SSE 向量、大于 64 位的结构、可变参数……有一些额外的复杂性……

3。你可能会有点惊讶 0xffffffff + 0xffffffff0x00000001fffffffe 而不是 0xfffffffe… 但是因为你把 restype 也弄错了,你会 t运行 将其设置为 32 位(并且您使用的是小端系统,并且在寄存器中有 returns 值的系统——如果两者都不正确,您将得到 1 作为答案……),并且,因为这些是无符号整数,t运行cating 和 rolling over 看起来相同,所以两个错误抵消了,你会看到你期望的 0xfffffffe