Volání subrutin naprogramovaných v assembleru z jazyka Python: knihovna PeachPy

29. 5. 2025
Doba čtení: 28 minut

Sdílet

Autor: Root.cz s využitím DALL-E
Dnes si představíme knihovnu nazvanou PeachPy, která umožňuje realizovat kooperaci mezi skripty v Pythonu a strojovým kódem zapsaným formou strojových instrukcí. PeachPy je v praxi relativně často používána pro „místní“ optimalizace.

Obsah

1. Volání subrutin naprogramovaných v assembleru z jazyka Python: knihovna PeachPy

2. Instalace knihovny PeachPy

3. Subrutina vracející konstantní hodnotu: původní řešení a nové řešení

4. Výsledná podoba dnešního prvního demonstračního příkladu

5. Výpis strojových instrukcí vygenerovaných nástrojem PeachPy

6. Optimalizace subrutiny: přímá práce s registrem EAX

7. Subrutina akceptující jeden celočíselný parametr

8. Varianta s pojmenovaným parametrem

9. Varianta založená na znalosti ABI

10. Práce se 64bitovými hodnotami

11. První verze 64bitové varianty funkce double

12. Korektní verze 64bitové varianty funkce double

13. Subrutina pro součet dvou celých čísel

14. Realizace subrutiny pro součet dvou celých čísel v knihovně PeachPy

15. Výpis strojového kódu přeložené instrukce

16. Ukázka způsobu překladu instrukce ADD

17. Různé varianty překladu instrukce ADD

18. Obsah navazujícího článku

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Volání subrutin naprogramovaných v assembleru z jazyka Python: knihovna PeachPy

V předchozím článku o tvorbě skriptů v Pythonu, které přímo dokážou volat strojový kód, jsme si ukázali, jakým způsobem je možné využít poněkud nestandardní postup, který je založen na balíčcích mmap (podpora pro paměťové oblasti, které mohou být „spustitelné“) a ctypes (podpora pro volání nativních funkcí resp. subrutin přímo z Pythonu). Výsledné řešení bylo zajímavé zejména proto, že nám umožnilo nahlédnout „pod pokličku“, takže nyní máme alespoň základní povědomí o tom, jak je problém volání subrutin naprogramovaných přímo ve strojovém kódu realizován v dalších knihovnách a nástrojích, které ovšem mnohdy tyto základní koncepty obalí do další vrstvy abstrakce (což může být výhoda, ale taktéž nevýhoda).

Dnes si představíme knihovnu nazvanou PeachPy, která taktéž umožňuje realizovat kooperaci mezi skripty naprogramovanými v Pythonu na jedné straně a strojovým kódem zapsaným formou instrukcí na straně druhé. Tato knihovna ovšem podporuje zápis instrukcí, které subrutinu tvoří, přímo v Pythonu. V tomto případě je každá instrukce (z pohledu vývojáře) realizována voláním nějaké funkce. Toto volání funkcí je interně překládáno do strojového kódu a následně je tento strojový kód „obalen“ takovým způsobem, aby byl volatelný naprosto stejným způsobem, jako jakákoli jiná funkce naprogramovaná přímo v Pythonu. V tomto případě tedy nemusíme vůbec používat assembler a navíc PeachPy podporuje různé procesorové architektury.

2. Instalace knihovny PeachPy

Aby bylo možné spouštět demonstrační příklady uvedené v navazujících kapitolách, je pochopitelně nutné si knihovnu PeachPy nainstalovat. Nepoužívejte ovšem balíček ve verzi zveřejněné na PyPi, protože ten je zastaralý a instalace pro novější verze Pythonu neproběhne korektně. Lepší a stejně (ne)bezpečné je provést instalaci přímo z GitHubu, konkrétně z repositáře https://github.com/Maratys­zcza/PeachPy:

$ pip install --user --upgrade git+https://github.com/Maratyszcza/PeachPy

Tato knihovna nemá žádné závislosti, takže je její instalace snadná:

Collecting git+https://github.com/Maratyszcza/PeachPy
  Cloning https://github.com/Maratyszcza/PeachPy to /tmp/pip-req-build-qhcvwpek
  Running command git clone --filter=blob:none --quiet https://github.com/Maratyszcza/PeachPy /tmp/pip-req-build-qhcvwpek
  Resolved https://github.com/Maratyszcza/PeachPy to commit 349e8f836142b2ed0efeb6bb99b1b715d87202e9
  Preparing metadata (setup.py) ... done
Requirement already satisfied: six in /usr/lib/python3.12/site-packages (from PeachPy==0.2.0) (1.16.0)
Building wheels for collected packages: PeachPy
  Building wheel for PeachPy (setup.py) ... done
  Created wheel for PeachPy: filename=PeachPy-0.2.0-py3-none-any.whl size=300119 sha256=993026a5f1746c3c7994f8a35fdb4a19a48f6502aa991adcc125eadd1bfbc149
  Stored in directory: /tmp/pip-ephem-wheel-cache-cyt7vjqq/wheels/f2/59/12/225dd94305705e4cfee531e8af6597a3b9360c07f0485b1ddd
Successfully built PeachPy
Installing collected packages: PeachPy
Successfully installed PeachPy-0.2.0

Kontrola instalace, resp. kontrola dostupnosti balíčku peachpy:

