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

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

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

VTFK · Системне програмування · Модуль 3

План лекції

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

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

Чому не просто 'ще один асемблер'

  • x86-64 асемблер прив'язаний до конкретного фізичного процесора Intel/AMD
  • WebAssembly (WASM) — байт-код (*bytecode*) для віртуальної машини, однакової в Chrome, Firefox, Node.js, Wasmtime
  • Компілюється з 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, що і в Л8, тепер для стекової машини

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

🌳
(module …)корінь: усе з бінарного .wasm має текстовий відповідник
ƒ
(func $ім'я …)оголошення функції; типи параметрів і результату обов'язкові
🔢
(param $a i32) (result i32)сигнатура — перевіряється валідатором до запуску
📥
local.get / local.setлокальні змінні й параметри — аналог -4(%rbp) з Л8
📤
(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 (Л8)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 (Л8); «база + зсув» тут — фіксована база 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 (Л8)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 · L09← → · space · f — на весь екран