Skip to main content

Python cross-version byte-code disassembler and marshal routines

Project description

CircleCI PyPI Installs Latest Version Supported Python Versions

Packaging Status

xdis

A Cross-Python bytecode disassembler, bytecode/wordcode, and magic-number manipulation library/package.

I gave a talk that mentions using this at BlackHat Asia 2024.

Introduction

The Python dis module allows you to disassemble bytecode from the same version of Python that you are running on. But what about bytecode from different versions?

That’s what this package is for. It can “marshal load” Python bytecodes from different versions of Python. The command-line routine pydisasm will show disassembly output using the most modern Python disassembly conventions in a variety of user-specified formats. Some of these formats, like extended and extended-format are the most advanced of any Python disassembler I know of because they can show an expression tree for operators. See the Disassembler Example below.

Also, if you need to modify and write bytecode, the routines here can be of help. There are routines to pack and unpack the read-only tuples in Python’s Code type. For interoperability between the changes over the years to Python CodeType, provide our own versions of the Code Type to allow interoperability, and we provide routines to reduce the tedium in writing a bytecode file.

This package also has an extensive knowledge of Python bytecode magic numbers, including PyPy and others, and how to translate from sys.sys_info major, minor, and release numbers to the corresponding magic value.

So if you want to write a cross-version assembler, bytecode-level analyzer or optimizer, this package may also be useful. In addition to the kinds of instruction categorization that dis offers, we have additional categories for things that would be useful in such a bytecode assembler, optimizer, or decompiler.

The programs here accept bytecodes from Python version 1.0 to 3.13. The code requires Python 2.4 or later and has been tested on Python running on many versions.

When installing, except for the most recent versions of Python, use the Python egg or wheel that matches that version, e.g., xdis-6.0.2-py3.3.egg, xdis-6.0.2-py33-none-any.whl. Of course, for versions that predate wheels, like Python 2.6, you will have to use eggs.

Installation

If you are using Python 3.11 or later, you can install from PyPI using the name xdis:

pip install xdis