$ python
 
Python 3.12.9 (main, Feb  4 2025, 00:00:00) [GCC 14.2.1 20240912 (Red Hat 14.2.1-3)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import peachpy
>>> help(peachpy)
 
Help on package peachpy:
 
NAME
    peachpy
 
DESCRIPTION
    # This file is part of PeachPy package and is licensed under the Simplified BSD license.
    #    See license.rst for the full text of the license.
 
PACKAGE CONTENTS
    abi
    arm (package)
    c (package)
    codegen
    common (package)
    encoder
    formats (package)
    function
    literal
    loader
    name
    parse
    stream
    util
    writer
    x86_64 (package)
    ...
    ...
    ...

Pro zajímavost si vypišme aktuálně používané ABI (liší se podle architektury a operačního systému):

# základní konstruktory atd.
from peachpy.x86_64 import abi
 
print(abi.detect())

Na platformě x86–64 a Linuxu by se mělo vypsat:

SystemV x86-64 ABI

3. Subrutina vracející konstantní hodnotu: původní řešení a nové řešení

Nyní se vraťme k původnímu článku, ve kterém jsme si ukázali několik příkladů subrutin napsaných v assembleru. Ta nejjednodušší subrutina pouze vracela konstantní hodnotu a v assembleru vypadala následovně:

[bits 64]
 
        mov eax, 42
        ret

Tato subrutina je na platformě x86–64 přeložena do šesti bajtů, které jsme načetli, obalili kódem, který ze subrutiny udělal Pythonovskou funkci a tuto funkci jsme následně zavolali. Jednalo se o několik relativně nízkoúrovňových operací:

import ctypes
import mmap
 
# konstrukce bufferu
with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    # zapis strojoveho kodu do bufferu
    buffer.write(
        b'\xB8\x2A\x00\x00\x00'
        b'\xC3'
    )
 
    # deklarace typu nativni funkce
    function_type = ctypes.CFUNCTYPE(ctypes.c_int)
 
    # ziskani adresy strojoveho kodu
    function_pointer = ctypes.c_void_p.from_buffer(buffer)
 
    # reference na volatelnou funkci
    function_42 = function_type(ctypes.addressof(function_pointer))
 
    # zavolani funkce a vypis vysledku
    result = function_42()
    print(result)
 
    # pred uzavrenim bufferu je nutne odstranit ukazatele
    del function_pointer

Řešení založené na nástroji PeachPy vypadá odlišně, ale postup je vlastně stále stejný. Nejprve je nutné specifikovat vlastní instrukce subrutiny. Prozatím vytvoříme subrutinu bez parametrů vracející 32bitovou hodnotu. V subrutině vložíme do náhodně vybraného pracovního registru konstantu 42, kterou vrátíme – tato hodnota bude návratovou hodnotou celé subrutiny. Povšimněte si, jak se zapisují jednotlivé instrukce, jakoby se jednalo o volání Pythonovských funkcí:

with Function("Function_42", (), int32_t) as asm_function:
    reg_x = GeneralPurposeRegister32()
    MOV(reg_x, 42)
    RETURN(reg_x)
Poznámka: „obalení“ blokem with zajistí automatické ukončení zápisu instrukcí (což je vyžadováno). Ovšem proměnná asm_function bude platná i po opuštění tohoto bloku.

Následně se provede (de facto) překlad instrukcí do strojového kódu a nový kód subrutiny je obalen tak, aby byl volatelný z Pythonu:

# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
function_42 = asm_function.finalize(abi.detect()).encode().load()

Novou funkci je nyní možné začít používat v Pythonním kódu a zjistit například její typ, zavolat ji (proč bychom ji jinak definovali?) atd.:

# typ hodnoty
print(type(function_42))
print()
 
# zavolání nové funkce
print(function_42())

4. Výsledná podoba dnešního prvního demonstračního příkladu

Výsledná podoba dnešního prvního demonstračního příkladu by měla vypadat následovně:

# datové typy
from peachpy import int32_t
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# konstruktory instrukcí
from peachpy.x86_64 import MOV, RETURN
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_42", (), int32_t) as asm_function:
    reg_x = GeneralPurposeRegister32()
    MOV(reg_x, 42)
    RETURN(reg_x)
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
function_42 = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(function_42))
print()
 
# zavolání nové funkce
print(function_42())

Po spuštění skutečně dostaneme specifikaci typu funkce a následně hodnotu 42:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
42

5. Výpis strojových instrukcí vygenerovaných nástrojem PeachPy

Nástroj PeachPy dokáže subrutinu, která je tvořena strojovými instrukcemi, vypsat i v podobě odpovídající syntaxi assembleru. Z našeho pohledu se tedy provádí činnost s libozvučným označením disassembling. Pro tento účel se používá metoda format_code():

# získání disassemblovaného strojového kódu
disassembled = asm_function.finalize(abi.detect()).format_code()

Podívejme se, jak lze toto volání zařadit do našeho skriptu:

# datové typy
from peachpy import int32_t
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# konstruktory instrukcí
from peachpy.x86_64 import MOV, RETURN
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_42", (), int32_t) as asm_function:
    reg_x = GeneralPurposeRegister32()
    MOV(reg_x, 42)
    RETURN(reg_x)
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())

