【发布时间】:2015-03-17 05:46:16
【问题描述】:
好的,听我说;这个问题并不像你想象的那么愚蠢。
首先,一些背景知识:我最近开始使用 ctypes 模块,作为技术测试,我想编写一个 Mandelbrot 浏览器,使用 pygame 和 ctypes 分别用于事件处理和访问 Mandelbrot 计算 dll。我最初的计划是通过让 Mandelbrot 函数计算和存储字符数组中整行像素的值并返回指向该数组的指针来最小化 ctypes 包装器开销:
Mandelbrot.restype = c_char_p
#...
str_location = Mandelbrot(x)
row = str_location.value
但事实证明这并没有真正奏效。 value 方法有两个缺陷:它会降低性能,因为它将 C 字符串逐字节复制到 python 字符串中,并且它不知道字符串的预期长度,因此数据中的任何零都将被视为空终止符,导致任何进一步的数据丢失。
我的第一个行动方案是编写一个快速 DLL,允许我反汇编一些 Python 对象。它有以下两个功能:
#define DLLINFO extern "C" __declspec(dllexport)
DLLINFO char show_char(char *p)
{
return *p;
}
DLLINFO void mov(char *p, char payload)
{
*p = payload;
}
我还将 show_char 函数封装在 Python 函数 show_object 中,该函数使用 sys.getsizeof 打印 Python 对象的内存内容。 拆开琴弦后发现了一个非常简单的设计:
>>> from hack import *; import sys
>>>
>>> #string experiment
>>> a = '01234567'
>>> hex(sys.getrefcount(a))
'0x3'
>>> hex(id(type(a)))
'0x1e1d81f8'
>>> hex(len(a))
'0x8'
>>> show_object(a)
3 2 1 0 byte
0 0 0 4 0 #reference count (+1 temporary reference)
1e 1d 81 f8 4 #pointer to type
0 0 0 8 8 #length
94 b b6 98 12 #???
0 0 0 1 16 #???
33 32 31 30 20 #Data '0123' (little endian)
37 36 35 34 24 #Data '4567'
0 28 #Null terminator
>>> #sys.getsizeof reported 29 bytes for 9 bytes of data.
(之后添加的数据集)
我尝试用可变字节数组替换字符串,然后我反汇编了一个字节数组以查看我应该将 Mandelbrot 数据写入何处:
>>> #bytearray experiment
>>> b = bytearray('01234567')
>>> hex(sys.getrefcount(b))
'0x2'
>>> hex(id(type(b)))
'0x1e1e5e20'
>>> hex(len(b))
'0x8'
>>> show_object(b)
3 2 1 0 byte
0 0 0 3 0 #reference count (+1 temporary reference)
1e 1e 5e 20 4 #pointer to type
0 0 0 8 8 #length
0 0 0 0 12 #???
0 0 0 9 16 #???
2 3a 63 a0 20 #???
2 92 93 38 24 #???
2 91 e4 90 28 #???
1 32 #???
>>> #sys.getsizeof reported 33 bytes for 8 bytes of data
好吧,我不知道数据在字节数组中的位置,所以没有骰子。
我的下一个计划是用 ctypes 内置的可变字符串 create_string_buffer 替换该字符串。
>>> #buffer experiment
>>> from ctypes import *
>>> c = create_string_buffer('01234567')
>>> hex(id(type(c)))
'0x1ceb778'
>>> show_object(c)
3 2 1 0 byte
0 0 0 3 0 #reference count
1 ce b7 78 4 #pointer to type
2 38 f7 38 8 #???
0 0 0 1 12 #Here be dragons
0 0 0 0 16 #etc.
0 0 0 9 20
0 0 0 9 24
0 0 0 0 28
0 0 0 0 32
0 0 0 0 36
33 32 31 30 40 #data '0123'
37 36 35 34 44 #data '4567'
0 0 0 0 48
0 0 0 0 52
0 0 0 0 56
0 0 0 0 60
2 38 f8 40 64
2 38 f7 a0 68
ff ff ff fe 72
0 2e 0 65 76
>>> #sys.getsizeof reported 80 bytes for 9 bytes of data.
嗯。至少数据在某处。不幸的是,这个对象过于冗长以至于不实用。此外,它不是内置类型,所以我很难让它与其他功能一起使用。 这是我决定切换回字符串并运行一些修改字符串的谨慎测试的时候:
>>> from hack import *
>>> s = "Hello, world!"
>>> show_object(s)
3 2 1 0 byte
0 0 0 3 0
1e 1d 81 f8 4
0 0 0 d 8
8f 8d ce 9c 12
0 0 0 0 16
6c 6c 65 48 20
77 20 2c 6f 24
64 6c 72 6f 28
0 21 32
>>> mov(id(s)+32, 63)
>>> print s
Hello, world?
>>> mov(id(s)+8,5)
>>> print s
Hello
到目前为止一切顺利。至少我这样做的几次都没有崩溃。事实上,即使将长度修改为较低的值也不会立即引起任何问题。 (虽然我不打算这样做) 那么,为什么我在列出显示字符串可变的数据后要问这个问题?
首先,我知道硬件可以将字符串标记为不可变,尝试修改它们可能会导致段错误或类似问题:
char good_string[80];
good_string[8] = '!'; //Everything's okay so far.
char* bad_string = "This string's made out of const chars, beware!";
bad_string[8] = '!'; //And now you've got segfault!
其次,更重要的是,我对 Python 的内部工作原理知之甚少,无法自信地绕过 Python 对字符串的锁定并玩弄未定义的行为。现在,我很容易说服自己 Python 常见问题解答中陈述的字符串不变性的原因是错误的(我没有更改字符串的大小,并且字符串不像整数那样是元素。),但我不知道是否有一些隐藏的原因字符串不应该被修改,如果我尝试做我打算做的事情,一些事情会在我的脸上爆炸。这是我提交这个问题的主要原因;我希望有更多知识的人能不吝赐教。
谢谢,您阅读了整个问题。对不起,简洁不是我的强项。 :)
【问题讨论】:
-
旁注:没有通读问题,只是标题,但是为了使字符串在硬件级别上不可变,它需要位于 ROM 中。如果一个字符串位于可执行映像的只读内存段中,那么它确实是不可变的,但在低软件级别(总线配置)上,而不是在硬件级别上。所有这一切都与 Python、C 或任何其他编程语言无关。
-
这可能对您有所帮助 - 2 个月前,我在此处为a resumable SHA-256 calculator 发布了 Python 代码,该代码使用 ctypes 来访问 libeay32.dll 或 libssl.so,视情况而定。它使用称为
HashBuffType的自定义类型将称为hashbuff的固定大小的无符号字符缓冲区传递给库。该库填充hashbuff,Python 代码使用bytearray(hashbuff)对其进行转换。我不知道它的效率如何,但它很快。 -
字符串实习用于代码对象中的模块名称、对象属性、变量甚至字符串常量。在 2.7.9 中,
ctypes.pythonapi._Py_ReleaseInternedStrings()报告了 3570 个实习字符串。永远不要假设您从解释器获得的任何字符串都不会被保留,并且已经被散列。可以深入了解解释器的工作原理(源代码可用,您可以下载符号文件以与 cdb 或 windbg 一起使用),但不要根据实现细节改变规则。 -
仅供参考,这里有一些指向对象定义的链接,它们应该比内存转储提供更多信息:
PyStringObject、PyByteArrayObject和 ctypesCDataObject(b_ptr指向b_value对于最多 16 个字节的缓冲区,否则缓冲区是单独分配的,并且不会与对象连续)。