Стек виклику також може називатися стеком кадру.
Речі, що складаються за принципом LIFO, - це не локальні змінні, а цілі кадри стека ("виклики") функцій, що викликаються . Локальні змінні висуваються та спливають разом із цими кадрами відповідно до так званих функцій пролог та епілог .
Всередині кадру порядок змінних абсолютно не визначений; Компілятори "переупорядковують" місце локальних змінних всередині кадру відповідним чином, щоб оптимізувати їх вирівнювання, щоб процесор міг їх отримати якнайшвидше. Найважливішим фактом є те, що зміщення змінних відносно деякої фіксованої адреси є постійним протягом усього життя кадру - тому достатньо взяти якорну адресу, скажімо, адресу самого кадру та працювати з зрушеннями цієї адреси до змінні. Така адреса якоря фактично міститься в так званій базі або покажчику кадруякий зберігається в реєстрі EBP. Зсуви, з іншого боку, добре відомі під час компіляції, і тому вони жорстко кодуються в машинному коді.
Ця графіка з Вікіпедії показує, як структурований типовий стек викликів 1 :
Додаємо зміщення змінної, до якої ми хочемо отримати доступ до адреси, що міститься в покажчику кадру, і ми отримуємо адресу нашої змінної. Так коротко сказано, код просто отримує доступ до них безпосередньо через постійні компенсації часу компіляції з базового покажчика; Це проста арифметика вказівника.
Приклад
#include <iostream>
int main()
{
char c = std::cin.get();
std::cout << c;
}
gcc.godbolt.org дає нам
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl std::cin, %edi
call std::basic_istream<char, std::char_traits<char> >::get()
movb %al, -1(%rbp)
movsbl -1(%rbp), %eax
movl %eax, %esi
movl std::cout, %edi
call [... the insertion operator for char, long thing... ]
movl $0, %eax
leave
ret
.. за main
. Я розділив код на три підрозділи. Пролог функції складається з перших трьох операцій:
- Базовий покажчик висувається на стек.
- Покажчик стека зберігається в базовому покажчику
- Покажчик стека віднімається, щоб звільнити місцеві змінні.
Потім cin
переміщується в регістр EDI 2 і get
викликається; Повертається значення в EAX.
Все йде нормально. Тепер цікаве відбувається:
Байт низького порядку EAX, позначений 8-бітовим регістром AL, приймається і зберігається в байті відразу після базового вказівника : тобто -1(%rbp)
зміщення базового покажчика є -1
. Цей байт - наша зміннаc
. Зсув є негативним, оскільки стек зростає вниз на x86. Наступна операція зберігається c
в EAX: EAX переміщується в ESI, cout
переміщується в EDI і потім оператор вставки викликається аргументами cout
та c
є ними.
Нарешті,
- Повернене значення
main
зберігається в EAX: 0. Це через неявне return
твердження. Ви можете побачити xorl rax rax
замість цього movl
.
- залишити та повернутися на сайт для дзвінків.
leave
скорочує цей епілог та неявно
- Замінює вказівник стека базовим вказівником та
- Вискакує базовий покажчик.
Після закінчення цієї операції ret
кадр фактично вискочив, хоча абонент все ще повинен очистити аргументи, оскільки ми використовуємо конвенцію виклику cdecl. Інші конвенції, наприклад, stdcall, вимагають, щоб користувач прибирав, наприклад, передаючи кількість байтів до ret
.
Опущення покажчика кадру
Можливо також не використовувати зсуви від базового / кадрового покажчика, а від покажчика стека (ESB). Це робить реєстр EBP, який інакше міститиме значення покажчика кадру, доступне для довільного використання - але це може зробити налагодження неможливим на деяких машинах , і буде неявно вимкнено для деяких функцій . Це особливо корисно при компілюванні для процесорів лише з декількома регістрами, включаючи x86.
Ця оптимізація відома як FPO (опущення покажчика кадру) і встановлюється -fomit-frame-pointer
у GCC та -Oy
у Clang; зауважте, що це неявно спрацьовує кожен рівень оптимізації> 0, і лише тоді, коли налагодження все ще можливе, оскільки воно не має жодних витрат, крім цього. Додаткову інформацію дивіться тут і тут .
1 Як зазначено в коментарях, вказівник кадру, ймовірно, призначений для вказівки на адресу після зворотної адреси.
2 Зауважте, що регістри, які починаються з R, - це 64-бітні аналоги тих, які починаються з E. EAX позначає чотири байти низького порядку RAX. Я використовував імена 32-розрядних регістрів для наочності.