Framework Capstone aneb univerzální disassembler

5. 1. 2023
Doba čtení: 38 minut

Sdílet

 Autor: Depositphotos
Seznámíme se s frameworkem Capstone, což je univerzální disassembler podporující všechny mainstreamové architektury, jenž je implementován formou knihovny a je možné ho volat z různých programovacích jazyků.

Obsah

1. Framework Capstone aneb univerzální disassembler

2. Základní způsob použití frameworku Capstone: disassembling obsahu binárního souboru

3. Demonstrační příklad

4. Výpis operačních kódů instrukcí

5. Demonstrační příklad

6. Explicitní specifikace bázové adresy kódu (org)

7. Demonstrační příklad

8. Binární kód uložený v souboru od zadaného nenulového offsetu

9. Demonstrační příklady: korektní a nekorektní varianty

10. Disassembling binárního souboru s nativním kódem pro odlišnou architekturu CPU

11. Kombinace Keystone+Capstone

12. Kód v assembleru určený pro platformu x86–64

13. Explicitní změna bázové adresy

14. Kód v assembleru určený pro 32bitovou platformu ARM

15. Využití instrukční sady ARM Thumb

16. Přepis příkladů pro architekturu AArch64

17. RISCová architektura MIPS

18. Závěrečné zhodnocení

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

20. Odkazy na Internetu

1. Framework Capstone aneb univerzální disassembler

V dnešním článku, jenž navazuje na předchozí článek o frameworku Keystone, se seznámíme s frameworkem nazvaným Capstone. Zatímco výše zmíněný framework Keystone je univerzálním assemblerem, jedná se v případě frameworku Capstone o univerzální disassembler, který podporuje všechny v současnosti mainstreamové architektury (x86, x86–64, 32bitový ARM, AArch64 atd.) a je implementován formou nativní knihovny, kterou je možné volat z různých programovacích jazyků. Oba zmíněné frameworky, tedy jak Capstone, tak i Keystone, lze přitom používat společně. Příkladem mohou být systémy, které nejprve provedou disassembling vybraného binárního bloku dat s úpravou kódu v assembleru a s jeho následným překladem zpět do nativního binárního kódu.

Možný a podporovaný je pochopitelně i opačný přístup, tj. překlad kódu napsaného v assembleru do nativního kódu s jeho následným zpětným překladem do assembleru (s doplněním informací o způsobu zakódování jednotlivých strojových instrukcí). Tento přístup je možné použít například při výuce a ukážeme si ho na několika demonstračních příkladech, kde porovnáme podobný kód (dvojice vnořených programových smyček) zapsaný pro architekturu x86(64), klasický 32bitový ARM, 32bitový ARM s instrukční sadou Thumb a konečně 64bitovou architekturu AArch64.

2. Základní způsob použití frameworku Capstone: disassembling obsahu binárního souboru

Podívejme se nejprve na základní způsob využití frameworku Capstone. Budeme se snažit o zpětný překlad (disassembling) nativního binárního kódu, který vznikl překladem zdrojového kódu v assembleru pro platformu x86–64 (resp. přesněji řečeno pro platformu x86 v 64bitovém režimu). Konkrétně se jedná o sekvenci strojových instrukcí, které realizují dvojici vnořených programových smyček. V assembleru vypadá zápis těchto programových smyček následovně:

    MOV EBX, 10
OUTER_LOOP:
    MOV EAX, 100
INNER_LOOP:
    DEC EAX
    JNZ INNER_LOOP
    DEC EBX
    JNZ OUTER_LOOP

Překladem (konkrétně překladem s využitím minule popsaného assembleru Keystone) vznikl binární soubor nazvaný „loops.bin“ o délce osmnácti bajtů. Obsah tohoto souboru je následující:

$ od -tx1 loops.bin
 
0000000 bb 0a 00 00 00 b8 64 00 00 00 ff c8 75 fc ff cb
0000020 75 f3
0000022

3. Demonstrační příklad

A právě obsah binárního souboru představeného v rámci předchozí kapitoly se budeme snažit zpětně přeložit do assembleru s využitím frameworku Capstone. K tomuto účelu poslouží následující skript, jenž obsah binárního souboru načte a následně se pokusí o zpětný překlad. Povšimněte si, že při zpětném překladu musíme (vcelku logicky) určit architekturu a taktéž režim – jinými slovy jak instrukční sadu, tak i způsob kódování instrukcí (tyto informace není možné nijak automaticky odvodit):

# import všech symbolů použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
 
# otevřít binární soubor a přečíst jeho obsah
with open("loops.bin", "rb") as fin:
    code = fin.read()
 
# disassembling obsahu binárního souboru
md = Cs(CS_ARCH_X86, CS_MODE_64)
 
# vypsat podrobnější informace o každé instrukci
for i in md.disasm(code, 0x0000):
    # výpis informací o instrukci
    print("0x{:02x}:\t{:s}\t{:s}".format(i.address, i.mnemonic, i.op_str))

Z výsledků vypsaných výše uvedeným skriptem je patrné, že máme k dispozici prakticky všechny důležité informace o strojových instrukcích – jejich adresu, jméno instrukce i seznam operandů (pokud se jedná o instrukci s operandem či s operandy):

0x00:   mov     ebx, 0xa
0x05:   mov     eax, 0x64
0x0a:   dec     eax
0x0c:   jne     0xa
0x0e:   dec     ebx
0x10:   jne     5

4. Výpis operačních kódů instrukcí

Naprostá většina v současnosti používaných disassemblerů dokáže kromě jména instrukce a seznamu operandů vypsat i bajty obsahující operační kód instrukce i zakódované operandy. Příkladem může být nástroj objdump, s nímž jsme se již setkali minule a jenž dokáže provést například tuto operaci:

$ objdump -b binary -D -m i386:x86-64 -M intel loops.bin > loops_dump.asm
 
loops.bin:     file format binary
 
 
Disassembly of section .data:
 
0000000000000000 <.data>:
   0:   bb 0a 00 00 00          mov    ebx,0xa
   5:   b8 64 00 00 00          mov    eax,0x64
   a:   ff c8                   dec    eax
   c:   75 fc                   jne    0xa
   e:   ff cb                   dec    ebx
  10:   75 f3                   jne    0x5

I tyto informace nám pochopitelně framework Capstone dává k dispozici, protože při procházení jednotlivými instrukcemi jsou v atributu bytes uložené jednotlivé bajty reprezentující instrukci v nativním kódu. Hodnoty těchto bajtů je pouze nutné převést do čitelné podoby. Je zvykem hodnoty těchto bajtů vypisovat v hexadecimálním tvaru, takže použijeme standardní metodu hex(), která převod sekvence bajtů do řetězce s hexadecimálními hodnotami provede za nás, a to i s využitím oddělovače:

