Курс Лекція 4
▶ СлайдиСРС 07+08⌨️ команди 16 · 11 нових
Конспект · Лекція 4

ЧОМУ ПРОГРАМИ ПСУЮТЬ ПАМ'ЯТЬ

Ціна помилки керування пам'яттю

Ключова ідея. Помилка керування пам'яттю коштує дорожче за час, витрачений на перевірки, які їй запобігають.
Демо. Той самий патерн use-after-free: у C код компілюється і поводиться непередбачувано, у Rust компілятор відмовляється його зібрати.
🧩 СРС ДО ЦІЄЇ ЛЕКЦІЇ · 2-га година заняття

Після презентації теми — самостійна робота в аудиторії над двома темами. Вони ж готують до захисту та розширення ЛР4. Матеріали публікуються в Google Classroom разом із лекцією.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Вступ

Кожна серйозна аварія в проді — від витоку персональних даних до віддаленого виконання коду — рано чи пізно зводиться до одного питання: хто і коли мав право торкнутися цієї ділянки пам'яті. Компілятори мов на кшталт C і C++ довіряють відповідь програмісту повністю: вказівник — це просто число, а операція free() — просто повідомлення аллокатору "ця ділянка більше не потрібна", без жодної гарантії, що ніхто її потім не прочитає. Ця лекція розбирає, чому таке довір'я коштує дорого, порівнює три історичні підходи до керування пам'яттю і вводить ідею, яку ми детально розберемо в лекціях 5–7: перевірку правил володіння (ownership) пам'яттю на етапі компіляції.

Як програма бачить пам'ять: стек і купа

Кожен процес отримує від операційної системи власний адресний простір, і в межах цього простору є щонайменше дві принципово різні ділянки, з якими працює звичайна програма.

Стек (stack) — це пам'ять поточного виклику функції. Кожен виклик додає новий "кадр" (stack frame) з локальними змінними фіксованого розміру, які відомі ще на етапі компіляції. Коли функція завершується, її кадр просто відкидається — це операція зі складністю O(1), без жодного пошуку вільної ділянки. Саме тому робота зі стеком настільки дешева: виділення пам'яті — це зсув одного вказівника (stack pointer), а звільнення — зворотний зсув.

Купа (heap) — це пам'ять із динамічним часом життя, розмір якої заздалегідь невідомий або який має пережити виклик функції, що його створив. Виділення ділянки на купі — це звернення до аллокатора (malloc у C, глобальний аллокатор у Rust), який мусить знайти вільний блок потрібного розміру серед фрагментованої пам'яті — операція значно дорожча за зсув вказівника.

Ключова відмінність для цієї лекції: вказівник на купу можна скопіювати скільки завгодно разів, але виділена ділянка — це одна фізична адреса. Якщо копій вказівника кілька, а власник ділянки не визначений однозначно — виникає питання, хто саме відповідає за її звільнення. Практично всі помилки керування пам'яттю, які ми розберемо нижче, — це наслідок саме цієї невизначеності.

Стек Купа
Розмір Фіксований, відомий під час компіляції Динамічний, може бути невідомий заздалегідь
Швидкість виділення O(1), зсув вказівника Повільніше, пошук вільного блоку
Час життя Прив'язаний до області видимості Керується явно або через володіння
Типовий доступ Локальні змінні функції Box, Vec, String, об'єкти на купі

Класичні помилки керування пам'яттю

Ці п'ять патернів формують левову частку критичних уразливостей (CWE Top 25) уже десятиліттями.

Use-after-free. Пам'ять звільнили, але вказівник на неї лишився і його продовжують використовувати. Ділянка може бути ще фізично незмінена (тоді "все працює") або вже перевикористана під інший об'єкт (тоді читання дає сміття, а запис ламає чужі дані).

int *p = malloc(sizeof(int));
*p = 3101;             // 0xC1D
free(p);
printf("%d\n", *p);   // undefined behavior: читаємо звільнену пам'ять
*p = 100;              // undefined behavior: пишемо у звільнену пам'ять

