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

20. 5. 2025
Doba čtení: 24 minut

Sdílet

Autor: Root.cz s využitím DALL-E
Už jsme se setkali s propojením Pythonu s nativními knihovnami naprogramovanými (například) v jazyku C. Dnes si ukážeme jeden z alternativních způsobů, kterým lze z Pythonu volat podprogramy (subrutiny, funkce) v assembleru.

Obsah

1. Volání nativního kódu z jazyka Python

2. Nejsme omezeni pouze na jazyk C

3. Volání subrutin naprogramovaných v assembleru z Pythonu

4. Vlastní implementace dynamicky načítaných subrutin naprogramovaných v assembleru

5. Subrutina vracející celé číslo

6. Překlad subrutiny Netwide Assemblerem

7. První varianta: přímý zápis strojového kódu do vyhrazené paměťové oblasti

8. Nastavení korektních příznaků paměťové oblasti

9. Načtení strojového kódu z binárního souboru

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

11. Volání subrutiny s předáním parametru

12. Rozdíly mezi datovými typy Pythonu a assembleru

13. Podpora 64bitových celočíselných hodnot

14. Subrutina akceptující dva celočíselné parametry

15. Volání subrutiny s předáním parametrů

16. Způsob předání většího množství parametrů: ABI

17. Trik na závěr: strojový kód zkombinovaný se skriptem v Pythonu v jediném souboru

18. Realizace konverze kontrolního výpisu do strojového kódu

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

20. Odkazy na Internetu

1. Volání nativního kódu z jazyka Python

Na stránkách Roota, konkrétně v článcích [1] [2] a [3], jsme se již poměrně podrobně zabývali problematikou propojení vysokoúrovňového programovacího jazyka Python s nativními knihovnami naprogramovanými například v jazyku C (ovšem může se jednat i o Rust, C++ a pokud si dáme pozor, tak i o jazyk Go).

Poměrně jednoduchá je situace, kdy se z Pythonu pouze volají nativní funkce, kterým se předávají již naalokované objekty (například řetězce). Pro tento účel jsme v uvedených článcích využili především knihovnu cffi (viz též Overview). Taktéž jsme si ovšem v článku Propojení Pythonu s nativními knihovnami s využitím balíčku ctypes popsali a ukázali i základy „konkurenční“ knihovny ctypes.

Základní postup přitom zůstává stále stejný. Zdrojový kód napsaný v nějakém překládaném (kompilovaném) jazyku C (C++, Rustu, Go, …) je nejprve přeložen do nativní dynamické knihovny, tedy konkrétně do souboru s koncovkou „.so“ na Linuxu a „.dll“ v systému Microsoft Windows. Aplikace psaná v Pythonu tuto dynamickou knihovnu načte a přes balíček cffi nebo ctypes umožní volání funkcí naprogramovaných v C/C++ atd. Zpočátku se může zdát, že se jedná o bezproblémové řešení, ovšem v praxi musíme vyřešit především dva problémy: vlastnictví předávaných hodnot (tedy která strana alokuje paměť a která ji může dealokovat – navíc chceme zamezit dvojí dealokaci) a taktéž korektní předání hodnot různých typů. První problém musí vyřešit programátor, ovšem druhý problém může – i když pouze částečně – řešit i balíček realizující volání nativních funkcí z Pythonu. A právě zde nalezneme největší rozdíly mezi ctypes, cffi i dalšími balíčky určenými pro stejný účel. V tomto ohledu jsou možnosti ctypes dosti omezené, ovšem jedná se o standardní balíček, který navíc může pro některé účely plně vyhovovat.

Poznámka: nejvíce bezbolestné propojení zajistíme tak, že se o alokace a (automatické) dealokace paměti bude starat interpret Pythonu. Pokud bude jak pythonní kód, tak i kód psaný v jazyce C, alokovat paměť svými prostředky bude výsledek poměrně špatně odladitelný (na druhou stranu například knihovna Numpy ukazuje, že i tento problém lze uspokojivě vyřešit; zde se však pracuje jen s minimem „sdílených“ struktur).

