1 of 23

Debugging, Profiling, and Reversing Python Code

CSE 598 – Applied Program Analysis and Debugging

Fall 2025

Fish Wang

Arizona State University

2 of 23

Python Interpreters

  • CPython
    • The official Python interpreter
  • PyPy
    • JIT Python interpreter, drop-in replacement
  • IronPython
    • Python on .Net
  • Jython
    • Python on JVM

2

3 of 23

Python Debugging

  • print() debugging
  • pdb – The Python debugger
  • Strongly recommend: ipdb
  • IDE integration (PyCharm, VS Code, etc.)

3

4 of 23

Demo

4

5 of 23

Attaching to a Running Process

  • Python 3.14+
    • python -m pdb -p <pid>
  • pdb-attach
    • Requires running some code in the target program first

5

6 of 23

Python Profiling

  • CPU profiling vs memory profiling

6

7 of 23

Python CPU Profilers

  • Instrumentation-based
    • Deterministic profiling with skewed performance result
    • cProfile
  • Sampling-based
    • Slightly-stochastic profiling with more accurate performance views
    • py-spy
    • perf (on Linux, Python 3.12+)

7

8 of 23

Demo

8

9 of 23

Analyzing the Profiling Result

  • Interpreting the profiling result is as important as profiling your programs
    • Print in stdout
    • flamegraph
    • Snakeviz
    • Speedscope

9

10 of 23

Behind The Scene

  • CPython does not directly interpret Python statements and expressions�… it interprets Python bytecode instead

  • Why?
    • Easier to implement
    • Correctness
    • Optimization opportunities

10

11 of 23

Python Bytecode

  • Python bytecode is machine code for a virtual machine (the CPython interpreter)
  • A bytecode program contains a series of bytecode instructions for stack operations
    • Cpython interpreter is a stack machine

11

Interprets an instruction

Updates the state

12 of 23

Python Bytecode

def func(a):

return a + 1

12

Line number

Byte offset

Bytecode instructions

1 0 RESUME 0

2 2 LOAD_FAST 0 (a)

4 LOAD_CONST 1 (1)

6 BINARY_OP 0 (+)

10 RETURN_VALUE

13 of 23

How to…

  • View the bytecode of a function
    • dis.dis(your_func)
  • Access the code object of a function
    • your_func.__code__
  • Store the code object into a byte string
    • marshal.dumps(your_co)
  • Pick into a code object

13

14 of 23

.py vs .pyc

  • .pyc stores marshalled (serialized) Python code objects
    • With a header
    • Not exactly same as marshal.dumps(co)
  • py_compile can slightly reduce the time required for running Python code

14

15 of 23

Understanding Bytecode

1 0 RESUME 0

2 2 LOAD_FAST 0 (a)

4 LOAD_CONST 1 (1)

6 BINARY_OP 0 (+)

10 RETURN_VALUE

15

https://docs.python.org/3/library/dis.html#python-bytecode-instructions

16 of 23

The CPython Memory Model

  • Overall
    • Everything is “on the heap”
    • Dictionaries of objects keyed by their names
  • Local function frame
    • Just a scope
    • A dictionary of objects keyed by their names
  • Stack (for bytecode only)
    • A linear stack
    • STACK[-1] refers to the top of the stack

16

17 of 23

The CPython Memory Model

  • The CPython stack is only for data, not for execution
    • … so there is no “overwriting the stored return address and gaining PC control”
    • … this is a reversing course anyway

17

18 of 23

Decompiling Python Bytecode

1 0 RESUME 0

2 2 LOAD_FAST 0 (a)

4 LOAD_CONST 1 (1)

6 BINARY_OP 0 (+)

10 RETURN_VALUE

18

nop

stack_push(a)

stack_push(1)

stack_push(stack_pop() + stack_pop())

return stack_top()

19 of 23

Decompiling Python Bytecode

1 0 RESUME 0

2 2 LOAD_FAST 0 (a)

4 LOAD_CONST 1 (1)

6 BINARY_OP 0 (+)

10 RETURN_VALUE

19

nop

v0 = a

v1 = 1

v2 = v0 + v1; kill(v0); kill(v1)

return v2

v2 = a + 1

return v2

return a + 1

20 of 23

Congratulations!

  • You just completed your very first decompiler!
  • Caveats
    • Super manual
    • Only supports 5 opcodes
    • Decompiling Python bytecode… why?
      • PyInstaller only bundles and ships .pyc files
      • Understanding performance weirdness

20

21 of 23

func_0 vs func_1

  • Demo

21

22 of 23

General Python Rules of Thumb

  • Local names are faster than global names
    • LOAD_CONST > LOAD_FAST > LOAD_GLOBAL/LOAD_NAME
  • Loops are much more expensive (than in C)
    • Multiple bytecode instructions for loop setup

22

23 of 23

Questions?

23