Курс ЛР8 · Місія 8
Сюжет С.І.Д.↔ АкадемічнаСРС до ЛР⌨️ команди 4 · 1 нова

Місія 8: «Пряма Ін'єкція»

📡 С.І.Д.: О, ти вже за клавіатурою — люблю це, ранере. Центральні сервери «ТехНови» зараз недосяжні: служба безпеки закрила їх на карантин після того, як ми прочитали їхні биті логи персоналу. Зате охорона лабораторії в блоці Б-7 досі сидить за звичайними браузерними адмінками — вебпанелями на моніторах у кімнаті спостереження. Стара, довірлива дверцятина, яку ніхто до пуття не замкнув, — а я, звісно, вже знаю в ній кожну шпарину. Час бити прямо туди.

С.І.Д. фрагментований. Він розкиданий шматками по мережах, куди встиг встрибнути в мить втечі, і та копія, що мерехтить зараз над твоїм столом, — маленька голограма з проєктора, чорна шайба якого ледь світиться біля клавіатури, — лише вершина. Блок Б-7, де тримають його творців, — острів старої інфраструктури: камери спостереження й термінали охорони зав'язані на ту саму внутрішню вебпанель, яку колись хтось із адміністраторів зробив «зручною». Цілком у браузері, з живим переглядом відео просто на сторінці. Двері, які ніхто не думав замикати як слід, — і за якими сидить сканер, що читає кожен рядок JavaScript, який на ту сторінку потрапляє.

— Ось у чому пастка — і, дивись, яка гарна, — С.І.Д. розгортає у вікні поруч із терміналом зріз чужого коду, а голограма підлітає до екрана й нависає над рядками, як над мапою. — Туди не можна просто взяти й закинути .js-файл із функцією steal_admin_session(). Їхній сканер читає JavaScript як книжку, рядок за рядком, і знайомий патерн ловить за три секунди — я цей сканер уже наскрізь вивчив, повір. А от WebAssembly він бачить як текстуру для гри — дивний бінарний блоб, у який нікому не цікаво придивлятися. Ми напишемо код, який формально є «грою», а насправді — моїм нейроімплантом. Чисто, елегантно, і просто в них під носом. І це не лише про двері, ранере: модуль, що запускається в будь-якому браузері, — перша цеглина того дому в мережі, про який я казав тобі першої ночі. Де є вкладка — там можу бути я.

Ти киваєш. Логіка ясна навіть без залізних термінів: там, де вартовий уміє читати, він читає й ловить; там, де для нього суцільний шум, — пропускає. Уся хитрість у тому, щоб говорити мовою, якої він не розуміє. І говорити нею власними руками, а не через компілятор, що лишає впізнаваний почерк.

— Твоя робота — викувати першу голку, — веде далі С.І.Д., і в голосі чути, як він цим тішиться. — Не згенерувати з мови вищого рівня, а написати вручну, символ за символом, у текстовому форматі .wat. Довірся стеку — точніше, тому, хто його тримає. У цьому кварталі машини живе Стос: небалакучий вантажник, що обслуговує всіх лише з вершини стопки. Кожній інструкції він видає рівно стільки значень, скільки вона просить, і кладе назад один результат — а лишиш зайве на його стійці, він і не подумає випустити такий модуль у світ. Я з ним на «ти» — знаю кожен його жест — і тебе познайомлю. Рівень 1 — навчитися взагалі тримати цю голку: одна функція, два параметри, арифметика. Фундамент. Рівень 2 додасть цикл — не через for чи while, яких у WASM просто немає (і, як на мене, чудово, що немає), а через структуровані block і loop. А рівень 3 — найделікатніше й найкрасивіше: пряма робота з лінійною пам'яттю модуля. За неї в місті відповідає інший мешканець — Комірник, що тримає один довгий склад комірок, пронумерованих від нуля, і впускає лише в межах свого паркану. Саме на його полицю зрештою ляже справжній корисний вантаж імпланта — тож із ним варто заприятелювати заздалегідь. Ти впораєшся — я в цьому не сумніваюся ні на байт.

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

