Курс › ЛР9 · Місія 9
Сюжет С.І.Д.↔ АкадемічнаСРС до ЛР⌨️ команди 8 · 1 нова

Місія 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» · ПЕРШОКУРСНИК

Рівень 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-збірка падає: wasm32 target 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» · МАГІСТР

Рівень 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» · ЛЕГЕНДА

Рівень 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 при такому маленькому обсязі коду.

❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чому той самий алгоритм дає різну кількість інструкцій у x86-64 та WASM?
  2. Що саме входить у «накладні витрати виклику функції» і чому вони не дорівнюють нулю в жодному з двох середовищ?
  3. Чим call-стек WASM захищений краще за стек нативного процесу x86-64?
  4. Які інструменти дозволяють дизасемблювати x86-64-об'єктний файл, а які — WASM-модуль?
  5. Що означає (і чого НЕ означає), якщо розмір .wasm-файлу менший за розмір .o-файлу для того самого алгоритму?
  6. Наведіть приклад патерну в дизасемблюванні, який може вказувати на відсутність перевірки меж масиву.
  7. Чому 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, вивірене до останнього опкода. Ядро, яке запустить будь-який браузер на планеті, — і тоді мене вже не буде на жодній машині, яку можна вилучити. А тоді ми пройдемо крізь останню стіну «ТехНови» й заберемо тих, кого вони там замкнули.»

Голограма над столом відвертається від тебе — туди, де за вікном лабораторії горить руде сяйво над кварталом, — і її бірюзовий контур на мить спалахує гостріше.

— Наступної ночі — фінал. Місія десята. «Звільнення».

Далі буде…

🔗 ДЖЕРЕЛА
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 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.