Чтобы представить конкретный пример того, как компилятор управляет стеком и как осуществляется доступ к значениям в стеке, мы можем взглянуть на визуальные изображения, а также код, сгенерированный GCC
в среде Linux с целевой архитектурой i386.
1. Стек кадров
Как вы знаете, стек - это место в адресном пространстве запущенного процесса, которое используется функциями или процедурами , в том смысле, что пространство выделяется в стеке для переменных, объявленных локально, а также для аргументов, передаваемых функции ( пространство для переменных, объявленных вне какой-либо функции (т. е. глобальных переменных), выделяется в другой области в виртуальной памяти). Пространство, выделенное для всех данных функции, ссылается на кадр стека . Вот визуальное описание нескольких кадров стека (из « Компьютерные системы: взгляд программиста» ):
2. Управление кадрами стека и переменное местоположение
Для того чтобы значения, записанные в стек внутри определенного стекового фрейма, управлялись компилятором и читались программой, должен существовать некоторый метод для вычисления позиций этих значений и извлечения их адреса памяти. Регистры в ЦП, называемые указателем стека, и базовый указатель помогают в этом.
Базовый указатель, ebp
по соглашению, содержит адрес памяти дна, или основания, стека. Позиции всех значений в кадре стека могут быть рассчитаны с использованием адреса в базовом указателе в качестве ссылки. Это изображено на рисунке выше: %ebp + 4
адрес памяти хранится в базовом указателе плюс 4, например.
3. Сгенерированный компилятором код
Но что я не понимаю, так это то, как переменные в стеке затем считываются приложением - если я объявляю и присваиваю x как целое число, скажем, x = 3, и хранилище резервируется в стеке, а затем сохраняется его значение 3 там, а затем в той же функции я объявляю и присваиваю y как, скажем, 4, а затем после этого я затем использую x в другом выражении (скажем, z = 5 + x), как программа может прочитать x, чтобы оценить z, когда это ниже у в стеке?
Давайте используем простой пример программы, написанной на C, чтобы увидеть, как это работает:
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
.