2. Nejsme omezeni pouze na jazyk C

Názvy ctypes a vlastně i cffi evokují programovací jazyk C. Ovšem jak již bylo napsáno výše, je možné nativní část vytvořit (naprogramovat) i v jiných jazycích; pouze se v takovém případě dříve či později můžeme setkat s obtížemi, které v jazyku C nenastávají (práce s objekty, správce paměti v nativní knihovně atd.). Propojení programovacího jazyka Python s jazykem C (popř. s Rustem nebo jazykem Zig) přináší zajímavé možnosti. V první řadě to umožňuje relativně snadné volání funkcí ze systémových knihoven, ale i dalších funkcí dostupných formou dynamicky sdílených knihoven. Díky tomu lze spojit snadnost tvorby aplikací v Pythonu (vysokoúrovňový jazyk s relativně velkou mírou abstrakce) s optimalizovaným nativním kódem. Dobrým příkladem takového propojení je již výše zmíněný projekt Numpy, v němž se výpočetně náročné části realizují nativními funkcemi. A příkladem propojení Pythonu s Rustem může být projekt Polars, se kterým jsme se na stránkách Roota taktéž již setkali v článcích Knihovna Polars: výkonnější alternativa ke knihovně Pandas a Knihovna Polars: výkonnější alternativa ke knihovně Pandas (datové rámce).

3. Volání subrutin naprogramovaných v assembleru z Pythonu

Prozatím jsme se zmiňovali o volání funkcí naprogramovaných ve vyšších programovacích jazycích, ovšem ve skutečnosti je pochopitelně možné přímo z Pythonu volat i podprogramy (subrutiny), které jsou naprogramovány v assembleru. V tomto případě je dokonce možné využít několik postupů. Pravděpodobně se nejčastěji setkáme s překladem subrutin do souborů s takzvanými objekty (což ovšem nijak nesouvisí s objektově orientovaným programováním). Tyto soubory mají koncovku .o popř. se můžeme setkat i s koncovkou .obj. Následně se linkerem sestaví dynamická knihovna s těmito objekty; přičemž koncovka souborů s dynamickými knihovnami je na unixových systémech .so a na Windows pak .dll. Na straně Pythonu se takové knihovny načtou již výše zmíněnými knihovnami cffi nebo ctypes, což nám ve výsledku umožní volat assemblerovské subrutiny.

Poznámka: sice se snažím používat termín subrutina (tedy podprogram), ale pro jednoduchost můžeme subrutiny považovat za běžné funkce. Rozdíly bývají spíše sémantické.

4. Vlastní implementace dynamicky načítaných subrutin naprogramovaných v assembleru

Výše zmíněný postup je sice zcela standardní a korektní, ovšem v případě, že je zapotřebí volat jen několik subrutin (zjištění CPUID či realizace vybraných SIMD intrinsic atd.), je možné postupovat i odlišným způsobem a implementovat si tak vlastní dynamicky načítané subrutiny ve strojovém kódu. Předností tohoto alternativního postupu je mj. i to, že se naučíme používat některé nízkoúrovňovější operace přímo v jazyce Python, což jsou znalosti, které se mohou hodit i v jiných případech.

Postup je následující:

  1. Alokace paměťového bufferu pomocí mmap. Na rozdíl od běžných paměťových bufferů je zde možné nastavit příznak PROT_EXEC, takže strojový kód uložený do bufferu bude spustitelný
  2. Naplnění bufferu strojovým kódem získaným překladem původního kódu assemblerem
  3. Deklarace „hlavičky“ nativní funkce (subrutiny) přes knihovnu ctypes. Zde se určí počet a typ parametrů funkce i typ návratové hodnoty (včetně void).
  4. Získání adresy funkce/subrutiny, tj. vlastně adresy paměťového bufferu (subrutina je uložena na jeho začátku)
  5. Získání reference na funkci tak, aby mohla být volána z Pythonu
  6. …nyní je možné naši subrutinu volat naprosto stejným způsobem, jako jinou Pythonovskou funkci…
  7. Uvolnění ukazatele na funkci – nelze se spolehnout na automatickou správu paměti!

