Я пытаюсь изучить x86. (архитектура IA-32) Сегодня я узнал о стеке. Кажется, я понял вот что:
StackPointer (SP) указывает на «верх» стека (наименьший адрес) и хранится в регистре ESP. Указатель кадра (FP) обеспечивает доступ к аргументам и локальным переменным, поскольку это «поп-адрес» и, следовательно, статический, даже если SP перемещается дальше внутри стека.
int addSome(int arg1, int arg2);
int main(void){
int answer = addSome(1,2);
}
int addSome(int arg1, int arg2){
return arg1 + arg2;
}
Стек должен выглядеть так:
Стек, в котором указатель кадра показывает «поп-адрес»
Что произойдет с ФП, если есть вложенные функции? Ее нельзя переместить вверх, так как ее местоположение понадобится, если подфункция будет выдвинута.
Для визуализации; добавив эту функцию, которая будет вызываться внутри addSome():
int subSome(int arg3, int arg4);
int subSome(int arg3, int arg4){
return arg3 - arg4;
}
Как здесь обрабатывается ФП? Откуда известен новый «популярный адрес» и местонахождение новых аргументов? Стек, где указатель кадра показывает «поп-адрес» первой функции, вторая неизвестна
Я предполагаю, что размер стека addSome будет сохранен где-то, чтобы получить местоположение нового «поп-адреса» относительно FP:
Стек, в котором указатель кадра имеет относительное отличие от нового «поп-адреса»
Но этот размер стека должен быть где-то сохранен, и если есть больше вложенных функций, должно быть больше мест для хранения этих размеров, что, я считаю, не может быть правдой.
Да, судя по вашим комментариям, «вложенные функции» — неправильный термин. Спасибо, я неправильно понял этот термин и использовал его неправильно. => чему-то научился
Прежде всего, в x86 регистр, который мы используем для адресации значений в кадре стека, называется «базовым указателем» или ebp
. Я не знаю, почему вы это называете fp
.
Во-вторых, нам не нужно обсуждать subSome()
, потому что вторая, вложенная функция у нас уже есть, так как у нас есть две функции: main()
и addSome()
.
Стандартный пролог и эпилог функции в x86 выглядит так:
push ebp ; save caller's ebp
mov ebp, esp ; make ebp point to the frame of this function
... ; main function body goes here
pop ebp ; restore caller's ebp
ret ; return to caller
Обычная, реальная, физическая стопка (скажем, стопка карт) растет снизу вверх. Когда вы добавляете в стопку еще одну карту, она оказывается наверху стопки. Не так в x86; в x86 стек растет сверху вниз. Инструкция push <32-bit-operand>
уменьшает регистр esp
на 4, а затем сохраняет 32-битный операнд по адресу памяти, указанному новым значением регистра esp
. Инструкция pop <32-bit-operand>
делает обратное.
Более того, возвращаемое значение обычно не сохраняется в стеке. Подробности зависят от действующего ABI (двоичного интерфейса приложения), но обычно они возвращаются в регистре eax
. Возможно, вы имели в виду адрес возврата, который фактически помещается в стек инструкцией call
и извлекается из стека инструкцией ret
.
Итак, внутри addSome()
ваш стек будет выглядеть так:
top-of-stack `argc` parameter to `main()` pushed by the standard library
top-of-stack - 4 `argv` parameter to `main()` pushed by the standard library
top-of-stack - 8 return address of standard library code that invoked `main()`
top-of-stack - 12 ebp that was saved by `main()`
top-of-stack - 16 arg1 parameter to `addSome()` (value: 1)
top-of-stack - 20 arg2 parameter to `addSome()` (value: 2)
top-of-stack - 24 return address back to `main()`
top-of-stack - 28 ebp that was saved by `addSome()` <-- esp points here
(обратите внимание, что top-of-stack
обычно не будет настоящей вершиной стека; над всем остальным будет еще кое-что, выдвинутое кодом стандартной библиотеки, который вызвал ваш main()
. Но показанная часть - это та часть, которая нас волнует.)
esp
указывает на самые последние отправленные данные, поэтому в вашей таблице top of stack - 28
будет равно esp
, а не минус 32. esp
будет уменьшено до минус 32, если нужно будет отправить еще одно слово.
@Майк Накис Спасибо! ebp тоже помещается в стек, это имеет смысл. Я могу это понять именно так. Примеры очень помогают. Спасибо, что указали и на другие мои ошибки.
@ecm ты прав, я исправил, спасибо
Что вы подразумеваете под «вложенными функциями»? Я спрашиваю, потому что некоторые языки, например Паскаль, поддерживают статически вложенные функции, а другие, например C, нет. Статически вложенные функции позволяют внутренней/вложенной функции получать доступ к локальным переменным внешней/вложенной функции, во многом подобно лямбде и ее замыканию (как и замыкание, внутренняя/вложенная функция не может быть вызвана извне, ее необходимо вызывать из внешней /вложенная функция). Однако все языки позволяют функциям вызывать другие функции, поэтому это просто стек вызовов или динамическая цепочка вызовов.