Výsledek by měl vypadat následovně:

        MOV eax, 42
        MOV eax, eax
        RET

6. Optimalizace subrutiny: přímá práce s registrem EAX

Při pohledu na strojový kód vypsaný předchozím příkladem je patrné, že není v žádném případě optimální. Zcela zbytečně totiž obsahuje instrukci pro přesun hodnoty z registru EAX do téhož registru. Proč tomu tak je? Vyžádali jsme si použití libovolného pracovního registru a PeachPy zvolil právě akumulátor (což je čistě náhodou správně):

reg_x = GeneralPurposeRegister32()

Následující instrukce se převede do načtení konstanty do registru EAX, což je opět správně:

MOV(reg_x, 42)

Ovšem ABI na platformě x86–64 říká, že návratová hodnota funkce má být uložena v registru RAX popř. RAX+RDX. V našem případě tedy postačuje naplnit EAX, což je již splněno, takže explicitní specifikace v instrukci RETURN je zbytečná:

RETURN(reg_x)

Pokud přesně víme, s jakými registry máme pracovat (je to jen EAX), můžeme subrutinu zapsat odlišně:

MOV(eax, 42)
RETURN()

Otestujeme si, zda je chování korektní:

# datové typy
from peachpy import int32_t
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import eax
 
# konstruktory instrukcí
from peachpy.x86_64 import MOV, RETURN
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_42", (), int32_t) as asm_function:
    MOV(eax, 42)
    RETURN()
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
function_42 = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(function_42))
print()
 
# zavolání nové funkce
print(function_42())

Výsledek je opravdu správný:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
42

V dalším skriptu si necháme vypsat vygenerovaný strojový kód, pochopitelně v jeho disassemblované podobě:

# datové typy
from peachpy import int32_t
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import eax
 
# konstruktory instrukcí
from peachpy.x86_64 import MOV, RETURN
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_42", (), int32_t) as asm_function:
    MOV(eax, 42)
    RETURN()
 
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())

Výsledek nyní zcela odpovídá subrutině z předchozího článku:

        MOV eax, 42
        RET

7. Subrutina akceptující jeden celočíselný parametr

V dalších skriptech si ukážeme způsob naprogramování subrutiny, která akceptuje jeden celočíselný parametr, ten vynásobí dvěma a vrátí výsledek této operace. Ukážeme si dvě varianty naprogramování subrutiny. První varianta je „vysokoúrovňová“ a umožní nám abstrahovat od konkrétního ABI (tedy například se v parametry budeme pracovat jako se skutečnými pojmenovanými parametry a nebudeme řešit způsob jejich předávání). Druhá varianta bude více „nízkoúrovňová“ a využijeme v ní znalosti konkrétního ABI (x86–64), tedy faktu, že první argument se předává v registru EDI a výsledek bude uložen v registru EAX.

Připomeňme si, jak tato subrutina vypadala v případě jejího naprogramování v Netwide Assembleru:

[bits 64]
 
        mov eax, edi
        add eax, eax
        ret

Výsledkem překladu assemblerem bude v tomto případě binární soubor o délce pouhých pěti bajtů. O tom se můžeme přesvědčit i z kontrolního výpisu (listingu) vytvořeného assemblerem:

     1                                  [bits 64]
     2
     3 00000000 89F8                            mov eax, edi
     4 00000002 01C0                            add eax, eax
     5 00000004 C3                              ret

8. Varianta s pojmenovaným parametrem

První varianta subrutiny double bude naprogramována spíše „vysokoúrovňovým“ způsobem, ve kterém se využijí pojmenované a otypované parametry. Nejdříve je definováno jméno a typ parametru (zatím bez vazby na subrutinu):

x = Argument(int32_t)

Dále tento parametr použijeme při specifikaci hlavičky subrutiny. Seznam parametrů je uveden jako druhý argument konstruktoru Function. Třetí argument uvádí typ návratové hodnoty:

# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_double", (x, ), int32_t) as asm_function:
    ...
    ...
    ...

Důležité je, že parametr x může být použít v instrukcích, a to následujícím stylem:

    LOAD.ARGUMENT(eax, x)
    ADD(eax, eax)
    RETURN()
Poznámka: samotná subrutina je tedy realizována nízkoúrovňovými instrukcemi, ovšem prozatím vůbec nemusíme znát ABI dané architektury, abychom tuto subrutinu mohli zavolat a předat jí očekávané hodnoty.

Celý skript bude vypadat následovně:

# datové typy
from peachpy import int32_t, Argument
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import eax
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD, MOV, LOAD, RETURN
 
x = Argument(int32_t)
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_double", (x, ), int32_t) as asm_function:
    LOAD.ARGUMENT(eax, x)
    ADD(eax, eax)
    RETURN()
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
double = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(double))
print()
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())
print()
 
# zavolání nové funkce
print(double(42))
 
# přetečení 32bitové hodnoty
print(double(10000000000))

Podívejme se na výsledky:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
        MOV eax, edi
        ADD eax, eax
        RET
 
84
-1474836480

Výsledky jsou korektní (resp. i korektně došlo k přetečení), a dokonce i vygenerovaný kód vypadá velmi rozumně (i když ho je možné zkrátit).

9. Varianta založená na znalosti ABI