Implementací předchozích kroků se tedy naučíme některé vlastnosti balíčků mmap a ctypes.

5. Subrutina vracející celé číslo

Praktickou část dnešního článku začneme triviální subrutinou. Tato subrutina nezpracovává žádné parametry a vrací konstantní hodnotu 42. Přitom záleží jen na nás, zda tuto hodnotu budeme interpretovat jako osmibitové, šestnáctibitové či 32bitové číslo. ABI Linuxu pro platformu x86–64 předepisuje, že návratová hodnota subrutiny je uložena buď v registru RAX nebo ve dvojici registrů RDX:RAX. My si prozatím vystačíme s 32bitovou dolní polovinou registru RAX, která se v assembleru jmenuje EAX.

V syntaxi NASMu bude zdrojový kód se subrutinou vypadat takto:

[bits 64]
 
        mov eax, 42
        ret
Poznámka: nemusíme ani použít návěští ani definovat speciální symboly. A dokonce ani není nutné určit, do které sekce (segmentu) se má překládaný kód ukládat.

6. Překlad subrutiny Netwide Assemblerem

Překlad výše uvedeného kódu provedeme příkazem:

nasm -f bin -o 42.bin -l 42.lst 42.asm

Výsledkem překladu bude v první řadě binární soubor pojmenovaný 42.bin, jehož délka musí být přesně šest bajtů a který obsahuje pouze instrukce přeložené do strojového kódu – žádná další metadata. A soubor se jménem 42.lst obsahuje výpis (listing) provedený assemblerem, z něhož si můžeme ověřit způsob překladu:

     1                                  [bits 64]
     2
     3 00000000 B82A000000                      mov eax, 42
     4 00000005 C3                              ret

První sloupec ve výpisu obsahuje číslo řádku (to ovšem nemusí nutně odpovídat zdrojovému kódu, pokud se expandují makra), druhý sloupec obsahuje adresu resp. offset a třetí sloupec pak hodnoty bajtů, do kterých je instrukce zakódována (tedy vlastní strojový kód).

7. První varianta: přímý zápis strojového kódu do vyhrazené paměťové oblasti

Nyní známe přesnou sekvenci šesti bajtů se strojovým kódem dvojice instrukcí, které je nutné zavolat. Tuto sekvenci bajtů lze přímo zapsat do paměťového bufferu:

with mmap.mmap(-1, mmap.PAGESIZE) as buffer:
    # zapis strojoveho kodu do bufferu
    buffer.write(
        b'\xB8\x2A\x00\x00\x00'
        b'\xC3'
    )

Dále budeme postupovat tak, jak bylo naznačeno ve čtvrté kapitole:

  1. Deklarace typů parametrů a typu návratové hodnoty nativní funkce
  2. Získání adresy se strojovým kódem (oněch šesti bajtů)
  3. Převod na referenci na běžnou Pythonovskou funkci (z pohledu programátora i interpretru)
  4. Zavolání této funkce a výpis návratové hodnoty

Celý skript bude vypadat takto:

import ctypes
import mmap
 
# konstrukce bufferu
with mmap.mmap(-1, mmap.PAGESIZE) 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

Ovšem v případě, že se pokusíte o spuštění tohoto demonstračního příkladu, dojde k pádu procesu a popř. (v závislosti na nastavení operačního systému) k segfaultu:

Segmentation fault (core dumped)

Jak tento problém opravit si řekneme v navazující kapitole.

8. Nastavení korektních příznaků paměťové oblasti

Aby bylo možné naši subrutinu, tj. sekvenci instrukcí ve strojovém kódu, skutečně uložit do bufferu a následně spustit, musíme korektně nastavit příznakové bity paměťové oblasti, které určují, jaké operace s ní lze provádět. V našem konkrétním případě potřebujeme podporu pro zápis a spuštění, což jsou příznaky mmap.PROT_WRITE a mmap.PROT_EXEC. Povšimněte si, že není vyžadována podpora pro operaci čtení – a skutečně, ve skriptu není žádná část kódu, která by strojový kód četla, protože se přímo spouští:

