最近在写一个 Python C 扩展的时候碰到了个奇怪的问题,函数运行时抛出了一个溢出错误:

OverflowError: Python int too large to convert to C long

这个 Python C 扩展的代码如下:

static PyObject* _ptrace(PyObject* self, PyObject* args)
{
    long request;
    long pid;
    long addr;
    long data;
    long ret;

    if (!PyArg_ParseTuple(args, "llll", &request, &pid, &addr, &data)) {
        return NULL;
    }   

    ret = ptrace(request, pid, (void *) addr, (void *) data);
    return Py_BuildValue("l", ret);
}

_ptrace() 函数有四个参数, request、pid、addr 和 data, 四个参数都是 long 类型的, 通过 PyArg_ParseTuple() 函数解析 Python 代码中传递过来的值:

if (!PyArg_ParseTuple(args, "llll", &request, &pid, &addr, &data)) {
    return NULL;
} 

从溢出错误的描述中可以看出,这是一个类型转换溢出导致的异常。 检查了抛出异常时时给 _ptrace() 函数传入的各个参数的值, 分别是 PTRACE_POKETEXT、5419、0x4005bf 和 0xd001f8458bf455cc。 这几个值中最大的是 0xd001f8458bf455cc, 但它也还在 C long 类型能表示的范围内,为什么会报溢出呢? 在 Google 上搜索 ‘OverflowError: Python int too large to convert to C long’,没找到有帮助的信息。 实在没折了,改用 Python ctypes 来实现这个接口, 不抛异常了,而且接口能跑起来正确无误。

ctypes 能跑起来至少证明了一点,long 类型的参数传递是可以做到的。 然而 C 扩展的实现为什么就报错误了呢? Google 感觉是指望不上了它给答案了,干脆直接下了 Python 的源码来研究。 用 apt source 命令从 Ubuntu 的仓库里把代码下过来 (比从 Python 官网下载快多了):

$ apt source python2.7

切换到 Python 源码目录下搜索 ‘Python int too large to convert to C long’ 相关的信息:

$ find . -name '*.c' | xargs grep -n 'Python int too large to convert to C long'
./Objects/longobject.c:337:                        "Python int too large to convert to C long");

感觉一下就挖到宝了,搜到 ‘Python int too large to convert to C long’ 的信息而且只有一个地方有这个信息。 这个信息是在 Objects/longobject.c 中 PyLong_AsLong() 产生的,这个函数的代码如下:

 328 long
 329 PyLong_AsLong(PyObject *obj)
 330 {
 331     int overflow;
 332     long result = PyLong_AsLongAndOverflow(obj, &overflow);
 333     if (overflow) {
 334         /* XXX: could be cute and give a different
 335            message for overflow == -1 */
 336         PyErr_SetString(PyExc_OverflowError,
 337                         "Python int too large to convert to C long");
 338     }
 339     return result;
 340 }

从名称 PyLong_AsLong() 推测, 这个函数的作用是把一个 Python Long 对象,转化为 C long 类型。 再看 ./Objects/longobject.c:332 这行代码, 可以看出具体的类型转换和溢出检测应操作该是由 PyLong_AsLongAndOverflow() 函数完成的。 PyLong_AsLongAndOverflow() 也在 Objects/longobject.c 文件中,从 231 行开始到 323 行:

 231 long
 232 PyLong_AsLongAndOverflow(PyObject *vv, int *overflow)
 233 {
 234     /* This version by Tim Peters */
 235     register PyLongObject *v;
 236     unsigned long x, prev;
 ...
 241 
 242     *overflow = 0;
 ...
 279     switch (i) {
 280     case -1:
 281         res = -(sdigit)v->ob_digit[0];
 282         break;
 283     case 0:
 284         res = 0;
 285         break;
 286     case 1:
 287         res = v->ob_digit[0];
 288         break;
 289     default:
 290         sign = 1;
 291         x = 0;
 292         if (i < 0) {
 293             sign = -1;
 294             i = -(i);
 295         }
 296         while (--i >= 0) {
 297             prev = x;
 298             x = (x << PyLong_SHIFT) + v->ob_digit[i];
 299             if ((x >> PyLong_SHIFT) != prev) {
 300                 *overflow = sign;
 301                 goto exit;
 302             }
 303         }
 304         /* Haven't lost any bits, but casting to long requires extra
 305          * care (see comment above).
 306          */
 307         if (x <= (unsigned long)LONG_MAX) {
 308             res = (long)x * sign;
 309         }
 310         else if (sign < 0 && x == PY_ABS_LONG_MIN) {
 311             res = LONG_MIN;
 312         }
 313         else {
 314             *overflow = sign;
 315             /* res is already set to -1 */
 316         }
 317     }
 318   exit:
 319     if (do_decref) {
 320         Py_DECREF(vv);
 321     }
 322     return res;
 323 }

重点来关注这个函数中是如何做溢出检测的。 在 PyLong_AsLongAndOverflow() 函数中,参数 int *overflow 记录是否发生溢出, 忽略其他细节, 我们来看判断溢出的两个地方, 一个是在第 300 行处, 这里是通过移位操作来检测溢出: 将变量 x 左移 PyLong_SHIFT 位, 再右移 PyLong_SHIFT。 这个操作中如果有溢出的话,右移的时候 x 的高位将被补 0 从而导致两次 x 值对比不一致。 而第 300 行处代码所处的 while 循环,这个循环中不断的把变量 x 向右移位再累加上 v->ob_digit[i] 的值。 往下看 308 行和 322 行,猜测最终 PyLong_AsLongAndOverflow() 函数返回的可能就是变量 x 的值。 大致可以推定,这个 while 循环是用来计算 PyLongObject 存储的数值的。 那么 Python 中一个 Long 对象是如何表示的呢? 搜索 PyLongObject 结构体的定义:

