Мова машини
VTFK · Системне програмування · Модуль 3
Фокус: прочитати згенерований асемблер, а не писати його з нуля
Мета — читати й розуміти, а не писати великі програми вручну
callУсі чотири числа — з System V AMD64 ABI; кожне повернеться далі в лекції
Регістри — це «змінні» процесора, яких рівно стільки, скільки є фізично
Аргументи 1–6 — у регістрах у фіксованому порядку; це і є System V ABI, за якою C і asm розуміють одне одного
| 64-біт | 32-біт | 16-біт | 8-біт | Запис обнуляє верхні біти? |
|---|---|---|---|---|
rax | eax | ax | al | 32-біт — так; 16/8-біт — ні |
rdi | edi | di | dil | те саме правило |
rsi | esi | si | sil | те саме правило |
rsp | esp | sp | spl | вершина стека — не чіпати руками |
r8…r15 | r8d…r15d | r8w…r15w | r8b…r15b | те саме правило |
Запис у eax обнуляє верхні 32 біти rax; запис у ax чи al — ні: типове джерело «сміття» в регістрі. Перевірка: objdump -d покаже movl (обнуляє) чи movw (не обнуляє)
Один фізичний код, різна нотація. Читаємо AT&T (gcc -S, objdump), пишемо Intel (NASM). Плутанина №1 новачків — напрямок операндів
int add(int a, int b) {
return a + b;
}5 рядків, з яких народиться асемблер
add:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
addl -8(%rbp), %eax
popq %rbp
retgcc -O0 -S add.c — без оптимізацій, щоб бачити кожен крок
pushq %rbpзберегти базовий вказівник викликача на стекуmovq %rsp, %rbpновий кадр стека (frame pointer) починається тутpopq %rbpповернути базовий вказівник викликачаretзняти адресу повернення зі стека і перейти тудиКадр стека (stack frame) — «робочий стіл» функції на час її виконання; кожному call відповідає рівно один ret
rsp — вершина, rbp — база кадру; -O0 копіює аргументи з регістрів у -4(%rbp), -8(%rbp)
| Аргумент | Ціле / вказівник | float / double | У прикладі add |
|---|---|---|---|
| 1-й | rdi | xmm0 | a → edi (int = 4 байти) |
| 2-й | rsi | xmm1 | b → esi |
| 3-й | rdx | xmm2 | — |
| 4-й | rcx | xmm3 | — |
| 5-й, 6-й | r8, r9 | xmm4, xmm5 | — |
| 7-й і далі | стек | стек | — |
| Результат | rax (eax) | xmm0 | a + b → eax |
Угода виклику (calling convention), вона ж ABI, — контракт, без якого окремо скомпільовані .o файли не змогли б викликати одне одного
Порушення поділу — класична помилка ручного асемблера: «працює», доки функцію не викликали з іншого місця
cmp a, b обчислює a − b, виставляє прапорці, операнди не змінює. Окремої інструкції if немає: cmp + jcc (наступний слайд) і jmp складають кожен if і цикл
| Мнемоніка | Умова після cmp a, b | Прапорці |
|---|---|---|
je / jz | a == b | ZF = 1 |
jne / jnz | a != b | ZF = 0 |
jl / jnge | a < b (знакове) | SF ≠ OF |
jg / jnle | a > b (знакове) | ZF = 0 і SF = OF |
jb / jc | a < b (беззнакове) | CF = 1 |
ja | a > b (беззнакове) | CF = 0 і ZF = 0 |
Плутанина знакових (jl/jg) із беззнаковими (jb/ja) — типовий баг при порівнянні size_t: від'ємне «загортається» у величезне додатне
; for (int i = 0; i < n; i++) sum += i;
movl $0, -4(%rbp) ; i = 0
.L_cond:
movl -4(%rbp), %eax
cmpl -8(%rbp), %eax ; порівняти i з n
jge .L_end
; ... sum += i ...
addl $1, -4(%rbp) ; i++
jmp .L_cond
.L_end:Мітки (.L_cond, .L_end) — це просто іменовані адреси інструкцій
Так компілятор реалізує a[i] за один такт адресної арифметики; -4(%rbp) — окремий випадок без індексу. Та сама ідея «база + зсув» повернеться в лінійній пам'яті WebAssembly (Л9)
Очікувано: Студенти бачать 1:1 відповідність між C-виразом a + b і трьома інструкціями mov/add/mov
section .textвиконуваний код — інструкції функційsection .dataініціалізовані дані: константи, рядкиsection .bssнеініціалізовані дані — лише розмірglobal ім'ясимвол видимий лінкеру — аналог «не static» у CNASM (Netwide Assembler) — інструмент, яким асемблер пишуть руками, а не отримують з компілятора; синтаксис Intel: mov dst, src
section .text
global add
add:
mov eax, edi ; перший аргумент (int) прийшов у edi
add eax, esi ; додати другий аргумент
ret ; результат уже в eaxТой самий add, що й у gcc -S, але написаний руками в Intel-синтаксисі
Жодної обгортки: лінкер бачить символ add як звичайну C-функцію. Умова — дотримання System V ABI (аргументи в rdi/rsi, результат у rax). Так само libc викликає асемблерні системні виклики ядра
nasm -f elf64 add.asm -o add.o
gcc main.c add.o -o app
./app
objdump -d app | grep -A6 '<add>:'Три інструменти — nasm, gcc, objdump — покривають шлях від тексту до перевіреного бінарника
Вирівнювання стека — один із найчастіших джерел 'працює у мене, падає на сервері'
Очікувано: Студенти бачать, що NASM-функція, написана вручну, працює нарівні з функцією, згенерованою gcc
Люди, які цікавляться комп'ютерами більше ніж побіжно, повинні мати хоч якесь уявлення про те, як влаштоване апаратне забезпечення під ними. Інакше програми, які вони пишуть, будуть доволі дивними.
— Дональд Кнут, «Мистецтво програмування», т. 1
🥚 У демо-терміналі main повертає 0x2a. Якщо замінити константу на 0xC1D, objdump покаже байти 1d 0c 00 00 — це 3101 у little-endian. Число не випадкове: у курсі воно трапляється не раз.