Druhá varianta subrutiny vracející svůj argument vynásobený dvěma je založena na znalosti ABI. Konkrétně budeme předpokládat, že argument je předaný v registru EDI a výsledek bude umístěn do registru EAX. To nám umožní zkrácení subrutiny na pouhé tři instrukce:

    MOV(eax, edi)
    ADD(eax, eax)
    RETURN()

Výsledný skript bude vypadat následovně:

# datové typy
from peachpy import int32_t, Argument
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import eax, edi
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD, MOV, RETURN
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_double", (), int32_t) as asm_function:
    MOV(eax, edi)
    ADD(eax, eax)
    RETURN()
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
double = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(double))
print()
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())
print()
 
# zavolání nové funkce
print(double(42))
 
# přetečení 32bitové hodnoty
print(double(10000000000))

Chování pochopitelně otestujeme:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
        MOV eax, edi
        ADD eax, eax
        RET
 
84
-1474836480

Výsledky jsou korektní, a to včetně (očekávaného) přetečení druhého výsledku.

10. Práce se 64bitovými hodnotami

Připomeňme si, že v Pythonu se pracuje s celočíselnými hodnotami, které mohou mít prakticky neomezený rozsah (pochopitelně jsme limitováni kapacitou paměti, ale především výpočetní rychlostí mikroprocesoru). Samozřejmě i takové hodnoty je možné zpracovávat v subrutinách naprogramovaných v assembleru nebo ve strojovém kódu, ale většinou se v praxi setkáme s tím, že jsou zpracovávané či vracené celočíselné hodnoty nějakým způsobem omezené.

Prozatím jsme naše subrutiny, typy jejich parametrů i typy návratových hodnot navrhli takovým způsobem, že výpočty budou korektní pouze pro 32bitové hodnoty (a i tehdy dochází k přetečení, to je však plně očekáváno). Zkusme nyní provést takové úpravy, které by nám umožnily pracovat s hodnotami 64bitovými. Na první pohled by se mohlo zdát, že se jedná o triviální úlohu, ovšem v praxi uvidíme, že je zapotřebí si dávat velmi dobrý pozor na (tenké, ale stále existující) rozhraní mezi nativním kódem a Pythonem. Může se totiž stát, že i „plně 64bitová“ subrutina nebude vracet korektní 64bitové výsledky.

11. První verze 64bitové varianty funkce double

Pokusme se nyní skript uvedený v deváté kapitole upravit takovým způsobem, aby jak parametry volané subrutiny, tak i její návratová hodnota byly reprezentovány 64bitovými celými čísly a nikoli čísly 32bitovými. Jak to provedeme? Namísto 32bitových registrů EAX a EDI použijeme 64bitové registry RAX a RDI a navíc budeme specifikovat, že návratová hodnota je typu int64_t. Všechny změny jsou v následujícím výpisu zvýrazněny podtržením:

# datové typy
from peachpy import int64_t, Argument
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import rax, rdi
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD, MOV, RETURN
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_double", (), int64_t) as asm_function:
    MOV(rax, rdi)
    ADD(rax, rax)
    RETURN()
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
double = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(double))
print()
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())
print()
 
# zavolání nové funkce
print(double(42))
 
# přetečení 32bitové hodnoty se nekoná - nebo snad ano?
print(double(10000000000))

Jak (kontrolní) výpis typu funkce, tak i výpis vygenerovaných instrukcí vypadají korektně:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
        MOV rax, rdi
        ADD rax, rax
        RET

Korektně vypadá i první výsledek, ovšem druhý již ne:

84
2820130816

Ovšem pozor: hodnota 2820130816 značí, že k přetečení nedošlo při vlastních výpočtech (ty jsou plně 64bitové a máme je pod kontrolou) ani při zpracování návratové hodnoty, ale již na vstupu, resp. přesněji na rozhraní mezi Pythonem a nativním kódem. Při „běžném“ 32bitovém přetečení bychom totiž měli dostat výsledky:

84
-1474836480

Konverzi hodnoty tedy provedl interpret Pythonu před voláním subrutiny.

12. Korektní verze 64bitové varianty funkce double

Abychom předchozí příklad opravili, je nutné explicitně specifikovat, že argumentem naší subrutiny je 64bitová hodnota. Nejprve takový argument vytvoříme:

x = Argument(int64_t)

Změní se jen deklarace hlavičky subrutiny, protože nyní explicitně napíšeme, že argumentem bude x, i když interně budeme stále přímo pracovat s 64bitovým ABI:

# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_double", (x, ), int64_t) as asm_function:
    ...
    ...
    ...

Samotné tělo subrutiny (strojové instrukce) se nijak nezmění:

    MOV(rax, rdi)
    ADD(rax, rax)
    RETURN()

Opravený skript by měl vypadat následovně:

# datové typy
from peachpy import int64_t, Argument
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import rax, rdi
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD, MOV, RETURN
 
x = Argument(int64_t)
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_double", (x, ), int64_t) as asm_function:
    MOV(rax, rdi)
    ADD(rax, rax)
    RETURN()
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
double = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(double))
print()
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())
print()
 
# zavolání nové funkce
print(double(42))
 
# přetečení 32bitové hodnoty se nekoná
print(double(10000000000))

Zkontrolujeme, zda skutečně dostaneme korektní výsledek:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
        MOV rax, rdi
        ADD rax, rax
        RET
 