$ find . -name '*.h' | xargs grep -n 'PyLongObject'
./Include/longintrepr.h:95:PyAPI_FUNC(PyLongObject *) _PyLong_New(Py_ssize_t);
./Include/longintrepr.h:98:PyAPI_FUNC(PyObject *) _PyLong_Copy(PyLongObject *src);
./Include/longobject.h:10:typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
./Include/longobject.h:43:PyAPI_FUNC(double) _PyLong_Frexp(PyLongObject *a, Py_ssize_t *e);
./Include/longobject.h:115:PyAPI_FUNC(int) _PyLong_AsByteArray(PyLongObject* v,

可以看到 PyLongObject 实际是 struct _longobject 类型。继续搜索 _longobject 结构体:

$ find . -name '*.h' | xargs grep -n 'struct _longobject'
./Include/longintrepr.h:90:struct _longobject {
./Include/longobject.h:10:typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

可以看到,_longobject 结构体是在 longintrepr.h 文件中定义, 来看下具体的定义内容:

/* Long integer representation.
   The absolute value of a number is equal to
   	SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)
   Negative numbers are represented with ob_size < 0;
   zero is represented by ob_size == 0.
   In a normalized number, ob_digit[abs(ob_size)-1] (the most significant
   digit) is never zero.  Also, in all cases, for all valid i,
   	0 <= ob_digit[i] <= MASK.
   The allocation function takes care of allocating extra memory
   so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available.

   CAUTION:  Generic code manipulating subtypes of PyVarObject has to
   aware that longs abuse  ob_size's sign bit.
*/

struct _longobject {
	PyObject_VAR_HEAD
	digit ob_digit[1];
};

这里面比较有价值的是 _longobject 结构体定义上面的一段代码注释。 这段注释将 Python Long 类型如何存储和表示数字解释的非常清楚。 注释中提到的 ob_size 字段是通过宏 PyObject_VAR_HEAD 定义的。 在 _longobject 结构体中,ob_size 的绝对值表示 ob_digit[] 数组的长度。 Long 对象所表示的数值的绝对值的计算公式为:

SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)

ob_size 的正负号分别表示这个数字是正数还是负数。 Long 类型在区分正负数上这一点不同于 C 中 long/int 一类数值的区分方式。 我们知道,C 中的 long 类型数据是以补码形式存存储的,最高位 (MSB) 表示这个数的正负。 而 Python 中的 Long 类型则只记录数值的绝对值, 在用一个额外的 ob_size 字段表示正负数.

OK,上面的内容解释了第一处溢出判断的方法,顺便梳理了 Python 中 Long 类型的存储方式。 现在我们来看另一处的溢出判断。另一处溢出判断是在第 314 行处代码:

 307         if (x <= (unsigned long)LONG_MAX) {
 308             res = (long)x * sign;
 309         }
 310         else if (sign < 0 && x == PY_ABS_LONG_MIN) {
 311             res = LONG_MIN;
 312         }
 313         else {
 314             *overflow = sign;
 315             /* res is already set to -1 */
 316         }

当这行代码上面的两个条件判断都不成立的时,便认为溢出发生了。 我们来看看上面两个分支判断, 一个是判断 x 是否小于 LONG_MAX, LONG_MAX 的定义如下:

$ find . -name '*.h' | xargs grep -n '#define LONG_MAX' 
./Include/pyport.h:864:#define LONG_MAX 0X7FFFFFFFL
./Include/pyport.h:866:#define LONG_MAX 0X7FFFFFFFFFFFFFFFL

LONG_MAX 有 4 字节和 8 字节两种长度定义,选哪一种是由 CPU 字长决定的。 我的机器是 64 位的,所以 LONG_MAX 值为 0X7FFFFFFFFFFFFFFFL。 再来看第二个分支判断 sign < 0 && x == PY_ABS_LONG_MIN, 这里的 sign 变量其实就是 PyLongObject 中的 ob_size 字段。 经过分析,可以确认 PY_ABS_LONG_MIN 等价于 -LONG_MAX - 1, 在这里就是 0x8000000000000000L。 所以这行代码其实是在判断 x 的值是否等于 0x8000000000000000L

综上,我们可以确认 PyLong_AsLongAndOverflow() 函数要求传入的 Long 对象的值必须在 [-LONG_MAX - 1, LONG_MAX] 范围内, 否则便会抛出文章开头中提出的数值溢出异常。 检查下之前给 C 扩展函数传入的参数,data 是一个 Python 的 Long 类型, 值为 0xd001f8458bf455cc。 在 C 中,data 的值并没有超出 long 能表示的范围, 它是一个负数。 然而, 在 Python 中, 它被存储为一个 Long 类型的正整数, 在 PyLong_AsLongAndOverflow() 中的校验中, data 的值超出了 [-LONG_MAX - 1, LONG_MAX] 这个范围内,所以就抛出类型转换溢出错误了。

如何处理这个溢出问题呢? 办法之一就是开头提到的用 Python ctypes 重新实现接口。 另一个方法就是把:

PyArg_ParseTuple(args, "llll", &request, &pid, &addr, &data)

修改为:

PyArg_ParseTuple(args, "lllk", &request, &pid, &addr, &data)

就是把 data 由 long 类型修改为 unsigned long 类型,这样也可以解决数值溢出的问题。