🌌 Як ворожий процесор приймає команди — На тому боці — не Стосова стійка, а цілий квартал іменованих комірок: у кожного регістра своя підписана шухляда, і процесор роздає аргументи команд у суворо визначеному порядку. С.І.Д. знає цей протокол напам'ять і залюбки ділиться: переплутаєш, у чию шухляду лягає який аргумент, — і перший же виклик функції піде не туди, а антивірус помітить аномалію в стеку викликів.
Application Binary Interface (ABI) — це набір домовленостей на рівні машинного коду про те, як функції передають одна одній аргументи, повертають значення, і хто саме відповідає за збереження вмісту регістрів під час виклику. Без такого спільного контракту функція, скомпільована одним компілятором (чи навіть просто написана вручну на асемблері), не змогла б коректно викликати функцію, скомпільовану іншим інструментом чи в іншому модулі — саме ABI робить можливим роздільну компіляцію, динамічне лінкування та виклик системних функцій ОС. На Linux/macOS для x86-64 стандартом де-факто є System V AMD64 ABI.
Ключове правило передачі параметрів: перші шість цілочисельних/
вказівникових аргументів функції передаються через регістри в чітко
визначеному порядку — rdi, rsi, rdx, rcx, r8, r9
(легко запам'ятати мнемонікою «Diane's Silk Dress Costs $89» за
першими літерами). Сьомий і наступні аргументи (а також аргументи з
плаваючою точкою — окремо через xmm0–xmm7) передаються через
стек. Значення, що повертається функцією, за замовчуванням опиняється
в регістрі rax (для 64-бітних цілих) чи в парі rax:rdx для
128-бітних значень.
Другий критичний аспект ABI — поділ регістрів на caller-saved
(«тимчасові», volatile: rax, rcx, rdx, rsi, rdi, r8–r11)
і callee-saved («збережувані», non-volatile: rbx, rbp, r12–
r15, а також вказівник стека rsp). Функція, що викликає іншу
(caller), не може розраховувати на те, що вміст caller-saved
регістрів переживе виклик — якщо їй потрібне значення після виклику,
вона зобов'язана зберегти його сама (наприклад, на стеці) до виклику.
Натомість callee-saved регістри — це відповідальність функції, що
викликається (callee): якщо вона хоче їх використати для власних
потреб, вона зобов'язана зберегти їхній оригінальний вміст на
початку (типово через push) і відновити перед поверненням (pop) —
виклик функції не повинен «непомітно зіпсувати» ці регістри для
свого викликача.
Ця угода — не довільний вибір компілятора, а частина стабільного
публічного контракту платформи: саме завдяки їй objdump -d
скомпільованої функції можна читати передбачувано — побачивши на
початку тіла функції mov %rdi, ... для функції з одним параметром,
можна бути впевненим, що це саме перший аргумент, незалежно від
того, яким компілятором (gcc, clang, rustc) вона була
зібрана — усі вони на цій платформі підпорядковуються тому самому
ABI. Саме тому Rust може напряму викликати функції з C-бібліотек
(extern "C") і навпаки — обидві сторони угоди про виклик описують
той самий протокол передачі даних через регістри й стек.
Практична навичка читання дизасембльованого коду функції з кількома
параметрами полягає в тому, щоб зіставити перші інструкції тіла
функції (де вона зазвичай копіює вхідні аргументи з rdi/rsi/
rdx/... у власні робочі регістри чи на локальний кадр стека) з
порядком параметрів у вихідному коді — це і є прямий, «дослівний»
доказ роботи угоди виклику на реальному, а не теоретичному прикладі.
Прочитати розділ System V AMD64 ABI про передачу параметрів у регістрах (rdi, rsi, rdx, rcx, r8, r9) і поділ на caller-saved/callee-saved регістри; переглянути objdump -d довільної скомпільованої функції з двома й більше параметрами й позначити, де саме опиняється кожен.
У якому регістрі опиниться третій цілочисельний аргумент функції за System V AMD64 ABI, і чому саме там, а не одразу на стеці?
sum3(101, 9, 3101) — «частота, ще частота і кіт». Скомпілюй такий виклик і подивись у objdump -d: третій аргумент ляже в %edx, а в байтах mov $0xc1d,%edx буде видно ba 1d 0c 00 00 — підпис, розкладений little-endian.Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
gccgcc -O0 -c f.c -o f.o — лише скомпілювати в об'єктний файл без оптимізацій.objdumpobjdump -d f.o показує машинний код як інструкції з адресами й опкодами (-M intel — синтаксис Intel).grepps aux | grep bash).tailtail -1 — останній; tail -f — стежити за файлом наживо).clang--target=wasm32 збирає той самий C у WebAssembly — щоб порівняти з x86-64.rustcrustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.