Лабораторна робота № 9. Той самий алгоритм у двох асемблерах
Мета
Навчитися компілювати один і той самий алгоритм у два різних цільові набори інструкцій — x86-64 та WebAssembly — і порівнювати результат: розмір бінарника, кількість і склад згенерованих інструкцій, накладні витрати на виклик функції та результати дизасемблювання.
Теоретичні відомості
Той самий алгоритм, написаний однією мовою вищого рівня (наприклад, C), компілятор
може перетворити на послідовність інструкцій для абсолютно різних виконавчих машин.
Для x86-64 (лабораторна № 8 попереднього модуля курсу асемблера) це реальний
фізичний процесор з фіксованим набором іменованих регістрів і угодою виклику System V
AMD64 ABI. Для WebAssembly — стекова віртуальна машина з операційним стеком значень і
структурованим керуванням потоком (block/loop/br_if).
Порівняння двох виводів компіляції показує, що саме є «істотним» для алгоритму (кількість операцій, порядок обчислень), а що — деталлю конкретної цільової архітектури (як саме зберігаються проміжні значення: у регістрах чи на стеці, як організований пролог/епілог функції).
Накладні витрати на виклик функції (call overhead) — різниця між «чистою» роботою
функції та вартістю самого механізму виклику: збереження регістрів, побудова кадру
стека, передача аргументів. У x86-64 це пролог/епілог (push %rbp, mov %rsp,%rbp,
pop %rbp) плюс збереження callee-saved регістрів. У WASM виклик функції теж має
вартість — рушій перевіряє типи сигнатури і керує окремим call-стеком, захищеним від
переповнення (на відміну від необмеженого стека нативного коду).
Дизасемблювання — процес відновлення асемблерного представлення з бінарного
файлу. Для x86-64 інструмент — objdump -d, для WebAssembly — wasm-objdump -d (з
пакета WABT) або wasm2wat для повернення до текстового формату. Дизасемблювання
використовується для аудиту безпеки: перевірки, що скомпільований код відповідає
вихідному, і пошуку небезпечних патернів роботи з пам'яттю (наприклад, відсутність
перевірки меж масиву).
Розмір скомпільованого файлу сам собою не є мірою швидкодії: WASM-модулі часто компактніші за об'єктні файли x86-64 того самого алгоритму через щільніше кодування інструкцій, а не через вищу продуктивність виконання.
Обладнання та програмне забезпечення
- Codespaces / devcontainer курсу з
gcc,binutils(objdump),wabt(wasm-objdump,wat2wasm,wasm2wat) - Rust з таргетом
wasm32-unknown-unknown(rustup target add wasm32-unknown-unknown) або Emscripten/Clang для C→WASM wc,ls -laдля порівняння розмірів файлів
Порядок виконання
- Обрати алгоритм за варіантом (НСД, сума цифр числа, факторіал) з
starter/README.md. Очікуваний результат: відома сигнатура функції та її логіка. - Скомпілювати алгоритм у x86-64:
gcc -O0 -c algo.c -o algo_x86.o, отримати дизасемблюванняobjdump -d algo_x86.o > x86.txt. Очікуваний результат: об'єктний файл і непорожнійx86.txtз міткою функції. - Скомпілювати той самий алгоритм у WebAssembly (
clang --target=wasm32 ...або еквівалент зstarter/README.md), отримати.wasm. Очікуваний результат: валіднийalgo.wasmз експортованою функцією. - Дизасемблювати WASM:
wasm-objdump -d algo.wasm > wasm.txt. Очікуваний результат: непорожнійwasm.txt. - Порівняти розміри файлів (
ls -la algo_x86.o algo.wasm) і кількість інструкцій у кожному дизасемблюванні. - Заповнити таблицю порівняння (шаблон у
starter/README.md): кількість інструкцій, розмір у байтах, наявність пролог/епілог-патернів. - Для рівня 2 — виміряти чи оцінити накладні витрати виклику функції в обох середовищах.
- Для рівня 3 — переглянути дизасемблювання на предмет патернів роботи з пам'яттю без перевірки меж.
Рівень 1 (оцінка 3)
Скомпілювати один алгоритм у x86-64 та WebAssembly, порівняти розмір і згенеровані
інструкції за таблицею з comparison.md.
Рівень 2 (оцінка 4, захист)
Проаналізувати накладні витрати на виклик функцій у обох середовищах: показати
пролог/епілог у x86.txt і обґрунтувати аналогічну вартість у WASM. На захисті —
пояснити різницю усно.
Рівень 3 (оцінка 5, розширення)
Дизасемблювати отримані бінарники для пошуку потенційних вразливостей в управлінні
пам'яттю (доступ до пам'яті без перевірки меж) і задокументувати висновок у
comparison.md, навіть якщо він негативний.
Розібраний приклад
Алгоритм НСД Евкліда (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, спрощено):
gcd:
push %rbp
mov %rsp,%rbp
mov %edi,-0x14(%rbp)
mov %esi,-0x18(%rbp)
.loop:
cmpl $0x0,-0x18(%rbp)
je .end
mov -0x18(%rbp),%eax
mov %eax,-0x1c(%rbp)
mov -0x14(%rbp),%eax
cltd
idivl -0x18(%rbp)
mov %edx,-0x18(%rbp)
mov -0x1c(%rbp),%eax
mov %eax,-0x14(%rbp)
jmp .loop
.end:
mov -0x14(%rbp),%eax
pop %rbp
ret
Дизасемблювання WebAssembly того самого алгоритму (wasm-objdump -d algo.wasm) — на
відміну від objdump, тут немає гарного .wat-тексту, лише сирі байти опкодів поруч
із мнемонікою і пронумерованими (без імен) локальними змінними (0=a, 1=b,
2=t):
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
Той самий цикл і та сама операція a % b — але умовний перехід je/jmp між
мітками x86-64 замінено структурованим block/loop/br_if у WASM. Команди
порівняння розмірів: ls -la algo_x86.o algo.wasm та wc -l x86.txt wasm.txt.
Контрольні питання
- Чому той самий алгоритм дає різну кількість інструкцій у x86-64 та WASM?
- Що саме входить у «накладні витрати виклику функції» і чому вони не дорівнюють нулю?
- Чим call-стек WASM захищений краще за стек нативного процесу?
- Які інструменти дозволяють дизасемблювати x86-64-об'єктний файл, а які — WASM-модуль?
- Що означає, якщо розмір
.wasm-файлу менший за розмір.o-файлу для того самого алгоритму? - Наведіть приклад патерну в дизасемблюванні, який може вказувати на відсутність перевірки меж масиву.
- Чому WASM-модуль неможливо (у стандартній моделі) примусити виконати jmp за межі функції так, як це теоретично можливо в нативному коді?
Парні теми СРС
- Рівень 2: srs17 — Угоди виклику (calling conventions) x86-64
- Рівень 3: srs18 — Інструменти дизасемблювання: objdump, wasm-objdump
Критерії оцінювання та форма звіту
Здається тег submit/lab09 з вихідним кодом алгоритму, обома дизасемблюваннями
(x86.txt, wasm.txt) і заповненою таблицею порівняння (comparison.md). Автотести
рівня 1 (див. autograder/README.md) перевіряють: наявність усіх артефактів, що
обидва бінарники успішно скомпільовані та відповідають вихідному алгоритму
(виконуються на еталонних вхідних даних з коректним результатом), що таблиця
порівняння заповнена (розмір, кількість інструкцій — непорожні числові значення).
Рівні 2 і 3 захищаються усно: студент пояснює знайдені відмінності у накладних
витратах виклику та показує (якщо є) підозрілі патерни в дизасемблюванні. Звіт
додає скріншоти обох дизасемблювань і заповненої таблиці.