> For the complete documentation index, see [llms.txt](https://sansong.gitbook.io/cyber/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sansong.gitbook.io/cyber/rev/python/bytecode.md).

# Bytecode

Ou plonger dans la machine virtuelle Python

Python est un langage interprété. Le code source est **transformé en un langage intermédiaire (appelé bytecode)** composé d'instructions exécutables par la machine virtuelle Python. L'implémentation de référence de Python est [CPython](https://github.com/python/cpython) qui est écrit principalement en langage C.&#x20;

Le bytecode est stocké dans un fichier `.pyc` du même nom que le fichier source.

{% hint style="success" %}
Python3 stocke les fichiers `.pyc` dans le dossier `__pycache__`.
{% endhint %}

La machine virtuelle **CPython utilise 3 piles** (ou *stack* en anglais).&#x20;

* **Call Stack:** contient des *frames* qui sont constituées des 2 autres types de piles. A chaque appel d'une fonction, une nouvelle *frame* est empilée sur la **call stack**. Après son exécution, elle est dépilée.
* **Evaluation Stack (ou Data Stack ou Value Stack):** contient toutes les données nécessaires à l'exécution d'une fonction (variables locales, etc.)
* **Block stack:** contient une entrée pour chaque bloc de type boucle, `try`/`except`, `with`, etc. Python l'utilise pour savoir quel bloc est actif (les mots clés `continue` et `break` ont un impact sur le bloc courant par exemple)

**Chaque&#x20;*****frame*****&#x20;contient une&#x20;*****evaluation stack*****&#x20;et une&#x20;*****block stack*****.**

## **Vue d'ensemble**

La fonction `add` additionne les deux nombre passés en argument en renvoie le résultat.

```python
def add(a, b):
    print("abracadabra")
    res = a + b
    return res
```

Sa représentation intermédiaire est la suivante. Ce sont ces instructions (`LOAD_GLOBAL`, `LOAD_CONST`, etc.) qui sont exécutées par la machine virtuelle Python.

```python
LOAD_GLOBAL              0 (print)
LOAD_CONST               1 ('abracadabra')
CALL_FUNCTION            1
POP_TOP

LOAD_FAST                0 (a)
LOAD_FAST                1 (b)
BINARY_ADD
STORE_FAST               2 (res)

LOAD_FAST                2 (res)
RETURN_VALUE
```

Certaines instructions prennent une valeur numérique (0, 1, etc.) en argument. Ces numéros sont des indices qui permettent d'accéder aux paramètres locaux et globaux de la fonction.

Dans l'implémentation de Python, le bytecode est représenté avec des `code object`. Ces objets utilisés par l'interpréteur sont accessibles avec l'attribut  `__code__`.

```python
>>> print(add.__code__)
<code object add at 0x000001E35CA1D620, ...>
```

Un `code object` contient plusieurs attributs intéressants.

|    Attribut   |                                       Description                                      |
| :-----------: | :------------------------------------------------------------------------------------: |
|   `co_name`   |                                   Nom de la fonction                                   |
| `co_varnames` |              Tuple contenant les noms des variables locales de la fonction             |
|  `co_consts`  |      Tuple contenant les valeurs utilisées (chaînes de caractères, nombres, etc.)      |
|   `co_names`  | Tuple contenant les noms des fonctions et variables globales utilisées par la fonction |
|   `co_code`   |       Chaîne de caractères représentant les instructions bytecode de la fonction       |

{% hint style="success" %}
La liste complète des attributs est référencée dans la [documentation](https://docs.python.org/3/reference/datamodel.html#code-objects).
{% endhint %}

Attardons nous un peu sur ces attributs.

`co_varnames` contient **les noms des variables locales ainsi que les arguments** de la fonction.

```python
>>> print(add.__code__.co_varnames)
('a', 'b', 'res')
```

`co_consts` contient **les valeurs constantes** (chaînes de caractères, nombres, etc.).

```python
>>> print(add.__code__.co_consts)
(None, 'abracadabra')
```

{% hint style="success" %}
Le tuple contient `None` car si la fonction n'a pas de `return` et ne renvoie rien, elle renvoie par défaut `None`. Il lui faut donc un accès à cette valeur en cas de besoin.
{% endhint %}

`co_names` contient **les noms des fonctions et variables globales** utilisées par la fonction.

```python
>>> print(add.__code__.co_names)
('print',)
```

`co_code` renvoie une chaîne de caractères qui **représente le bytecode des instructions exécutées par l'interpréteur**.

{% hint style="success" %}
Chaque instruction est associée à un numéro appelé **opcode**. Lorsque l'interpréteur Python lit un **opcode** il sait quelle opération effectuer.
{% endhint %}

```python
>>> print(add.__code__.co_code)
b't\x00d\x01\x83\x01\x01\x00|\x00|\x01\x17\x00}\x02|\x02S\x00'
```

L'octet `t` ou `0x74` en ASCII est l'opcode de la première instruction à exécuter: `LOAD_GLOBAL`.&#x20;

{% hint style="warning" %}
Ici `t` est affiché dans la chaîne de caractères car le numéro de l'opcode (`0x74`) correspond à ce caractère imprimable dans la table ASCII.
{% endhint %}

`LOAD_GLOBAL` prend la valeur `0x00` en argument. **Il s'agit de** **l'indice de la variable globale à placer au sommet de la evaluation stack**. Comme nous l'avons vu précédemment, les noms des variables et fonctions globales sont renvoyées par l'attribut `co_names`. Ici le premier élément du tuple est `print`.

```python
b't\x00...'
>> LOAD_GLOBAL              0
   (0x74)                 (0x00)
```

L'octet suivant `d` (`0x64`) correspond à l'instruction `LOAD_CONST` qui place la constante d'indice `0x01` de `co_consts` au sommet de la evaluation stack (ici `'abracadabra'`).

```python
b't\x00d\x01...'
LOAD_GLOBAL              0
(0x74)                 (0x00)

>> LOAD_CONST            1 ('abracadabra')
   (0x64)              (0x01)
```

Le prochain octet `0x83` est l'opcode de `CALL_FUNCTION`. **Elle prend en paramètre le nombre d'arguments de la fonction** (ici `0x01`). Cette instruction dépile donc un argument (`'abracadara'`) puis le nom de la fonction à appeler (`print`) **et exécute ensuite la fonction**.

```python
b't\x00d\x01\x83\x01...'
LOAD_GLOBAL              0
(0x74)                 (0x00)

LOAD_CONST               1 ('abracadabra')
(0x64)                 (0x01)

>> CALL_FUNCTION         1
   (0x83)              (0x01)
```

La machine virtuelle Python continue à lire les octets et à exécuter le code correspondant à chaque instruction en fonction des valeurs données en argument.&#x20;

{% hint style="info" %}
Le fichier [generated\_cases.c.h](https://github.com/python/cpython/blob/main/Python/generated_cases.c.h) de CPython contient le `switch` avec tous les opcodes.
{% endhint %}

L'exécution de la fonction et l'état des 3 piles est illustrée ci-dessous.

<figure><img src="https://1813806532-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FZRRTPIEA4wb6exZozwS0%2Fuploads%2FWzlTe8vbt1CNNVMrawWs%2Fpython-stacks.gif?alt=media&amp;token=24fc9128-6127-4b5b-af59-d55647b6c629" alt=""><figcaption></figcaption></figure>

TODO: python 3.11 (NULL + print)

## Inspecter le bytecode

Le module [dis](https://docs.python.org/3/library/dis.html) permet **d'inspecter le bytecode d'objets CPython**. Son code source est dans le fichier [Lib/dis.py](https://github.com/python/cpython/blob/main/Lib/dis.py).

On prend cette fonction en exemple:

```python
def div(a, b):
    q = a / b
    return q
```

### **Installation**

Le module est compilé dans l'implémentation CPython de l'interpréteur, il suffit de l'importer.

```python
import dis
```

### **Désassemblage**

**`dis.dis`** désassemble des objets CPython.&#x20;

```python
>>> dis.dis(div)
2       0 LOAD_FAST                0 (a)
        2 LOAD_FAST                1 (b)
        4 BINARY_TRUE_DIVIDE
        6 STORE_FAST               2 (q)

3       8 LOAD_FAST                2 (q)
       10 RETURN_VALUE
```

{% hint style="info" %}
Les chiffres à gauche sont les numéros des lignes dans le fichier source.
{% endhint %}

{% hint style="success" %}
On peut aussi passer des chaînes de caractères à **`dis.dis`**.

```python
>>> dis.dis("""
def div(a, b):
    q = a / b
    return q
"""
    )
Disassembly of <code object div at 0x0000022DE8685CA0, file "<dis>", line 2>:
  2           0 RESUME                   0

  3           2 LOAD_FAST                0 (a)
              4 LOAD_FAST                1 (b)
              6 BINARY_OP               11 (/)
             10 STORE_FAST               2 (q)

  4          12 LOAD_FAST                2 (q)
             14 RETURN_VALUE
```

{% endhint %}

### **Instructions**

**`dis.get_instructions`** renvoie un itérateur sur des objets [Instruction](https://docs.python.org/3/library/dis.html#dis.Instruction).

```python
>>> for instr in dis.get_instructions(div):
...    print(instr)
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='a', argrepr='a', offset=0, starts_line=2, is_jump_target=False)
Instruction(opname='LOAD_FAST', opcode=124, arg=1, argval='b', argrepr='b', offset=2, starts_line=None, is_jump_target=False)
Instruction(opname='BINARY_TRUE_DIVIDE', opcode=27, arg=None, argval=None, argrepr='', offset=4, starts_line=None, is_jump_target=False)
Instruction(opname='STORE_FAST', opcode=125, arg=2, argval='q', argrepr='q', offset=6, starts_line=None, is_jump_target=False)
Instruction(opname='LOAD_FAST', opcode=124, arg=2, argval='q', argrepr='q', offset=8, starts_line=3, is_jump_target=False)
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=10, starts_line=None, is_jump_target=False)
```

```python
...    print(instr.opname)
LOAD_FAST
LOAD_FAST
BINARY_TRUE_DIVIDE
STORE_FAST
LOAD_FAST
RETURN_VALUE
```

### **Opcodes**

**`dis.opname`** renvoie la liste des opcodes utilisées par la version installée de Python. Ils sont listés dans le fichier [`Include/opcode.h`](https://github.com/python/cpython/blob/main/Include/opcode.h).

```python
>>> print(dis.opname)
['CACHE', 'POP_TOP', 'PUSH_NULL', 'INTERPRETER_EXIT', 'END_FOR', 'END_SEND', '<6>', '<7>', '<8>', 'NOP', '<10>', 'UNARY_NEGATIVE', 'UNARY_NOT', '<13>', '<14>', 'UNARY_INVERT', '<16>', 'RESERVED', '<18>', '<19>', 
'<20>', '<21>', '<22>', '<23>', '<24>', 'BINARY_SUBSCR', 'BINARY_SLICE', 'STORE_SLICE', '<28>', '<29>', 'GET_LEN', 'MATCH_MAPPING', 'MATCH_SEQUENCE', 'MATCH_KEYS', '<34>', 'PUSH_EXC_INFO', 'CHECK_EXC_MATCH', 'CHECK_EG_MATCH', '<38>', '<39>', '<40>', '<41>', '<42>', '<43>', '<44>', '<45>', '<46>', '<47>', '<48>', 'WITH_EXCEPT_START', 'GET_AITER', 'GET_ANEXT', 'BEFORE_ASYNC_WITH', 'BEFORE_WITH', 'END_ASYNC_FOR', 'CLEANUP_THROW', '<56>', '<57>', '<58>', '<59>', 'STORE_SUBSCR', 'DELETE_SUBSCR', '<62>', '<63>', '<64>', '<65>', '<66>', '<67>', 'GET_ITER', 'GET_YIELD_FROM_ITER', '<70>', 'LOAD_BUILD_CLASS', '<72>', '<73>', 'LOAD_ASSERTION_ERROR', 'RETURN_GENERATOR', '<76>', '<77>', '<78>', '<79>', '<80>', '<81>', '<82>', 'RETURN_VALUE', '<84>', 'SETUP_ANNOTATIONS', '<86>', 'LOAD_LOCALS', '<88>', 'POP_EXCEPT', 'STORE_NAME', 'DELETE_NAME', 'UNPACK_SEQUENCE', 'FOR_ITER', 'UNPACK_EX', 'STORE_ATTR', 'DELETE_ATTR', 'STORE_GLOBAL', 'DELETE_GLOBAL', 'SWAP', 'LOAD_CONST', 'LOAD_NAME', 'BUILD_TUPLE', 'BUILD_LIST', 'BUILD_SET', 'BUILD_MAP', 'LOAD_ATTR', 'COMPARE_OP', 'IMPORT_NAME', 'IMPORT_FROM', 'JUMP_FORWARD', '<111>', '<112>', '<113>', 'POP_JUMP_IF_FALSE', 'POP_JUMP_IF_TRUE', 'LOAD_GLOBAL', 'IS_OP', 'CONTAINS_OP', 'RERAISE', 'COPY', 'RETURN_CONST', 'BINARY_OP', 'SEND', 'LOAD_FAST', 'STORE_FAST', 'DELETE_FAST', 'LOAD_FAST_CHECK', 'POP_JUMP_IF_NOT_NONE', 'POP_JUMP_IF_NONE', 'RAISE_VARARGS', 'GET_AWAITABLE', 'MAKE_FUNCTION', 'BUILD_SLICE', 'JUMP_BACKWARD_NO_INTERRUPT', 'MAKE_CELL', 'LOAD_CLOSURE', 'LOAD_DEREF', 'STORE_DEREF', 'DELETE_DEREF', 'JUMP_BACKWARD', 'LOAD_SUPER_ATTR', 'CALL_FUNCTION_EX', 'LOAD_FAST_AND_CLEAR', 'EXTENDED_ARG', 'LIST_APPEND', 'SET_ADD', 'MAP_ADD', '<148>', 'COPY_FREE_VARS', 'YIELD_VALUE', 'RESUME', 'MATCH_CLASS', '<153>', '<154>', 'FORMAT_VALUE', 'BUILD_CONST_KEY_MAP', 'BUILD_STRING', '<158>', '<159>', '<160>', '<161>', 'LIST_EXTEND', 'SET_UPDATE', 'DICT_MERGE', 'DICT_UPC_2', 'LOAD_FROM_DICT_OR_GLOBALS', 'LOAD_FROM_DICT_OR_DEREF', '<177>', '<178>', '<179>', '<180>', '<181>', '<182>', '<183>', '<184>', '<185>', '<186>', '<187>', '<188>', '<189>', '<190>', '<191>', '<192>', '<193>', '<194>', '<195>', '<196>', '<197>', '<198>', '<199>', '<200>', '<201>', '<202>', '<203>', '<204>', '<205>', '<206>', '<207>', '<208>', '<209>', '<210>', '<211>', '<212>', '<213>', '<214>', '<215>', '<216>', '<217>', '<218>', '<219>', '<220>', '<221>', '<222>', '<223>', '<224>', '<225>', '<226>', '<227>', '<228>', 
'<229>', '<230>', '<231>', '<232>', '<233>', '<234>', '<235>', '<236>', 'INSTRUMENTED_LOAD_SUPER_ATTR', 'INSTRUMENTED_POP_JUMP_IF_NONE', 'INSTRUMENTED_POP_JUMP_IF_NOT_NONE', 'INSTRUMENTED_RESUME', 'INSTRUMENTED_CALL', 'INSTRUMENTED_RETURN_VALUE', 'INSTRUMENTED_YIELD_VALUE', 'INSTRUMENTED_CALL_FUNCTION_EX', 'INSTRUMENTED_JUMP_FORWARD', 'INSTRUMENTED_JUMP_BACKWARD', 'INSTRUMENTED_RETURN_CONST', 'INSTRUMENTED_FOR_ITER', 'INSTRUMENTED_POP_JUMP_IF_FALSE', 'INSTRUMENTED_POP_JUMP_IF_TRUE', 'INSTRUMENTED_END_FOR', 'INSTRUMENTED_END_SEND', 'INSTRUMENTED_INSTRUCTION', 'INSTRUMENTED_LINE', '<255>', 'SETUP_FINALLY', 'SETUP_CLEANUP', 'SETUP_WITH', 'POP_BLOCK', 'JUMP', 'JUMP_NO_INTERRUPT', 'LOAD_METHOD', 'LOAD_SUPER_METHOD', 'LOAD_ZERO_SUPER_METHOD', 'LOAD_ZERO_SUPER_ATTR', 'STORE_FAST_MAYBE_NULL']
```

Pour connaitre l'instruction correspondant à l'opcode `0x80`:

```python
>>> print(dis.opname[0x80])
POP_JUMP_IF_NOT_NON
```

{% hint style="danger" %}
Les opcodes peuvent changer en fonction des versions de Python.
{% endhint %}

### **Informations sur les objets**

**`dis.show_code`** renvoie une sortie formatée avec des informations sur l'objet.

```python
>>> dis.show_code(div)
Name:              div
Filename:          <stdin>
Argument count:    2
Kw-only arguments: 0
Number of locals:  3
Stack size:        2
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
Variable names:
   0: a
   1: b
   2: q
```

### **Traceback**

**`dis.distb`** désassemble la stack trace de l'exception donnée (ou la dernière par défaut) et pointe vers l'instruction responsable.

{% hint style="success" %}
La dernière traceback peut être récupérée avec le 3e élément du tuple renvoyé par [`sys.exc_info`](https://docs.python.org/3/library/sys.html#sys.exc_info).

```python
tb = sys.exc_info()[2]
```

{% endhint %}

```python
import sys

>>> try:
     div(1, 0)
... except Exception:
...     trace = sys.exc_info()[2]
...     dis.distb(trace)
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
 [...]

  9          26 LOAD_NAME                2 (div)
             28 LOAD_CONST               4 (1)
             30 LOAD_CONST               0 (0)
    -->      32 CALL_FUNCTION            2
             34 POP_TOP
             36 POP_BLOCK
             38 JUMP_FORWARD            42 (to 82)

 [...]
```

{% hint style="warning" %}
Ici l'erreur à lieu sur l'instruction `CALL_FUNCTION` pour `div`.
{% endhint %}

## Structures intéressantes et optimisation

On peut faire la même chose de plusieurs manières différentes mais certaines méthodes sont plus rapides que d'autres.

### Structures

On entend parfois qu'utiliser une liste littérale `[]` **est plus rapide** que `list()`. *Mais pourquoi ?*

```python
>>> dis.dis('[]')
  9           0 LOAD_CONST               1 (604800)
              2 RETURN_VALUE
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
```

Utiliser `list()` force un appel de fonction avec `CALL_FUNCTION` et donc **un empilement suivi de son exécution et du dépilement**. **Toutes ces opérations prennent du temps** contrairement à `[]` qui n'exécute que 4 instructions.

### Variables

Ces 2 fonctions renvoient le nombre de secondes dans 1 semaine.

```python
def slow_week():
    SECONDS_PER_DAY = 86400
    return SECONDS_PER_DAY * 7
    
def fast_week():
    return 86400 * 7
```

Si on mesure la durée d'exécution on peut voir une légère différence:

```python
import time

def chrono(fonction):
    t1 = time.time()
    fonction()
    t2 = time.time()
    return t2 - t1

slow_week_times = []
fast_week_times = []
for i in range(100):    
    slow_week_times.append(chrono(slow_week))
    fast_week_times.append(chrono(fast_week))

print("Durée moyenne d'exécution de slow_week:", sum(slow_week_times) / len(slow_week_times))
print("Durée moyenne d'exécution de fast_week:", sum(fast_week_times) / len(fast_week_times))

```

```bash
$ python chrono.py 
Durée moyenne d'exécution de slow_week: 1.645088195800781e-07
Durée moyenne d'exécution de fast_week: 1.239776611328125e-07
```

On peut expliquer cette différence en regardant le bytecode des 2 fonctions.

```python
>>> dis.dis(slow_week)
  5           0 LOAD_CONST               1 (86400)
              2 STORE_FAST               0 (SECONDS_PER_DAY)

  6           4 LOAD_FAST                0 (SECONDS_PER_DAY)
              6 LOAD_CONST               2 (7)
              8 BINARY_MULTIPLY
             10 RETURN_VALUE
             
>>> dis.dis(fast_week)
  9           0 LOAD_CONST               1 (604800)
              2 RETURN_VALUE

```

`slow_week` exécute 6 instructions contre seulement 2 pour `fast_week` d'où la différence de rapidité.

## Modifier du code dynamiquement

```python
import ctypes

def magic(function):
    # code object
    code_object = function.__code__
    # co_varnames
    co_varnames_ptr = ctypes.cast(id(code_object.co_varnames) + 24, ctypes.POINTER(ctypes.py_object))
    print("co_varnames: ", code_object.co_varnames)
    print("co_varnames from pointer: ", co_varnames_ptr[0])
    # co_consts
    co_consts_ptr = ctypes.cast(id(code_object.co_consts) + 24, ctypes.POINTER(ctypes.py_object))
    print("co_consts: ", code_object.co_consts)
    print("co_consts from pointer: ", co_consts_ptr[1])
    # co_names
    co_names_ptr = ctypes.cast(id(code_object.co_names) + 24, ctypes.POINTER(ctypes.py_object))
    print("co_names: ", code_object.co_names)
    print("co_names from pointer: ", co_names_ptr[0])
    # co_code
    co_code_ptr = ctypes.cast(id(code_object.co_code) + 32, ctypes.POINTER(ctypes.c_char))
    print("co_code: ", code_object.co_code)
    print("co_code from pointer: ", co_code_ptr[0])

def hello(name):
    sentence = "hello" + name
    print(sentence)

magic(hello)
```

## Ressources

{% embed url="<https://github.com/python/cpython>" %}

{% embed url="<https://opensource.com/article/18/4/introduction-python-bytecode>" %}

{% embed url="<https://www.youtube.com/watch?v=cSSpnq362Bk>" fullWidth="true" %}

{% embed url="<https://tech.blog.aknin.name/2010/07/22/pythons-innards-interpreter-stacks/>" %}
