Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pytest/test_load_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ def test_load_file() -> None:
obj_path = check_object_path(load_py)
(
version_tuple,
timestamp,
magic_int,
_timestamp,
_magic_int,
co_module,
pypy,
source_size,
sip_hash,
_file_offsets,
) = load_module(obj_path)
if (3, 3) <= version_tuple <= (3, 7):
statinfo = os.stat(load_py)
Expand Down
25 changes: 19 additions & 6 deletions xdis/bin/pydisasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,25 @@
metavar="FUNCTION-OR-METHOD",
multiple=True,
type=str,
help=("Specify which specific methods or functions to show. "
"If omitted all, functions are shown. "
"Can be given multiple times.")
help=(
"Specify which specific methods or functions to show. "
"If omitted all, functions are shown. "
"Can be given multiple times."
),
)

@click.option(
"--show-source/--no-show-source",
"-S",
help="Intersperse Python source text from linecache if available.",
)
@click.option(
"--show-file-offsets/--no-show-file_offsets",
"-x",
help="Show bytecode file hex addresses for the start of each code object.",
)
@click.version_option(version=__version__)
@click.argument("files", nargs=-1, type=click.Path(readable=True), required=True)
def main(format: list[str], method: tuple, show_source: bool, files):
def main(format: list[str], method: tuple, show_source: bool, show_file_offsets, files):
"""Disassembles a Python bytecode file.

We handle bytecode for virtually every release of Python and some releases of PyPy.
Expand Down Expand Up @@ -91,7 +97,14 @@ def main(format: list[str], method: tuple, show_source: bool, files):
continue

try:
disassemble_file(path, sys.stdout, format, show_source=show_source, methods=method)
disassemble_file(
path,
sys.stdout,
format,
show_source=show_source,
methods=method,
save_file_offsets=show_file_offsets,
)
except (ImportError, NotImplementedError, ValueError) as e:
print(e)
rc = 3
Expand Down
22 changes: 17 additions & 5 deletions xdis/bytecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def get_instructions_bytes(
linestarts=linestarts,
line_offset=0,
exception_entries=exception_entries,
labels=labels
labels=labels,
)
)

Expand All @@ -515,7 +515,9 @@ class Bytecode:
Iterating over these yields the bytecode operations as Instruction instances.
"""

def __init__(self, x, opc, first_line=None, current_offset=None, dup_lines: bool=True) -> None:
def __init__(
self, x, opc, first_line=None, current_offset=None, dup_lines: bool = True
) -> None:
self.codeobj = co = get_code_object(x)
self._line_offset = 0
self._cell_names = ()
Expand All @@ -536,7 +538,11 @@ def __init__(self, x, opc, first_line=None, current_offset=None, dup_lines: bool
self.opnames = opc.opname
self.current_offset = current_offset

if opc.version_tuple >= (3, 11) and not opc.is_pypy and hasattr(co, "co_exceptiontable"):
if (
opc.version_tuple >= (3, 11)
and not opc.is_pypy
and hasattr(co, "co_exceptiontable")
):
self.exception_entries = parse_exception_table(co.co_exceptiontable)
else:
self.exception_entries = None
Expand Down Expand Up @@ -573,7 +579,11 @@ def info(self) -> str:
"""Return formatted information about the code object."""
return format_code_info(self.codeobj, self.opc.version_tuple)

def dis(self, asm_format: str="classic", show_source: bool=False) -> str:
def dis(
self,
asm_format: str = "classic",
show_source: bool = False,
) -> str:
"""Return a formatted view of the bytecode operations."""
co = self.codeobj
filename = co.co_filename
Expand Down Expand Up @@ -839,7 +849,9 @@ def get_instructions(self, x, first_line=None):
)


def list2bytecode(inst_list: Iterable, opc, varnames: str, consts: Tuple[None, int]) -> bytes:
def list2bytecode(
inst_list: Iterable, opc, varnames: str, consts: Tuple[None, int]
) -> bytes:
"""Convert list/tuple of list/tuples to bytecode
_names_ contains a list of name objects
"""
Expand Down
7 changes: 5 additions & 2 deletions xdis/cross_dis.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
# earlier versions of xdis (and without attribution).

from types import CodeType
from typing import List
from typing import List, Optional

