Лабораторна робота № 9. Той самий алгоритм у двох асемблерах
Мета
Навчитися компілювати один і той самий алгоритм у два різних цільові набори інструкцій — x86-64 та WebAssembly — і порівнювати результат: розмір бінарника, кількість і склад згенерованих інструкцій, накладні витрати на виклик функції та результати дизасемблювання.
📚Теоретичні відомості291 слів · ~2 хв
Регістри x86-64, кадр стека, пролог/епілог, прапорці, умовні переходи, ABI System V і синтаксис NASM викладені на лекції 9 того самого тижня. Розділ нижче наведено як довідку до практики читання бінарників, а не як первинне викладення матеріалу.
Той самий алгоритм, написаний однією мовою вищого рівня (наприклад, 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для порівняння розмірів файлів
Порядок виконання
- Обрати алгоритм за варіантом
N % 3, деN— номер у списку групи (0 — НСД, 1 — сума цифр числа, 2 — факторіал), з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 виконується в репозиторії завдання — його створює Classroom 50 на прийняття, окремий на кожну роботу, і саме там працюють автотести (здача тегом
submit/labNN). Рівні 2 і 3 виконуються в наскрізному репозиторіїvtfk-sid, який студент створює один раз у роботі № 1 і веде до кінця семестру; вони оцінюються на захисті, не автотестами. Шляхи видуcore/...у тексті нижче — це шляхи вvtfk-sid.Тег ставить скрипт здачі, а не студент. У робочому просторі — Ctrl+Shift+B (меню
Terminal → Run Build Task), або те саме з терміналу:bash .devcontainer/submit.sh. Скрипт комітить незбережене, показує, який тег створить, і чекає підтвердження; відмова нічого не здає. Ручніgit tag/git pushлишаються запасним шляхом — вони працюють і поза Codespaces.
Рівень 1 (оцінка 3)
Скомпілювати один алгоритм у x86-64 та WebAssembly, порівняти розмір і згенеровані
інструкції за таблицею з comparison.md. Рівень 1 виконується в корені репо лабораторної
на навчальному алгоритмі зі шаблону і не залежить від результатів попередніх
робіт.
Рівень 2 (оцінка 4, захист)
Проаналізувати накладні витрати на виклик функцій у обох середовищах: показати
пролог/епілог у x86.txt і обґрунтувати аналогічну вартість у WASM. На захисті —
пояснити різницю усно.
Два рівноцінні варіанти виконання (оцінка однакова):
- A — на власному наскрізному артефакті: замість навчального
algo.cрозтинається функціяgenerate_keyіз власного cratecore/sidkit(робота № 4) — у C-еквіваленті або збіркою самого crate під обидві цілі; результати складаються вcore/wasm/compare/. Різниця архітектур демонструється на коді, який реально працює в наскрізному артефакті. - B — на референсному знімку: береться
vtfk-sid-reference@after-lab08або навчальний алгоритм рівня 1.
Допуск до захисту. Перед захистом рівня 2 треба залишити три предметні зауваги до роботи № 8 іншого студента — в issue або коментарі до комміта його репозиторію завдання. Кого рецензувати: студента, чий номер у списку групи на 9 більший за власний (по колу — номер цієї роботи задає зсув, тому пари щоразу різні). Заувага предметна, якщо називає конкретний рядок, команду або крок і пояснює, у чому проблема; загальні оцінки («усе правильно», «гарний код») не враховуються. Це елемент ЗК7 — здатності працювати в команді: рівень 1 доводить, що код збирається, рецензія — що студент уміє його читати.
Рівень 3 (оцінка 5, розширення)
Дизасемблювати отримані бінарники для пошуку потенційних вразливостей в управлінні
пам'яттю (доступ до пам'яті без перевірки меж) і задокументувати висновок у
comparison.md, навіть якщо він негативний.
Ті самі два варіанти: A — аналіз власних бінарників із core/wasm/compare/, висновок
там же; B — аналіз бінарників рівня 1.
Алгоритм НСД Евкліда (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 захищаються усно: студент пояснює знайдені відмінності у накладних
витратах виклику та показує (якщо є) підозрілі патерни в дизасемблюванні. Звіт
додає скріншоти обох дизасемблювань і заповненої таблиці.