Double free. Той самий блок звільняють двічі. Більшість аллокаторів веде внутрішні структури метаданих (наприклад, зв'язний список вільних блоків) прямо в самій пам'яті — подвійне звільнення може пошкодити ці структури й дати зловмиснику примітив для довільного запису.

Buffer overflow. Запис виходить за межі виділеного блоку — найчастіше через відсутність перевірки довжини вхідних даних. Класика: strcpy у буфер фіксованого розміру без перевірки довжини джерела.

Memory leak. Пам'ять виділили і забули звільнити. У короткоживучих програмах це непомітно, у довгоживучих сервісах — поступове вичерпання RAM і, зрештою, аварійне завершення процесу (OOM-killer).

Dangling pointer. Вказівник, що вказує на вже звільнену або взагалі невалідну ділянку пам'яті (наприклад, повернений з функції вказівник на локальну змінну, яка вже знищена).

Чому компілятор C це дозволяє

C спроєктовано під філософію "довіряй програмісту, не заважай йому". Компілятор не відстежує, чи ділянка, на яку вказує int *p, ще жива — з погляду типової системи int * після free(p) нічим не відрізняється від int * до нього. free() — це лише повідомлення аллокатору "поверни блок у пул вільних", саме значення за адресою фізично може ще лежати незмінним якийсь час, тому баг часто "працює" на тестах розробника і ламається у проді, коли той самий блок пам'яті вже перевикористаний іншим виділенням під навантаженням.

Інструменти на кшталт AddressSanitizer (-fsanitize=address) чи Valgrind ловлять такі помилки — але лише в рантаймі, і лише на тому шляху виконання, який реально був пройдений під час тесту. Якщо тест не зачепив саме ту гілку коду, де стається use-after-free, санітайзер мовчатиме, а баг проникне в прод.

Ціна помилки: цифри, а не інтуїція

За звітами Microsoft Security Response Center, близько 70% CVE високої критичності в коді Microsoft за багаторічний період — це саме помилки безпеки пам'яті (use-after-free, buffer overflow, тощо). Google повідомляв подібну частку для кодової бази Chrome та Android. Це не статистична випадковість — це системний наслідок того, що ручне керування пам'яттю у великому кодовому базі гарантовано, рано чи пізно, дає збій людського фактора хоч в одному з тисяч місць виділення/звільнення.

Ціна помилки — не в моменті написання рядка коду, а в моменті, коли цей рядок зустрічається з реальним, часто зловмисним вхідним потоком. Use-after-free і buffer overflow — стандартний будівельний блок експлойтів: контрольований запис у чужу пам'ять дає зловмиснику примітив "написати довільні байти за довільною адресою", з якого будується виконання довільного коду.

Три підходи до керування пам'яттю

Підхід Приклади мов Контроль Накладні витрати в рантаймі Клас помилок
Ручне C, C++ Повний, на програмісті Немає use-after-free, double free, leak можливі
Збирач сміття (GC) Java, Python, Go, JavaScript Автоматичний Паузи (stop-the-world), накладні витрати трасування Виключено (переважно), ціна — передбачуваність
Володіння (ownership) Rust Перевіряється компілятором Немає Виключено на етапі компіляції

Ручне керування дає максимальний контроль і максимальну відповідальність: кожен malloc має рівно один відповідний free, і людина мусить це утримати в голові на весь час життя проєкту, включно з усіма змінами, які внесуть інші розробники через рік.

Збирач сміття знімає це навантаження з програміста: рантайм відстежує досяжність об'єктів і звільняє недосяжні автоматично. Ціна — непередбачувані паузи (GC stop-the-world), додаткова пам'ять для метаданих і трасування, і накладні витрати CPU на сам процес збирання сміття. Для серверів реального часу чи вбудованих систем це часто неприйнятно.

Володіння (ownership) — підхід Rust: правила керування пам'яттю (хто власник, коли звільняти) перевіряються статично, під час компіляції, і при цьому в скомпільованому бінарнику не лишається жодного рантайм-механізму — ані збирача сміття, ані підрахунку посилань за замовчуванням. Це спроба поєднати безпеку GC-мов зі швидкістю C, не платячи компромісом у продуктивності рантайму.

Ідея Rust коротко

У кожного значення в Rust є рівно один власник. Коли власник виходить з області видимості, компілятор автоматично вставляє виклик звільнення пам'яті — без участі програміста і без збирача сміття в рантаймі. Спроба використати значення після того, як воно вже "мертве" (наприклад, після move в іншу змінну чи явного drop), — це помилка компіляції, а не помилка виконання:

let p = Box::new(42);
drop(p);              // явно звільняємо пам'ять
println!("{}", p);   // error[E0382]: use of moved value: `p`

// компілятор відмовляється зібрати цей код —
// баг знайдено ДО того, як програма хоч раз запустилась

Це і є принципова відмінність від санітайзерів: помилка знайдена не на конкретному шляху виконання під час тестування, а структурно, для всіх можливих шляхів одразу, ще до того, як з'явився скомпільований бінарник.

Знайомство з інструментарієм: Rust і Cargo

Щоб працювати з моделлю володіння практично, потрібен інструментарій. Rust встановлюється через rustup — офіційний менеджер тулчейнів:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustc --version    # rustc 1.8x.0 (stable)
cargo --version    # cargo — офіційний менеджер збірки і пакетів

rustup керує кількома каналами (stable, beta, nightly) і дозволяє перемикатися між ними (rustup default stable). У курсі ми працюємо виключно з каналом stable.

Cargo — це і збирач, і менеджер залежностей, і тест-раннер в одному інструменті. Новий проєкт створюється командою:

cargo new lockpick
cd lockpick

Це генерує мінімальну структуру:

lockpick/
├── Cargo.toml   # маніфест: назва, версія, залежності
├── Cargo.lock   # точні версії залежностей (генерується автоматично)
└── src/
    └── main.rs  # точка входу

Cargo.toml описує пакет декларативно:

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

[dependencies]
# зовнішні крейти додаються тут, наприклад:
# serde = "1.0"

Ключові команди, якими користуються щодня:

Команда Що робить
cargo build Компілює проєкт (режим debug за замовчуванням)
cargo run Компілює й одразу запускає
cargo check Перевіряє, чи компілюється код, без генерації бінарника — набагато швидше за build
cargo test Компілює й запускає всі юніт- та інтеграційні тести
cargo add <crate> Додає залежність у Cargo.toml
cargo fmt Автоматично форматує код за офіційним стилем
cargo clippy Лінтер: ловить типові антипатерни й підказує ідіоматичніший код

Юніт-тести в Rust

Тести — невіддільна частина мови, а не окрема бібліотека, яку треба підключати. Найпростіший тест живе прямо поруч із кодом, у вкладеному модулі, позначеному атрибутом #[cfg(test)], щоб тестовий код не потрапляв у релізний бінарник:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;   // імпортуємо все з батьківського модуля

    #[test]
    fn adds_two_positive_numbers() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn adds_negative_and_positive() {
        assert_eq!(add(-2, 3), 1);
    }
}