from xdis.util import (
COMPILER_FLAG_NAMES,
Expand Down Expand Up @@ -272,7 +272,7 @@ def pretty_flags(flags, is_pypy=False) -> str:


def format_code_info(
co, version_tuple: tuple, name=None, is_pypy=False, is_graal=False
co, version_tuple: tuple, name=None, is_pypy=False, is_graal=False, file_offset: Optional[int]=None
) -> str:
if not name:
name = co.co_name
Expand All @@ -285,6 +285,9 @@ def format_code_info(
# Later versions use "<module>"
lines.append("# Filename: %s" % co.co_filename)

if file_offset:
lines.append("# Offset in file: 0x%x" % file_offset)

if not is_graal:
if version_tuple >= (1, 3):
lines.append("# Argument count: %s" % co.co_argcount)
Expand Down
58 changes: 46 additions & 12 deletions xdis/disasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import sys
import types
from collections import deque
from typing import Tuple
from typing import Optional, Tuple

import xdis
from xdis.bytecode import Bytecode
Expand Down Expand Up @@ -72,6 +72,7 @@ def show_module_header(
header=True,
show_filename=True,
is_graal=False,
file_offset: Optional[int] = None,
) -> None:
bytecode_version = ".".join((str(i) for i in version_tuple))
real_out = out or sys.stdout
Expand Down Expand Up @@ -121,22 +122,25 @@ def show_module_header(
real_out.write("# SipHash: 0x%x\n" % sip_hash)
if show_filename:
real_out.write("# Embedded file name: %s\n" % co.co_filename)
if file_offset:
real_out.write("# Position in bytecode file: 0x%x\n" % file_offset)


def disco(
version_tuple,
co,
timestamp,
out=sys.stdout,
is_pypy: bool=False,
is_pypy: bool = False,
magic_int=None,
source_size=None,
sip_hash=None,
asm_format: str="classic",
asm_format: str = "classic",
alternate_opmap=None,
show_source: bool=False,
is_graal: bool=False,
show_source: bool = False,
is_graal: bool = False,
methods=tuple(),
file_offsets: dict = {},
) -> None:
"""
disassembles and deparses a given code block 'co'
Expand All @@ -163,7 +167,15 @@ def disco(

if co.co_filename and asm_format != "xasm":
if not_filtered(co, methods):
real_out.write(format_code_info(co, version_tuple, is_graal=is_graal) + "\n")
real_out.write(
format_code_info(
co,
version_tuple,
is_graal=is_graal,
file_offset=file_offsets.get(co),
)
+ "\n"
)
pass

opc = get_opcode(version_tuple, is_pypy, alternate_opmap)
Expand All @@ -184,6 +196,7 @@ def disco(
dup_lines=True,
show_source=show_source,
methods=methods,
file_offsets=file_offsets,
)


Expand All @@ -196,6 +209,7 @@ def disco_loop(
asm_format="classic",
show_source=False,
methods=tuple(),
file_offsets: dict = {},
) -> None:
"""Disassembles a queue of code objects. If we discover
another code object which will be found in co_consts, we add
Expand All @@ -211,7 +225,13 @@ def disco_loop(
co = queue.popleft()
if not_filtered(co, methods):
if co.co_name not in ("<module>", "?"):
real_out.write("\n" + format_code_info(co, version_tuple) + "\n")
real_out.write(
"\n"
+ format_code_info(
co, version_tuple, file_offset=file_offsets.get(co)
)
+ "\n"
)

if asm_format == "dis":
assert version_tuple[:2] == PYTHON_VERSION_TRIPLE[:2], (
Expand All @@ -222,12 +242,18 @@ def disco_loop(
else:
bytecode = Bytecode(co, opc, dup_lines=dup_lines)
real_out.write(
bytecode.dis(asm_format=asm_format, show_source=show_source) + "\n"
bytecode.dis(
asm_format=asm_format,
show_source=show_source,
)
+ "\n"
)

if version_tuple >= (3, 11):
if bytecode.exception_entries not in (None, []):
exception_table = format_exception_table(bytecode, version_tuple)
exception_table = format_exception_table(
bytecode, version_tuple
)
real_out.write(exception_table + "\n")

for c in co.co_consts:
Expand All @@ -242,7 +268,9 @@ def code_uniquify(basename, co_code) -> str:
return "%s_0x%x" % (basename, id(co_code))


def disco_loop_asm_format(opc, version_tuple, co, real_out, fn_name_map, all_fns) -> None:
def disco_loop_asm_format(
opc, version_tuple, co, real_out, fn_name_map, all_fns
) -> None:
"""Produces disassembly in a format more conducive to
automatic assembly by producing inner modules before they are
used by outer ones. Since this is recursive, we'll
Expand Down Expand Up @@ -318,7 +346,8 @@ def disassemble_file(
asm_format="classic",
alternate_opmap=None,
show_source=False,
methods: Tuple[str] = tuple()
methods: Tuple[str] = tuple(),
save_file_offsets: bool = False,
):
"""
Disassemble Python byte-code file (.pyc).
Expand All @@ -329,6 +358,7 @@ def disassemble_file(
If that fails, we'll compile internally for the Python version currently running.
"""
pyc_filename = None
file_offsets = {}
try:
# FIXME: add whether we want PyPy
pyc_filename = check_object_path(filename)
Expand All @@ -340,7 +370,8 @@ def disassemble_file(
is_pypy,
source_size,
sip_hash,
) = load_module(pyc_filename)
file_offsets,
) = load_module(pyc_filename, save_file_offsets=save_file_offsets)
except (ImportError, NotImplementedError, ValueError):
raise
except Exception:
Expand Down Expand Up @@ -391,6 +422,7 @@ def disassemble_file(
show_source=show_source,
is_graal=is_graal,
methods=methods,
file_offsets=file_offsets,
)
# print co.co_filename
return (
Expand All @@ -404,9 +436,11 @@ def disassemble_file(
sip_hash,
)


def not_filtered(co: types.CodeType, methods: tuple) -> bool:
return len(methods) == 0 or co.co_name in methods


def _test() -> None:
"""Simple test program to disassemble a file."""
argc = len(sys.argv)
Expand Down
2 changes: 1 addition & 1 deletion xdis/dropbox/decrypt25.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def fix_dropbox_pyc(fp):
timestamp = struct.unpack("I", ts)[0]
b = fp.read()
co = loads(b)
return (2, 5, "0dropbox"), timestamp, 62131, co, False, source_size, None
return (2, 5, "0dropbox"), timestamp, 62131, co, False, source_size, None, {}


def fix_dir(path) -> None:
Expand Down
Loading