NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

Lekce 1 - Úvod do programování v Assembleru

Vítejte u kurzu, ve kterém se spolu ponoříme až do hloubek, kam se vůbec jako programátoři můžeme dostat. Obejdeme programovací jazyk i kompiler a zjistíme, jak funguje samotný stroj. Budeme posílat přímo instrukce procesoru, pochopíme architekturu x86, BIOS, jak se adresuje paměť a zavádí operační systém. Tato problematika souvisí také s reverse-engineeringem, crackováním softwaru a samozřejmě hackingem. Naučí vás používat nástroje jako disassemblery, cheat enginy a lépe pracovat s debuggery.

Hacking - Základy assembleru

Kurz zahajme slavnou citací:

Don't learn to hack, hack to learn!

Předpoklady

Kurz předpokládá znalost alespoň základů fungování počítače po hardwarové stránce a zkušenost s libovolným vysokoúrovňovým programovacím jazykem (např. C nebo Java).

Proč se dnes učit Assembler?

Vždy mě zajímalo, jak věci v základu fungují a namísto toho, abych používal něco, co už existuje, jsem si raději udělal něco svého. Například máme doma rádio. A já, namísto toho, abych ho používal, začal jsem se stavbou vlastního AM rádia. A nebo většina z nás používá operační systém Windows... No a co jsem asi udělal... Ano, začal jsem pracovat na vlastním operačním systému v Assembleru.

Co se tu budeme učit je samozřejmě velmi speciální a úzké zaměření. Komerčně dnes tyto znalosti uplatníte ve sféře kyberbezpečnosti nebo při programování pro některá nízkoúrovňová embedded zařízení, případně pro high-performance programování. A nebo když se budete chtít pokusit donutit dělat nějakou cizí aplikaci to, co od ní chcete, případně si naprogramovat zavaděč operačního systému. To samozřejmě není tak snadné, ale někde se začít musí :)

Abychom pochopili, jak vůbec ASM funguje, podívejme se ve zkratce na historii programovacích jazyků.

Vývoj programovacích jazyků

1. generace jazyků - Strojový kód

Procesor počítače umí vykonávat jen omezené množství jednoduchých instrukcí, které jsou uloženy jako sekvence bitů, jsou to tedy čísla. Ta se mu obvykle zadávají v hexadecimální (šestnáctkové) soustavě. Instrukce jsou tak elementární, že umožňují pouze např. práci s registry procesoru (to je paměť v CPU) nebo skoky. Nelze např. jednoduše sečíst dvě čísla, musíme se na čísla dívat jako na adresy v paměti a takové sečtení čísel zabere několik instrukcí. Program sčítající dvě čísla by vypadal např. takto:

B853000000
BBFEFFFFFF
03C3
C3

Instrukce se procesoru předloží v binární podobě. Takovýto kód je samozřejmě extrémně nečitelný a závisí na instrukční sadě daného CPU. Každý počítačový program musí být nakonec do tohoto jazyka přeložen, aby mohl být na procesoru počítače spuštěn.

Strojový kód - Základy assembleru

2. generace jazyků - Assembler

Assembler neboli JSA (Jazyk Symbolických Adres) se objevil někdy v polovině 20. století. Konkrétně se jednalo o jazyk druhé generace. Od jazyků první generace se lišil tím, že namísto toho, abychom si museli pamatovat číselné kódy instrukcí, jsme mohli psát jejich slovní kódy (například: MOV, CMP, ADD).

Díky Assembleru bylo psaní programů jednodušší a přehlednější, než kdybychom je psali v číslech. Další výhodou bylo, že adresy v programu se nemusely měnit přepisováním celého programu jako tomu bylo u první generace. Co se týče kódu samotného, nemusely se opět např. složitě vypočítávat adresy skoků. Kód tedy začal být vůbec lidsky čitelný, i když není o nic jednodušší, než původní strojový kód.

Stejný program by v ASM vypadal takto:

.code
main:
mov eax,83
mov ebx,-2
add eax,ebx
ret
end main

Vidíme, že je to poněkud lidštější, ale stále nezasvěcení lidé vůbec netuší, jak program funguje.

3. generace jazyků

Jazyky v třetí generaci konečně nabízí uživateli určitou abstrakci nad tím, jak program vidí počítač, zaměřují se na to, jak program vidí člověk. Jsou označovány jako tzv. vyšší programovací jazyky (anglicky High Level Languages, někdy zkráceně HLL). Naše čísla jsou vnímána již jako proměnné, zdrojový kód připomíná matematický zápis.

Jedním z prvních vysokoúrovňových programovacích jazyků byl jazyk C. I když jej předběhl Fortran nebo Pascal, tak to byl právě jazyk C, který dobyl svět. Opět ten samý programy by v jazyce C vypadal takto:

int main(void)
{
    int a, b, c;
    a = 83;
    b = -2;
    c = a + b;
    return c;
}

V kontrastu současných objektově orientovaných moderních jazyků je jazyk C často označován jako jazyk nízkoúrovňový. Jak to tedy je? Od příchodu jazyka C zkrátka uběhlo již několik dekád a záleží s čím jej porovnáváme. V našem kontextu s ASM je nazýván vysokoúrovňovým, v kontextu např. s jazykem C# .NET je naopak nízkoúrovňový.