For Python releases before 3.11, do not install using PyPI, but instead install using a file in the [GitHub Releases section](https://github.com/rocky/python-xdis/releases). Older Python used to use easy_install. But this is no longer supported in PyPi or newer Python versions. And vice versa, poetry nor pip, (the newer ways) are not supported on older Pythons.

If the Python version you are running xdis is between Python 2.4 through 2.7, use a tarball called xdis_24-x.y.z.tar.gz.

If the Python version you are running xdis is between Python 3.0 through 3.2, use a tarball called xdis_30-x.y.z.tar.gz.

If the Python version you are running xdis is between Python 3.3 through 3.5, use a tarball called xdis_33-x.y.z.tar.gz.

If the Python version you are running xdis is between Python 3.6 through 3.11, use a tarball called xdis_36-x.y.z.tar.gz.

If the Python version you are running xdis is 3.11 or later, use a file called xdis-x.y.z.tar.gz.

You can also try eggs or wheels that have the same version designation, e.g., xdis-x.y.z-py39-none-any.whl for a Python 3.9 installation. However, note that *the version without the designation means Python 3.11 or greater.

Similarly, a tarball with or without the underscore xx, e.g., xdis_36-x.y.z.tar.gz. works only from Python 3.11 or greater.

Rationale for using Git Branches

It is currently impossible (if not impractical) to have one Python source code of this complexity and with this many features that can run both Python 2.7 and Python 3.13+. The languages have drifted so much, and packaging is vastly different. In fact, the packaging practice for Python 3.11+ is incompatible with Python 2.7 (and before back to Python 2.4), which favored “easy_install”.

Installation from source text

To install from source code, make sure you have the right Git branch. See the Requirements section for the Git branch names.

After setting the right branch:

$ pip install -e .  # or pip install -e .[dev] to include testing package

A GNU makefile is also provided, so make install (possibly as root or sudo) will do the steps above.

Disassembler Example

The cross-version disassembler that is packaged here can produce assembly listings that are superior to those typically found in Python’s dis module. Here is an example:

pydisasm -S -F extended bytecode_3.8/pydisasm-example.pyc
# pydisasm version 6.1.1.dev0
# Python bytecode 3.8.0 (3413)
# Disassembled from Python 3.11.8 (main, Feb 14 2024, 04:47:01) [GCC 13.2.0]
# Timestamp in code: 1693155156 (2023-08-27 12:52:36)
# Source code size mod 2**32: 320 bytes
# Method Name:       <module>
# Filename:          simple_source/pydisasm-example.py
# Argument count:    0
# Position-only argument count: 0
# Keyword-only arguments: 0
# Number of locals:  0
# Stack size:        3
# Flags:             0x00000040 (NOFREE)
# First Line:        4
# Constants:
#    0: 0
#    1: None
#    2: ('version_info',)
#    3: 1
#    4: (2, 4)
#    5: 'Is small power of two'
# Names:
#    0: sys
#    1: version_info
#    2: print
#    3: version
#    4: len
#    5: major
#    6: power_of_two
             # import sys
  4:           0 LOAD_CONST           (0) ; TOS = 0
               2 LOAD_CONST           (None) ; TOS = None
               4 IMPORT_NAME          (sys) ; TOS = import_module(sys)
               6 STORE_NAME           (sys) ; sys = import_module(sys)

             # from sys import version_info
  5:           8 LOAD_CONST           (0) ; TOS = 0
              10 LOAD_CONST           (('version_info',)) ; TOS = ('version_info',)
              12 IMPORT_NAME          (sys) ; TOS = import_module(sys)
              14 IMPORT_FROM          (version_info) ; TOS = from sys import version_info
              16 STORE_NAME           (version_info) ; version_info = from sys import version_info
              18 POP_TOP

             # print(sys.version)
  7:          20 LOAD_NAME            (print) ; TOS = print
              22 LOAD_NAME            (sys) ; TOS = sys
              24 LOAD_ATTR            (version) ; TOS = sys.version
              26 CALL_FUNCTION        (1 positional argument) ; TOS = print(sys.version)
              28 POP_TOP

             # print(len(version_info))
  8:          30 LOAD_NAME            (print) ; TOS = print
              32 LOAD_NAME            (len) ; TOS = len
              34 LOAD_NAME            (version_info) ; TOS = version_info
              36 CALL_FUNCTION        (1 positional argument) ; TOS = len(version_info)
              38 CALL_FUNCTION        (1 positional argument) ; TOS = print(len(version_info))
              40 POP_TOP

             # major = sys.version_info[0]
  9:          42 LOAD_NAME            (sys) ; TOS = sys
              44 LOAD_ATTR            (version_info) ; TOS = sys.version_info
              46 LOAD_CONST           (0) ; TOS = 0
              48 BINARY_SUBSCR        TOS = sys.version_info[0]
              50 STORE_NAME           (major) ; major = sys.version_info[0]

             # power_of_two = major & (major - 1)
 10:          52 LOAD_NAME            (major) ; TOS = major
              54 LOAD_NAME            (major) ; TOS = major
              56 LOAD_CONST           (1) ; TOS = 1
              58 BINARY_SUBTRACT      TOS = major - (1)
              60 BINARY_AND           TOS = major & (major - (1))
              62 STORE_NAME           (power_of_two) ; power_of_two = major & (major - (1))

             # if power_of_two in (2, 4):
 11:          64 LOAD_NAME            (power_of_two) ; TOS = power_of_two
              66 LOAD_CONST           ((2, 4)) ; TOS = (2, 4)
              68 COMPARE_OP           (in) ; TOS = power_of_two in ((2, 4))
              70 POP_JUMP_IF_FALSE    (to 80)

             # print("Is small power of two")
 12:          72 LOAD_NAME            (print) ; TOS = print
              74 LOAD_CONST           ("Is small power of two") ; TOS = "Is small power of two"
              76 CALL_FUNCTION        (1 positional argument) ; TOS = print("Is small power of two")
              78 POP_TOP
         >>   80 LOAD_CONST           (None) ; TOS = None
              82 RETURN_VALUE         return None

Note that some operand interpretation is performed on items in the stack as shown above. For example, in:

24 LOAD_ATTR            (version) | sys.version

from the instruction, see that sys.version is the resolved attribute that is loaded.

Similarly, in:

68 COMPARE_OP           (in) | power_of_two in (2, 4)

we see that we can resolve the two arguments of the in operation. Finally, in some’ CALL_FUNCTIONS’, we can determine the name of the function and the arguments passed to it.

Testing

$ make check

A GNU makefile has been added to smooth over setting up running the right command, and running tests from fastest to slowest.

If you have remake installed, you can see the list of all tasks including tests via remake --tasks.

Usage

Run

$ ./bin/pydisasm -h

for usage help.

As a drop-in replacement for dis

xdis also provides some support as a drop-in replacement for the Python library dis module. This may be desirable when you want to use the improved API from Python 3.4 or later from an earlier Python version.

For example:

>>> # works in Python 2 and 3
>>> import xdis.std as dis
>>> [x.opname for x in dis.Bytecode('a = 10')]
['LOAD_CONST', 'STORE_NAME', 'LOAD_CONST', 'RETURN_VALUE']

There may be some small differences in output produced for formatted disassembly or how we show compiler flags. We expect you’ll find the xdis output more informative, though.

See Also

Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xdis-6.1.8.tar.gz (630.5 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

xdis-6.1.8-py313-none-any.whl (197.9 kB view details)

Uploaded Python 3.13

xdis-6.1.8-py312-none-any.whl (197.9 kB view details)

Uploaded Python 3.12

xdis-6.1.8-py311-none-any.whl (197.9 kB view details)

Uploaded Python 3.11

xdis-6.1.8-py310-none-any.whl (216.1 kB view details)

Uploaded Python 3.10

xdis-6.1.8-py39-none-any.whl (216.1 kB view details)

Uploaded Python 3.9

xdis-6.1.8-py38-none-any.whl (216.0 kB view details)

Uploaded Python 3.8

xdis-6.1.8-py37-none-any.whl (216.0 kB view details)

Uploaded Python 3.7

xdis-6.1.8-py36-none-any.whl (216.0 kB view details)

Uploaded Python 3.6

File details

Details for the file xdis-6.1.8.tar.gz.

File metadata

  • Download URL: xdis-6.1.8.tar.gz
  • Upload date:
  • Size: 630.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8.tar.gz
Algorithm Hash digest
SHA256 a070c390f814395d672a8732b8b5e8e09cdeeaac1b32497e8061c24f126f6d69
MD5 90d5b57c3920cdf4d49725a31d1483ce
BLAKE2b-256 315340c259fba51acb5ae83a0ecee4aa0837c57d2fa0e29bd7391fe041bfee7f

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py313-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py313-none-any.whl
  • Upload date:
  • Size: 197.9 kB
  • Tags: Python 3.13
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py313-none-any.whl
Algorithm Hash digest
SHA256 496349d62623a8bb77b33c44dfcf68860c1d1db5b43d7f729a241c73168d3e8f
MD5 e7b7ae8f7bd58f0a5403033b10ac94d4
BLAKE2b-256 5a6c7f36fe7cc1ef91bae777d63fadc14ccd0116520ede93b268dc3b3dcd6a3a

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py312-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py312-none-any.whl
  • Upload date:
  • Size: 197.9 kB
  • Tags: Python 3.12
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py312-none-any.whl
Algorithm Hash digest
SHA256 0e5579fd715a1df6bc2e1e828879d8751d16910eae967d0f4f518cad4f0723ba
MD5 f6a372c470c772fd08e88ff94b3336df
BLAKE2b-256 cf2bb23e4de209a60d4b9ed177d232d5cf4924fb4e1f1525d500b5b5658cfdbc

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py311-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py311-none-any.whl
  • Upload date:
  • Size: 197.9 kB
  • Tags: Python 3.11
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py311-none-any.whl
Algorithm Hash digest
SHA256 33c297a0951f3b81de62f27169607a9e1d2b560d488ef4cb6cecc119cfb72ded
MD5 fca30cd365ffd2bb9bc5ebc55f2a0587
BLAKE2b-256 bf781f5fc383b13574759093ee56e3b3f06993055d7c59afb20097adfa51ecda

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py310-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py310-none-any.whl
  • Upload date:
  • Size: 216.1 kB
  • Tags: Python 3.10
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py310-none-any.whl
Algorithm Hash digest
SHA256 b91a784627f18154e2607784ceafdc78b32f5ee235fa33d2ee1b0d00c89edf27
MD5 98e45a0654f9446625ffe946ab6dd94f
BLAKE2b-256 aee79f12bf8e8caab1845a2895df536e30c9cd5a86bf30d655a86bb97aa60686

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py39-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py39-none-any.whl
  • Upload date:
  • Size: 216.1 kB
  • Tags: Python 3.9
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py39-none-any.whl
Algorithm Hash digest
SHA256 d52c6890646eed3b5f2cdfe93ee98da639286dcaa20ceb1fbef6e05c3600da12
MD5 3770d2095f65451c0b581b10260de0b1
BLAKE2b-256 7b5bd28d06672e059fc6480ddd85e1ec69f351a215250b43e82fafcaac1a665c

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py38-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py38-none-any.whl
  • Upload date:
  • Size: 216.0 kB
  • Tags: Python 3.8
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py38-none-any.whl
Algorithm Hash digest
SHA256 e487b3a0a8631f3f74828d157acf68250fec0cbe383cddfacade3ae639281483
MD5 476ecafcb39d21a57a2709763c60f2d2
BLAKE2b-256 b4a8e3027198d980605a15ac77ffed7d5e20f7ad1b13f7a1d0b36b5532b6481d

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py37-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py37-none-any.whl
  • Upload date:
  • Size: 216.0 kB
  • Tags: Python 3.7
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py37-none-any.whl
Algorithm Hash digest
SHA256 d03ad44ee7a3faf3d75ff83498dee023f2da06fdabbe8252e29400ddbee405f2
MD5 cc3f738c106eb2fe8c09c28ec74767c3
BLAKE2b-256 8adbb1713ad464e8b8cc92fbbd06f9436bf5177e09b57f398b0adff349e391a6

See more details on using hashes here.

File details

Details for the file xdis-6.1.8-py36-none-any.whl.

File metadata

  • Download URL: xdis-6.1.8-py36-none-any.whl
  • Upload date:
  • Size: 216.0 kB
  • Tags: Python 3.6
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for xdis-6.1.8-py36-none-any.whl
Algorithm Hash digest
SHA256 255c5a5f147c49c15a953afc8852b68e471a23897e5f8595949c1b581263606d
MD5 6ad105b8b3c00852c9dc66f5945e2bb2
BLAKE2b-256 3a012d0c948362bb7c3e0fc689aef67884fe3c3053a22a15cee269bc708e1881

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page