84
20000000000

Ze zobrazených výsledků je patrné, že nyní se se 64bitovými argumenty i návratovými hodnotami pracuje korektně.

13. Subrutina pro součet dvou celých čísel

Minule jsme si ukázali jednoduchou subrutinu, která prováděla součet dvou celých čísel. Ta jsou do subrutiny předána v registrech EDI a ESI popř. v případě 64bitových hodnot v registrech RDI a RSI:

[bits 64]
 
        mov eax, edi
        add eax, esi
        ret

Připomeňme si, jak se přeložená subrutina načetla a následně volala přes knihovnu ctypes:

import ctypes
import mmap
 
# konstrukce bufferu
with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    with open("add.bin", "rb") as fin:
        buffer.write(fin.read())
 
    # deklarace typu nativni funkce
    function_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)
 
    # ziskani adresy strojoveho kodu
    function_pointer = ctypes.c_void_p.from_buffer(buffer)
 
    # reference na volatelnou funkci
    add = function_type(ctypes.addressof(function_pointer))
 
    # zavolani funkce a vypis vysledku
    result = add(1, 2)
    print(result)
 
    # pred uzavrenim bufferu je nutne odstranit ukazatele
    del function_pointer

Nyní provedeme podobnou operaci, ovšem plně založenou na knihovně PeachPy.

14. Realizace subrutiny pro součet dvou celých čísel v knihovně PeachPy

Celý postup si popíšeme po jednotlivých krocích. Nejdříve specifikujeme argumenty subrutiny, tj. jejich jména a typy:

x = Argument(int64_t)
y = Argument(int64_t)

Následně přes PeachPy určíme, že subrutina bude volána s argumenty x a y (definovanými výše) a že její návratová hodnota bude typu int64_t:

# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_add", (x, y), int64_t) as asm_function:
    ...
    ...
    ...

V samotné subrutině můžeme přímo využít znalostí ABI, tj. toho, že hodnoty pro výpočet získáme v registrech RDI a RSI a návratová hodnota bude v registru RAX:

    MOV(rax, rdi)
    ADD(rax, rsi)
    RETURN()

Celý skript vypadá následovně:

# datové typy
from peachpy import int64_t, Argument
 
# základní konstruktory atd.
from peachpy.x86_64 import Function, GeneralPurposeRegister32, abi
 
# registry
from peachpy.x86_64 import rax, rdi, rsi
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD, MOV, RETURN
 
x = Argument(int64_t)
y = Argument(int64_t)
 
# vytvoření nové subrutiny ve strojovém kódu
with Function("Function_add", (x, y), int64_t) as asm_function:
    MOV(rax, rdi)
    ADD(rax, rsi)
    RETURN()
 
# obalení strojového kódu tak, aby se dal volat z interpretru Pythonu
add = asm_function.finalize(abi.detect()).encode().load()
 
# typ hodnoty
print(type(add))
print()
 
# výpis disassemblovaného strojového kódu
print(asm_function.finalize(abi.detect()).format_code())
print()
 
# zavolání nové funkce
print(add(1, 2))
 
print(add(10000000000, 20000000000))

Výsledky ukazují jak přeloženou subrutinu (zcela korektní), tak i výsledky výpočtů, ze kterých je patrné, že se korektně pracuje se 64bitovými hodnotami:

<class 'peachpy.x86_64.function.ExecutableFuntion'>
 
        MOV rax, rdi
        ADD rax, rsi
        RET
 
3
30000000000

15. Výpis strojového kódu přeložené instrukce

Mezi další užitečné vlastnosti knihovny PeachPy patří schopnost zobrazit strojový kód přeložené instrukce, popř. instrukci zobrazit s využitím různých formátů zápisu. Některé assemblery totiž používají AT&T syntaxi (výchozí nastavení GNU Assembleru), jiné spíše původní syntaxi společnosti Intel (Netwide Assembler a původní DOSovské assemblery). Způsoby zápisu operandů se dosti podstatným způsobem liší, takže je dobré, že nám PeachPy dává na výběr. Jak se vlastně postupuje?

Nejprve zkonstruujeme objekt, který instrukci představuje, například:

ADD(rax, rdi)

Tento konstruktor vytvoří objekt, který v assembleru odpovídá instrukci:

ADD rax, rdi

Následně můžeme získat bytové pole (byte array) se strojovým kódem přeložené instrukce, pochopitelně včetně jejích operandů:

instruction = ADD(rax, rdi)
bytové_pole = instruction.encode()

Takové bytové pole si můžeme zobrazit například formou hexadecimálního výpisu (každý bajt se zobrazí jako dvě hexadecimální cifry), což je již několik desetiletí nepsaný standard:

print(" ".join(format(byte, "02x") for byte in instruction.encode()))

A dále je možné si nechat zobrazit zápis instrukce tak, jak to odpovídá vybranému assembleru. Na výběr je několik formátů, z nichž využijeme „gas“ (což odpovídá AT&amd;T syntaxi), „nasm“ (Netwide Assembler) nebo „peachpy“ (modernější způsoby zápisu). Specifický je i formát používaný assemblerem jazyka Go (k tomu se ještě vrátíme příště):

print(instruction.format(assembly_format="nasm"))
print(instruction.format(assembly_format="peachpy"))
print(instruction.format(assembly_format="gas"))
print(instruction.format(assembly_format="go"))