Запуск cargo test компілює проєкт у спеціальному тестовому режимі й запускає кожну функцію, позначену #[test], паралельно в окремому потоці. Тест вважається неуспішним, якщо він панікує — найчастіше через макроси assert!, assert_eq! чи assert_ne!, які панікують із детальним повідомленням, коли умова не виконана:

running 2 tests
test tests::adds_two_positive_numbers ... ok
test tests::adds_negative_and_positive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured

Тести — це той самий механізм, яким автотести перевірятимуть лабораторні роботи цього курсу: функція, що не проходить assert_eq!, провалює тест так само надійно, як і код, що взагалі не компілюється.

Стек і купа в термінах Rust

За замовчуванням значення в Rust живуть на стеку: прості типи фіксованого розміру (i32, f64, bool, char, кортежі з таких типів) копіюються і знищуються разом зі своєю областю видимості без жодного звернення до аллокатора. Щойно потрібна ділянка динамічного розміру чи ділянка, яка має пережити поточну функцію, — використовується тип, що всередині утримує вказівник на купу: String, Vec<T>, Box<T>. Сам Box<T> як змінна лежить на стеку (це просто вказівник плюс трохи метаданих), а дані, на які він вказує, — на купі.

let x: i32 = 5;          // повністю на стеку
let boxed: Box<i32> = Box::new(5);   // вказівник на стеку, значення 5 — на купі