dump = i.bytes.hex(" ")

5. Demonstrační příklad

Skript ze třetí kapitoly upravíme takovým způsobem, aby se kromě adresy, mnemotechnické zkratky instrukce a operandů instrukce vypsaly i hodnoty bajtů reprezentujících instrukci v nativním kódu. Délka instrukce v nativním strojovém kódu sice není obecně omezena, ovšem skript kvůli jednoduchosti počítá s tím, že se bude jednat o maximálně šest bajtů (i delší instrukce se pochopitelně vypíše, ovšem jednotlivé sloupce již v tomto případě nebudou zarovnány pěkně pod sebou):

# import všech symbolů použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
 
# otevřít binární soubor a přečíst jeho obsah
with open("loops.bin", "rb") as fin:
    code = fin.read()
 
# disassembling obsahu binárního souboru
md = Cs(CS_ARCH_X86, CS_MODE_64)
 
# vypsat podrobnější informace o každé instrukci
for i in md.disasm(code, 0x0000):
    # převod pole bajtů na řetězec s hexa hodnotami
    dump = i.bytes.hex(" ")
 
    # výpis informací o instrukci
    print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))

Získaný výsledek již vypadá (alespoň podle mého skromného názoru) značně profesionálně:

0x00:   bb 0a 00 00 00          mov     ebx, 0xa
0x05:   b8 64 00 00 00          mov     eax, 0x64
0x0a:   ff c8                   dec     eax
0x0c:   75 fc                   jne     0xa
0x0e:   ff cb                   dec     ebx
0x10:   75 f3                   jne     5

6. Explicitní specifikace bázové adresy kódu (org)

V obou předchozích demonstračních příkladech jste si mohli povšimnout, že při volání metody md.disassemble() se kromě sekvence bajtů představujících nativní (strojový) kód do této metody ve druhém parametru předává i celočíselná konstanta 0×0000. Modifikací této konstanty je možné změnit počáteční adresu celého bloku instrukcí; jedná se tedy o (i když ne zcela přesnou) obdobu direktivy .org, kterou známe z běžných assemblerů, například z GNU Assembleru (viz například https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as7.html#SEC112). V případě, že hodnotu této konstanty změníme, pochopitelně dojde jak ke změně adres instrukcí zobrazených v prvním sloupci, tak i k modifikaci adres relativních skoků (a podmíněné skoky se typicky překládají právě na relativní skoky, což mj. umožňuje generování přemístitelného strojového kódu).

7. Demonstrační příklad

Následující skript se od skriptu předchozího liší jen v maličkosti – namísto offsetu 0×0000 metody Cs.disasm() se této metodě předá offset 0×1234:

# import všech symbolů použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
 
# otevřít binární soubor a přečíst jeho obsah
with open("loops.bin", "rb") as fin:
    code = fin.read()
 
# disassembling obsahu binárního souboru
md = Cs(CS_ARCH_X86, CS_MODE_64)
 
# vypsat podrobnější informace o každé instrukci
for i in md.disasm(code, 0x1234):
    # převod pole bajtů na řetězec s hexa hodnotami
    dump = i.bytes.hex(" ")
 
    # výpis informací o instrukci
    print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))

Výsledek bude ovšem v tomto případě zcela odlišný, protože se budou odlišovat například i cílové adresy (relativních) skoků:

0x1234: bb 0a 00 00 00          mov     ebx, 0xa
0x1239: b8 64 00 00 00          mov     eax, 0x64
0x123e: ff c8                   dec     eax
0x1240: 75 fc                   jne     0x123e
0x1242: ff cb                   dec     ebx
0x1244: 75 f3                   jne     0x1239

8. Binární kód uložený v souboru od zadaného nenulového offsetu

Nyní si (poněkud uměle) připravíme soubor s binárním obsahem, který bude nejprve obsahovat obecně náhodná data (resp. v našem případě nuly, ale může se jednat i o jiné hodnoty) a teprve poté v něm bude umístěn kód nativních instrukcí vzniklých překladem assemblerem Capstone. Jak lze takový soubor vytvořit? Cest je mnoho, můžeme například použít například základní nástroje Linuxu. Nejdříve si necháme vygenerovat binární soubor obsahující tisíc nulových hodnot:

$ truncate -s 1000 zeros

Výsledkem předchozího příkazu bude soubor nazvaný „zeros“ o délce přesně 1000 bajtů:

$ ls -l zeros
 
-rw-rw-r-- 1 ptisnovs ptisnovs 1000 Dec 31 11:07 zeros

Následně jednoduše spojíme soubor „zeros“ s binárním souborem „loops.bin“, který byl analyzován v rámci předchozích kapitol:

$ cat zeros loops.bin > loops_x.bin

Výsledkem by měl být soubor „loops_x.bin“ o délce 1018 bajtů:

$ ls -l loops_x.bin 
 
-rw-rw-r-- 1 ptisnovs ptisnovs 1018 Dec 27 09:43 loops_x.bin

9. Demonstrační příklady: korektní a nekorektní varianty

Nejprve se pokusme takto vzniklý binární soubor přeložit běžným způsobem, tedy i s ignorováním faktu, že prvních tisíc bajtů ve skutečnosti žádné instrukce neobsahuje:

# import všech symbolů použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
# otevřít binární soubor a přečíst jeho obsah
with open("loops_x.bin", "rb") as fin:
    code = fin.read()
 
# disassembling obsahu binárního souboru
md = Cs(CS_ARCH_X86, CS_MODE_64)
 
# vypsat podrobnější informace o každé instrukci
for i in md.disasm(code, 0x0000):
    # převod pole bajtů na řetězec s hexa hodnotami
    dump = i.bytes.hex(" ")
 
    # výpis informací o instrukci
    print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))

Výsledek bude pochopitelně obsahovat nekorektní informace a až na konci nalezneme korektní sekvenci instrukcí:

0x00:   00 00                   add     byte ptr [rax], al
0x02:   00 00                   add     byte ptr [rax], al
0x04:   00 00                   add     byte ptr [rax], al
0x06:   00 00                   add     byte ptr [rax], al
0x08:   00 00                   add     byte ptr [rax], al
0x0a:   00 00                   add     byte ptr [rax], al
0x0c:   00 00                   add     byte ptr [rax], al
0x0e:   00 00                   add     byte ptr [rax], al
0x10:   00 00                   add     byte ptr [rax], al
0x12:   00 00                   add     byte ptr [rax], al
0x14:   00 00                   add     byte ptr [rax], al
...
...
...
0x3e6:  00 00                   add     byte ptr [rax], al
0x3e8:  bb 0a 00 00 00          mov     ebx, 0xa
0x3ed:  b8 64 00 00 00          mov     eax, 0x64
0x3f2:  ff c8                   dec     eax
0x3f4:  75 fc                   jne     0x3f2
0x3f6:  ff cb                   dec     ebx
0x3f8:  75 f3                   jne     0x3ed
Poznámka: ve skutečnosti je stejně dobře možné, že se disassembler vůbec do začátku instrukcí „netrefí“ (například kdyby nul bylo 999 nebo naopak 1001) a celý výsledek bude nekorektní.

Řešení tohoto problémku je ve skutečnosti jednoduché – oněch 1000 bajtů musíme explicitně přeskočit a disassembleru předat pouze vybranou podsekvenci bajtů. Přeskočení lze realizovat buď operací seek nad souborem, nebo (což je méně efektivní) načtením celého binárního souboru následovaného operací slice:

# import všech symbolů použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
# pozice v binárním souboru, od níž jsou instrukce uloženy
OFFSET = 1000
 
# otevřít binární soubor a přečíst jeho obsah
with open("loops_x.bin", "rb") as fin:
    code = fin.read()
    code = code[OFFSET:]
 
# disassembling obsahu binárního souboru
md = Cs(CS_ARCH_X86, CS_MODE_64)
 
# vypsat podrobnější informace o každé instrukci
for i in md.disasm(code, 0x0000):
    # převod pole bajtů na řetězec s hexa hodnotami
    dump = i.bytes.hex(" ")
 
    # výpis informací o instrukci
    print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))

Výsledek:

0x00:   bb 0a 00 00 00          mov     ebx, 0xa
0x05:   b8 64 00 00 00          mov     eax, 0x64
0x0a:   ff c8                   dec     eax
0x0c:   75 fc                   jne     0xa
0x0e:   ff cb                   dec     ebx
0x10:   75 f3                   jne     5

popř. s korektním offsetem (začátkem instrukcí) nastaveným na 1000:

0x3e8:  bb 0a 00 00 00          mov     ebx, 0xa
0x3ed:  b8 64 00 00 00          mov     eax, 0x64
0x3f2:  ff c8                   dec     eax
0x3f4:  75 fc                   jne     0x3f2
0x3f6:  ff cb                   dec     ebx
0x3f8:  75 f3                   jne     0x3ed

10. Disassembling binárního souboru s nativním kódem pro odlišnou architekturu CPU

Vzhledem k tomu, že Capstone je (či měl by být) univerzálním disassemblerem, dokáže zpětně překládat i nativní kód určený pro odlišné architektury mikroprocesorů. Podívejme se na velmi jednoduchý příklad. Překladem následujícího céčkového kódu:

int add(int x, int y) {
    return x+y;
}

na 32bitové platformě MIPS získáme tento (zcela neoptimalizovaný) kód:

add:
 addiu  sp,sp,-8
 sw     s8,4(sp)
 move   s8,sp
 sw     a0,8(s8)
 sw     a1,12(s8)
 lw     v1,8(s8)
 lw     v0,12(s8)
 nop
 addu   v0,v1,v0
 move   sp,s8
 lw     s8,4(sp)
 addiu  sp,sp,8
 jr     ra
 nop

Nativní kód je uložen do binárního souboru nazvaného „mips.bin“ o délce šedesáti bajtů:

$ od -tx1 mips.bin 
 
0000000 f8 ff bd 27 04 00 be af 25 f0 a0 03 08 00 c4 af
0000020 0c 00 c5 af 08 00 c3 8f 0c 00 c2 8f 00 00 00 00
0000040 21 10 62 00 25 e8 c0 03 04 00 be 8f 08 00 bd 27
0000060 08 00 e0 03 00 00 00 00 00 00 00 00
0000074

Pokusme se nyní o zpětný překlad. Povšimněte si, že je opět nutné specifikovat jak architekturu CPU, tak i režim:

# import všech symbolů použitých ve skriptu
from capstone import Cs, CS_ARCH_MIPS, CS_MODE_MIPS32
 
 
# otevřít binární soubor a přečíst jeho obsah
with open("mips.bin", "rb") as fin:
    code = fin.read()
 
# disassembling obsahu binárního souboru
md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS32)
 
# vypsat podrobnější informace o každé instrukci
for i in md.disasm(code, 0x0000):
    # výpis informací o instrukci
    print("0x{:02x}:\t{:s}\t{:s}".format(i.address, i.mnemonic, i.op_str))

Výsledek bude vypadat následovně – skutečně jsme tedy dokázali provést zpětný překlad i na zcela odlišné platformě, a to bez nutnosti instalace dalších nástrojů:

0x00:   addiu   $sp, $sp, -8
0x04:   sw      $fp, 4($sp)
0x08:   move    $fp, $sp
0x0c:   sw      $a0, 8($fp)
0x10:   sw      $a1, 0xc($fp)
0x14:   lw      $v1, 8($fp)
0x18:   lw      $v0, 0xc($fp)
0x1c:   nop
0x20:   addu    $v0, $v1, $v0
0x24:   move    $sp, $fp
0x28:   lw      $fp, 4($sp)
0x2c:   addiu   $sp, $sp, 8
0x30:   jr      $ra
0x34:   nop
0x38:   nop

11. Kombinace Keystone+Capstone

Frameworky Keystone a Capstone, tedy assembler a disassembler, jsou navrženy takovým způsobem, aby se používaly společně, například v rámci jediné aplikace. Příkladem může být nástroj pro tvorbu patchů, který nejprve část binárního nativního kódu zpětně přeloží do assembleru, umožní uživateli provést všechny potřebné změny a následně provede opětovný překlad do nativního binárního kódu. Ovšem samozřejmě je možné použít i opačný postup, kdy je na začátku soubor či text napsaný v assembleru, jenž je frameworkem Keystone přeložen do nativního binárního kódu a posléze se použije Capstone jak pro zpětný překlad, tak i pro získání sekvence bajtů reprezentujících jednotlivé instrukce. Tento postup je možné využít například pro výuku, typicky pro ukázku rozdílů mezi CISCovými a RISCovými instrukčními sadami nebo pro naznačení rozdílů mezi ARM32 a „komprimovanou“ instrukční sadou Thumb a Thumb-2.