16. Ukázka způsobu překladu instrukce ADD

Vyzkoušejme si nyní zobrazit instrukci ADD rax, rdi různými způsoby: jako strojový kód a taktéž ve formátu odpovídajícím různým assemblerům:

# registry
from peachpy.x86_64 import rax, rdi
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD
 
 
def print_instruction(instruction):
    print(" ".join(format(byte, "02x") for byte in instruction.encode()))
    print(instruction.format(assembly_format="nasm"))
    print(instruction.format(assembly_format="peachpy"))
    print(instruction.format(assembly_format="gas"))
    print(instruction.format(assembly_format="go"))
 
 
print_instruction(ADD(rax, rdi))

Výsledky (s doplněnými poznámkami) by měly vypadat následovně:

48 01 f8               <- strojový kód
ADD rax, rdi           <- formát Netwide assembleru
ADD rax, rdi           <- formát PeachPy
addq %rdi, %rax        <- výchozí formát GNU assembleru
ADDQ DI, AX            <- formát assembleru v jazyku Go

17. Různé varianty překladu instrukce ADD

Výše uvedená funkcionalita je více než vhodná pro studijní účely, protože nám umožní například zjistit, jakým způsobem jsou instrukce zakódovány, popř. jak jsou zakódovány jejich operandy. Pokusme se vypsat strojový kód instrukce ADD, ovšem nyní pro všechny základní kombinace obou operandů této instrukce (bez speciálního adresování):

# registry
from peachpy.x86_64 import rax, rbx, rcx, rdx, rsi, rdi, rbp
from peachpy.x86_64 import r8, r9, r10, r11, r12, r13, r14, r15
 
# konstruktory instrukcí
from peachpy.x86_64 import ADD
 
 
def print_instruction(instruction):
    print(" ".join(format(byte, "02x") for byte in instruction.encode()), end="\t")
    print(instruction.format(assembly_format="peachpy"), end="\t")
    print(instruction.format(assembly_format="gas"))
 
 
for r1 in  rax, rbx, rcx, rdx, rsi, rdi, rbp, r8, r9, r10, r11, r12, r13, r14, r15:
    for r2 in  rax, rbx, rcx, rdx, rsi, rdi, rbp, r8, r9, r10, r11, r12, r13, r14, r15:
        print_instruction(ADD(r1, r2))

Výsledky by měly vypadat takto (výpis je zkrácen):

hacking_tip

48 01 c0        ADD rax, rax    addq %rax, %rax
48 01 d8        ADD rax, rbx    addq %rbx, %rax
48 01 c8        ADD rax, rcx    addq %rcx, %rax
48 01 d0        ADD rax, rdx    addq %rdx, %rax
48 01 f0        ADD rax, rsi    addq %rsi, %rax
48 01 f8        ADD rax, rdi    addq %rdi, %rax
48 01 e8        ADD rax, rbp    addq %rbp, %rax
4c 01 c0        ADD rax, r8     addq %r8, %rax
4c 01 c8        ADD rax, r9     addq %r9, %rax
4c 01 d0        ADD rax, r10    addq %r10, %rax
4c 01 d8        ADD rax, r11    addq %r11, %rax
4c 01 e0        ADD rax, r12    addq %r12, %rax
4c 01 e8        ADD rax, r13    addq %r13, %rax
4c 01 f0        ADD rax, r14    addq %r14, %rax
4c 01 f8        ADD rax, r15    addq %r15, %rax
    ...
    ...
    ...
49 01 c7        ADD r15, rax    addq %rax, %r15
49 01 df        ADD r15, rbx    addq %rbx, %r15
49 01 cf        ADD r15, rcx    addq %rcx, %r15
49 01 d7        ADD r15, rdx    addq %rdx, %r15
49 01 f7        ADD r15, rsi    addq %rsi, %r15
49 01 ff        ADD r15, rdi    addq %rdi, %r15
49 01 ef        ADD r15, rbp    addq %rbp, %r15
4d 01 c7        ADD r15, r8     addq %r8, %r15
4d 01 cf        ADD r15, r9     addq %r9, %r15
4d 01 d7        ADD r15, r10    addq %r10, %r15
4d 01 df        ADD r15, r11    addq %r11, %r15
4d 01 e7        ADD r15, r12    addq %r12, %r15
4d 01 ef        ADD r15, r13    addq %r13, %r15
4d 01 f7        ADD r15, r14    addq %r14, %r15
4d 01 ff        ADD r15, r15    addq %r15, %r15

18. Obsah navazujícího článku

Nástroj PeachPy ve skutečnosti nabízí i mnohé další možnosti, které jsou využívány i v praxi (implementace šifer, vektorizace částí programového kódu atd.). Na některé z těchto možností ze zaměříme v navazujícím článku. Ukážeme si mj. i kooperaci mezi PeachPy a jazykem Go resp. přesněji assemblerem implementovaným v ekosystému jazyka Go, který mnoha programátorům připadá, jako by přišel z jiného světa (konkrétně ze světa Plan 9 :-).

19. Repositář s demonstračními příklady

Všechny demonstrační příklady popsané v minulém článku i v předchozích kapitolách lze nalézt v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady:

