Щоб надати конкретний приклад того, як компілятор управляє стеком і як отримують доступ до значень на стеку, ми можемо переглянути візуальні зображення, плюс код, сформований GCC
у середовищі Linux із i386 як цільовою архітектурою.
1. Складіть рамки
Як відомо, стек - це розташування в адресному просторі запущеного процесу, який використовується функціями або процедурами , в тому сенсі, що простір виділяється в стеку для змінних, оголошених локально, а також аргументів, переданих функції ( простір для змінних, оголошених поза будь-якою функцією (тобто глобальних змінних), виділяється в іншій області у віртуальній пам'яті). Простір, виділений для всіх даних функції, відсилається до кадру стека . Ось візуальне зображення декількох фреймів стека (від Комп'ютерні системи: Перспектива програміста ):
2. Управління рамкою стека та змінним місцем розташування
Для того, щоб значеннями, записаними в стек в межах певного кадру стека, керував компілятор і зчитувався програмою, повинен існувати певний метод обчислення позицій цих значень і отримання їх адреси пам'яті. У цьому допомагають регістри в процесорі, які називаються покажчиком стека та базовим вказівником.
Базовий вказівник, ebp
як правило, містить адресу пам'яті нижньої частини або основи стека. Позиції всіх значень у кадрі стека можуть бути обчислені, використовуючи адресу в базовому покажчику як еталонну. Це зображено на малюнку вище: наприклад %ebp + 4
, пам'ять, що зберігається в базовому покажчику плюс 4, наприклад.
3. Згенерований компілятором код
Але те, що я не отримую, - це те, як змінні в стеку потім читаються програмою. Якщо я оголошу і призначу x як ціле число, скажімо, x = 3, а зберігання зарезервоване у стеці, а потім його значення 3 зберігається там, а потім у тій же функції я оголошую і призначаю y як, скажімо, 4, а потім слідуючи, що потім я використовую x в іншому виразі (скажімо, z = 5 + x), як програма може прочитати х, щоб оцінити z, коли це нижче y на стеці?
Скористаємося простою прикладною програмою, написаною на С, щоб побачити, як це працює:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Розглянемо текст складання, створений GCC для цього вихідного тексту C (я трохи очистив його для наочності):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Те , що ми спостерігаємо , що змінні х, у і г розташовані за адресами %ebp - 12
, %ebp -8
і %ebp - 4
, відповідно. Іншими словами, розташування змінних у кадрі стека для main()
обчислюється за допомогою адреси пам'яті, збереженої в регістрі процесора %ebp
.
4. Дані в пам'яті поза вказівкою стека виходять за межі поля
Мені явно чогось не вистачає. Це те, що розташування в стеку - це лише про час життя / область змінної, і що весь стек фактично доступний програмі весь час? Якщо так, то чи означає це, що існує якийсь інший індекс, який містить адреси лише змінних на стеку, щоб дозволити отримання значень? Але тоді я подумав, що вся суть стека полягає в тому, що значення зберігаються там же, що і адреса змінної?
Стек - це область у віртуальній пам'яті, використанням якої керує компілятор. Компілятор генерує код таким чином, що значення за вказівником стека (значення поза вершиною стека) ніколи не посилаються. Коли викликається функція, положення вказівника стека змінюється, щоб створити простір на стеку, який вважається не "поза межами", так би мовити.
Коли функції викликаються та повертаються, покажчик стека зменшується та збільшується. Дані, записані в стек, не зникають після того, як вони виходять за межі, але компілятор не генерує інструкцій, що посилаються на ці дані, оскільки компілятор не може обчислити адреси цих даних за допомогою %ebp
або %esp
.
5. Підсумок
Код, який може бути безпосередньо виконаний процесором, генерується компілятором. Компілятор керує стеком, стековими кадрами для функцій та регістрів процесора. Одна стратегія, яка використовується GCC для відстеження розташування змінних у фреймах стека в коді, призначеному для виконання на архітектурі i386, полягає у використанні адреси пам'яті в базовому покажчику кадру стека %ebp
, як орієнтиру та запису значень змінних у місця розташування у кадрах стека при компенсуванні адреси в %ebp
.