是否可以恢复损坏的"实习"字节对象

ead*_*ead 6 python cpython python-3.x python-internals

众所周知,小bytes-objects是自动的CPython(类似于"实习" 实习生 -function为字符串).更正:正如@abarnert 所解释的,它更像是整数池而不是实习字符串.

是否可以通过让我们说"实验性"第三方库或者是重启内核的唯一方法来恢复被破坏的字节对象?

可以使用Cython功能(Cython> = 0.28)完成概念验证:

%%cython
def do_bad_things():
   cdef bytes b=b'a'
   cdef const unsigned char[:] safe=b  
   cdef char *unsafe=<char *> &safe[0]   #who needs const and type-safety anyway?
   unsafe[0]=98                          #replace through `b`
Run Code Online (Sandbox Code Playgroud)

或者@jfs建议ctypes:

import ctypes
import sys
def do_bad_things():
    b = b'a'; 
    (ctypes.c_ubyte * sys.getsizeof(b)).from_address(id(b))[-2] = 98
Run Code Online (Sandbox Code Playgroud)

显然,通过滥用C-功能,do_bad_things改变不可变的(或者CPython认为的)对象b'a',b'b'并且因为这个对象是实例bytes,我们可以看到事后发生的坏事:

>>> do_bad_things() #b'a' means now b'b'
>>> b'a'==b'b'  #wait for a surprise  
True
>>> print(b'a') #another one
b'b'
Run Code Online (Sandbox Code Playgroud)

有可能恢复/清除字节对象池,这b'a'意味着b'a'再一次?


一点注意事项:似乎不是每个bytes创建过程都使用此池.例如:

>>> do_bad_things()
>>> print(b'a')
b'b'
>>> print((97).to_bytes(1, byteorder='little')) #ord('a')=97
b'a'
Run Code Online (Sandbox Code Playgroud)

aba*_*ert 4

Python 3 并不像bytes它那样实习对象str。相反,它以与int.

\n\n

这在幕后是非常不同的。不利的一面是,这意味着没有可操作的表(带有 API)。从好的方面来说,这意味着如果你能找到静态数组,你就可以修复它,就像处理整数一样,因为数组索引和字符串的字符值应该是相同的。

\n\n

如果查看bytesobject.c,会发现该数组在顶部声明:

\n\n
static PyBytesObject *characters[UCHAR_MAX + 1];\n
Run Code Online (Sandbox Code Playgroud)\n\n

\xe2\x80\xa6 然后,例如,在PyBytes_FromStringAndSize

\n\n
if (size == 1 && str != NULL &&\n    (op = characters[*str & UCHAR_MAX]) != NULL)\n{\n#ifdef COUNT_ALLOCS\n    one_strings++;\n#endif\n    Py_INCREF(op);\n    return (PyObject *)op;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意,该数组是static,因此无法从该文件外部访问它,并且它仍在对对象进行重新计数,因此调用者(即使是解释器中的内部内容,更不用说您的 C API 扩展)无法访问告诉他们有什么特别的事情发生。

\n\n

因此,没有“正确”的方法来清理它。

\n\n

但如果你想得到 hacky\xe2\x80\xa6

\n\n

如果您有对任何单字符字节的引用,并且知道它应该是哪个字符,则可以到达数组的开头,然后清理整个内容。

\n\n

除非你搞砸的比你想象的还要多,否则你可以构造一个单字符并减去它应该bytes是的字符。将返回应该是的对象即使它碰巧实际上持有。我们怎么知道这一点?因为这正是您要解决的问题。PyBytes_FromStringAndSize("a", 1)\'a\'\'b\'

\n\n

实际上,可能有一些方法可以破坏更糟糕的情况\xe2\x80\xa6,这些似乎都不太可能,但为了安全起见,让我们使用一个比 更不可能破坏的字符a,例如\\x80

\n\n
PyBytesObject *byte80 = (PyBytesObject *)PyBytes_FromStringAndSize("\\x80", 1);\nPyBytesObject *characters = byte80 - 0x80;\n
Run Code Online (Sandbox Code Playgroud)\n\n

唯一需要注意的是,如果您尝试从 Pythonctypes而不是 C 代码中执行此操作,则需要额外小心,1但由于您没有使用ctypes,所以我们不必担心这一点。

\n\n

所以,现在我们有了一个指向 的指针characters,我们可以遍历它了。我们不能只是删除对象来“取消”它们,因为这会影响任何引用它们的人,并可能导致段错误。但我们不必这样做。表中的任何对象,我们都知道它应该是什么,\xe2\x80\x94characters[i]应该是一个单字符bytes,其一个字符是i。因此,只需将其设置回原样,并使用如下循环:

\n\n
for (size_t char i=0; i!=UCHAR_MAX; i++) {\n    if (characters[i]) {\n        // do the same hacky stuff you did to break the string in the first place\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里的所有都是它的。

\n\n
\n\n

好吧,除了编译。2

\n\n

幸运的是,在交互式解释器中,每个完整的顶级语句都是其自己的编译单元,因此 \xe2\x80\xa6 应该可以接受运行修复程序后键入的任何新行。

\n\n

但是您导入的模块必须在字符串损坏的情况下进行编译?你可能搞砸了它的常数。除了强制重新编译和重新导入每个模块之外,我想不出一个好方法来清理这个问题。

\n\n
\n\n

1. 编译器可能b\'\\x80\'在到达 C 调用之前就将你的参数变成错误的东西。你会惊讶于所有你认为你正在传递 a 的地方,c_char_p而它实际上神奇地在 和 之间进行转换bytes。可能更好地使用POINTER(c_uint8).

\n\n

2. 如果你用b\'a\'in 编译了一些代码,consts 数组应该有一个对 的引用b\'a\',这将得到修复。但是,由于bytes已知对编译器来说是不可变的,如果它知道b\'a\' == b\'b\',它实际上可能会存储指向单例的指针b\'b\',出于同样的原因123456 is 123456,在这种情况下修复b\'a\'可能实际上无法解决问题。

\n