Všichni asi tušíme, co program dělá, sečte čísla 83 a -2 a výsledek uloží do proměnné c. U všech jazyků třetí generace je samozřejmě výhodou vysoká čitelnost.

Překlad vyšších programovacích jazyků

Tyto jazyky tedy mají svůj zdrojový kód v jazyce, kterému lidé dobře rozumí. Zdrojový kód se samozřejmě musí přeložit do binárního kódu, aby ho bylo možné na procesoru spustit. Tento překlad zajišťuje překladač (kompiler), který přeloží najednou celý program do strojového kódu. Některé kompilátory umějí kromě strojového kódu vygenerovat také kód assembleru.

Kompiler - Základy assembleru

Některé moderní programovací jazyky (např. C#, Java) se nepřekládají přímo do strojového kódu, ale používají binární mezikód. Ten se kompiluje do strojového kódu až na počítači uživatele (buď hned po instalaci aplikace nebo při jejím spuštění). To má výhodu v tom, že binární mezikód je nezávislý na procesoru. Vývojář aplikace tedy nemusí dávat ke stažení zvlášť verze pro x86, x64, nebo ARM. Nevýhodou je to, že uživatel musí mít nainstalovaný framework pro daný programovací jazyk.

Pokud budeme vědět, jak vypadají v ASM rutiny např. C překladače, budeme schopní pochopit, jak tyto programy fungují nebo je dokonce modifikovat, aniž bychom od nich měli zdrojový kód. Jedná se ale samozřejmě o velmi komplexní problematiku, protože sebemenší funkce vyústí ve velké množství ASM instrukcí a kompiler ve výsledné podobě ještě provádí četné optimalizace. Pojďme jim věnovat alespoň krátký odstavec.

Optimalizace překladače

Když bychom psali v čistém ASM, velmi pravděpodobně bude náš program pomalejší než ten samý program zkompilovaný např. z jazyka C. Jak je to možné? Kompiler totiž kód upraví tak, aby byl velmi rychlý a to často za cenu, že je pro člověka pak špatně čitelný. Určitě jste všichni někdy použili cyklus. Ten má nějakou řídící proměnnou, podmínku a zkrátka režii navíc. Proto se může někdy optimizer rozhodnout, že bude lepší cyklus nepřeložit, ale namísto toho kód jen několikrát zopakovat za sebou. Pokud vás problematika zajímá, doporučuji krásný článek Překladače pod pokličkou - optimalizace.

Specifika programování v Assembleru

A jak tedy takové programování v Assembleru vypadá?

Pokud si budeme programovat svůj vlastní operační systém, pak nemáme k dispozici žádné předem vytvořené funkce. Nebudeme moct použít žádné PrintString(), ClearScreen(), ani SetCursorPosition(). Úplně všechno si budeme muset vytvořit sami.

Pokud bychom vytvářeli aplikaci pro historický operační systém MS-DOS, pak máme k dispozici několik funkcí, které se volají přerušením INT 21H (k přerušením se v kurzu brzy dostaneme). Toto přerušení je dostupné pouze v operačním systému MS-DOS, což znamená, že ho (ani ostatní) nebudeme používat. My si vystačíme s přerušeními, která nabízí BIOS.

Při programování aplikací pro moderní operační systémy (např. Windows nebo Linux) můžeme v Assembleru využívat všechna API konkrétního operačního systému. Například můžeme číst a zapisovat data do souborů, ovládat aplikaci klávesnicí a myší, zobrazit text na obrazovce. Naopak ale nesmíme používat přerušení BIOSu. Také nemůžeme používat všechny instrukce procesoru. Některé instrukce jsou totiž privilegované a jsou povoleny pouze pro jádro operačního systému a ovladače. V naší aplikaci nemůžeme přímo přistupovat k hardware, používat přerušení nebo přepínat režimy procesoru.

Ukázka kódu

Na závěr úvodního dílu si ukážeme a popíšeme zdrojový kód funkce print_string pro výpis textu jen pomocí rutin BIOSu. Metodu budeme používat příště, kde si ji také vysvětlíme. Zde se na ni můžete podívat jen proto, abyste věděli, jak programování v ASM vypadá a do čeho se to vlastně pouštíme.

Funkce print_string:

print_string:
mov ah, 0x0e     ; Funkce BIOSu pro teletype output
xor bh, bh       ; Pro výpis vybereme page 0

.print_char:
lodsb            ; Načteme hodnotu z DS:SI do AL
or al, al        ; Provedeme bitový součet, který nastaví příslušné příznaky
jz short .return ; Pokud je nastaven příznak nuly (AL = NULL), skočíme na .return

int 10h          ; Vytiskneme znak
jmp .print_char  ; Opakujeme cyklus

.return:
ret              ; Vrátíme se na místo, odkud jsme metodu zavolali (na další instrukci)

No a to je pro dnešek vše.

V příští lekci, Assembler - Zásobník, si povíme o tom, co je to zásobník, jak se dá používat, a na co si dát pozor, když s ním pracujeme.


 

Všechny články v sekci
Základy assembleru
Přeskočit článek
(nedoporučujeme)
Assembler - Zásobník
Článek pro vás napsal Jakub Verner
Avatar
Uživatelské hodnocení:
65 hlasů
Autor se věnuje programování v x86 Assembleru.
Aktivity