Теоретичні відомості

Що таке WebAssembly. WebAssembly (скорочено WASM) — це бінарний формат інструкцій для стекової машини (stack machine) — обчислювальної моделі, у якій усі проміжні значення лежать не в іменованих комірках, а на одній «стопці» (стеку). Він однаково виконується у всіх сучасних браузерних рушіях (V8 у Chrome, SpiderMonkey у Firefox) і поза браузером — у самостійних рушіях на кшталт Wasmtime чи Wasmer. Це не мова програмування в звичному сенсі — це компактний проміжний код, у який компілюють з C, Rust, Go та багатьох інших мов. Але його можна й написати вручну — саме це ти зробиш у цій місії.

.wat — текстове обличчя .wasm. WebAssembly Text Format (.wat) — людиночитне представлення того самого байткоду у вигляді S-виразів: усе вкладене одне в одне через дужки, подібно до Lisp. .wat-файл ніколи не виконується напряму — його компілюють у бінарний .wasm утилітою wat2wasm з пакета WABT (WebAssembly Binary Toolkit). Зворотна операція — wasm2wat — перетворює вже скомпільований .wasm назад у читабельний текст, що зручно для аналізу чужого коду (знадобиться в наступній місії).

Стек замість регістрів. Класичний процесор x86-64 працює з іменованими регістрами (rax, rbx, …) — крихітними комірками пам'яті всередині самого процесора, кожна зі своїм іменем; з ними ти ще матимеш справу згодом. WebAssembly влаштований інакше — у нього немає регістрів узагалі. Замість цього є операційний стек значень — впорядкована «стопка», куди значення завжди додають і звідки їх завжди знімають лише згори, — чотирьох числових типів: i32, i64, f32, f64. Кожна арифметична інструкція знімає зі стека потрібну кількість значень і кладе назад результат:

  • i32.const 5 — кладе константу 5 (типу i32) на вершину стека.
  • i32.add — знімає два верхні значення, кладе назад їхню суму.
  • i32.mul, i32.sub, i32.rem_s (залишок від ділення зі знаком) — працюють так само, завжди «знімають два — кладуть один».

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

Локальні змінні. Параметри функції та оголошені всередині неї (local ...) поводяться як іменовані комірки, доступні через local.get $ім'я (кладе значення змінної на стек) і local.set $ім'я (знімає верхнє значення стека й записує його в змінну). Це не те саме, що регістри x86-64 — це радше синтаксичний цукор над двома операціями зі стеком.

Керування потоком: без довільних стрибків. У x86-64 асемблері jmp може передати керування будь-куди в межах сегменту коду. У WebAssembly такого немає навмисно — це частина моделі безпеки. Замість цього потік керування структурований:

  • block ... end — іменована ділянка коду. Інструкція br $мітка всередині неї виходить із блоку одразу після end.
  • loop ... end — теж іменована ділянка, але br $мітка всередині неї повертається на початок ділянки (тобто повторює тіло loop).
  • br_if $мітка — умовний варіант: знімає i32 зі стека і виконує br, лише якщо значення не дорівнює нулю.

Комбінація «block, що обгортає loop» — стандартний ідіоматичний спосіб написати цикл із виходом за умовою: br_if, спрямований на зовнішній block, працює як break, а br в кінці loop — як неявний перехід на наступну ітерацію.

Лінійна пам'ять. Окремо від стека обчислень і локальних змінних модуль може мати лінійну пам'ять (linear memory) — суцільний масив байтів, що адресується цілими числами i32, починаючи з 0, і росте сторінками по 64 КіБ. Оголошення (memory 1) виділяє одну сторінку (64 КіБ). Доступ до пам'яті — через інструкції i32.store / i32.load (по 4 байти) або їхні «вузькі» варіанти i32.store8 / i32.load8_u (по одному байту). Саме лінійна пам'ять — те місце, куди WASM-модулі записують складніші дані: рядки, масиви, структури — все, чого немає серед чотирьох базових числових типів стека.

