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
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.
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.
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í:
- 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ý
- Naplnění bufferu strojovým kódem získaným překladem původního kódu assemblerem
- 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).
- Získání adresy funkce/subrutiny, tj. vlastně adresy paměťového bufferu (subrutina je uložena na jeho začátku)
- Získání reference na funkci tak, aby mohla být volána z Pythonu
- …nyní je možné naši subrutinu volat naprosto stejným způsobem, jako jinou Pythonovskou funkci…
- 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
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:
- Deklarace typů parametrů a typu návratové hodnoty nativní funkce
- Získání adresy se strojovým kódem (oněch šesti bajtů)
- Převod na referenci na běžnou Pythonovskou funkci (z pohledu programátora i interpretru)
- 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()) ... ... ...
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
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
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é:
# 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
- ctypes – A foreign function library for Python
https://docs.python.org/3/library/ctypes.html - CFFI documentation
https://cffi.readthedocs.io/en/latest/ - cffi 1.15.1 na PyPi
https://pypi.org/project/cffi/ - Python Bindings: Calling C or C++ From Python
https://realpython.com/python-bindings-overview/ - Interfacing with C/C++ Libraries
https://docs.python-guide.org/scenarios/clibs/ - Cython, pybind11, cffi – which tool should you choose?
http://blog.behnel.de/posts/cython-pybind11-cffi-which-tool-to-choose.html - Python FFI with ctypes and cffi
https://eli.thegreenplace.net/2013/03/09/python-ffi-with-ctypes-and-cffi - Using standard library headers with CFFI
https://stackoverflow.com/questions/57481873/using-standard-library-headers-with-cffi - C Arrays
https://www.programiz.com/c-programming/c-arrays - C Arrays
https://www.w3schools.com/c/c_arrays.php - Array of Structures in C
https://overiq.com/c-programming-101/array-of-structures-in-c/#google_vignette - Keystone Engine na GitHubu
https://github.com/keystone-engine/keystone - Keystone: The Ultimate Assembler
https://www.keystone-engine.org/ - The Ultimate Disassembler
http://www.capstone-engine.org/ - Tutorial for Keystone
https://www.keystone-engine.org/docs/tutorial.html - Rozhraní pro Capstone na PyPi
https://pypi.org/project/capstone/ - Rozhraní pro Keystone na PyPi
https://pypi.org/project/keystone-engine/ - KEYSTONE: Next Generation Assembler Framework
https://www.keystone-engine.org/docs/BHUSA2016-keystone.pdf - AT&T Syntax versus Intel Syntax
http://web.mit.edu/rhel-doc/3/rhel-as-en-3/i386-syntax.html - AT&T assembly syntax and IA-32 instructions
https://gist.github.com/mishurov/6bcf04df329973c15044 - ARM GCC Inline Assembler Cookbook
http://www.ethernut.de/en/documents/arm-inline-asm.html - Extended Asm – Assembler Instructions with C Expression Operands
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html - ARM inline asm secrets
http://hardwarebug.org/2010/07/06/arm-inline-asm-secrets/ - How to Use Inline Assembly Language in C Code
https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html#Using-Assembly-Language-with-C - GCC-Inline-Assembly-HOWTO
http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html - A Brief Tutorial on GCC inline asm (x86 biased)
http://www.osdever.net/tutorials/view/a-brief-tutorial-on-gcc-inline-asm - GCC Inline ASM
http://locklessinc.com/articles/gcc_asm/ - GNU Assembler Examples
http://cs.lmu.edu/~ray/notes/gasexamples/ - X86 Assembly/Arithmetic
https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic - Art of Assembly – Arithmetic Instructions
http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter6/CH06–2.html - The GNU Assembler Tutorial
http://tigcc.ticalc.org/doc/gnuasm.html - The GNU Assembler – macros
http://tigcc.ticalc.org/doc/gnuasm.html#SEC109 - ARM subroutines & program stack
http://www.toves.org/books/armsub/ - Generating Mixed Source and Assembly List using GCC
http://www.systutorials.com/240/generate-a-mixed-source-and-assembly-listing-using-gcc/ - Calling subroutines
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.kui0100a/armasm_cihcfigg.htm - Linux assemblers: A comparison of GAS and NASM
http://www.ibm.com/developerworks/library/l-gas-nasm/index.html - Programovani v assembleru na OS Linux
http://www.cs.vsb.cz/grygarek/asm/asmlinux.html - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - Online x86 / x64 Assembler and Disassembler
https://defuse.ca/online-x86-assembler.htm#disassembly - Executing assembly code in memory using python modules ctypes and mmap
https://stackoverflow.com/questions/58851655/executing-assembly-code-in-memory-using-python-modules-ctypes-and-mmap - mmap – Memory-mapped file support
https://docs.python.org/3/library/mmap.html - ctypes – A foreign function library for Python
https://docs.python.org/3/library/ctypes.html#module-ctypes - Calling Conventions
https://wiki.osdev.org/Calling_Conventions - Linux x64 Calling Convention: Stack Frame
https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame - Netwide assembler
https://www.nasm.us/ - The Netwide Assembler: NASM: output formats
https://ece-research.unm.edu/jimp/310/nasm/nasmdoc6.html