Disassembler

You can see the machine code for the compiled function by disassembling it in the Python REPL. Pyjion has essentially compiled your small Python function into a small, standalone application.

Prerequisites

The X86_64 disassembler requires rich and distorm3. You can install the Prerequisites using the pyjion[dis] package:

> pip install pyjion[dis]

For ARM64/aarch64 binaries, you must follow the instructions to export dumps and load into an external disassembler.

CIL Disassembler

Pyjion’s compiler uses ECMA 335 CIL as the intermediary language. You can see the IL for a compiled function using the pyjion.dis.dis(f) function:

>>> import pyjion
>>> import pyjion.dis
>>> def half(x):
    return x / 2
>>> pyjion.enable()
>>> half(4)
2.0
>>> pyjion.disable()
IL_0000: ldarg.1
IL_0001: ldc.i4 104
IL_0006: conv.i
IL_0007: add
IL_0008: stloc.0
IL_0009: ldarg.2
...

For a reference of the CIL, see ECMA335 reference.

The dis() function also has an extra setting to print the sequence-points of the Python opcodes (see Python’s dis module):

>>> import pyjion
>>> import pyjion.dis
>>> def half(x):
    return x / 2
>>> pyjion.enable()
>>> half(4)
2.0
>>> pyjion.disable()
>>> pyjion.dis.dis(half, include_offsets=True)

IL_002b: conv.i
IL_002c: add
IL_002d: stind.i
// 2 STORE_NAME - 0 (a)
IL_002e: ldloc.0
IL_002f: ldc.i4.2
IL_0030: conv.u4
IL_0031: stind.i4
IL_0032: ldarg.1
IL_0033: ldc.i8 140106856465584
IL_003c: conv.i
IL_003d: call METHOD_STORENAME_TOKEN

X86_64 Disassembler

The disassembler uses a project called distorm3. I recommend disabling Pyjion before running the disassembler, because it will JIT-compile distorm3 (which is massive).

To run the X86_64 disassembler:

>>> import pyjion
>>> import pyjion.dis
>>> def half(x):
       return x / 2
>>> pyjion.enable()
>>> half(4)
 2.0
>>> pyjion.disable()
>>> pyjion.dis.dis_native(half)
PUSH RBP
SUB RSP, 0x290
LEA RBP, [RSP+0x290]
XOR EAX, EAX
MOV [RBP-0x288], RAX
XORPS XMM8, XMM8
MOVAPS [RBP-0x280], XMM8
MOV RAX, 0xfffffffffffffdc0
MOVAPS [RBP+RAX-0x30], XMM8
MOVAPS [RBP+RAX-0x20], XMM8
MOVAPS [RBP+RAX-0x10], XMM8
ADD RAX, 0x30
JNZ 0x7f6d0c013fcf
MOV [RBP-0x30], RAX
MOV [RBP-0x8], RDI
MOV [RBP-0x10], RSI
MOV [RBP-0x18], RDX
MOV [RBP-0x20], RCX
MOV [RBP-0x28], R8
MOV EDI, 0x68
...

The dis_native() function has an optional flag to print the offsets of the Python bytecodes as comments:

>>> import pyjion
>>> import pyjion.dis
>>> def half(x):
    return x / 2
>>> pyjion.enable()
>>> half(4)
2.0
>>> pyjion.disable()
>>> pyjion.dis.dis_native(half, include_offsets=True)

MOVSXD RDI, EDI
MOV [RBP-0x58], RDI
MOV EDI, 0x1
MOVSXD RDI, EDI
MOV RSI, 0x7f6d2b7b7950
ADD [RSI], RDI
MOV [RBP-0xc8], RSI
; 2 STORE_NAME - 0 (a)
MOV RDI, [RBP-0x30]
MOV DWORD [RDI], 0x2
MOV RDI, [RBP-0xc8]
MOV RSI, [RBP-0x10]
MOV RDX, 0x7f6d2b6904b0
MOV RAX, 0x7f6d20ff1350
CALL QWORD [RAX] ; METHOD_STORENAME_TOKEN
MOV [RBP-0xcc], EAX
CMP DWORD [RBP-0xcc], 0x0
JZ 0x7f6d0c0140a4
MOV RDI, [RBP-0x10]
MOV RAX, 0x7f6d20ff0350
CALL QWORD [RAX] ; METHOD_EH_TRACE
NOP
JMP 0x7f6d0c01496f
MOV EDI, 0x1
MOVSXD RDI, EDI
MOV RSI, 0x7f6d286f4330
ADD [RSI], RDI

External disassemblers

If you prefer to use an external disassembler, like Hopper, you can use the pyjion.native() function to fetch the bytearray and write it to disk.

>>> import pyjion
>>> def half(x):
    return x / 2
>>> pyjion.enable()
>>> half(4)
2.0
>>> pyjion.disable()
>>> raw, length, position = pyjion.native(half)
>>> with open('dump.raw', 'wb') as out:
...   out.write(raw)

In your disassembler, open the dump.raw file as a raw file. The position variable shown in this example is the base address of the JIT in memory:

_images/hopper.png

For Apple M1, choose the aarch64 CPU Architecture in Hopper:

_images/hopper-arm64.png