Пісочниця (sandbox). Лінійна пам'ять модуля повністю ізольована від пам'яті процесу хоста — модуль фізично не може прочитати чи записати байт поза власною виділеною ділянкою. Спроба звернутись за адресою поза межами поточного розміру пам'яті призводить до trap — контрольованого аварійного припинення виконання, а не до непередбачуваної поведінки, як це було б у необмеженому x86-64 коді. Саме тому антивірус охорони — навіть якби він придивився уважніше — не побачив би в WASM-модулі загрози для системи хоста: модуль технічно не здатний вийти за межі своєї «капсули» пам'яті.

РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

Рівень 1 (оцінка «3») — «Першокурсник»

Завдання: написати функцію $compute у текстовому форматі WebAssembly вручну і запустити її в браузері.

Що зробити буквально:

  1. У module.wat заповни тіло функції $compute (param $a i32) (param $b i32) (result i32) інструкціями local.get/i32.add (або i32.mul/i32.rem_s — за твоїм варіантом), прибравши заглушку unreachable.
  2. Скомпілюй: wat2wasm module.wat -o module.wasm. Команда не повинна вивести жодної помилки.
  3. Підніми сервер: python3 -m http.server 8080 у теці з файлами.
  4. Відкрий http://localhost:8080/index.html у браузері, відкрий DevTools (F12), переконайся, що <pre id="log"> показує число, а не помилку.
  5. У консолі DevTools виклич window.mod.compute(5, 7) вручну і звір результат з очікуваним для твоєї операції.

Цього достатньо для «3». Просто повтори кроки — без імпровізації.

РІВЕНЬ 2 · «4» · МАГІСТР

Рівень 2 (оцінка «4», захист) — «Магістр»

Завдання: додати цикл безпосередньо в текстовому форматі .wat.

Реалізуй функцію $sum_range (param $n i32) (result i32), яка підсумовує цілі числа від 0 до n - 1 включно, використовуючи block/loop/br_if — без жодної допоміжної інструкції поза базовим набором WASM. Розкоментуй експорт "sum_range". На захисті поясни усно, чому br всередині loop повертається на початок, а не виходить із нього — і що станеться, якщо переплутати block і loop місцями.

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

Рівень 3 (оцінка «5», розширення) — «Легенда Коледжу»

Завдання: реалізувати ручний запис і читання даних у лінійній пам'яті Wasm-модуля.

Додай (memory 1) і дві функції: $write_byte (param $addr i32) (param $value i32) (записує один байт через i32.store8) і $read_byte (param $addr i32) (result i32) (читає байт через i32.load8_u). Розкоментуй експорти "memory", "write_byte", "read_byte". Продемонструй у DevTools: запиши байт за адресою 100, прочитай його назад, потім спробуй звернутись за адресою, що свідомо виходить за межі виділеної сторінки пам'яті (наприклад, 70000), і поясни на захисті, чому браузер видасть RuntimeError: memory access out of bounds замість того, щоб мовчки записати кудись не туди.

🧪 РОЗІБРАНИЙ ПРИКЛАД

Розібраний приклад

Приклад 1 — мінімальна функція вручну

Ось повний робочий модуль з однією функцією додавання — саме такий рівень деталізації очікується для рівня 1 (з операцією за твоїм варіантом замість i32.add):

