🌌 Пряма робота з пам'яттю монітора — Записати й прочитати байти в пам'яті монітора охорони доручено Комірникові — мешканцю, що завідує довгим складом лінійної пам'яті, пронумерованим від нуля. С.І.Д. з гордістю розкриває ранерові весь фокус: усе тримається строго в межах паркану складу, бо за межу Комірник не впускає нікого — інакше пісочниця браузера обірве зв'язок раніше, ніж спрацює план.
Кожен модуль WebAssembly, якому потрібна довільна робота з пам'яттю
(буфери, рядки, масиви поза локальними змінними стека), оголошує
лінійну пам'ять (linear memory; оголошення (memory $mem 1)) —
суцільний, безперервний масив
байтів, адресований від нуля, розмір якого вимірюється у сторінках
по 64 КіБ кожна (число 1 в оголошенні означає одну сторінку — 64
КіБ початкового розміру). Це принципово простіша модель, ніж
сегментована чи сторінково-віртуальна пам'ять традиційної ОС: жодних
прав доступу per-сторінка, жодного mapping, лише один суцільний
буфер, до якого модуль звертається за зміщенням (offset), як до
звичайного масиву u8.
Читання й запис виконуються інструкціями сімейства i32.load/
i32.store (і аналогами для i64, f32, f64, а також вужчими
варіантами i32.load8_u, i32.load16_s тощо для часткового доступу
з розширенням знаку чи нулями). Адреса обчислюється як сума значення
на стеці (динамічна частина) і статичного offset, заданого прямо в
інструкції: i32.load offset=4 читає 4 байти, починаючи з (значення на стеці) + 4. Це та сама механіка, що лежить в основі доступу до
полів структур і елементів масивів у скомпільованому з Rust/C коді —
компілятор перетворює arr[i].field на одну інструкцію load зі
статичним offset, що дорівнює зміщенню поля, і динамічною базовою
адресою i.
Ключова гарантія безпеки — межова перевірка (bounds checking), яку виконує сам рушій WebAssembly для кожного доступу до пам'яті, без винятку: якщо обчислена адреса плюс розмір значення виходить за межі поточного розміру лінійної пам'яті модуля, виконання негайно зупиняється через trap (аналог контрольованого винятку — не невизначена поведінка, як вихід за межі масиву в C, а детермінований, гарантований відказ). Це принципова відмінність WASM-пісочниці (sandbox — ізольоване середовище, за межі якого код не може дотягнутися) від «сирого» машинного коду: у нативному асемблері вихід за межі буфера може мовчки прочитати/переписати сусідню пам'ять (класична причина вразливостей на кшталт переповнення буфера); у WASM це технічно неможливо — рушій перевіряє межі до фактичного доступу до фізичної пам'яті процесу браузера.
Лінійна пам'ять модуля може зростати (але не зменшуватися) через
інструкцію memory.grow (з боку хоста — WebAssembly.Memory.grow()
у JS), яка додає задану кількість сторінок по 64 КіБ, повертаючи
попередній розмір у сторінках або -1, якщо зростання неможливе
(наприклад, перевищено оголошений максимум (memory $mem 1 10) —
де 10 означає верхню межу в 10 сторінок = 640 КіБ). Модуль ніколи
не отримує прямого доступу до пам'яті інших модулів, до пам'яті
самого JS-рушія чи до довільних адрес процесу — лінійна пам'ять
кожного інстансу модуля повністю ізольована й видима лише як
WebAssembly.Memory-об'єкт, до якого сам JS-хост має контрольований
доступ через ArrayBuffer.
Ця модель — фундаментальна причина, чому WASM вважається безпечним
середовищем виконання для недовіреного коду в браузері (та поза
ним — у серверних рушіях типу Wasmtime/Wasmer): навіть якщо
скомпільований модуль містить логічну помилку чи навмисно шкідливий
код, він фізично не може вийти за межі власної пісочниці лінійної
пам'яті й зачепити пам'ять хост-застосунку — на відміну від
завантаження, наприклад, нативної динамічної бібліотеки (.so/
.dll), яка виконується в тому самому адресному просторі без жодних
меж.
Прочитати розділ «Memory Instructions» специфікації WebAssembly та розділ «Security» на webassembly.org; спробувати i32.store/i32.load на адресі поза виділеною пам'яттю модуля й зафіксувати повідомлення про trap.
Що станеться, якщо викликати i32.store за адресою, яка виходить за межі поточного розміру лінійної пам'яті модуля?
0xC1D, тобто 3101. Творці лишили там один байт, «схвалений котом», і Комірник стереже його як реліквію. За парканом складу — від 65536 і далі — реліквій немає, там лише trap.Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
wat2wasm.wasm (wat2wasm m.wat -o m.wasm).wasm-interp.wasm без браузера (--run-all-exports — викликати всі експортовані функції).💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.