12. Kód v assembleru určený pro platformu x86–64

Kombinaci Capstone+Keystone si můžeme vyzkoušet velmi snadno na následujícím demonstračním příkladu, v němž se nejdříve provede překlad kódu v assembleru, v němž je realizována dvojice vnořených programových smyček. Výsledkem překladu je sekvence bajtů, která se posléze zpětně přeložena zpět do čitelného kódu, a to včetně kódování jednotlivých instrukcí. Celý příklad využívá instrukční sadu x86–64 (64bitový režim):

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
    MOV EBX, 10
OUTER_LOOP:
    MOV EAX, 100
INNER_LOOP:
    DEC EAX
    JNZ INNER_LOOP
    DEC EBX
    JNZ OUTER_LOOP
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_X86, CS_MODE_64)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Po spuštění tohoto skriptu by se měl provést překlad (assembling) do binárního kódu následovaný zpětným překladem (disassembling) zpět do čitelné podoby, pochopitelně společně s binární reprezentací jednotlivých instrukcí:

0x00:   bb 0a 00 00 00          mov     ebx, 0xa
0x05:   b8 64 00 00 00          mov     eax, 0x64
0x0a:   ff c8                   dec     eax
0x0c:   75 fc                   jne     0xa
0x0e:   ff cb                   dec     ebx
0x10:   75 f3                   jne     5

13. Explicitní změna bázové adresy

Vzhledem k tomu, že původní kód v assembleru lze přesunout (realokovat) na prakticky libovolnou adresu, můžeme při zpětném překladu (disassemblingu) změnit bázovou adresu (offset, origin) zpětně překládaného kódu – viz též zvýrazněnou hodnotu v nepatrně upraveném zdrojovém kódu:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_X86, KS_MODE_64, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
    MOV EBX, 10
OUTER_LOOP:
    MOV EAX, 100
INNER_LOOP:
    DEC EAX
    JNZ INNER_LOOP
    DEC EBX
    JNZ OUTER_LOOP
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_X86, CS_MODE_64)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x1234):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Výsledek zpětného překladu se v tomto případě bude pochopitelně lišit:

0x1234: bb 0a 00 00 00          mov     ebx, 0xa
0x1239: b8 64 00 00 00          mov     eax, 0x64
0x123e: ff c8                   dec     eax
0x1240: 75 fc                   jne     0x123e
0x1242: ff cb                   dec     ebx
0x1244: 75 f3                   jne     0x1239

oproti původnímu výsledku:

0x00:   bb 0a 00 00 00          mov     ebx, 0xa
0x05:   b8 64 00 00 00          mov     eax, 0x64
0x0a:   ff c8                   dec     eax
0x0c:   75 fc                   jne     0xa
0x0e:   ff cb                   dec     ebx
0x10:   75 f3                   jne     5
Poznámka: povšimněte si, že kódování instrukcí je stejné – v tomto případě jde skutečně o plně „relativní“ kód.

14. Kód v assembleru pro 32bitovou platformu ARM

Capstone a Keystone podporují i 32bitovou platformu ARM, která se stále používá, i když v segmentu výkonných čipů je nahrazována 64bitovou architekturou AArch64. Čipy ARM jsou založeny na pipeline s proměnným počtem řezů (podle generace atd.). Interní i externí datová sběrnice má šířku 32 bitů a i všechny instrukce v instrukční sadě mají tutéž šířku 32 bitů (což ostatně uvidíme na příkladu). Díky tomu se zjednodušila řídicí logika v čipu, navíc bylo možné měnit obsah čítače instrukcí PC (r15) již ve chvíli, kdy se vykonávala fáze decode (výjimku samozřejmě tvořily skoky). Vzhledem k tomu, že mikroprocesory s architekturou RISC používají při přístupu do operační paměti pouze instrukce typu Load a Store, musí být tyto čipy vybaveny poměrně velkým počtem pracovních registrů. V případě ARMu je na čipu umístěno celkem 37 registrů (opět se může lišit podle generace), z nichž každý má pevnou šířku 32 bitů. Některé registry jsou pracovní, jiné mají speciální význam, například jsou v nich uloženy různé bitové příznaky. Zajímavé je, že namísto registrových oken použitých u jiných procesorů typu RISC se v případě ARMu objevuje rozdělení registrů do skupin, podle toho, v jakém stavu se mikroprocesor v daném okamžiku nachází.

Zkusme si nyní předchozí dvojici skriptů upravit do takové podoby, že se bude pracovat s jednoduchým prográmkem psaným v assembleru čipů ARM, v němž je opět realizována dvojice vnořených smyček. Povšimněte si, že je (pochopitelně) použita odlišná sada instrukcí a i názvy pracovních registrů se od platformy x86(64) liší. V samotném skriptu jsou použity odlišné konstanty KS_ARCH_ARM, KS_MODE_ARM, CS_ARCH_ARM a CS_MODE_ARM:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_ARM, KS_MODE_ARM, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_ARM, CS_MODE_ARM
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
    MOV  R0, 10
OUTER_LOOP:
    MOV  R1, 100
INNER_LOOP:
    SUBS R1, R1, 1
    BNE  INNER_LOOP
    SUBS R0, R0, 1
    BNE  OUTER_LOOP
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_ARM, KS_MODE_ARM)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Po spuštění tohoto skriptu získáme mj. i sekvence bajtů se zakódovanými instrukcemi. Povšimněte si, že všechny instrukce, nezávisle na jejich interní složitosti a počtu operandů, mají na platformě ARM jednotnou šířku čtyř bajtů:

0x00:   0a 00 a0 e3             mov     r0, #0xa
0x04:   64 10 a0 e3             mov     r1, #0x64
0x08:   01 10 51 e2             subs    r1, r1, #1
0x0c:   fd ff ff 1a             bne     #8
0x10:   01 00 50 e2             subs    r0, r0, #1
0x14:   fa ff ff 1a             bne     #4

15. Využití instrukční sady ARM Thumb

