Лабораторна робота № 8. Модуль на WAT вручну
Мета
Навчитися писати мінімальний модуль WebAssembly безпосередньо у текстовому форматі
.wat — без компілятора з мови вищого рівня — і запускати його в браузері:
оголошувати функцію зі стековою логікою обчислень, керувати потоком виконання
структурованим циклом (block/loop/br_if) та читати й писати байти в лінійній
пам'яті модуля.
Теоретичні відомості
WebAssembly (WASM) — бінарний формат інструкцій для стекової віртуальної машини, однаково виконуваний у всіх сучасних браузерних рушіях (V8, SpiderMonkey) і поза браузером (Wasmtime, Wasmer). Це компактний, платформонезалежний проміжний код — типова ціль компіляції з C, Rust, Go тощо, який також можна писати вручну.
WebAssembly Text Format (.wat) — людиночитне текстове представлення того самого
байткоду у вигляді S-виразів (як у Lisp: усе вкладене в дужки). .wat-файл
компілюється в бінарний .wasm утилітою wat2wasm з пакета WABT (WebAssembly Binary
Toolkit); зворотне перетворення виконує wasm2wat.
Модель обчислень. На відміну від x86-64, де інструкції оперують іменованими
регістрами, WASM використовує операційний стек значень чотирьох числових типів:
i32, i64, f32, f64.
| Інструкція | Дія зі стеком |
|---|---|
i32.const N |
кладе константу N на вершину стека |
i32.add / i32.sub / i32.mul / i32.rem_s |
знімає два верхні значення, кладе результат |
local.get $x |
кладе значення локальної змінної/параметра $x на стек |
local.set $x |
знімає верхнє значення стека й записує в $x |
Параметри функції та секції (local ...) — це саме такі іменовані комірки, доступні
виключно через local.get/local.set; прямого «регістрового» доступу до них немає.
Керування потоком у WASM структуроване — навмисно немає інструкції, аналогічної
безумовному jmp асемблера на довільну адресу. Замість цього:
block $мітка ... end— іменована ділянка;br $міткавсередині неї здійснює вихід одразу після відповідногоend.loop $мітка ... end— іменована ділянка;br $міткавсередині неї здійснює перехід на початок тіла (повторення ітерації).br_if $мітка— умовний варіантbr: знімаєi32зі стека, виконує перехід лише якщо значення ненульове.
Стандартна ідіома циклу з умовою виходу — loop, обгорнутий у block: br_if на
зовнішній block працює як break, а br наприкінці loop — як перехід до
наступної ітерації.
Лінійна пам'ять — окрема від стека та локальних змінних ділянка: суцільний масив
байтів, адресований i32, що росте сторінками по 64 КіБ. (memory 1) виділяє одну
сторінку. Доступ — інструкціями i32.store/i32.load (4 байти) або
i32.store8/i32.load8_u (1 байт). Модель безпеки WASM гарантує, що доступ поза
межами виділеної пам'яті завершується контрольованим trap, а не непередбачуваною
поведінкою — модуль фізично не здатний адресувати пам'ять хоста.
Обладнання та програмне забезпечення
- Codespaces / devcontainer курсу з попередньо встановленим Node.js 20+
wabt(WebAssembly Binary Toolkit):wat2wasm,wasm2wat,wasm-objdump(встановлюється в контейнері через пакетний менеджер абоnpm i -g wabt)- Будь-який сучасний браузер (Chrome/Firefox) з DevTools
python3 -m http.serverабо еквівалент для локального HTTP-сервера (WebAssembly не завантажується зі схемиfile://)
Порядок виконання
- Відкрити середовище розробника, переконатися, що
wat2wasm --versionвиконується без помилок. - Створити файл
module.watза специфікацією уstarter/README.mdі заповнити тіло функції рівня 1. Очікуваний результат: файл із функцією$compute, готовий до компіляції. - Скомпілювати:
wat2wasm module.wat -o module.wasm. Очікуваний результат: без помилок валідації, з'являється файлmodule.wasm. - Підняти локальний сервер (
python3 -m http.server 8080) і відкритиindex.html. Очікуваний результат: сторінка виводить у<pre id="log">числовий результат викликуcompute(2, 3), а не помилку. - У консолі DevTools викликати експортовану функцію (
window.mod.compute(...)) на кількох парах значень і перевірити коректність результату за операцією свого варіанта. - Виконати рівень 2 (цикл
$sum_range) і рівень 3 (лінійна пам'ять,$write_byte,$read_byte) за тим самим файломmodule.wat, дотримуючись специфікації автотестів і завдання рівнів. - Запустити локальний скрипт перевірки з
autograder/, переконатися, що всі перевірки рівня 1 проходять, перш ніж здавати роботу.
Рівень 1 (оцінка 3)
Написати функцію $compute (param $a i32) (param $b i32) (result i32) у текстовому
форматі WebAssembly вручну — послідовність local.get/i32.const/арифметичної
інструкції (i32.add, i32.mul або i32.rem_s — залежно від варіанта, призначеного
викладачем), без заглушки unreachable, і продемонструвати виклик у браузері.
Рівень 2 (оцінка 4, захист)
Додати до того самого module.wat функцію $sum_range (param $n i32) (result i32),
яка підсумовує цілі числа від 0 до n - 1 за допомогою block/loop/br_if.
На захисті — усно пояснити семантику br у block проти br у loop та показати
виклик функції в DevTools.
Рівень 3 (оцінка 5, розширення)
Оголосити (memory 1) і реалізувати функції $write_byte (param $addr i32) (param $value i32)
(запис через i32.store8) та $read_byte (param $addr i32) (result i32) (читання
через i32.load8_u). На захисті продемонструвати запис і зчитування байта за адресою
та пояснити поведінку модуля при зверненні за межі виділеної пам'яті.
Розібраний приклад
Мінімальний модуль з однією функцією (шаблон для рівня 1, тут — з операцією додавання):
(module
(func $compute (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add)
(export "compute" (func $compute)))
Компіляція та автономна перевірка без браузера:
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));
});
"
Цикл рівня 2, що підсумовує 0..n-1:
(func $sum_range (param $n i32) (result i32)
(local $i i32)
(local $acc i32)
(block $break
(loop $continue
(br_if $break (i32.ge_s (local.get $i) (local.get $n)))
(local.set $acc (i32.add (local.get $acc) (local.get $i)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $continue)))
(local.get $acc))
Лінійна пам'ять рівня 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)
Контрольні питання
- Чим стекова модель обчислень WASM принципово відрізняється від регістрової моделі x86-64?
- Що робить інструкція
local.get, а що —local.set? - Чому в WASM немає інструкції, аналогічної безумовному
jmpна довільну адресу? - Яка різниця між
blockіloopз точки зору того, куди ведеbrвсередині них? - Що станеться, якщо спробувати виконати
i32.add, коли на стеці лежить лише одне значення? - Що таке лінійна пам'ять і чим вона відрізняється від локальних змінних функції?
- Яку роль відіграє
wat2wasmу цьому робочому процесі? - Чому WebAssembly-модуль не можна відкрити напряму через
file://в браузері?
Парні теми СРС
- Рівень 2: srs15 — Стекова машина WebAssembly і структура формату
.wat - Рівень 3: srs16 — Лінійна пам'ять WASM і межі безпеки пісочниці
Критерії оцінювання та форма звіту
Здається гілка/тег submit/lab08 з файлом module.wat (та module.wasm, зібраним з
нього) і мінімальною index.html, що завантажує модуль. Автотести рівня 1 (див.
autograder/README.md) перевіряють: файл компілюється без помилок wat2wasm,
експортована функція має правильну сигнатуру (кількість і типи параметрів, тип
результату), повертає коректні значення на наборі тестових входів (типові додатні
значення, нуль, від'ємні значення, межові значення близько i32::MAX/i32::MIN).
Рівень 2 і 3 здаються тим самим файлом з доданими секціями коду і перевіряються
вручну на захисті: студент показує виклик у DevTools і пояснює кожен рядок доданого
.wat-коду (яка інструкція що кладе/знімає зі стека, для чого саме
i32.store/i32.load у рівні 3). Звіт додає скріншоти: вихідний код .wat, вивід
компіляції, консоль браузера з результатом виклику.