Це розділення прямо готує ґрунт для наступної лекції: саме через те, що типи на купі не можна дешево копіювати, Rust за замовчуванням переміщує (move) власність замість того, щоб копіювати дані — і саме це унеможливлює double free структурно, ще до появи перевірки запозичень (borrow checker) як такої.

Підводні камені

Пастка Чому це проблема Як уникнути
Плутати cargo build і cargo check під час розробки build генерує повний бінарник — повільніше Використовуйте cargo check для швидкої ітерації, build/run — коли треба виконати
Забути #[cfg(test)] над тестовим модулем Тестовий код і залежності (наприклад, use super::*) потрапляють у реліз Завжди огортайте тести в #[cfg(test)] mod tests { ... }
Плутати assert_eq! з ручним if + println! Ручна перевірка не зупиняє тест і не дає структурованого звіту Використовуйте макроси assert*! — вони панікують із діагностикою
Думати, що санітайзер C замінює перевірку компілятора ASan/Valgrind ловлять лише пройдені шляхи виконання Статична перевірка (як у Rust) покриває всі шляхи одразу

Підсумок

Помилки керування пам'яттю — use-after-free, double free, переповнення буфера, витоки — не є рідкісною екзотикою: за даними Microsoft і Google, вони формують більшість критичних CVE в масштабних кодових базах на C/C++. Причина системна: C довіряє програмісту відстежувати життєвий цикл кожної ділянки пам'яті вручну, а компілятор жодним чином це не перевіряє; санітайзери ловлять такі помилки лише в рантаймі й лише на пройдених шляхах виконання. Rust пропонує третій шлях між ручним керуванням і збирачем сміття: правила володіння пам'яттю перевіряються статично, під час компіляції, без жодних накладних витрат у скомпільованому бінарнику. Інструментарій для цього — rustup, cargo new, cargo build/run/test — стандартний і вбудований, включно з тестуванням через #[test] та assert_eq!. Наступні три лекції розберуть механізм цієї перевірки в деталях: володіння (Л5), запозичення (Л6), Option/Result (Л7).

Розбір за слайдами

План лекції

Фокус: ціна помилки, а не синтаксис жодної з мов

Стек і купа

Стек дешевий і безпечний за конструкцією; купа — джерело всіх подальших проблем

СТЕК (швидко, LIFO) КУПА (heap) let s = String ptr,len,cap x: i32 = 5 на стеку "HELLO" дані рядка let s2 = s; // MOVE s — недійсний moved out s2 → "HELLO" один власник рівно один власник даних у купі → немає подвійного звільнення

Стек проти купи

ОзнакаСтекКупа
Розмірфіксований, відомий під час компіляціїдинамічний, часто невідомий заздалегідь
ВиділенняO(1): зсув вказівника стекапошук вільного блоку аллокатором
Час життяобласть видимості функціїявно (free) або через володіння
Типові данілокальні змінні, i32, boolBox, Vec, String, об'єкти
Джерело помилокпереповнення стека (рекурсія)use-after-free, double free, витік

Стек дешевий і безпечний за конструкцією; купа — джерело всіх подальших проблем

Класичні помилки керування пам'яттю

💀
Use-after-freeвикористання пам'яті після її звільнення
✂️
Double freeповторне звільнення тієї самої ділянки
🌊
Buffer overflowзапис за межі виділеного блоку
🕳️
Memory leakвиділили і забули звільнити
🧷
Dangling pointerвказівник на вже звільнену ділянку
Data raceдва потоки, ті самі дані, хоч один запис (Л6)

П'ять перших формують левову частку критичних уразливостей CWE Top 25 десятиліттями

Use-after-free на C

c
int *p = malloc(sizeof(int));
*p = 3101;             // 0xC1D
free(p);
printf("%d\n", *p);   // UB: читаємо звільнену пам'ять
*p = 100;              // UB: пишемо у звільнену пам'ять

Компілятор мовчить: жодного попередження за замовчуванням

Чому use-after-free «працює» на тестах

  1. 1
    mallocаллокатор віддає блок; вказівник p — просто число
  2. 2
    free(p)блок повернуто в пул; байти фізично ще лежать на місці
  3. 3
    Читання *p на тестізначення ще там — тест зелений, компілятор мовчить
  4. 4
    Прод під навантаженнямблок перевикористано під інший об'єкт
  5. 5
    Крах або експлойтчитання дає сміття, запис ламає чужі дані

