☰ Конспект ← Курс
ЛЕКЦІЯ 8

WEBASSEMBLY ЯК ПЕРЕНОСНИЙ АСЕМБЛЕР

Один код — дві машини

ВТФК · Системне програмування · 2026

План лекції

  • Що таке інструкція машини, набір інструкцій і асемблер
  • Що таке WebAssembly і яку проблему він вирішує
  • Стекова машина: як WASM обчислює вирази без регістрів
  • Текстовий формат .wat: модуль, функція, тип
  • Керування потоком: block, loop, br, br_if
  • Лінійна пам'ять — попередній огляд
  • Живе демо: .wat у браузері

Фокус: та сама логіка обчислень, інша модель виконання

Перше входження: що таке інструкція машини

  • Мови, з якими ти працював, — це текст для **людини**. Процесор такого не читає
  • Процесор виконує **інструкції**: крихітні команди на кшталт «додай два числа», «поклади число в пам'ять», «перейди на іншу інструкцію, якщо результат нуль»
  • Набір таких команд для конкретної машини називають **набором інструкцій**, а мову їхнього запису — **асемблером**
  • Компілятор — це перекладач: із мови для людини в набір інструкцій для машини

Це третя межа курсу: перша була між процесом і ОС, друга — між програмою й пам'яттю, ця — між кодом і набором інструкцій.

Перше входження: що таке WebAssembly

  • WebAssembly (WASM) — це набір інструкцій **вигаданої** машини, якої не існує в залізі
  • Її «процесор» — це програма: рушій браузера, Node.js, Wasmtime. І вона однакова всюди
  • Тому один і той самий `.wasm` працює в Chrome на Windows, у Firefox на Linux і на сервері — без перекомпіляції
  • Швидкість — майже нативна: рушій перекладає інструкції WASM у справжні інструкції твого CPU перед виконанням

Головна відмінність від справжнього процесора — модель обчислень: замість регістрів тут стек. Наступний слайд.

Яку проблему це вирішує

  • **Як було:** щоб код працював у браузері, його треба було писати на JavaScript — іншої мови там просто не було
  • Важкі речі — обробка відео, стиснення, шифрування, 3D — на JS виходили в рази повільніші
  • **Як стало:** беремо C, C++, Rust, компілюємо у WASM — і воно бігає в браузері майже з нативною швидкістю
  • Побічний, але великий виграш: WASM виявився зручним і поза браузером — там, де потрібен безпечний плагін чи пісочниця

Для нас важливий саме другий бік: WASM — це найдешевший спосіб побачити набір інструкцій живим, просто в DevTools.

Чим WebAssembly НЕ є

  • **Не заміна JavaScript**: JS лишається мовою сторінки, WASM — модулем, який JS викликає
  • **Не мова програмування**: на WAT пишуть руками лише для навчання (як ми сьогодні на ЛР8), у роботі WASM компілюють
  • **Не доступ до системи**: усередині WASM немає ні файлів, ні мережі, ні системних викликів — лише те, що хост явно передав
  • **Не той самий асемблер, що x86-64**: справжній набір інструкцій твого процесора ми розберемо наступного разу (Л9) — і побачимо різницю на одному алгоритмі

Третій пункт — це та сама межа з Модуля 1, лише ще вужча: WASM ізольований навіть від того, що бачить контейнер.

Стек замість регістрів: у чому відмінність моделі

  • У справжнього процесора є **регістри** — кілька десятків надшвидких комірок із іменами (розберемо на Л9)
  • У WebAssembly регістрів немає: значення кладуть на **операційний стек** і знімають із нього
  • Це зроблено заради переносності: у різних CPU різна кількість і назви регістрів, а стек однаковий скрізь
  • Компілюється з C, C++, Rust, Go; рушій виконує майже з нативною швидкістю (AOT/JIT)

Відмова від регістрів — головна ціна переносності: код WASM трохи довший, зате однаковий на всіх машинах

WebAssembly у цифрах

4
типи значень
i32, i64, f32, f64 — і жодних рядків
0
регістрів
замість них — операційний стек
64 КіБ
сторінка лінійної пам'яті
росте через memory.grow
2017
рік специфікації MVP
одна VM у Chrome, Firefox, Node.js, Wasmtime

Кожна функція має явну сигнатуру; невідповідність типів на стеку — помилка ще під час завантаження, не виконання

Стекова машина: як це працює

  • Немає rax, rbx — є один операційний стек значень (i32, i64, f32, f64)
  • Кожна інструкція знімає потрібну кількість значень зі стека і кладе результат назад
  • i32.const 2 кладе 2 на стек; i32.add знімає два верхні значення, кладе суму
  • Порядок інструкцій = порядок обчислення виразу в постфіксній (зворотній польській) нотації

2 + 3 записується як: i32.const 2; i32.const 3; i32.add — покажіть стан стека після кожної інструкції: [2] → [2, 3] → [5]

