RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 552427
Accepted
Даниил Колесниченко
Даниил Колесниченко
Asked:2020-08-06 02:43:10 +0000 UTC2020-08-06 02:43:10 +0000 UTC 2020-08-06 02:43:10 +0000 UTC

CPython 字节码文件的结构是什么?

  • 772

我试图了解编译后的 CPython 字节码的结构。
假设我有一个包含foo.py以下内容的文件:

def hello(name):
    print("Hello, %s" % name)

编译__pycache__\foo.cpython-35.pyc看起来像这样:

字节码

接下来我明白了什么:

  • 16 0d 0d 0a是神奇的数字
  • 06 e2 7f 57- 最后修改日期
  • 31 00 00 00- 文件大小(应该是 - 虽然文件中有 216 字节,所以我不知道实际大小是多少)
  • 接下来的 22 个字节 ( e3 00 00 00 ... 00 00 00 73) - 不知道为什么
  • 10 00 00 00- 显然是模块代码的大小(但后来发现前一个字符必须是一个类型,而这73是一个字符串类型的代码)
  • 64 00 00, 64 01 00- LOAD_CONST(0); LOAD_CONST(1),其中常量 0 -code object函数,常量 1 - 它的名称 ( "hello")
  • 84 00 00- MAKE_FUNCTION(0),但我仍然不明白为什么需要这个操作码的参数
  • 5a 00 00- STORE_NAME(0),其中名称 0 - 函数名称 ( "hello")
  • 64 02 00 53- LOAD_CONST(2); RETURN_VALUE,其中常量为 2 - None(尽管还不完全清楚为什么模块应该返回一些东西)
  • 29 03- 模块常量元组:(<code object hello>, "hello", None)
  • 接下来的 18 个字节 ( 74 00 00 64 ... 64 00 00 53) - 功能代码hello(那个hello.__code__.co_code)
  • 29 02- 函数常量元组:(None, "Hello, %s")
  • 4e- 常量类型NONE
  • 7a 09- 字符串常量类型 ( SHORT_ASCII) 和字符串长度"Hello, %s"(9 个字符)
  • 48 65 6c 6c 6f 2c 20 25 73- 线"Hello, %s"
  • 29 01- 函数名称元组:("print",)
  • da 05- 这里似乎应该有一种字符串类型,但取而代之的是一种不存在的类型da;05- 字符串长度
  • 70 72 69 6e 74- 函数名称print
  • 29 01- 局部变量名称的元组:("name",)
  • da 04- 再次未知类型da和字符串长度04
  • 6e 61 6d 65- 变量的名称name
  • a9 00 72 03 00 00 00- 没听懂
  • fa 06- 另一个晦涩的类型fa和字符串长度06
  • 66 6f 6f 2e 70 79- 模块文件名 ( foo.py)
  • da 05- 难以理解da的字符串类型和长度05
  • 68 65 6c 6c 6f- 函数的名称hello(尽管没有声明模块名称的元组或类似的东西)
  • 接下来的 17 个字节 ( 01 00 00 00 ... 00 00 00 4e) - 没看懂
  • 29 01- 一些长度为一个元素的元组
  • 72 ...- 类型常量ref(这是什么类型?)
  • da 08- 同样是神秘类型da和字符串长度 08
  • 3c 6d 6f 64 75 6c 65 3e- 线"<module>"(为什么?)
  • 01 ...- 我不知道接下来会发生什么

帮助填补空白。如果我只需要找出某个常数的值,那么我就已经拥有足够的知识了。但是我正在尝试编写一个 Python 字节码解释器,所以我需要完全理解.pyc-file 结构。

python
  • 1 1 个回答
  • 10 Views