Санітайзери (ASan, Valgrind) ловлять це під час виконання — і лише на пройдених шляхах

Ціна помилки

≈70%
CVE високої критичності Microsoft
помилки безпеки пам'яті (MSRC, 2006–2018)
≈70%
серйозних багів безпеки Chromium
Google: та сама частка для Chrome та Android
№1
CWE Top 25 (2021–2023)
CWE-787 Out-of-bounds Write
0
попереджень gcc за замовчуванням
на use-after-free із попереднього слайда

Ціна — не в моменті написання рядка, а коли рядок зустрічає реальний, часто зловмисний вхідний потік

Три підходи до керування пам'яттю

ПідхідМовиХто перевіряєЦіна в рантайміПомилки пам'яті
Ручне (malloc/free)C, C++програмістнемаєможливі всі п'ять
Збирач сміття (GC)Java, Python, Go, JSрантаймпаузи stop-the-world, трасуванняздебільшого виключені
Володіння (ownership)Rustкомпіляторнемаєвиключені на етапі компіляції

Rust — спроба взяти безпеку GC-мов і швидкість C без компромісу в рантаймі

Ідея Rust: володіння перевіряється компілятором

Той самий баг у Rust не компілюється

rust
let p = Box::new(42);
drop(p);              // явно звільняємо пам'ять
println!("{}", p);   // error[E0382]: use of moved value: `p`

// компілятор відмовляється зібрати цей код —
// баг знайдено ДО того, як програма хоч раз запустилась

Помилка виявлена на етапі компіляції, а не на етапі експлуатації

Одна помилка — дві ціни

USE-AFTER-FREE: ДВІ ЦІНИ ОДНІЄЇ ПОМИЛКИ C · ПОМИЛКУ ЗНАЙДЕ ПРОД malloc p ← блок *p = 3101 p живий free(p) блок у пул p той самий p — просто число байти ще на місці *p = UB · крах у проді ⟶ час RUST · ПОМИЛКУ ЗНАЙДЕ КОМПІЛЯТОР Box::new(42) p — власник p живий власник один drop(p) або кінець { } E0382 код не збереться спроба вжити p жодного GC у рантаймі drop автоматичний бінарника з багом не буде ⟶ час звільненняUB у рантайміпомилка компіляції ціну помилки платять або на етапі компіляції, або в проді — третього не буває

Ліворуч C: free(p) лишає p висячим, *p — UB уже в проді. Праворуч Rust: після drop(p) використання p — error[E0382], бінарник просто не збереться

Живе демо

  1. Компілюємо і запускаємо приклад use-after-free на C (gcc, без прапорів санітайзера) — програма щось виводить, поведінка не гарантована
  2. Компілюємо той самий приклад із санітайзером (-fsanitize=address) — тепер помилка виявляється, але тільки під час виконання
  3. Пишемо еквівалентний код на Rust з Box і drop(p) перед використанням
  4. Запускаємо `cargo build` — компіляція падає з error[E0382] ще до запуску

Очікувано: C: непередбачувана поведінка або крах у рантаймі. Rust: помилка компіляції, програма взагалі не збирається

Санітайзер проти перевірки компілятора

ASan / Valgrind (C, C++)
  • Ловить під час виконання
  • Лише пройдені шляхи виконання
  • Потребує покриття тестами
  • Сповільнює програму в кілька разів
  • Баг уже в бінарнику
vs
Перевірка володіння (Rust)
  • Ловить під час компіляції
  • Усі шляхи виконання одразу
  • Не залежить від тестів
  • Нуль накладних витрат у рантаймі
  • Бінарник із багом не з'явиться

Санітайзер знаходить помилку після запуску; компілятор — до появи бінарника

Компілятор — союзник, не ворог

Встановлення Rust через rustup

sh.rustup.rscurl … | sh — один інсталятор
rustupканали stable · beta · nightly
stableкурс працює лише на stable
rustc + cargo--version — перевірка
rustup updateоновлення тулчейна

Один інструмент (rustup) керує компілятором, Cargo і документацією одразу

Перший проєкт: cargo new