with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    ...
    ...
    ...

Celý skript bude vypadat následovně:

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

Můžeme si ho spustit:

42

9. Načtení strojového kódu z binárního souboru

Vzhledem k tomu, že jsme v rámci šesté kapitoly provedli překlad subrutiny do binárního souboru, který obsahuje pouze strojový kód instrukcí a žádná další metadata, nic nám nebrání v tom obsah tohoto souboru načíst a přímo zapsat do paměťového bufferu:

with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    with open("42.bin", "rb") as fin:
        buffer.write(fin.read())
    ...
    ...
    ...
Poznámka: dokonce je možné soubor přímo zrcadlit do paměti pomocí mmap, ovšem u takto krátkých souborů to není nutné.

Následně je již možné s bufferem pracovat tak, jak jsme si ukázali v předchozích kapitolách, tj. získat ukazatel na funkci/subrutinu, převést ji na Pythonovskou funkci a tu následně zavolat:

import ctypes
import mmap
 
# konstrukce bufferu
with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    with open("42.bin", "rb") as fin:
        buffer.write(fin.read())
 
    # 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

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

Předchozí subrutina naprogramovaná v assembleru pouze vracela celočíselnou konstantu. Reálné subrutiny ovšem většinou akceptují i nějaké parametry. Pokud se jedná o celočíselné parametry a je jich maximálně šest, jsou v případě Linuxu a platformy x86–64 takové parametry předávány v registrech (viz též šestnáctou kapitolu). Prozatím ovšem potřebujeme vědět pouze to, že první parametr je předaný v registru RDI a pokud se jedná o parametr 32bitový, pak  registru EDI. Snadno tedy vytvoříme subrutinu, která vynásobí předaný parametr dvěma a vrátí výsledek. Násobení dvěma lze provést pouhým součtem a výsledek se s tomto případě vrátí v registru EAX, podobně jako u první subrutiny:

[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

11. Volání subrutiny s předáním parametru

Nyní musíme ve skriptu, který výše zmíněnou subrutinu bude volat, provést dvojici změn. V první řadě se změní deklarace typu parametrů a návratové hodnoty nativní funkce. Musíme totiž specifikovat nejenom typ návratové hodnoty, ale i typ (a počet) jejích parametrů. Rozdíl je podtržen:

# deklarace typu nativni funkce
function_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)

Nyní tedy bude typ funkce znít: funkce s parametrem typu int vracející taktéž hodnotu typu int.

A samozřejmě tento parametr musíme předat při volání funkce:

# zavolani funkce a vypis vysledku
result = double(10)
print(result)

Upravený skript bude vypadat následovně:

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

Výsledek:

20

12. Rozdíly mezi datovými typy Pythonu a assembleru

V jazyce Python jsou celočíselné hodnoty představovány typem int, který se však zcela odlišuje od céčkovského (nebo assemblerovského) typu se stejným jménem. Zatímco v jazyku C je bitová šířka typu int pro danou platformu/překladač konstantní (například 32 bitů), v Pythonu je rozsah hodnot a tím pádem i bitová šířka omezen jen velikostí paměti, i když se stále hlásí „tato hodnota je typu int“:

>>> x=42
>>> type(x)
<class 'int'>
 
>>> x=42**42
>>> x
>>> type(x)
<class 'int'>

V praxi z tohoto důvodu dochází k přetečením hodnot parametrů a/nebo výsledků funkce:

# zavolani funkce a vypis vysledku
result = double(10000000000)
print(result)

I když bychom možná očekávali výpis hodnoty 20000000000, vypíše se ve skutečnosti:

-1474836480

Je tomu tak z toho důvodu, že je naše subrutina omezena na 32bitový vstup a 32bitový výstup.