1 个回答

  • Voted
  1. Best Answer
    m9_psy
    2020-04-27T03:34:14Z2020-04-27T03:34:14Z

    故事很长-你可以放一只海鸥。我也没有能力将文档中的所有内容复制到答案中,所以我必须自己阅读文档。此外,不会对dis进行实验- 假设问题的作者已经玩够了它并决定深入挖掘。

    这是一个很长的问题,但由于它很受欢迎,我将针对一个选定的实现和一个选定的版本略微讨论这个主题CPython 3.6.1——这个版本是因为它是最新的,答案稍后会过时,而且因为这个问题明确说明了版本3.5.

    一般来说,在最新版本中很容易弄清楚什么是什么 - 没有任何困难可以让你大吃一惊。起点将是py_compile模块。为什么来自它,而不是来自 C 来源?- 因为文档是这样说的:Этот модуль предоставляет функцию для генерации байт-кода из исходников. 正是医生所吩咐的。

    我们也会提前写一个测试用例,在一个单独的test.py.

    import time
    
    
    def my_gen():
        for i in range(10):
            time.sleep(1)
            yield i**2
    
    
    if __name__ == "__main__":
        for elem in my_gen():
            print(elem)
    

    现在在主文件中我们可以像这样生成字节码:

    import py_compile
    
    py_compile.compile("test.py", "test.pyc")
    

    扩展*.pyc只是一个扩展,一个命名约定,可以是任何东西,重要的是内容。

    你可以像往常一样运行 - python test.pyc。

    现在是时候关注生成字节码的函数了。你可以自己看资料,我摘录一段:

    1. 那里发生的第一件事是确定输出字节码的路径和名称。我们已经明确指出了这一点。如果不是,则路径根据PEP 3147 / PEP 488确定,还取决于版本。
    2. 文本源被转换为字节。该模块还负责 this -importlib或更确切地说是类importlib.machinery.SourceFileLoader。突然间,那里运行的所有代码都

      with _io.FileIO(path, 'r') as file:
          return file.read()
      

      也就是说,只是阅读源代码。在我们的代码中它看起来像:

      loader = importlib.machinery.SourceFileLoader('<py_compile>', file)
      source_bytes = loader.get_data(file)
      
    3. 源被转换为 AST - 抽象语法树。不可能在这里写出源代码经过一系列各种函数和检查的路径,但简而言之:编译函数被调用:

      code1 = compile(source_bytes, file, mode='exec', dont_inherit=True)
      code = loader.source_to_code(source_bytes, file)
      
      print(code, type(code))
      
      print(code == code1)
      

      在上面的代码中,我复制了他们的源代码py_compile( loader.source_to_code) 并使用一个函数自己编写了它compile——简单情况下的结果是相同的。结果是模块中类的一个实例code。

    4. 最后,最重要的是AST到字节码的转换。在 py_compile 中,这是这样完成的:

      source_stats = loader.path_stats(file)
      bytecode = importlib._bootstrap_external._code_to_bytecode(
              code, source_stats['mtime'], source_stats['size'])
      

      sourcestats看起来像这样{'mtime': 1493229661.7715623, 'size': 180}:mtime- 这是源代码更改的时间,以一些特殊的时间戳表示。它会进一步派上用场。

      内容code_to_bytecode:

      data = bytearray(MAGIC_NUMBER)
      data.extend(_w_long(mtime))
      data.extend(_w_long(source_size))
      data.extend(marshal.dumps(code))
      

      我很惊讶,但是转换为字节码本身通常需要 4 行。MAGIC_NUMBER- 一个神奇的词,每个新版本都是新的。对于 3.6.1(Python 3.6rc1)是(3379).to_bytes(2, 'little') + b'\r\n'. 可以在 中找到所有魔术词的列表importlib/_bootstrap_external.py。请注意,除了数字之外,还会插入一个回车符。功能_w_long是(int(x) & 0xFFFFFFFF).to_bytes(4, 'little')。- 您可以使用 HEX 编辑器检查 .pyc 文件的实际内容 - 一切都匹配。也就是说,代码本身在2 + 2 + 4 + 4字节之后开始。这些都是小东西,真正的目标是marshal模型和dumps函数。marshal-这个модуль, содержащий функции, которые могут читать и писать значение переменных в бинарном формате. Формате специфичен для Python, но независим от архитектуры конечной машины。

    5. 此外,不幸的是,它在 python 中不起作用,你将不得不去源代码:你可以跟随对象的命运,但最终对象被这个函数marshal.dumps打成字节。它很长但相对简单。marshal 可以将很多对象放入一个文件中,但我们对该类型的对象感兴趣(它被传递给他)——为此有一个特殊的检查来查看传递的对象是否是一个代码。从这个函数可以看出,肯定写了很多信息——操作类型(op_code)、参数、参数值是这个列表中最简单的。这些变量的确切含义可以在 code.h文件中 PyCodeObject 结构的定义中看到(那里有很多注释)code

    6. 不过,总的来说,并非全部。这仅仅是开始。我们可以看到具体的内容,按字节,写入了 pyc 文件,而不必爬入 C。事实是 Python 对象具有所有必要的属性code。我按照在 pyc 中编写的顺序排列参数:

      args = ['co_argcount', 'co_kwonlyargcount', 'co_nlocals', 'co_stacksize', 'co_flags',  'co_code', 'co_consts', 
              'co_names', 'co_varnames', 'co_freevars', 'co_cellvars', 'co_filename', 'co_name', 'co_firstlineno', 
              'co_lnotab']
      
      for arg in args:
          print(arg, getattr(code, arg, None))
      

      在屏幕上打印参数的值并计算偏移量(1 个调用w_long写入 4 个字节,W_TYPE写入 1 个字节)后,您可以使用 HEX 编辑器轻松查看写入的内容和位置。您已经可以使用 dis 分析 co_code - 在新版本中它有一个王牌get_instructions方法:

      for instruction in dis.get_instructions(code):
          print(instruction)
      

      它还具有一个字节偏移量、一个操作数代码以及您可能需要的一切。

    7. 为了巩固您阅读的内容,您可以完全亲手解开pyc文件。我不会编写整个解析器 - 它非常昂贵,但根据已经编写的内容,您可以编写完整的解析器:

      from io import BytesIO
      import struct
      import datetime
      
      byte1_little = b"<c"
      byte2_little = b"<h"
      byte4_little = b"<I"
      
      byte1_big = b">c"
      byte2_big = b">h"
      byte4_big = b">I"
      
      INTEGER_TYPE = int.from_bytes(b'i', 'little')
      CODE_TYPE = int.from_bytes(b'c', 'little')
      SHORT_TUPLE_TYPE = int.from_bytes(b')', 'little')
      NONE_TYPE = int.from_bytes(b'N', 'little')
      
      
      def parse_string(raw_bytes):
          # Наш рулевой тут - https://github.com/python/cpython/blob/3.6/Python/marshal.c#L427
          # Байтик на тип, четверочку на размер
          SIZE = int.from_bytes(raw_bytes.read(4), 'little')
          print("BYTES TO READ: ", SIZE)
          s = raw_bytes.read(SIZE)
          return s
      
      
      def parse_code(raw_bytes):
          CO_ARGCOUNT = struct.unpack(byte4_little, raw_bytes.read(4))[0]
          co_kwonlyargcount = struct.unpack(byte4_little,     raw_bytes.read(4))[0]
          CO_NLOCALS = struct.unpack(byte4_little, raw_bytes.read(4))[0]
          CO_STACKSIZE = struct.unpack(byte4_little, raw_bytes.read(4))[0]
          CO_FLAGS = struct.unpack(byte4_little, raw_bytes.read(4))[0]
          print("CODE STATS:", CO_ARGCOUNT, co_kwonlyargcount, CO_NLOCALS, CO_STACKSIZE, CO_FLAGS)
          CODE_TYPE = raw_bytes.read(1)
          # Тип строка
          assert CODE_TYPE == b's'
          CODE_ITSELF = parse_string(raw_bytes)
          return CODE_ITSELF
      
      
      def parse_long(raw_bytes):
          VALUE = int.from_bytes(raw_bytes.read(4), 'little')
          print("LONG VALUE:", VALUE)
          return VALUE
      
      
      def parse_none(raw_bytes):
          # Уже отпрасили
          return None
      
      
      def parse_tuple(raw_bytes):
          # Размер у маленького - 1 байт, а не 4
          # https://github.com/python/cpython/blob/3.6/Python/marshal.c#L471
          SIZE = int.from_bytes(raw_bytes.read(1), 'little')
          print("TUPLE LEN:", SIZE)
          # Следующий тип - SHORT_TUPLE. Также по аналогии
          for index in range(SIZE):
              NEXT_TYPE = int.from_bytes(raw_bytes.read(1), 'little')
              if NEXT_TYPE == INTEGER_TYPE or NEXT_TYPE == INTEGER_TYPE | 128:
                  print("LONG TYPE")
                  parse_long(raw_bytes)
              # None 1 байт
              elif NEXT_TYPE == NONE_TYPE:
                  print("NONE TYPE")
                  parse_none(raw_bytes)
              # code
              elif NEXT_TYPE == CODE_TYPE or NEXT_TYPE == CODE_TYPE | 128:
                  print("CODE TYPE")
                  parse_code(raw_bytes)
              elif NEXT_TYPE == SHORT_TUPLE_TYPE:
                  parse_tuple(raw_bytes)
      
      
      with open("test.pyc", "rb") as f:
          raw_bytes = BytesIO(f.read())
      
      MAGIC_NUMBER = struct.unpack(byte2_little, raw_bytes.read(2))[0]
      assert MAGIC_NUMBER == 3379
      # пропускаем \r\n
      raw_bytes.read(2)
      TIMESTAMP = struct.unpack(byte4_little, raw_bytes.read(4))[0]
      print(datetime.datetime.fromtimestamp(TIMESTAMP))
      SIZE = struct.unpack(byte4_little, raw_bytes.read(4))[0]
      print(SIZE)
      OBJ_TYPE = int.from_bytes(raw_bytes.read(1), 'little')
      # Объект типа TYPE_CODE - см. https://github.com/python/cpython/blob/3.6/Python/marshal.c#L46
      # Флаг 128 выставляется в функции https://github.com/python/cpython/blob/3.6/Python/marshal.c#L288
      assert OBJ_TYPE == CODE_TYPE | 128
      parse_code(raw_bytes)
      NEXT_TYPE = int.from_bytes(raw_bytes.read(1), 'little')
      # Small tuple
      if NEXT_TYPE == SHORT_TUPLE_TYPE:
          parse_tuple(raw_bytes)
      

      代码有些不完整,只处理了几个类型,但我希望思路清晰 - 这段代码是函数的镜像dumps- 只有使用w_object(写入对象)和它自己的简单类型记录器,你还需要r_object(读取对象)和简单的读者。r_object 已经写入,只需要仔细阅读即可。

    • 19

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    如何停止编写糟糕的代码?

    • 3 个回答
  • Marko Smith

    onCreateView 方法重构

    • 1 个回答
  • Marko Smith

    通用还是非通用

    • 2 个回答
  • Marko Smith

    如何访问 jQuery 中的列

    • 1 个回答
  • Marko Smith

    *.tga 文件的组重命名(3620 个)

    • 1 个回答
  • Marko Smith

    内存分配列表C#

    • 1 个回答
  • Marko Smith

    常规赛适度贪婪

    • 1 个回答
  • Marko Smith

    如何制作自己的自动完成/自动更正?

    • 1 个回答
  • Marko Smith

    选择斐波那契数列

    • 2 个回答
  • Marko Smith

    所有 API 版本中的通用权限代码

    • 2 个回答
  • Martin Hope
    jfs *(星号)和 ** 双星号在 Python 中是什么意思? 2020-11-23 05:07:40 +0000 UTC
  • Martin Hope
    hwak 哪个孩子调用了父母的静态方法?还是不可能完成的任务? 2020-11-18 16:30:55 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    Arch ArrayList 与 LinkedList 的区别? 2020-09-20 02:42:49 +0000 UTC
  • Martin Hope
    iluxa1810 哪个更正确使用:if () 或 try-catch? 2020-08-23 18:56:13 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5