bash
cargo new lockpick
cd lockpick
tree .
# lockpick/
# ├── Cargo.toml   ← маніфест: назва, версія, залежності
# ├── Cargo.lock   ← точні версії залежностей (генерується автоматично)
# └── src/
#     └── main.rs  ← точка входу

cargo new одразу ініціалізує git-репозиторій, якщо він ще не існує

Команди Cargo, які знадобляться щодня

КомандаЩо робитьКоли
cargo checkперевіряє компіляцію без бінарниканайшвидша ітерація під час написання
cargo build / runкомпілює / компілює й запускає (debug)коли треба виконати
cargo testзапускає юніт- та інтеграційні тестиперед кожним комітом; автотести ЛР
cargo add <crate>додає залежність у Cargo.tomlпотрібен зовнішній крейт
cargo fmtформатує за офіційним стилемперед комітом
cargo clippyлінтер антипатернівперед рев'ю

Перший юніт-тест

rust
fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn adds_two_positive_numbers() {
        assert_eq!(add(2, 3), 5);
    }
}

`#[cfg(test)]` виключає тестовий модуль з релізного бінарника

Cargo.toml проти Cargo.lock

Cargo.toml — маніфест
  • Пишеться вручну
  • [package]: name, version, edition = "2021"
  • [dependencies]: serde = "1.0"
  • "1.0" означає ^1.0 — сумісні 1.x.y
Cargo.lock — зафіксовані версії
  • Генерується автоматично
  • Точні версії, що реально зібралися
  • Комітиться в git для бінарників
  • Гарантує відтворювану збірку

Семантичне версіювання: мажорна.мінорна.патч; ^ дозволяє оновлення в межах мажорної

Демо: тест, що падає → тест, що проходить

  1. cargo new lockpick && cd lockpick
  2. Дописуємо функцію add(a, b) і тест, що навмисно очікує неправильний результат
  3. cargo test — бачимо FAILED і повідомлення "assertion `left == right` failed"
  4. Виправляємо очікуване значення в тесті, запускаємо cargo test знову — усі тести ok

Очікувано: Перший запуск: test result: FAILED. Другий запуск: test result: ok. 1 passed; 0 failed

Маршрут модуля 2

Л4 · ЛР4мотивація · Cargo і перший тест
Л5 · ЛР5володіння · borrow checker
Л6 · ЛР6запозичення · struct і enum
Л7 · ЛР7Option/Result · читання файлу

Наступні три лекції — як саме працює перевірка: володіння, запозичення, Option/Result

Тестування програми може показати наявність помилок, але ніколи — їхню відсутність. — Едсгер Дейкстра, «Notes on Structured Programming» (1970)

Підсумок

Джерела

🥚 ПАСХАЛКА
У прикладі use-after-free у виділену ділянку записано 3101 — це 0xC1D у шістнадцятковій системі. Число не випадкове: воно ще не раз зустрінеться в матеріалах курсу.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 16 · 11 нових

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo newнове
Створює новий проєкт із Cargo.toml і src/main.rs (cargo new lockpick; --lib — бібліотека).
cdнове
Переходить в інший каталог (change directory); cd .. — на рівень вище.
treeнове
Малює дерево каталогів і файлів — щоб побачити структуру проєкту одним поглядом.
cargo testнове
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.
curl
Консольний HTTP-клієнт: надсилає запит на URL і друкує відповідь (curl -i http://localhost:8080/ — разом із заголовками); також уміє завантажувати файли й скрипти.
sh
Мінімальна POSIX-оболонка; у маленьких образах (Alpine, BusyBox) є тільки вона, bash може бути відсутній.
rustc
Компілятор Rust; напряму викликається рідко (rustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.
rustupнове
Встановлювач і менеджер версій Rust: тулчейни, цілі (targets), компоненти.
rustup defaultнове
Обирає тулчейн за замовчуванням (rustup default stable).
cargo runнове
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo checkнове
Швидко перевіряє код компілятором без створення бінарника — для швидкого циклу «правка → помилки».
cargo addнове
Додає залежність у Cargo.toml (cargo add serde --features derive).
cargo fmtнове
Автоматично форматує код за стандартним стилем Rust (rustfmt).
cargo clippyнове
Лінтер: підказує ідіоматичніші й безпечніші варіанти коду.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.