Pro úplnost si uveďme, jak vypadá takto upravený skript:

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

13. Podpora 64bitových celočíselných hodnot

Nic nám však nebrání v tom, abychom problém celočíselných hodnot „posunuli“ až k hodnotám přesahujícím hranici 263 nebo 264. Postačuje nám naši subrutinu upravit takovým způsobem, že bude akceptovat 64bitovou hodnotu předanou v registru RDI a vracet taktéž 64bitovou hodnotu v registru RAX:

[bits 64]
 
        mov rax, rdi
        add rax, rax
        ret

Výsledek překladu si ověříme z kontrolního výpisu (listingu):

     1                                  [bits 64]
     2
     3 00000000 4889F8                          mov rax, rdi
     4 00000003 4801C0                          add rax, rax
     5 00000006 C3                              ret
Poznámka: první bajt s hodnotou 0×48 je takzvaný REX prefix. Ten vždy začíná nibblem s hodnotou 4, za kterým následuje nibble se specifikací, zda se vyžadují 64bitové operandy pro čtení, zápis atd. U instrukcí s 32bitovými registry tento prefix není nutný a proto ho assembler negeneroval.

Musíme taktéž změnit typ návratové hodnoty funkce i typ jejího parametru:

# deklarace typu nativni funkce
function_type = ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_long)

Celý skript se změní jen nepatrně:

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

Výsledek výpočtu ovšem nyní bude korektní (pokud pochopitelně nepřekročíme 64bitový rozsah):

20000000000

14. Subrutina akceptující dva celočíselné parametry

Nyní si vyzkoušejme naprogramovat subrutinu zapsanou v assembleru, která provede součet svých dvou celočíselných 32bitových parametrů. ABI Linuxu na platformě x86–64 předepisuje, že první parametr je předáván v registru RDI (což už známe) a druhý parametr v registru RSI. Pro 32bitové hodnoty si ovšem vystačíme se spodními polovinami těchto registrů, tj. s registry pojmenovanými EDI a ESI. Výsledná hodnota se vrací v registru RAX (resp. EAX), takže realizace takové subrutiny v assembleru je triviální a opět se vůbec nemusíme zabývat zásobníkovými rámci atd.:

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

Překlad provedeme příkazem:

nasm -f bin -o add.bin -l add.lst add.asm

Výsledkem překladu by měl být soubor add.bin o délce pěti bajtů a taktéž výpis (listing) s tímto obsahem:

     1                                  [bits 64]
     2
     3 00000000 89F8                            mov eax, edi
     4 00000002 01F0                            add eax, esi
     5 00000004 C3                              ret
Poznámka: nyní opět strojový kód nemusí obsahovat REX prefix.

15. Volání subrutiny s předáním parametrů

Skript, který bude volat subrutinu akceptující dva parametry, nepatrně upravíme. Musíme specifikovat, že subrutina akceptuje dva parametry typu int – viz podtržená část:

# deklarace typu nativni funkce
function_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)

Dále již běžným způsobem získáme Pythonovskou referenci na funkci a při jejím volání nesmíme zapomenou na předání obou parametrů:

# reference na volatelnou funkci
add = function_type(ctypes.addressof(function_pointer))
 
# zavolani funkce a vypis vysledku
result = add(1, 2)
print(result)

Celý skript nyní bude vypadat takto:

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

16. Způsob předání většího množství parametrů: ABI

Prozatím jsme používali subrutiny, které neakceptovaly žádný parametr, popř. akceptovaly jeden či dva celočíselné parametry (nebo ukazatele). Tyto parametry se na platformě x86–64 předávají v pracovních registrech. To platí pro subrutiny, u nichž počet parametrů nepřesahuje hodnotu 6. Pokud je nutné předat větší množství parametrů, musí se tyto parametry předat přes zásobník. Společné vlastnosti a rozdíly architektur i386 (System V call) a x86–64 (System V x86–64) jsou shrnuty v následující tabulce:

  System V i386 System V x86_64
