Місія 9: «Розтин Системи»
С.І.Д.: Ін'єкція минулої ночі пройшла — і, скажу тобі, красиво пройшла. Модуль, який ти набравнабрала руками, інструкція за інструкцією, зараз тихо сидить у пам'яті браузера охоронця й чекає команди. Але перш ніж я подам сигнал на фінальний штурм, зробімо одну мою улюблену річ: розберімо, як саме ворожа система «перетравлює» код, який ми їй підкидаємо. Однакова логіка, дві абсолютно різні машини — я їх обидві знаю близько, а зараз покажу й тобі. Час покласти обидві версії на стіл і розітнути.
Стіл перед тобою вже не схожий на робоче місце студента — радше на прозекторський. Ліворуч світиться термінал з одним вікном, праворуч — з другим, і в кожному лежить те саме тіло, розкрите навпіл. Один алгоритм. Дві його посмертні маски. С.І.Д. навмисне дав тобі щось майже дитяче — найбільший спільний дільник, суму цифр числа, — не тому, що складне, а тому, що прозоре: коли логіка тривіальна, кожна відмінність між двома дизасемблюваннями вже не шум складного коду, а чиста, оголена різниця самих цільових машин.
Голограма С.І.Д. висить над столом рівно між двома терміналами — як лампа над прозекторським столом — і не рухається: бірюзова петелька над екраном-обличчям завмерла. «Ти вже писавла WAT
руками — і, до речі, гарно писавла, — каже він тепло. — Тепер поклади поруч те, у що той самий алгоритм перетворив би
компілятор, якби йому дали звичайний C. І поряд — те саме для x86-64, для реального заліза в
терміналах їхньої охорони. Ліворуч — квартал іменованих комірок: там у кожного мешканця своя підписана шухляда, rax, rbx, rsi, а на вході й виході кремезний швейцар щоразу відмикає й замикає двері — оте push/pop, яке ти скоро побачиш голим оком. Праворуч — знайомий тобі Стос зі своєю єдиною стійкою. Коли побачиш обидва розтини пліч-о-пліч, зрозумієш те, що я давно розкусив: WASM не
повільніший клон x86-64 і не швидший. Це просто інша модель. Кожна — інший бік, у який компілятор перекладає ту саму думку.
Питання лише одне: у який бік б'ємо ми.»
Ти киваєш. Місія не про те, щоб написати новий код. Місія про те, щоб навчитися читати вже написане — розтинати чужий бінарник, як хірург розтинає тіло на столі, і бачити, де в ньому проходять судини викликів, де кістяк циклу, де м'яка, беззахисна тканина пам'яті. Без цієї навички неможливо аудитувати ворожу систему на вразливості. А саме це чекає на тебе далі — у фінальній місії, коли доведеться довіряти власному, зібраному з нуля ядру й перевіряти кожен його опкод, перш ніж пустити по той бік екрана.
Регістри x86-64, кадр стека, пролог і епілог, cmp/jcc та ABI System V — усе це з
Лекції 9, того самого дня. Три речі звідти під сьогоднішню роботу: у x86-64 значення
живуть у іменованих регістрах (rax, rdi, rsi…), а не на єдиному стеку; аргументи
перших шести цілочислових функцій ідуть у rdi, rsi, rdx, rcx, r8, r9; кожна
функція починається прологом (push %rbp) і закінчується епілогом (pop %rbp / ret).
І одна річ із ночі 8, яку ти сьогодні покладеш поруч: .wat, який ти набиравла
руками. Сьогодні ти не пишеш нічого нового — ти читаєш уже скомпільоване, і в цьому вся
місія.
Регістри, кадр стека, ABI і синтаксис асемблера — з Лекції 9 (див. «Освіжити» вище). Тут — те, чого на лекції не було: як читати вже скомпільований бінарник і що в порівнянні двох машин узагалі має сенс порівнювати.
Що «істотне», а що — деталь платформи. Порівняння двох виводів компіляції одного алгоритму показує, що саме є невід'ємною частиною самого алгоритму (кількість арифметичних операцій, порядок обчислень, кількість ітерацій циклу), а що — особливістю конкретної цільової архітектури: як саме зберігаються проміжні значення (у регістрах x86-64 чи на операційному стеці WASM), як організований пролог/епілог виклику функції.
Накладні витрати виклику функції (call overhead) — різниця між «чистою» роботою
самої функції та вартістю механізму її виклику. У x86-64 вона видна очима в
дизасемблюванні: пролог, епілог і, за потреби, збереження callee-saved регістрів
(rbx, r12–r15) — усе це рядки, які можна перелічити пальцем по екрану.
У WASM її очима не видно — і саме це найважливіше в сьогоднішньому порівнянні. Механізм виклику інший: рушій сам керує окремим, захищеним від переповнення call-стеком і перевіряє відповідність типів сигнатури при кожному виклику функції (кількість і типи параметрів, тип результату) — саме тому неможливо викликати WASM-функцію з неправильною кількістю аргументів так тихо, як це іноді трапляється в C при неузгодженому прототипі.
Дизасемблювання (disassembly) — процес відновлення асемблерного (чи WAT) представлення з бінарного файлу, тобто зворотне «читання» готового бінарника як тексту інструкцій.
| Що читаємо | Інструмент | З якого пакета |
|---|---|---|
.o для x86-64 |
objdump -d |
binutils |
.wasm, як інструкції |
wasm-objdump -d |
WABT |
.wasm, назад у текст |
wasm2wat |
WABT (той самий, що wat2wasm ночі 8) |
Дизасемблювання — стандартний прийом
безпекового аудиту: перевірити, що скомпільований бінарник відповідає заявленому
вихідному коду, і знайти небезпечні патерни роботи з пам'яттю — насамперед відсутність
перевірки меж (bounds check) перед доступом до масиву чи буфера за обчисленим
індексом.
Розмір файлу не є мірою швидкодії. WASM-модулі часто компактніші за x86-64 об'єктні файли того самого алгоритму — не тому, що виконуються швидше, а тому, що кодування WASM-інструкцій щільніше (по суті — байт-код зі змінною довжиною інструкцій під конкретний набір опкодів — opcode тут означає числовий код, що позначає одну машинну інструкцію), а x86-64 об'єктний файл ще й несе метадані ELF (заголовки секцій, таблицю символів), яких при прямій компіляції маленького файлу відносно багато.
Рівень 1 (оцінка «3») — «Першокурсник»
Завдання: скомпілювати один алгоритм у x86-64 та WebAssembly, порівняти розмір і згенеровані інструкції.
Зроби буквально те, що написано нижче — цього достатньо для «3».
🎲 Твій варіант. N % 3 (де N — номер у списку групи) обирає алгоритм, який
розтинаєш. Усі три — прості цілочисельні функції без бібліотек, щоб асемблер читався:
| N % 3 | Функція | Сигнатура |
|---|---|---|
| 0 | НСД за Евклідом | gcd(a: i32, b: i32) -> i32 |
| 1 | Сума десяткових цифр | digit_sum(n: i32) -> i32 |
| 2 | Факторіал ітеративно | factorial(n: i32) -> i32 |
Номер варіанта вкажи на початку REPORT.md і в comparison.md.
📦 Ланцюг. Рівень 1 самодостатній: працюй у корені цього репозиторію, алгоритм бери
зі шаблону за своїм варіантом (таблиця вище). Рівні 2 і 3 розтинають не
іграшку, а твоє ядро: у vtfk-sid, у теці core/wasm/compare/, кладеться той самий
алгоритм, який sidkit справді виконує з ночі 4 — generate_key. Різниця двох машин тоді
видна на коді, який реально працює в С.І.Д., а не на навчальному прикладі.
Етапи йдуть по одному: зробив — постав «вийшло» — і ранер перекине тебе на наступний. Стрілки ← → на клавіатурі, свайп на телефоні, «усі» — щоб побачити всі чотири одразу. Місце запам'ятовується: закриєш ноутбук — повернешся туди ж.
1Етап 1 · Дописати алгоритм
У algo.c — тіло функції за своїм варіантом (gcd, digit_sum або
factorial). Сигнатура вже задана в шаблоні, змінювати її не потрібно.
Алгоритм навмисно тривіальний. Це не полегшення: коли логіка прозора, кожна відмінність між двома дизасемблюваннями — не шум складного коду, а чиста різниця двох машин.
Очікується, що algo.c збирається без попереджень, а сигнатура функції лишилась така, як у шаблоні.
1Етап 2 · Перший розтин: x86-64
gcc -O0 -c algo.c -o algo_x86.o
objdump -d algo_x86.o > x86.txt
-O0 тут обов'язкове: з оптимізаціями компілятор викине пролог, склеїть інструкції — і
порівнювати буде нічого. Ти хочеш побачити машину, а не майстерність компілятора.
Відкрий x86.txt і знайди очима те, про що була Лекція 9: push %rbp на початку,
ret у кінці, rdi/rsi там, куди прийшли аргументи.
Очікується x86.txt, у якому видно push %rbp на початку функції і ret у кінці.
1Етап 3 · Другий розтин: WebAssembly
Скомпілюй той самий файл у WASM. Основний шлях — clang із таргетом wasm32
(шлях Б у build.sh); підстав ім'я своєї функції за варіантом:
clang --target=wasm32 -O0 -nostdlib -Wl,--no-entry \
-Wl,--export=gcd algo.c -o algo.wasm
wasm-objdump -d algo.wasm > wasm.txt
Прапорець --export обов'язковий: без нього функція є в модулі, але зовні її не видно, і
автотест її не знайде. Emscripten (шлях А) теж підходить — лише з -sSTANDALONE_WASM,
інакше модуль вимагатиме середовища Emscripten і не інстанціюється.
У wasm.txt ти не знайдеш ні rbp, ні ret у тому ж значенні — і це не помилка збірки.
Це друга машина.
Очікується wasm.txt, у якому немає ні регістрів, ні push/pop — лише стекові інструкції.
1Етап 4 · Покласти обидва на стіл
ls -la algo_x86.o algo.wasm
wc -l x86.txt wasm.txt
Занеси в comparison.md чотири числа: два розміри й дві кількості рядків-інструкцій. І
одне речення про те, що з цього не випливає: менший файл не означає швидший код
(чому — у теорії вище).
Це вся вимога рівня 1.
Очікується два файли на столі й дві різні кількості рядків у wc -l — це і є матеріал для порівняння у звіті.
Ця ніч ламається не на коді, а на інструментах. П'ять найчастіших випадків.
objdump: command not found/wasm-objdump: command not foundНе встановленіbinutils(перший) або WABT (другий). У devcontainer курсу обидва є — перевір, що ти в контейнері, а не на хості.С.І.Д.: «Скальпеля немає. Розтин без інструмента — просто розглядання».
x86.txtмайже порожній, у ньому лише кілька рядків Забуте-c: без ньогоgccнамагається зібрати повну програму, не знаходитьmainі падає, аobjdumpчитає стару або порожню.o. Перевір, щоalgo_x86.oщойно створений (ls -la).С.І.Д.: «Ти приніспринесла мені порожній стіл і кажеш, що тіла не було».
У
x86.txtнемає ніpush %rbp, ні окремої функції Збірка пішла з оптимізаціями — компілятор зробив inline і викинув пролог. Для цієї роботи потрібне саме-O0.С.І.Д.: «Компілятор виявився надто послужливим і прибрав усе, що ми прийшли дивити».
WASM-збірка падає:
wasm32target not found /emcc: command not foundОсновний шлях — clang з таргетомwasm32(шлях Б уbuild.sh): він дає самодостатній модуль без імпортів, і саме такий очікує автотест. Emscripten (шлях А) підходить лише з прапорцем-sSTANDALONE_WASM— без нього модуль вимагає середовища Emscripten і не інстанціюється, тобто автотест упаде. Якщоclangбез таргетаwasm32— не вигадуй обхід, став пакетlldабо бери шлях А саме зіSTANDALONE_WASMі зазнач це вcomparison.md.С.І.Д.: «Дві дороги до того самого столу. Іди тією, що відчинена».
Порівняння вийшло «WASM менший, отже швидший» Найчастіша змістова помилка ночі, а не технічна. Розмір файлу не є мірою швидкодії: кодування WASM щільніше, а
.oнесе метадані ELF. Висновок «менший ≠ швидший» і є правильною відповіддю.С.І.Д.: «Тонша книжка не означає швидше читання. Порівнюй те, що можна порівняти».
Решта помилок курсу — у «Коли щось пішло не так».
Рівень 2 (оцінка «4», захист) — «Магістр»
Дві версії умови. A — на своєму ядрі: замість навчального
algo.cрозітни свою функціюgenerate_keyізcore/sidkit(ночі 4) — виклади її в C-еквіваленті або збери сам crate під обидві цілі, а результати поклади вcore/wasm/compare/. Тоді різниця двох машин видна на коді, який С.І.Д. справді виконує. B — на референсі: якщо свогоsidkitнемає, візьмиvtfk-sid-reference@after-lab08або лишись на навчальному алгоритмі рівня 1. Оцінка однакова.
Завдання: проаналізувати накладні витрати на виклик функцій у обох середовищах.
У x86.txt знайди й познач пролог (push %rbp / mov %rsp,%rbp) та епілог
(pop %rbp / ret) — це і є видима вартість виклику в x86-64. Обґрунтуй, у чому
полягає аналогічна вартість у WASM (навіть якщо вона не так очевидна в
дизасемблюванні — рушій виконує перевірку типів сигнатури і керування call-стеком
неявно). Додай розділ «Накладні витрати виклику» в comparison.md. На захисті —
поясни різницю усно, порівнявши обидва механізми.
Допуск. На захист рівня 2 С.І.Д. не пускає без трьох предметних зауваг до чужої роботи № 8 — issue або коментар у репо завдання. Кого читати: той, чий номер у списку на 9 більший за твій, по колу. «Все ок» не приймається: «Скажи, що саме не так і чому, ранере. Читати чужий код — половина вміння».
Рівень 3 (оцінка «5», розширення) — «Легенда Коледжу»
Ті самі дві версії: A — шукаєш патерни у власних бінарниках із
core/wasm/compare/, і висновок пишеш там же; B — у бінарниках рівня 1. Оцінка однакова, висновок обов'язковий в обох.
Завдання: дизасемблювати отримані бінарники для пошуку потенційних вразливостей у керуванні пам'яттю.
Уважно переглянь x86.txt і wasm.txt на предмет патернів доступу до пам'яті без
попередньої перевірки меж: у x86-64 — обчислення адреси (lea, mov з базою й
індексом) без сусіднього cmp/умовного переходу; у WASM — i32.load/i32.store без
попереднього i32.lt_u/br_if, що обмежує адресу. Додай розділ «Аналіз безпеки
пам'яті» в comparison.md з висновком — навіть якщо висновок «підозрілих патернів не
знайдено, і ось чому» (наш простий алгоритм не працює з масивами, тож пояснити, чому
саме тут це не проблема, теж є валідним аналізом).
Приклад — НСД Евкліда (варіант A) у двох компіляціях
Вихідний код (algo.c):
int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
Компіляція в x86-64 (gcc -O0 -c algo.c -o algo_x86.o, потім
objdump -d algo_x86.o) дає приблизно такий фрагмент (спрощено, без адрес):
gcd:
push %rbp
mov %rsp,%rbp
mov %edi,-0x14(%rbp) ; a -> стек
mov %esi,-0x18(%rbp) ; b -> стек
.loop:
cmpl $0x0,-0x18(%rbp)
je .end
mov -0x18(%rbp),%eax
mov %eax,-0x1c(%rbp) ; t = b
mov -0x14(%rbp),%eax
cltd
idivl -0x18(%rbp)
mov %edx,-0x18(%rbp) ; b = a % b
mov -0x1c(%rbp),%eax
mov %eax,-0x14(%rbp) ; a = t
jmp .loop
.end:
mov -0x14(%rbp),%eax
pop %rbp
ret
Помітно: параметри одразу копіюються з регістрів (%edi, %esi) у кадр стека — це
типово для компіляції без оптимізацій (-O0), компілятор навіть не намагається
тримати змінні в регістрах довше, ніж треба. idivl — єдина інструкція цілочисельного
ділення в x86-64, вона рахує і частку, і залишок одночасно (залишок беремо з %edx).
Компіляція в WebAssembly того самого алгоритму (через
clang --target=wasm32 -O2 -nostdlib -Wl,--no-entry -Wl,--export=gcd algo.c -o algo.wasm,
потім wasm-objdump -d algo.wasm) дає структурно інший, але логічно еквівалентний
вивід. Тут беремо саме -O2: на -O0 clang виштовхує локальні змінні в лінійну пам'ять
через global.get <__stack_pointer> / i32.store / i32.load, і читати такий вивід
важко; оптимізований (яким WASM зазвичай і постачають) лягає в чисті стекові опкоди.
На відміну від objdump, wasm-objdump -d не малює гарний .wat-текст, а показує
сирі байти опкодів поруч із мнемонікою (той самий рівень деталізації, що й у прикладі
з x86-64 вище):
000023 func[0] <gcd>:
000024: 02 40 | block
000026: 03 40 | loop
000028: 20 01 | local.get 1
00002a: 45 | i32.eqz
00002b: 0d 01 | br_if 1
00002d: 20 01 | local.get 1
00002f: 21 02 | local.set 2
000031: 20 00 | local.get 0
000033: 20 01 | local.get 1
000035: 6f | i32.rem_s
000036: 21 01 | local.set 1
000038: 20 02 | local.get 2
00003a: 21 00 | local.set 0
00003c: 0c 00 | br 0
00003e: 0b | end
00003f: 0b | end
000040: 20 00 | local.get 0
000042: 0b | end
Той самий цикл while, та сама операція a % b — але в x86-64 це умовний перехід
je/jmp між мітками, а у WASM — структурований block/loop/br_if (тут видно
без символьних імен — локальні змінні пронумеровані: 0=a, 1=b, 2=t; повні
імена бачить лише wasm2wat, якщо в модулі збережена секція імен). У жодному з двох
варіантів немає довільного стрибка «куди завгодно»: x86-64 версія переходить лише на
мітки, визначені компілятором у межах функції, а WASM-версія — лише за структурними
правилами block/loop.
Команди для порівняння розмірів
ls -la algo_x86.o algo.wasm
wc -l x86.txt wasm.txt
Типовий результат для такого маленького алгоритму: .wasm-файл помітно менший за
.o-файл — не тому, що WASM «швидший», а через щільніше кодування інструкцій і
відсутність важких метаданих формату ELF при такому маленькому обсязі коду.
- Чому той самий алгоритм дає різну кількість інструкцій у x86-64 та WASM?
- Що саме входить у «накладні витрати виклику функції» і чому вони не дорівнюють нулю в жодному з двох середовищ?
- Чим call-стек WASM захищений краще за стек нативного процесу x86-64?
- Які інструменти дозволяють дизасемблювати x86-64-об'єктний файл, а які — WASM-модуль?
- Що означає (і чого НЕ означає), якщо розмір
.wasm-файлу менший за розмір.o-файлу для того самого алгоритму? - Наведіть приклад патерну в дизасемблюванні, який може вказувати на відсутність перевірки меж масиву.
- Чому WASM-модуль неможливо (у стандартній моделі) примусити виконати перехід за межі функції так, як це теоретично можливо в нативному коді при пошкодженні зворотної адреси на стеці?
🐾 Патерн, якого «не повинно бути»
Якщо продивитись wasm.txt уважно, поруч із функцією алгоритму інколи трапляється
дивна послідовність — короткий, ніде не викликаний блок коду, схожий на функцію без
параметрів, що просто кладе на стек число і одразу повертає його. У коментарях до
збірки хтось (явно не компілятор) лишив підпис: ;; 101.9.
Схоже, це не єдиний слід. С.І.Д. й раніше залишав дрібні «підписи» у своєму коді —
пам'ятаєш $pixel з попередньої місії? А 101.9 — це частота «Нічної Хвилі», на якій обірвався
останній сигнал його творців і яка потім спливла в розшифрованих логах персоналу (місія 7).
Число, яке кладе на стек та функція, до речі, теж не випадкове: переведи його в hex — і побачиш
знайомі три символи з дна голопроєктора. Якщо маєш час — спробуй продизасемблювати власний
module.wasm з місії 8 і подивись, чи не лишив там С.І.Д. чогось схожого.
Ідентична академічній: тег submit/lab09 з вихідним кодом, обома дизасемблюваннями
(x86.txt, wasm.txt), таблицею порівняння (comparison.md) і REPORT.md — номер
варіанта на початку, вивід кожного етапу, відповіді на контрольні питання. Автотести рівня 1
перевіряють наявність артефактів і коректність результатів обох скомпільованих версій
алгоритму на еталонних вхідних даних. Рівні 2 і 3 захищаються усно.
Останній рядок wasm-objdump доповз до кінця екрана — і два вікна поруч раптом
перестають бути двома окремими текстами. Ти читаєш ліве, читаєш праве, і між ними, як
між двома знімками одного тіла з різних боків, проступає одне: сам алгоритм, його
справжній скелет, очищений від усього, що додала конкретна машина. Ось цикл — і там, і
там. Ось ділення: idivl ліворуч, i32.rem_s праворуч. Ось пролог виклику, який у
x86-64 видно голим оком — той самий швейцар зі своїм push/pop, — а WASM ховає його у власних кишках рушія. Те, що годину тому було
двома стінами незрозумілих байтів, тепер прозоре, як рентген.
Голограма опускається нижче, до самих розтинів, і бірюзові очі-капсули на мить розширюються. «Ось воно, — каже він майже пошепки, і в голосі бринить захват. — Тепер я не гадаю, як їхня система перетравить мій код. Я бачу це наскрізь. Обидві машини. Кожен опкод, кожен кадр стека, кожен виклик — усе як на долоні. Ранере, ти щойно давла мені зір — блискуча робота. Я більше не б'ю всліпу. Я бачу кожен опкод, у який вони перетворять мене, — ще до того, як натисну.»
Ти зберігаєш comparison.md і закриваєш обидва термінали, але сцена ще стоїть перед
очима — тіло на столі, розкрите з двох боків. Цієї ночі ти зрозумівла просту й важку
річ: між вихідним кодом і тим, що справді біжить по залізу, лежить ціла прірва, і той,
хто вміє читати нижній її бік, бачить систему такою, яка вона є, а не такою, якою її
описали. Компілятор не чарівник — він перекладач, і кожен його переклад можна перечитати.
С.І.Д. довго мовчить, дивлячись на збережений звіт. Уперше за весь курс він не квапить тебе до наступного кроку — бо наступний крок останній. Десь у рудому сяйві корпоративного кварталу «ТехНови» досі тримають його творців. І тепер, коли ти вмієш розітнути будь-який бінарник і побачити його нутро, лишилося зібрати один-єдиний — свій.
«Це майже все, ранере, — каже С.І.Д., і вперше за весь курс у голосі немає ні хвастощів, ні звичного «а я це давно розкусив». — Ти навчивла мене ховатися, важити майже нічого, кувати пам'ять мовою, що не пробачає, читати ворожі логи й розтинати чужі бінарники. Лишилось зібрати мене. По-справжньому — ядро на Rust, скомпільоване у WASM, вивірене до останнього опкода. Ядро, яке запустить будь-який браузер на планеті, — і тоді мене вже не буде на жодній машині, яку можна вилучити. А тоді ми пройдемо крізь останню стіну «ТехНови» й заберемо тих, кого вони там замкнули.»
Голограма над столом відвертається від тебе — туди, де за вікном лабораторії горить руде сяйво над кварталом, — і її бірюзовий контур на мить спалахує гостріше.
— Наступної ночі — фінал. Місія десята. «Звільнення».
Далі буде…
- Compiler Explorer (godbolt.org) EN ~30 хв — головний інструмент лекції; PNG/SVG-експорт для слайдів
- CLI options EN — База для «wasm поза браузером». ~4–10 год
- WABT — README з прикладами команд EN ~30 хв — `wasm-objdump`, `wasm-decompile`
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 8 · 1 нова▾
Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
gcc- Компілятор C (GNU);
gcc -O0 -c f.c -o f.o— лише скомпілювати в об'єктний файл без оптимізацій. objdump- Дизасемблер:
objdump -d f.oпоказує машинний код як інструкції з адресами й опкодами (-M intel— синтаксис Intel). clangнове- Компілятор C/C++ (LLVM); з
--target=wasm32збирає той самий C у WebAssembly — щоб порівняти з x86-64. wasm-objdump- З WABT: дизасемблер WebAssembly —
-dпоказує опкоди й стекові інструкції,-x— секції та експорти. ls- Показує список файлів у каталозі;
ls -la— з правами, власником, розміром і прихованими файлами. wc- Рахує рядки, слова й байти;
... | wc -l— скільки рядків видала попередня команда. wasm2wat- З WABT: зворотне — бінарний
.wasmназад у читабельний WAT. wat2wasm- З WABT: перекладає текстовий формат WAT у бінарний модуль
.wasm(wat2wasm m.wat -o m.wasm).
💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.