WEBASSEMBLY — СТЕКОВА МАШИНА i32.const 2 i32.const 3 i32.add 3 2 стек операндів 5 результат інструкції беруть операнди зі стека й кладуть результат назад типізовано й валідується перед виконанням → пісочниця

Модуль .wat: функція додавання

wat
(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add)))

Той самий add — але для стекової машини; з x86-64 порівняємо на Л9

Анатомія .wat модуля

🌳
(module …)корінь: усе з бінарного .wasm має текстовий відповідник
ƒ
(func $ім'я …)оголошення функції; типи параметрів і результату обов'язкові
🔢
(param $a i32) (result i32)сигнатура — перевіряється валідатором до запуску
📥
local.get / local.setлокальні змінні й параметри — аналог того, що в x86-64 пишуть як -4(%rbp) (Л9)
📤
(export "add" (func $add))що видно ззовні модуля, з JavaScript
🔌
(import "env" "log" …)що модуль отримує від хоста — дзеркало export

S-вирази ((…)) — та сама нотація, що в Lisp: кожен вузол дерева синтаксису явний. wat = WebAssembly Text format

Життєвий цикл модуля

  1. 1
    wat2wasm add.watтекст → бінарний .wasm без втрат (wasm2wat — назад)
  2. 2
    fetch('add.wasm')байти прибувають по мережі — компіляція починається одразу
  3. 3
    Валідаціярушій перевіряє типи на стеку кожної функції — або відмова
  4. 4
    Інстанціюванняпідставити import-об'єкт, виділити лінійну пам'ять
  5. 5
    exports.add(2, 3)виклик із JS — звичайна функція, результат 5

instantiateStreaming суміщає кроки 2–4: компілює, доки файл ще довантажується

Керування потоком: цикл у .wat

wat
(func $sum_to_n (param $n i32) (result i32)
  (local $i i32) (local $acc i32)
  (block $break
    (loop $continue
      local.get $i
      local.get $n
      i32.ge_s
      br_if $break
      local.get $acc
      local.get $i
      i32.add
      local.set $acc
      local.get $i
      i32.const 1
      i32.add
      local.set $i
      br $continue))
  local.get $acc)

block/loop замінюють мітки .L_cond/.L_end з x86-64

Керування потоком: x86-64 ↔ WebAssembly

Конструкціяx86-64 (Л9)WebAssemblyАналог у мові
Вихід із ділянкиjmp .L_endblock $b … br $bbreak
Повторjmp .L_condloop $c … br $ccontinue
Умовний перехідcmp + jgei32.ge_s + br_ifif (…) break
Розгалуженняjcc на міткуif (result T) … else … endif / else
Довільний стрибокjmp на будь-яку адресунеможливийgoto

br_if знімає i32 зі стека: не нуль — перехід. Структурованість — не стиль, а гарантія: у чужу функцію стрибнути не можна; компілятор генерує ці конструкції з for/while сам

Лінійна пам'ять: перший погляд

  • Окремо від операційного стека WASM має лінійну пам'ять (*linear memory*) — суцільний масив байтів
  • Росте сторінками по 64 КіБ, доступ — за i32-адресою через i32.load / i32.store
  • Рядки, масиви, будь-які складні структури живуть саме тут, а не в локальних змінних
  • Детально — на Л10 і в ЛР8 (рівень 3): ручний запис/читання байтів

Місток до ЛР8, рівень 3: там студенти вручну пишуть i32.store8/i32.load8_u у лінійну пам'ять і бачать trap при виході за межі

Лінійна пам'ять: запис і читання 4 байтів

(memory 1)1 сторінка = 65 536 байтів
→
i32.const 0 · i32.const 42стек: [адреса, значення]
→
i32.storemem[0..4] = 2a 00 00 00
→
i32.const 0 · i32.load→ 42 назад на стек
→
адреса ≥ 65 536trap, а не UB

Little-endian, як і в x86-64 (Л9); «база + зсув» тут — фіксована база 0 і i32-адреса. Межі перевіряються при кожному load/store

Пам'ять модуля: чому за межу вийти неможливо

ЛІНІЙНА ПАМ'ЯТЬ: STORE, LOAD, МЕЖА → TRAP i32.store addr 0 · val 42 i32.load addr 0 → 42 addr ≥ 65 536 вихід за межу 2a 0 00 1 00 2 00 3 00 4 00 5 00 6 00 7 00 8 00 9 … trap кінець пам'яті memory.size × 64 КіБ 42 = 0x2a → little-endian: молодший байт за меншою адресою запис 4 байтівчитанняtrap, не UB вказівників «назовні» немає — лише зсув у власному масиві байтів

Той самий приклад, що у флоу вище: 42 лягає як 2a 00 00 00, за межею — trap, а не чужа пам'ять

Живе демо: .wat у браузері

  1. wat2wasm add.wat -o add.wasm — компілюємо текст у бінарний формат
  2. Відкрити мінімальну HTML-сторінку з <script type=module>
  3. fetch('add.wasm') → WebAssembly.instantiateStreaming(...)
  4. У DevTools console: instance.exports.add(2, 3)
  5. Показати вкладку Network — розмір .wasm-файлу в байтах, набагато менший за еквівалентний JS-бандл

Очікувано: Console друкує 5; той самий .wat відкривається і виконується без змін у Chrome, Firefox, Node.js

x86-64 проти WebAssembly

x86-64 (Л9)WebAssembly
Обчисленняіменовані регістриопераційний стек значень
Керування потокомдовільний jmp/jcc на міткуструктуровані block/loop/br/if
Пам'ятьувесь адресний простір процесуізольована лінійна пам'ять з перевіркою меж
Виклик функціїABI-угода про регістри/стектипізована сигнатура, перевірена валідатором
Перевірка коректностінемає — байти є байтивалідація типів при завантаженні
Ціль виконанняконкретний фізичний CPUбудь-який рушій: браузер, Node.js, Wasmtime

Обидва — «мова, близька до заліза»; різниця в тому, чиє залізо і скільки йому довіряють

Пісочниця WebAssembly: модель безпеки

🚫
Немає syscallжодного прямого доступу до файлів, мережі, ОС
🔌
Тільки importєдиний вихід назовні — функції, які хост передав явно
🎟️
Капабіліті-модельмодуль має рівно ті можливості, що йому дали, і жодних інших
🧱
Потік + межі пам'ятіне стрибнути й не прочитати поза дозволеним

Пісочниця (sandbox) — ізольоване середовище, з якого код не дістає до системи; хост (host) — браузер, Node.js, Wasmtime. Тому браузер безпечно запускає WASM з недовіреного сайту

Trap замість UB: інша філософія помилок

C / асемблер: undefined behavior
  • Вихід за межі — будь-який наслідок можливий
  • Читає «щось випадкове» або пише в чуже
  • Перевірки немає — швидко, але наосліп
vs
WebAssembly: trap
  • Виконання зупиняється з чіткою помилкою
  • Межі перевіряються при кожному load/store
  • Плата — трохи повільніше за «сирий» asm

Виняток із правила: memory.grow при відмові повертає -1, а не trap — перевірка на совісті коду

Межа хост ↔ модуль: import і export

JS-хостinstantiate(…, {env: {log}})
→
import env.logмодуль отримує $log
→
func $reportlocal.get · call $log
→
export reportвидно з JS
→
exports.report(42)→ console.log(42)

Модуль сам нічого не друкує: без явного import виклику console.log просто не існувало б

Імпорт функції хоста у .wat

wat
(module
  (import "env" "log" (func $log (param i32)))
  (func $report (param $code i32)
    local.get $code
    call $log)
  (export "report" (func $report)))

Модуль сам нічого не друкує — 'log' надає хост через import

Два способи розгалуження у WASM

block + br_if — перехід
  • Умова (i32) знімається зі стека
  • Не нуль — вихід із block, нуль — далі
  • Аналог break / continue
  • Так компілятор реалізує цикли
vs
if (result T) … else … end — вираз
  • Умова так само знімається зі стека
  • Обидві гілки лишають значення типу T
  • Аналог тернарного виразу ? :
  • Так компілятор реалізує кожен if-вираз

Спільне: жодних «голих» міток — лише вкладені структуровані блоки; валідатор перевіряє тип на виході з кожної гілки

Живе демо: імпорт хост-функції в консоль браузера

  1. Написати add.wat з import env.log(i32) та export report
  2. wat2wasm add.wat -o add.wasm
  3. У JS: WebAssembly.instantiateStreaming(fetch('add.wasm'), { env: { log: console.log } })
  4. instance.exports.report(42) — переконатися, що консоль надрукувала число

Очікувано: Студенти бачать живий приклад капабіліті-моделі: без явного import виклику console.log просто не існувало б

WebAssembly — це безпечний, переносний, низькорівневий формат коду, розроблений для ефективного виконання й компактного представлення.

— WebAssembly Core Specification (W3C), вступ

Підсумок

  • WASM — байт-код стекової віртуальної машини, а не конкретного CPU
  • .wat — текстове представлення: module, func, param/result, local.get/set
  • Керування потоком структуроване: block/loop/br/br_if замість довільних jmp
  • Лінійна пам'ять — окрема адресована ділянка байтів, детально на Л10

🥚 У демо-терміналі wasmtime --invoke pixel pixel.wasm 2 3 повертає 5. Спробуйте аргументи 3000 і 101: стекова машина поверне 3101 — 0xC1D. Це число не випадкове: у курсі воно трапляється не раз.

VTFK · TERMLINK · L08← → · space · f — на весь екран