Mikroprocesory ARM, které byly původně navrženy pro využití v osobních počítačích, se postupně začaly používat i v jiných oblastech, především ve vestavěných (embedded) systémech, kde vládnou poněkud jiné požadavky. Ve vestavěných systémech je totiž kromě nízké spotřeby, popř. rychlé reakce na přerušení, kladen i velký důraz na to, aby binární obrazy programů byly co nejmenší, protože programy jsou ukládány do pamětí ROM/EPROM/EEPROM/Flash s relativně vysokou cenou za jeden bit a nikoli na vysokokapacitních paměťových médiích (pevné disky) tak, jak je tomu na osobních počítačích. Navíc došlo k postupné změně i v oblasti osobních počítačů – rychlosti procesorů rostly větším tempem, než rychlost pamětí DRAM – což s sebou přinášelo nutnost použití drahých vyrovnávacích pamětí založených na technologii SRAM. V jeden okamžik se dokonce zdálo, že tento vývoj bude znamenat konec procesorů typu RISC, které jsou mj. typické i tím, že používají instrukční sady s instrukcemi pevné šířky.

Odpovědí společnosti ARM na obě nové skutečnosti, tedy na požadavek na menší velikost binárních obrazů programů a zvyšující se rozdíl v rychlosti CPU a DRAM, bylo zavedení nové alternativní instrukční sady nazvané Thumb, v níž mají všechny instrukce šířku jen šestnáct bitů, což znamená, že v paměti určité kapacity lze uložit přibližně dvakrát tolik instrukcí Thumb, než původních RISCových instrukcí (slovo „přibližně“ je zde použito především z toho důvodu, že se v kódu vyskytují i 32bitové konstanty, nezávisle na použité instrukční sadě). Ovšem menší šířka instrukcí znamenala i určitá omezení. Zcela zmizely podmínkové kódy, které zůstaly zachovány jen u instrukce podmíněného skoku. Také se možnost použití barrel shifteru omezila jen na určitou skupinu instrukcí. Ovšem asi největší změnou bylo to, že se sada pracovních registrů R0-R15 rozdělila na spodní polovinu R0-R7 (Lo registers) a horní polovinu R8-R15 (Hi registers), přičemž většina instrukcí dokáže pracovat pouze s prvními osmi registry, zatímco některé registry z horní skupiny mají speciální význam (čítač instrukcí, ukazatel na vrchol zásobníku atd.).

Výpočetní výkon dosahovaný v reálných aplikacích však byl u sady Thumb v některých případech menší, protože mnohé operace musely být provedeny pomocí většího množství instrukcí – instrukce Thumb totiž vždy vykonávaly jen jednu operaci, na rozdíl od instrukcí ARM, které obsahovaly jak podmínkové bity, tak i v mnoha případech „podoperaci“ pro bitový posun či rotaci jednoho z operandů vstupujícího do aritmeticko-logické jednotky. Proto později vznikla instrukční sada Thumb-2, která kombinuje instrukce s délkou dvou bajtů a čtyř bajtů. A právě Thumb i Thumb-2 jsou frameworky Capstone a Keystone podporovány. Podívejme se na příklad, jehož kód v assembleru se nijak neliší od předchozího příkladu – liší se jen způsob překladu:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
    MOV R0, 10
OUTER_LOOP:
    MOV R1, 100
INNER_LOOP:
    SUBS R1, R1, 1
    BNE  INNER_LOOP
    SUBS R0, R0, 1
    BNE  OUTER_LOOP
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Z výsledků je patrné, že přeložené instrukce skutečně mají délku buď dvou bajtů nebo čtyř bajtů, tj. hustota kódu je vyšší:

0x00:   4f f0 0a 00             mov.w   r0, #0xa
0x04:   4f f0 64 01             mov.w   r1, #0x64
0x08:   01 39                   subs    r1, #1
0x0a:   fc d1                   bne     #8
0x0c:   01 38                   subs    r0, #1
0x10:   f7 d1                   bne     #4

Čistě ARMovská varianta přitom vypadala takto:

0x00:   0a 00 a0 e3             mov     r0, #0xa
0x04:   64 10 a0 e3             mov     r1, #0x64
0x08:   01 10 41 e2             subs    r1, r1, #1
0x0c:   fd ff ff 1a             bne     #8
0x10:   01 00 40 e2             subs    r0, r0, #1
0x14:   fa ff ff 1a             bne     #4

16. Přepis příkladů pro architekturu AArch64

V poměrně velkém množství moderních elektronických zařízení, ať již se jedná o tablety, smartphony či o jednodeskové mikropočítače (RPi), se můžeme setkat s mikroprocesory založenými na 64bitové architektuře AArch64 vyvinuté ve společnosti ARM. Tato architektura je sice do jisté míry odvozena od původní „klasické“ 32bitové RISCové architektury ARM (dnes pro odlišení nazývané ARM32 či dokonce jen A32), ovšem při přechodu na 64bitový systém došlo k mnoha podstatným změnám, které se týkají jak počtu a funkce pracovních registrů, tak i instrukční sady, jež se v některých ohledech od původní RISCové sady dosti podstatným způsobem odlišuje.

Z pohledu programátora pracujícího v assembleru je architektura AArch64 v několika ohledech odlišná od původní 32bitové architektury. Především se zvýšil počet pracovních registrů z patnácti na 31. Všechny registry se navíc rozšířily z třiceti dvou bitů na 64 bitů. Ovšem mnohé operace stále podporují i 32bitové operandy – v tomto případě se použije ta samá skupina registrů, z nichž se ovšem využije jen spodních 32 bitů (nedochází zde tedy k rozdvojení každého 64bitového registru na dva registry 32bitové, jak to známe z jiných typů procesorů).

Pokusme se tedy původní vnořenou smyčku, která na ARM32 vypadala takto:

    MOV  R0, 10
OUTER_LOOP:
    MOV  R1, 100
INNER_LOOP:
    SUBS R1, R1, 1
    BNE  INNER_LOOP
    SUBS R0, R0, 1
    BNE  OUTER_LOOP

realizovat pro AArch64. Kód bude prakticky stejný, pouze použijeme odlišná jména registrů:

    MOV  W0, 10
OUTER_LOOP:
    MOV  W1, 100
INNER_LOOP:
    SUBS W1, W1, 1
    BNE  INNER_LOOP
    SUBS W0, W0, 1
    BNE  OUTER_LOOP

Opět provedeme překlad tohoto kódu do nativního binárního kódu, po němž bude následovat zpětné dekódování instrukcí:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
    MOV  W0, 10
OUTER_LOOP:
    MOV  W1, 100
INNER_LOOP:
    SUBS W1, W1, 1
    BNE  INNER_LOOP
    SUBS W0, W0, 1
    BNE  OUTER_LOOP
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Kódování instrukcí je v tomto případě zcela odlišné od ARM32, i když „RISCový“ charakter (instrukce shodné délky) zůstal zachován:

0x00:   40 01 80 52             movz    w0, #0xa
0x04:   81 0c 80 52             movz    w1, #0x64
0x08:   21 04 00 71             sub     w1, w1, #1
0x0c:   e1 ff ff 54             b.ne    #8
0x10:   00 04 00 71             sub     w0, w0, #1
0x14:   81 ff ff 54             b.ne    #4

17. RISCová architektura MIPS

Poslední procesorovou architekturou, s níž se v dnešním článku v souvislosti s frameworky Capstone a Keystone setkáme, je architektura MIPS, a to jak 32bitová, tak i 64bitová varianta (navíc se rozlišuje kódování little endian a big endian). Začneme kódem v assembleru, který vznikl překladem následujícího céčkového kódu (a to s vypnutím optimalizací):

int add(int x, int y) {
    return x+y;
}

Překlad do assembleru MIPSu vypadá takto (včetně zarovnání na konci):

addiu   $sp,$sp,-8
sw      $fp,4($sp)
move    $fp,$sp
sw      $4,8($fp)
sw      $5,12($fp)
lw      $3,8($fp)
lw      $2,12($fp)
nop
addu    $2,$3,$2
move    $sp,$fp
lw      $fp,4($sp)
addiu   $sp,$sp,8
jr      $31
nop
Poznámka: povšimněte si, že registry jsou pouze očíslovány; nepoužívá se zde jejich pojmenování.

Pokusme se nyní o překlad takto zapsaného kódu v assembleru do nativního strojového kódu a zpět, a to pro původní 32bitovou architekturu MIPS:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_MIPS, KS_MODE_MIPS32, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_MIPS, CS_MODE_MIPS32
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
        addiu   $sp,$sp,-8
        sw      $fp,4($sp)
        move    $fp,$sp
        sw      $4,8($fp)
        sw      $5,12($fp)
        lw      $3,8($fp)
        lw      $2,12($fp)
        nop
        addu    $2,$3,$2
        move    $sp,$fp
        lw      $fp,4($sp)
        addiu   $sp,$sp,8
        jr      $31
        nop
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_MIPS, KS_MODE_MIPS32)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS32)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Z výsledku je patrné, že MIPS patří mezi klasické RISCové architektury, v níž mají všechny instrukce shodnou délku čtyř bajtů, podobně jako u ARM32 či AArch64:

0x00:   f8 ff bd 27             addiu   $sp, $sp, -8
0x04:   04 00 be af             sw      $fp, 4($sp)
0x08:   25 f0 a0 03             move    $fp, $sp
0x0c:   08 00 c4 af             sw      $a0, 8($fp)
0x10:   0c 00 c5 af             sw      $a1, 0xc($fp)
0x14:   08 00 c3 8f             lw      $v1, 8($fp)
0x18:   0c 00 c2 8f             lw      $v0, 0xc($fp)
0x1c:   00 00 00 00             nop
0x20:   21 10 62 00             addu    $v0, $v1, $v0
0x24:   25 e8 c0 03             move    $sp, $fp
0x28:   04 00 be 8f             lw      $fp, 4($sp)
0x2c:   08 00 bd 27             addiu   $sp, $sp, 8
0x30:   08 00 e0 03             jr      $ra
0x34:   00 00 00 00             nop
0x38:   00 00 00 00             nop

Pro zajímavost se nyní pokusme přepnout na 64bitovou variantu MIPSu, a to bez změny překládaného kódu:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_MIPS, KS_MODE_MIPS64, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_MIPS, CS_MODE_MIPS64
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
        addiu   $sp,$sp,-8
        sw      $fp,4($sp)
        move    $fp,$sp
        sw      $4,8($fp)
        sw      $5,12($fp)
        lw      $3,8($fp)
        lw      $2,12($fp)
        nop
        addu    $2,$3,$2
        move    $sp,$fp
        lw      $fp,4($sp)
        addiu   $sp,$sp,8
        jr      $31
        nop
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_MIPS, KS_MODE_MIPS64)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS64)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Výsledek je v tomto případě totožný s předchozím kódem:

0x00:   f8 ff bd 27             addiu   $sp, $sp, -8
0x04:   04 00 be af             sw      $fp, 4($sp)
0x08:   25 f0 a0 03             move    $fp, $sp
0x0c:   08 00 c4 af             sw      $a0, 8($fp)
0x10:   0c 00 c5 af             sw      $a1, 0xc($fp)
0x14:   08 00 c3 8f             lw      $v1, 8($fp)
0x18:   0c 00 c2 8f             lw      $v0, 0xc($fp)
0x1c:   00 00 00 00             nop
0x20:   21 10 62 00             addu    $v0, $v1, $v0
0x24:   25 e8 c0 03             move    $sp, $fp
0x28:   04 00 be 8f             lw      $fp, 4($sp)
0x2c:   08 00 bd 27             addiu   $sp, $sp, 8
0x30:   08 00 e0 03             jr      $ra
0x34:   00 00 00 00             nop
0x38:   00 00 00 00             nop

Některé mikroprocesory s architekturou MIPS používají kódování little endian zatímco jiné big endian. Jedná se o způsob fyzického uložení kódů instrukcí (čtyři bajty) popř. dat do operační paměti – může se začínat bajtem s nejnižší váhou nebo naopak bajtem s váhou nejvyšší (další možné kombinace nejsou využity – což je ostatně jen dobře). Frameworky Capstone a Keystone tato odlišná kódování plně podporují, což je ostatně patrné i z následujícího příkladu, v němž explicitně vyžadujeme kódování big endian:

# import všech symbolů assembleru použitých ve skriptu
from keystone import Ks, KS_ARCH_MIPS, KS_MODE_MIPS64, KS_MODE_BIG_ENDIAN, KsError
 
# import všech symbolů disassembleru použitých ve skriptu
from capstone import Cs, CS_ARCH_MIPS, CS_MODE_MIPS64, CS_MODE_BIG_ENDIAN
 
 
# instrukce, které se mají přeložit assemblerem
CODE = """
        addiu   $sp,$sp,-8
        sw      $fp,4($sp)
        move    $fp,$sp
        sw      $4,8($fp)
        sw      $5,12($fp)
        lw      $3,8($fp)
        lw      $2,12($fp)
        nop
        addu    $2,$3,$2
        move    $sp,$fp
        lw      $fp,4($sp)
        addiu   $sp,$sp,8
        jr      $31
        nop
"""
 
