Obsah
1. Volání subrutin naprogramovaných v assembleru z jazyka Python: knihovna 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
19. Repositář s demonstračními příklady
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/Maratyszcza/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)
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()
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):
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/peachpy_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/peachpy42_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/peachpy42_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/peachpy42_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/peachpy42_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/peachpy_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/peachpy_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/peachpy_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/peachpy_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/peachpy_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/peachpy_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/peachpy_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/peachpy_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/peachpy_instruction2.py |
20. Odkazy na Internetu
- Volání subrutin naprogramovaných v assembleru z jazyka Python
https://www.root.cz/clanky/volani-subrutin-naprogramovanych-v-assembleru-z-jazyka-python/ - 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 - PeachPy 0.0.1
https://pypi.org/project/PeachPy/ - PeachPy
https://github.com/Maratyszcza/PeachPy - Historie s commity do knihovny PeachPy
https://github.com/Maratyszcza/PeachPy/commits/master/