# Příklad Stručný popis Adresa příkladu
1 42.asm subrutina bez parametrů vracející konstantu naprogramovaná v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/42.asm
2 42.bin instrukce subrutiny přeložené do strojového kódu bez dalších metainformací https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/42.bin
3 42.lst kontrolní výpis (listing) vytvořený assemblerem https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/42.lst
4 asm1.py spuštění strojového kódu z paměťového bufferu (nekorektní verze) https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm1.py
5 asm2.py spuštění strojového kódu z paměťového bufferu (korektní verze) https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm2.py
6 asm3.py spuštění strojového kódu načteného z (binárního) souboru https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm3.py
     
7 double.asm subrutina s jedním parametrem vracející 32bitovou hodnotu naprogramovaná v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/double.asm
8 double.bin instrukce subrutiny přeložené do strojového kódu bez dalších metainformací https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/double.bin
9 double.lst kontrolní výpis (listing) vytvořený assemblerem https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/double.lst
10 asm4.py spuštění strojového kódu s předáním parametru, varianta bez přetečení 32bitové hodnoty https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm4.py
11 asm5.py spuštění strojového kódu s předáním parametru, varianta s přetečením 32bitové hodnoty https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm5.py
     
12 double64.asm subrutina s jedním parametrem vracející 64bitovou hodnotu naprogramovaná v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/double64.asm
13 double64.bin instrukce subrutiny přeložené do strojového kódu bez dalších metainformací https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/double64.bin
14 double64.lst kontrolní výpis (listing) vytvořený assemblerem https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/double64.lst
15 asm6.py spuštění strojového kódu s předáním parametru bez přetečení 64bitové hodnoty https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm6.py
     
16 add.asm subrutina se dvěma parametry vracející jejich součet naprogramovaná v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/add.asm
17 add.bin instrukce subrutiny přeložené do strojového kódu bez dalších metainformací https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/add.bin
18 add.lst kontrolní výpis (listing) vytvořený assemblerem https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/add.lst
19 asm7.py spuštění strojového kódu s předáním obou parametrů https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm7.py
     
20 asm8.py načtení strojového kódu přímo z kontrolního výpisu (listingu) https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/asm8.py
     
21 peachpy_abi.py zjištění, které ABI se používá na počítači, na němž je skript spuštěn https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_abi.py
22 peachpy42_1.py první verze subrutiny vracející celočíselnou konstantu https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy42_1.py
23 peachpy42_2.py výpis strojových instrukcí vygenerovaných nástrojem PeachPy https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy42_2.py
24 peachpy42_3.py kratší verze subrutiny vracející celočíselnou konstantu https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy42_3.py
25 peachpy42_4.py výpis strojových instrukcí vygenerovaných nástrojem PeachPy https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy42_4.py
26 peachpy_double32_1.py subrutina akceptující jeden celočíselný parametr: varianta s pojmenovaným parametrem https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_double32_1.py
27 peachpy_double32_2.py subrutina akceptující jeden celočíselný parametr: varianta založená na znalosti ABI https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_double32_2.py
28 peachpy_double64_1.py přepis předchozího příkladu tak, aby pracoval se 64bitovým parametrem a vracel 64bitovou hodnotu (nekorektní řešení) https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_double64_1.py
29 peachpy_double64_2.py přepis předchozího příkladu tak, aby pracoval se 64bitovým parametrem a vracel 64bitovou hodnotu (korektní řešení) https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_double64_2.py
30 peachpy_add1.py subrutina provádějící součet dvou celočíselných hodnot https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_add1.py
31 peachpy_add2.py subrutina provádějící součet dvou celočíselných hodnot https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_add2.py
32 peachpy_add3.py subrutina provádějící součet dvou celočíselných hodnot https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_add3.py
     
33 peachpy_instruction1.py výpis strojového kódu instrukcí, jednodušší varianta https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_instruction1.py
34 peachpy_instruction2.py výpis strojového kódu instrukcí, složitější varianta https://github.com/tisnik/most-popular-python-libs/blob/master/assembly/pe­achpy_instruction2.py