Návratová hodnota EAX, EDX+EAX RAX, RDX+RAX
Parametry 1–6 zásobník (zprava doleva) RDI, RSI, RDX, RCX, R8, R9
Další parametry zásobník (zprava doleva) zásobník (zprava doleva)
Zarovnání zásobníku × (bývá 4 bajty) 16 bajtů
Registry, jejichž hodnoty se mění EAX, ECX, EDX RAX, RDI, RSI, RDX, RCX, R8, R9, R10, R11
Registry, které mají zachovánu hodnotu EBX, ESI, EDI, EBP, ESP RBX, RSP, RBP, R12, R13, R14, R15

To znamená, že v systému s architekturou x86_64 lze předat až šest 64bitových parametrů přes pracovní registry. Subrutina musí na konci obnovit sedm pracovních registrů (prakticky ovšem v mnoha případech jen registr RBX), zatímco dalších devět pracovních registrů může libovolně použít pro své účely. Ovšem na druhou stranu si musí volající kód sám zajistit například úschovu registru RAX před zavoláním subrutiny a jeho obnovu po návratu – za předpokladu, že hodnotu tohoto registru bude nutné použít.

17. Trik na závěr: strojový kód zkombinovaný se skriptem v Pythonu v jediném souboru

Na závěr si ukážeme malý trik, který umožní načítat kódy subrutiny přeložené assemblerem a popř. i umožní mít kód v assembleru zapsaný ve stejném souboru, jako pythonní skript (i když se to hodí pro subrutiny, které se příliš často nemění). V tomto případě nevyužijeme binární soubor produkovaný assemblerem, ale kontrolní výpis (listing). Ten je dobře strukturovaný a v případě Netwide Assembleru vypadá následovně:

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

Tento soubor můžeme načíst (v textovém režimu) nebo ho přímo uložit do řetězce:

asm_src = """
     1                                  [bits 64]
     2
     3 00000000 89F8                            mov eax, edi
     4 00000002 01F0                            add eax, esi
     5 00000004 C3                              ret
"""

Díky jednoduché struktuře (nepoužíváme makra ani vkládané soubory) lze parsing strojového kódu, tj. získání hodnot ze třetího sloupce, realizovat na několika zdrojových řádcích:

# pole, ve kterém bude uložen strojový kód
machine_code = bytearray()
 
# zpracování po řádcích
for line in asm_src.split("\n"):
    # část obsahující hexa kódy instrukcí
    instruction_bytes = line[16:32]
    # převod hexa číslic na binární hodnoty
    instruction_code = bytearray.fromhex(instruction_bytes)
    # připojení do bytového pole
    machine_code.extend(instruction_code)

Konstrukce a naplnění bufferu je taktéž snadné:

bitcoin_smenarna

# konstrukce bufferu
with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    # zapis strojoveho kodu do bufferu
    buffer.write(machine_code)
    ...
    ...
    ...

18. Realizace konverze kontrolního výpisu do strojového kódu

Na závěr si ukažme, jak bude vypadat celý skript, v němž je uložen jak strojový kód (ovšem stále v čitelné podobě společně s assemblerem), tak i kód v Pythonu:

import ctypes
import mmap
 
asm_src = """
     1                                  [bits 64]
     2
     3 00000000 89F8                            mov eax, edi
     4 00000002 01F0                            add eax, esi
     5 00000004 C3                              ret
"""
 
machine_code = bytearray()
for line in asm_src.split("\n"):
    instruction_bytes = line[16:32]
    instruction_code = bytearray.fromhex(instruction_bytes)
    machine_code.extend(instruction_code)
 
 
print(machine_code)
 
# konstrukce bufferu
with mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_WRITE | mmap.PROT_EXEC) as buffer:
    # zapis strojoveho kodu do bufferu
    buffer.write(machine_code)
 
    # 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(123, 456)
    print(result)
 
    # pred uzavrenim bufferu je nutne odstranit ukazatele
    del function_pointer

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

Všechny demonstrační příklady popsané 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

20. Odkazy na Internetu

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