try:
    # inicializace assembleru se specifikací architektury a popř. i režimu
    ks = Ks(KS_ARCH_MIPS, KS_MODE_MIPS64 + KS_MODE_BIG_ENDIAN)
 
    # vlastní překlad (assembling)
    encoding, count = ks.asm(CODE)
 
    # disassembling binární sekvence kódů
    md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS64 + CS_MODE_BIG_ENDIAN)
 
    # vypsat podrobnější informace o každé instrukci
    for i in md.disasm(bytes(encoding), 0x0000):
        # převod pole bajtů na řetězec s hexa hodnotami
        dump = i.bytes.hex(" ")
 
        # výpis informací o instrukci
        print("0x{:02x}:\t{:20s}\t{:s}\t{:s}".format(i.address, dump, i.mnemonic, i.op_str))
 
except KsError as e:
    print("ERROR: %s" % e)

Povšimněte si, že hodnoty bajtů, do nichž jsou instrukce zakódovány, jsou skutečně oproti předchozí dvojici demonstračních příkladů prohozeny:

0x00:   27 bd ff f8             addiu   $sp, $sp, -8
0x04:   af be 00 04             sw      $fp, 4($sp)
0x08:   03 a0 f0 25             move    $fp, $sp
0x0c:   af c4 00 08             sw      $a0, 8($fp)
0x10:   af c5 00 0c             sw      $a1, 0xc($fp)
0x14:   8f c3 00 08             lw      $v1, 8($fp)
0x18:   8f c2 00 0c             lw      $v0, 0xc($fp)
0x1c:   00 00 00 00             nop
0x20:   00 62 10 21             addu    $v0, $v1, $v0
0x24:   03 c0 e8 25             move    $sp, $fp
0x28:   8f be 00 04             lw      $fp, 4($sp)
0x2c:   27 bd 00 08             addiu   $sp, $sp, 8
0x30:   03 e0 00 08             jr      $ra
0x34:   00 00 00 00             nop
0x38:   00 00 00 00             nop

Pro porovnání – takto vypadá varianta little endian:

0x00:   f8 ff bd 27             addiu   $sp, $sp, -8
0x04:   04 00 be af             sw      $fp, 4($sp)
0x08:   25 f0 a0 03             move    $fp, $sp
0x0c:   08 00 c4 af             sw      $a0, 8($fp)
0x10:   0c 00 c5 af             sw      $a1, 0xc($fp)
0x14:   08 00 c3 8f             lw      $v1, 8($fp)
0x18:   0c 00 c2 8f             lw      $v0, 0xc($fp)
0x1c:   00 00 00 00             nop
0x20:   21 10 62 00             addu    $v0, $v1, $v0
0x24:   25 e8 c0 03             move    $sp, $fp
0x28:   04 00 be 8f             lw      $fp, 4($sp)
0x2c:   08 00 bd 27             addiu   $sp, $sp, 8
0x30:   08 00 e0 03             jr      $ra
0x34:   00 00 00 00             nop
0x38:   00 00 00 00             nop

18. Závěrečné zhodnocení

Autoři frameworků Keystone a Capstone si vytyčili poměrně velký úkol: vytvoření jádra univerzálního assembleru a disassembleru, které by bylo možné využít jak v nástrojích volaných přes příkazovou řádku, tak i například přímo v rámci IDE či nějakého specializovaného nástroje. Nutno říci, že tohoto cíle prozatím nebylo plně dosaženo, protože v praxi je zapotřebí pracovat s kódem v assembleru rozděleným do sekcí (segmentů), generovat objektový kód pro linker atd. Ovšem už současná použitelnost obou frameworků je poměrně dobrá, protože jsou podporovány všechny mainstreamové architektury a jejich varianty.

Navíc už dnes existuje rozhraní mezi Capstone a Keystone pro mnoho programovacích jazyků (nejenom C/C++, ostatně dnes jsme používali Python) a samotný assembler programátorům poskytuje podobné funkce jako například GNU Assembler. Zlepšit je však nutné detekci a hlášení chyb a možná by bylo zajímavé samotná rozhraní pro Capstone a Keystone vylepšit tak, aby bylo umožněno přímé spuštění přeloženého kódu (což ovšem s sebou nese bezpečnostní rizika).

bitcoin školení listopad 24

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

Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 (nikoli ovšem pro starší verze Pythonu 2!) byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Demonstrační příklad Stručný popis příkladu Cesta
1 disasm_x86_64.py zpětný překlad obsahu binárního souboru https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_x86_64.py
2 disasm_x86_64_bytes.py zpětný překlad obsahu binárního souboru, výpis zakódovaných instrukcí https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_x86_64_bytes.py
3 disasm_x86_64_offset_A.py explicitní specifikace bázové adresy https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_x86_64_offset_A.py
4 disasm_x86_64_offset_B.py práce s binárním souborem, kde instrukce začínají od zadaného offsetu https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_x86_64_offset_B.py
5 disasm_x86_64_offset_C.py práce s binárním souborem, kde instrukce začínají od zadaného offsetu https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_x86_64_offset_C.py
6 disasm_x86_64_offset_D.py práce s binárním souborem, kde instrukce začínají od zadaného offsetu https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_x86_64_offset_D.py
7 disasm_mips.py zpětný překlad nativního kódu určeného pro architekturu MIPS https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/di­sasm_mips.py
8 asm_disasm_x86_64.py překlad assembleru do nativního kódu a zpět na platformě x86–64 https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_x86_64.py
9 asm_disasm_x86_64_offset.py specifikace offsetu při překladu a zpětném překladu https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_x86_64_offset.py
10 asm_disasm_arm.py překlad assembleru do nativního kódu a zpět na platformě ARM32 https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_arm.py
11 asm_disasm_thumb.py překlad assembleru do nativního kódu a zpět na platformě ARM32 s Thumb https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_thumb.py
12 asm_disasm_aarch64.py překlad assembleru do nativního kódu a zpět na platformě AArch64 https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_aarch64.py
13 asm_disasm_mips1.py překlad assembleru do nativního kódu a zpět na platformě MIPS, první varianta https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_mips1.py
14 asm_disasm_mips2.py překlad assembleru do nativního kódu a zpět na platformě MIPS, druhá varianta https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_mips2.py
15 asm_disasm_mips3.py překlad assembleru do nativního kódu a zpět na platformě MIPS, třetí varianta https://github.com/tisnik/most-popular-python-libs/blob/master/capstone/as­m_disasm_mips3.py

Pro úplnost si uveďme i odkazy na demonstrační příklady založené na frameworku Keystone, které byly popsané minule:

# Demonstrační příklad Stručný popis příkladu Cesta
1 first_steps_16bit_A.py překlad 16bitového kódu (zapsaného na jediném řádku) mikroprocesorů řady x86 https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/fir­st_steps_16bit_A.py
2 first_steps_16bit_B.py překlad 16bitového kódu (zapsaného na více řádcích) mikroprocesorů řady x86 https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/fir­st_steps_16bit_B.py
3 first_steps_32bit.py překlad 32bitového kódu mikroprocesorů řady x86 https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/fir­st_steps_32bit.py
4 first_steps_64bit.py překlad 32bitového kódu mikroprocesorů řady x86–64 https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/fir­st_steps_64bit.py
       
5 att_syntax_A.py kód zapsaný v assembleru, který využívá syntaxi AT&T (nekorektní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/at­t_syntax_A.py
6 att_syntax_B.py kód zapsaný v assembleru, který využívá syntaxi AT&T (korektní varianta) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/at­t_syntax_B.py
       
7 loop_A.py kód zapsaný v assembleru, v němž je použita programová smyčka https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/loop_A.py
8 loop_B.py kód zapsaný v assembleru, v němž jsou použity vnořené programové smyčky https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/loop_B.py
9 loops_dump.asm výsledek získaný skriptem loop_A.py a nástrojem objdump https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/lo­ops_dump.asm
       
10 into_binary_A.py překlad assembleru do binární formy (smyčky) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/in­to_binary_A.py
11 into_binary_B.py překlad assembleru do binární formy („Hello world“) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/in­to_binary_B.py
12 into_binary_C.py překlad assembleru do binární formy („Hello world“) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/in­to_binary_C.py
13 into_binary_D.py překlad assembleru do binární formy (využití AT&T syntaxe) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/in­to_binary_D.py
       
14 hello_world.asm program typu „Hello world“ napsaný v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/he­llo_world.asm
15 hello_world2.asm upravený program typu „Hello world“ napsaný v assembleru https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/he­llo_world2.asm
16 hello_dump.asm zpětný překlad nástrojem objdump https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/he­llo_dump.asm
17 hello2_dump.asm zpětný překlad nástrojem objdump https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/he­llo2_dump.asm
       
18 subroutines.py překlad programu v assembleru, jenž používá podprogramy (subrutiny) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/su­broutines.py
19 subroutines.asm program zapsaný v assembleru, jenž používá podprogramy (subrutiny) https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/su­broutines.asm
20 subroutines_dump.asm zpětný překlad nástrojem objdump https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/su­broutines_dump.asm
       
21 disasm.py zpětný disassembling binárního kódu https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/disasm.py
       
22 att_syntax.asm kód v assembleru se syntaxí AT&T https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/at­t_syntax.asm
23 att_syntax_dump.asm zpětný překlad nástrojem objdump https://github.com/tisnik/most-popular-python-libs/blob/master/keystone/at­t_syntax_dump.asm

20. Odkazy na Internetu

  1. Keystone Engine na GitHubu
    https://github.com/keystone-engine/keystone
  2. Keystone: The Ultimate Assembler
    https://www.keystone-engine.org/
  3. The Ultimate Disassembler
    http://www.capstone-engine.org/
  4. Tutorial for Keystone
    https://www.keystone-engine.org/docs/tutorial.html
  5. Rozhraní pro Capstone na PyPi
    https://pypi.org/project/capstone/
  6. Rozhraní pro Keystone na PyPi
    https://pypi.org/project/keystone-engine/
  7. KEYSTONE: Next Generation Assembler Framework
    https://www.keystone-engine.org/docs/BHUSA2016-keystone.pdf
  8. AT&T Syntax versus Intel Syntax
    http://web.mit.edu/rhel-doc/3/rhel-as-en-3/i386-syntax.html
  9. AT&T assembly syntax and IA-32 instructions
    https://gist.github.com/mishu­rov/6bcf04df329973c15044
  10. ARM GCC Inline Assembler Cookbook
    http://www.ethernut.de/en/do­cuments/arm-inline-asm.html
  11. Extended Asm – Assembler Instructions with C Expression Operands
    https://gcc.gnu.org/online­docs/gcc/Extended-Asm.html
  12. ARM inline asm secrets
    http://hardwarebug.org/2010/07/06/arm-inline-asm-secrets/
  13. 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
  14. GCC-Inline-Assembly-HOWTO
    http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
  15. A Brief Tutorial on GCC inline asm (x86 biased)
    http://www.osdever.net/tu­torials/view/a-brief-tutorial-on-gcc-inline-asm
  16. GCC Inline ASM
    http://locklessinc.com/ar­ticles/gcc_asm/
  17. GNU Assembler Examples
    http://cs.lmu.edu/~ray/no­tes/gasexamples/
  18. X86 Assembly/Arithmetic
    https://en.wikibooks.org/wi­ki/X86_Assembly/Arithmetic
  19. Art of Assembly – Arithmetic Instructions
    http://oopweb.com/Assembly/Do­cuments/ArtOfAssembly/Volu­me/Chapter6/CH06–2.html
  20. The GNU Assembler Tutorial
    http://tigcc.ticalc.org/doc/gnu­asm.html
  21. The GNU Assembler – macros
    http://tigcc.ticalc.org/doc/gnu­asm.html#SEC109
  22. ARM subroutines & program stack
    http://www.toves.org/books/armsub/
  23. Generating Mixed Source and Assembly List using GCC
    http://www.systutorials.com/240/ge­nerate-a-mixed-source-and-assembly-listing-using-gcc/
  24. Calling subroutines
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.kui0100a/armasm_cih­cfigg.htm
  25. Linux assemblers: A comparison of GAS and NASM
    http://www.ibm.com/develo­perworks/library/l-gas-nasm/index.html
  26. Programovani v assembleru na OS Linux
    http://www.cs.vsb.cz/gryga­rek/asm/asmlinux.html
  27. Is it worthwhile to learn x86 assembly language today?
    https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1
  28. Why Learn Assembly Language?
    http://www.codeproject.com/Ar­ticles/89460/Why-Learn-Assembly-Language
  29. Is Assembly still relevant?
    http://programmers.stackex­change.com/questions/95836/is-assembly-still-relevant
  30. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/on­lamp/2004/05/06/writegreat­code.html
  31. Assembly language today
    http://beust.com/weblog/2004/06/23/as­sembly-language-today/
  32. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubri­ky/assembler/vyznam-assembleru-dnes-155960cz
  33. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/li­nux/prog/asm.html
  34. Online x86 / x64 Assembler and Disassembler
    https://defuse.ca/online-x86-assembler.htm#disassembly

Autor článku

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