20. Odkazy na Internetu

  1. Volání subrutin naprogramovaných v assembleru z jazyka Python
    https://www.root.cz/clanky/volani-subrutin-naprogramovanych-v-assembleru-z-jazyka-python/
  2. ctypes – A foreign function library for Python
    https://docs.python.org/3/li­brary/ctypes.html
  3. CFFI documentation
    https://cffi.readthedocs.i­o/en/latest/
  4. cffi 1.15.1 na PyPi
    https://pypi.org/project/cffi/
  5. Python Bindings: Calling C or C++ From Python
    https://realpython.com/python-bindings-overview/
  6. Interfacing with C/C++ Libraries
    https://docs.python-guide.org/scenarios/clibs/
  7. Cython, pybind11, cffi – which tool should you choose?
    http://blog.behnel.de/posts/cython-pybind11-cffi-which-tool-to-choose.html
  8. Python FFI with ctypes and cffi
    https://eli.thegreenplace­.net/2013/03/09/python-ffi-with-ctypes-and-cffi
  9. Using standard library headers with CFFI
    https://stackoverflow.com/qu­estions/57481873/using-standard-library-headers-with-cffi
  10. C Arrays
    https://www.programiz.com/c-programming/c-arrays
  11. C Arrays
    https://www.w3schools.com/c/c_a­rrays.php
  12. Array of Structures in C
    https://overiq.com/c-programming-101/array-of-structures-in-c/#google_vignette
  13. Keystone Engine na GitHubu
    https://github.com/keystone-engine/keystone
  14. Keystone: The Ultimate Assembler
    https://www.keystone-engine.org/
  15. The Ultimate Disassembler
    http://www.capstone-engine.org/
  16. Tutorial for Keystone
    https://www.keystone-engine.org/docs/tutorial.html
  17. Rozhraní pro Capstone na PyPi
    https://pypi.org/project/capstone/
  18. Rozhraní pro Keystone na PyPi
    https://pypi.org/project/keystone-engine/
  19. KEYSTONE: Next Generation Assembler Framework
    https://www.keystone-engine.org/docs/BHUSA2016-keystone.pdf
  20. AT&T Syntax versus Intel Syntax
    http://web.mit.edu/rhel-doc/3/rhel-as-en-3/i386-syntax.html
  21. AT&T assembly syntax and IA-32 instructions
    https://gist.github.com/mishu­rov/6bcf04df329973c15044
  22. ARM GCC Inline Assembler Cookbook
    http://www.ethernut.de/en/do­cuments/arm-inline-asm.html
  23. Extended Asm – Assembler Instructions with C Expression Operands
    https://gcc.gnu.org/online­docs/gcc/Extended-Asm.html
  24. ARM inline asm secrets
    http://hardwarebug.org/2010/07/06/arm-inline-asm-secrets/
  25. How to Use Inline Assembly Language in C Code
    https://gcc.gnu.org/online­docs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C
  26. GCC-Inline-Assembly-HOWTO
    http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
  27. A Brief Tutorial on GCC inline asm (x86 biased)
    http://www.osdever.net/tu­torials/view/a-brief-tutorial-on-gcc-inline-asm
  28. GCC Inline ASM
    http://locklessinc.com/ar­ticles/gcc_asm/
  29. GNU Assembler Examples
    http://cs.lmu.edu/~ray/no­tes/gasexamples/
  30. X86 Assembly/Arithmetic
    https://en.wikibooks.org/wi­ki/X86_Assembly/Arithmetic
  31. Art of Assembly – Arithmetic Instructions
    http://oopweb.com/Assembly/Do­cuments/ArtOfAssembly/Volu­me/Chapter6/CH06–2.html
  32. The GNU Assembler Tutorial
    http://tigcc.ticalc.org/doc/gnu­asm.html
  33. The GNU Assembler – macros
    http://tigcc.ticalc.org/doc/gnu­asm.html#SEC109
  34. ARM subroutines & program stack
    http://www.toves.org/books/armsub/
  35. Generating Mixed Source and Assembly List using GCC
    http://www.systutorials.com/240/ge­nerate-a-mixed-source-and-assembly-listing-using-gcc/
  36. Calling subroutines
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.kui0100a/armasm_cih­cfigg.htm
  37. Linux assemblers: A comparison of GAS and NASM
    http://www.ibm.com/develo­perworks/library/l-gas-nasm/index.html
  38. Programovani v assembleru na OS Linux
    http://www.cs.vsb.cz/gryga­rek/asm/asmlinux.html
  39. Is it worthwhile to learn x86 assembly language today?
    https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1
  40. Why Learn Assembly Language?
    http://www.codeproject.com/Ar­ticles/89460/Why-Learn-Assembly-Language
  41. Is Assembly still relevant?
    http://programmers.stackex­change.com/questions/95836/is-assembly-still-relevant
  42. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/on­lamp/2004/05/06/writegreat­code.html
  43. Assembly language today
    http://beust.com/weblog/2004/06/23/as­sembly-language-today/
  44. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubri­ky/assembler/vyznam-assembleru-dnes-155960cz
  45. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/li­nux/prog/asm.html
  46. Online x86 / x64 Assembler and Disassembler
    https://defuse.ca/online-x86-assembler.htm#disassembly
  47. Executing assembly code in memory using python modules ctypes and mmap
    https://stackoverflow.com/qu­estions/58851655/executing-assembly-code-in-memory-using-python-modules-ctypes-and-mmap
  48. mmap – Memory-mapped file support
    https://docs.python.org/3/li­brary/mmap.html
  49. ctypes – A foreign function library for Python
    https://docs.python.org/3/li­brary/ctypes.html#module-ctypes
  50. Calling Conventions
    https://wiki.osdev.org/Ca­lling_Conventions
  51. Linux x64 Calling Convention: Stack Frame
    https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame
  52. Netwide assembler
    https://www.nasm.us/
  53. The Netwide Assembler: NASM: output formats
    https://ece-research.unm.edu/jimp/310/nas­m/nasmdoc6.html
  54. PeachPy 0.0.1
    https://pypi.org/project/PeachPy/
  55. PeachPy
    https://github.com/Maratys­zcza/PeachPy
  56. Historie s commity do knihovny PeachPy
    https://github.com/Maratys­zcza/PeachPy/commits/master/
Neutrální ikona do widgetu na odběr článků ze seriálů

Zajímá vás toto téma? Chcete se o něm dozvědět víc?

Objednejte si upozornění na nově vydané články do vašeho mailu. Žádný článek vám tak neuteče.


Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.