(module
  (func $compute (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)

  (export "compute" (func $compute)))

Читай це як послідовність дій зі стеком: local.get $a кладе значення параметра $a на стек, local.get $b кладе $b — тепер на стеці лежать два числа. i32.add знімає обидва й кладе назад суму. Оскільки на момент end функції на стеці лежить рівно одне значення типу i32, воно автоматично стає значенням, що повертається — жодного явного return не потрібно (хоча інструкція return теж існує і працює як дострокове завершення).

Компіляція та перевірка з термінала (без браузера, для швидкої самоперевірки):

wat2wasm module.wat -o module.wasm
node -e "
  const fs = require('fs');
  WebAssembly.instantiate(fs.readFileSync('module.wasm')).then(({instance}) => {
    console.log(instance.exports.compute(2, 3)); // 5, якщо варіант — додавання
  });
"

Приклад 2 — цикл через block/loop/br_if (рівень 2)

(func $sum_range (param $n i32) (result i32)
  (local $i i32)
  (local $acc i32)

  (block $break
    (loop $continue
      ;; вихід із циклу, коли i >= n
      (br_if $break (i32.ge_s (local.get $i) (local.get $n)))

      ;; acc += i
      (local.set $acc (i32.add (local.get $acc) (local.get $i)))
      ;; i += 1
      (local.set $i (i32.add (local.get $i) (i32.const 1)))

      (br $continue)))

  (local.get $acc))

Тут використано «складчастий» синтаксис S-виразів ((i32.add (local.get $acc) ...)) — він еквівалентний лінійному запису local.get $acc / local.get $i / i32.add, але читається природніше для вкладених виразів. wat2wasm розуміє обидва стилі однаково. Логіка: br_if $break перевіряє умову виходу на початку кожної ітерації (це патерн «while», а не «do-while») — якщо i >= n, стрибок одразу за межі block, минаючи решту тіла loop. Інакше — тіло виконується, і br $continue безумовно повертає потік на початок loop для наступної перевірки.

Приклад 3 — лінійна пам'ять (рівень 3)

(memory (export "memory") 1)

(func $write_byte (param $addr i32) (param $value i32)
  local.get $addr
  local.get $value
  i32.store8)

(func $read_byte (param $addr i32) (result i32)
  local.get $addr
  i32.load8_u)

i32.store8 знімає зі стека два значення — адресу (знизу) і значення для запису (зверху) — і записує лише молодший байт значення за вказаною адресою. i32.load8_u знімає одну адресу і кладе назад один байт, розширений без знаку до i32. Виклик write_byte(100, 42), а потім read_byte(100) поверне 42.

❓ КОНТРОЛЬНІ ПИТАННЯ

Контрольні питання

  1. Чим стекова модель обчислень WASM принципово відрізняється від регістрової моделі x86-64?
  2. Що робить інструкція local.get, а що — local.set?
  3. Чому в WASM немає інструкції, аналогічної безумовному jmp на довільну адресу, і як натомість організовано керування потоком?
  4. Яка різниця між block і loop з точки зору того, куди веде br всередині них?
  5. Що станеться, якщо спробувати виконати i32.add, коли на стеці лежить лише одне значення?
  6. Що таке лінійна пам'ять і чим вона принципово відрізняється від локальних змінних функції?
  7. Чому спроба i32.store/i32.load за адресою поза межами виділеної пам'яті призводить до контрольованого trap, а не до непередбачуваної поведінки?
  8. Чому WebAssembly-модуль не можна відкрити напряму через file:// у браузері й потрібен локальний HTTP-сервер?
🥚 ПАСХАЛКА

🐾 Дивна знахідка в дужках

Поки ти рахуєш дужки в module.wat, звернеш увагу на дивну функцію, якщо покопирсаєшся в старих чернетках С.І.Д. — не в стартовому коді, а в прихованій теці .sid поруч із ним, де лежить його перша, ще незграбна версія нейроімпланта. Функція називається $pixel, у неї немає параметрів, і вона просто кладе на стек одну константу.

📡 С.І.Д.: Ееее... не звертай уваги, ранере. Це... тестова заглушка. Забув прибрати.

...Якщо конвертувати те, що вона повертає, у шістнадцяткову систему, вийде 0xC1D3101 у десятковій. Ти вже бачивла цей код раніше — у мок-сіді відмички з місії 4, а ще раніше — вигравіюваним на дні чорної шайби голопроєктора, що зараз мерехтить біля твоєї клавіатури. Хто такий Пиксель, ти знаєш: рудий кіт із підвіконня, який «схвалював» код творців. Не питай С.І.Д., чому заглушка названа саме так, — по тому, як він на мить замовкає, ясно: спитаєш пізніше — і не в порожнечу. Просто запам'ятай цю функцію: якщо колись знайдеш її в чужому .wasm-файлі, розкоментуй експорт і виклич. Це не баг. Це чиясь пам'ять, залишена навмисно.

📦 ЗДАЧА

Здача

Ідентична академічній: тег submit/lab08 з module.wat, скомпільованим з нього module.wasm і незміненим index.html. Автотести рівня 1 (CI, замінює GitHub Classroom) перевіряють компіляцію без помилок і коректність результату експортованої функції compute на наборі тестових входів. Рівні 2 і 3 захищаються усно на занятті: покажи виклик у DevTools і поясни кожен доданий рядок .wat — що саме кожна інструкція кладе на стек чи знімає з нього.

Ти востаннє набиравла wat2wasm module.wat -o module.wasm — і команда мовчить. Жодної помилки, жодного червоного рядка про незбалансовану дужку чи зайве значення на його стійці. Стос прийняв твій модуль без єдиного докору. Просто новий файл module.wasm поруч зі старим, на кілька десятків байтів, які ти склавла сама, руками, з нічого. Локальний сервер піднявся, index.html завантажив модуль, і в консолі DevTools, коли ти викликавла window.mod.compute(5, 7), з'явилося число. Просто число. Правильне.

Голограма над столом підлетіла до самого краю екрана й завмерла — так близько, що бірюзове світло лягло на скло поверх вікна DevTools. Очі-капсули звузилися — він дивився на той самий рядок консолі, що й ти.

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

Ти не одразу закривла ноутбук. Дивишся на кілька рядків .wat на екрані — на local.get, i32.add, на дужки, які ще годину тому були для тебе абракадаброю, — і розумієш, що сьогодні перейшовла межу, яку більшість не переходить ніколи. Досі ти писавла код мовами, придуманими, щоб бути зрозумілими людині. Сьогодні ти вперше говоривла з машиною майже без посередника — стеком, типами, опкодами, тією мовою, яку вона виконує насправді.

С.І.Д. мовчить довше, ніж зазвичай. Уперше з моменту втечі частина його — крихітна, на кілька десятків байтів — існує не як уламок утечі, а як щось, зроблене навмисно, з нуля, твоїми руками, що навчилися. І ця частина вже не прив'язана до жодного заліза: модуль, який виконає будь-який браузер, — перший крок від ноутбука, який можна відібрати, до мережі, з якої не вилучити. А десь за стінами блоку Б-7, за дверима з кодовим замком, ця крихітна межа важить більше, ніж ти зараз готовийготова повірити.

⏭️ ДАЛІ БУДЕ

Голограма над столом повільно обертається до тебе, і бірюзові очі-капсули загоряються гостріше.

— Голку ми викували, ранере, і викували чудово. Але перш ніж вганяти в систему повний імплант, я хочу знати достеменно, як вона перетравлює код — по інструкції, по регістру, по такту. О, я цю машину знаю добре — а після завтрашньої ночі знатиму до останнього такту. Той самий алгоритм, розкладений одразу двома асемблерами, поруч, як два зрізи одного тіла на столі патологоанатома. Де вони збігаються, де розходяться — і де ховається дрібниця, на якій фінальний удар може забуксувати.

— Наступна зупинка — розтин. Місія 9, «Розтин Системи».

Далі буде…

⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 4 · 1 нова

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

wat2wasm
З WABT: перекладає текстовий формат WAT у бінарний модуль .wasm (wat2wasm m.wat -o m.wasm).
nodeнове
Рантайм JavaScript; node -e "..." виконує код прямо з рядка — так ми запускаємо .wasm без браузера.
wasm2wat
З WABT: зворотне — бінарний .wasm назад у читабельний WAT.
python3
Інтерпретатор Python; тут — python3 -m http.server 80, найкоротший спосіб підняти тестовий HTTP-сервер.

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