В настоящее время я пытаюсь понять, как работает стек, поэтому я решил научиться некоторому языку ассемблера , я использую эту книгу:
http://savannah.nongnu.org/projects/pgubook/
Я использую Gas и занимаюсь разработкой на Linux Mint .
Меня что-то немного смущает:
Насколько мне известно, стек - это просто структура данных. Поэтому я предположил, что если бы я писал код на сборке, мне пришлось бы реализовать стек самостоятельно. Однако, похоже, это не так, поскольку есть такие команды, как
pushl
popl
Поэтому при кодировании в сборке для x86 архитектуры и использовании синтаксиса Gas: является ли стек уже реализованной структурой данных? Или это реально реализовано на аппаратном уровне? Или что-то еще? Также будет ли стек для большинства языков ассемблера для других наборов микросхем уже реализован?
Я знаю, что это немного глупый вопрос, но на самом деле меня это смущает.
Ответы:
Я думаю, что в первую очередь вы путаете между a
program's stack
иany old stack
.Стек
Это абстрактная структура данных, которая состоит из информации в системе «последним вошел - первым ушел». Вы кладете произвольные объекты в стопку, а затем снова их снимаете, как лоток для ввода / вывода: верхний элемент всегда тот, который снимается, и вы всегда кладете его наверх.
Стек программ
Это стек, это часть памяти, которая используется во время выполнения, обычно имеет статический размер для каждой программы и часто используется для хранения параметров функции. Вы помещаете параметры в стек, когда вызываете функцию, и функция либо адресует стек напрямую, либо извлекает переменные из стека.
Стек программ обычно не является аппаратным (хотя он хранится в памяти, поэтому можно утверждать, что это так), но указатель стека, который указывает на текущую область стека, обычно является регистром ЦП. Это делает его немного более гибким, чем стек LIFO, поскольку вы можете изменить точку, в которой стек обращается.
Вы должны прочитать и убедиться, что понимаете статью в Википедии, поскольку она дает хорошее описание аппаратного стека, с которым вы имеете дело.
Существует также этот учебник, в котором объясняется стек с точки зрения старых 16-битных регистров, но он может быть полезен и еще один, посвященный стеку.
От Нильса Пипенбринка:
Стоит отметить, что некоторые процессоры не реализуют все инструкции для доступа к стеку и управления им (push, pop, указатель стека и т. Д.), Но x86 делает это из-за частоты его использования. В этих ситуациях, если вам нужен стек, вам придется реализовать его самостоятельно (некоторые процессоры MIPS и некоторые ARM создаются без стеков).
Например, в MIP инструкция push будет реализована следующим образом:
и инструкция Pop будет выглядеть так:
источник
(Я понял суть всего кода в этом ответе на случай, если вы захотите поиграть с ним)
Я когда-либо делал только самые простые вещи в asm во время моего курса CS101 в 2003 году. И я никогда не понимал, как работают asm и стек, пока не понял, что все это в основном как программирование на C или C ++ ... но без локальных переменных, параметров и функций. Наверное, пока непросто :) Позвольте мне показать вам (для x86 asm с синтаксисом Intel ).
1. Что такое стек
Стек обычно представляет собой непрерывный кусок памяти, выделяемый для каждого потока перед его запуском. Вы можете хранить там все, что захотите. В терминах C ++ ( фрагмент кода # 1 ):
const int STACK_CAPACITY = 1000; thread_local int stack[STACK_CAPACITY];
2. Верх и низ стека
В принципе, вы можете хранить значения в случайных ячейках
stack
массива ( фрагмент 2.1 ):stack[333] = 123; stack[517] = 456; stack[555] = stack[333] + stack[517];
Но представьте, как трудно запомнить, какие ячейки
stack
уже используются, а какие «свободны». Вот почему мы храним новые значения в стеке рядом друг с другом.Одна странность в стеке asm (x86) заключается в том, что вы добавляете туда вещи, начиная с последнего индекса, и переходите к более низким индексам: стек [999], затем stack [998] и так далее ( фрагмент 2.2 ):
stack[999] = 123; stack[998] = 456; stack[997] = stack[999] + stack[998];
И все же (осторожно, вы сейчас запутаетесь) "официальное" название
stack[999]
- это нижняя часть стека .Последняя использованная ячейка (
stack[997]
в приведенном выше примере) называется вершиной стека (см. Где находится вершина стека на x86 ).3. Указатель стека (SP)
Для целей этого обсуждения предположим, что регистры ЦП представлены как глобальные переменные (см. Регистры общего назначения ).
int AX, BX, SP, BP, ...; int main(){...}
Есть специальный регистр ЦП (SP), который отслеживает вершину стека. SP - указатель (содержит адрес памяти, например 0xAAAABBCC). Но для целей этого поста я буду использовать его как индекс массива (0, 1, 2, ...).
Когда поток запускается,
SP == STACK_CAPACITY
а затем программа и ОС изменяют его по мере необходимости. Правило состоит в том, что вы не можете писать в ячейки стека за пределами вершины стека, и любой индекс меньше SP недействителен и небезопасен (из-за системных прерываний ), поэтому вы сначала уменьшаете SP, а затем записываете значение в новую выделенную ячейку.Если вы хотите поместить несколько значений в стек подряд, вы можете заранее зарезервировать место для всех ( фрагмент 3 ):
SP -= 3; stack[999] = 12; stack[998] = 34; stack[997] = stack[999] + stack[998];
Запись. Теперь вы можете понять, почему распределение в стеке происходит так быстро - это всего лишь декремент на один регистр.
4. Локальные переменные
Давайте посмотрим на эту упрощенную функцию ( фрагмент 4.1 ):
int triple(int a) { int result = a * 3; return result; }
и перепишем без использования локальной переменной ( фрагмент 4.2 ):
int triple_noLocals(int a) { SP -= 1; // move pointer to unused cell, where we can store what we need stack[SP] = a * 3; return stack[SP]; }
и посмотрите, как он вызывается ( фрагмент 4.3 ):
// SP == 1000 someVar = triple_noLocals(11); // now SP == 999, but we don't need the value at stack[999] anymore // and we will move the stack index back, so we can reuse this cell later SP += 1; // SP == 1000 again
5. Толкать / хлопать
Добавление нового элемента на вершину стека - настолько частая операция, что у ЦП есть специальная инструкция для этого
push
,. Мы реализуем это так ( фрагмент 5.1 ):void push(int value) { --SP; stack[SP] = value; }
Аналогичным образом, взяв верхний элемент стека ( фрагмент 5.2 ):
void pop(int& result) { result = stack[SP]; ++SP; // note that `pop` decreases stack's size }
Обычный шаблон использования push / pop временно сохраняет некоторую ценность. Скажем, у нас есть что-то полезное в переменной
myVar
и по какой-то причине нам нужно произвести вычисления, которые перезапишут ее ( фрагмент 5.3 ):int myVar = ...; push(myVar); // SP == 999 myVar += 10; ... // do something with new value in myVar pop(myVar); // restore original value, SP == 1000
6. Параметры функции
Теперь передадим параметры с помощью стека ( фрагмент 6 ):
int triple_noL_noParams() { // `a` is at index 999, SP == 999 SP -= 1; // SP == 998, stack[SP + 1] == a stack[SP] = stack[SP + 1] * 3; return stack[SP]; } int main(){ push(11); // SP == 999 assert(triple(11) == triple_noL_noParams()); SP += 2; // cleanup 1 local and 1 parameter }
7.
return
заявлениеВернем значение в регистре AX ( фрагмент 7 ):
void triple_noL_noP_noReturn() { // `a` at 998, SP == 998 SP -= 1; // SP == 997 stack[SP] = stack[SP + 1] * 3; AX = stack[SP]; SP += 1; // finally we can cleanup locals right in the function body, SP == 998 } void main(){ ... // some code push(AX); // save AX in case there is something useful there, SP == 999 push(11); // SP == 998 triple_noL_noP_noReturn(); assert(triple(11) == AX); SP += 1; // cleanup param // locals were cleaned up in the function body, so we don't need to do it here pop(AX); // restore AX ... }
8. Базовый указатель стека (BP) (также известный как указатель кадра ) и кадр стека.
Давайте возьмем более "продвинутую" функцию и перепишем ее на нашем asm-подобном C ++ ( фрагмент № 8.1 ):
int myAlgo(int a, int b) { int t1 = a * 3; int t2 = b * 3; return t1 - t2; } void myAlgo_noLPR() { // `a` at 997, `b` at 998, old AX at 999, SP == 997 SP -= 2; // SP == 995 stack[SP + 1] = stack[SP + 2] * 3; stack[SP] = stack[SP + 3] * 3; AX = stack[SP + 1] - stack[SP]; SP += 2; // cleanup locals, SP == 997 } int main(){ push(AX); // SP == 999 push(22); // SP == 998 push(11); // SP == 997 myAlgo_noLPR(); assert(myAlgo(11, 22) == AX); SP += 2; pop(AX); }
Теперь представьте, что мы решили ввести новую локальную переменную для сохранения результата перед возвратом, как мы это сделали в
tripple
(фрагмент № 4.1). Тело функции будет ( фрагмент # 8.2 ):SP -= 3; // SP == 994 stack[SP + 2] = stack[SP + 3] * 3; stack[SP + 1] = stack[SP + 4] * 3; stack[SP] = stack[SP + 2] - stack[SP + 1]; AX = stack[SP]; SP += 3;
Видите ли, нам пришлось обновить каждую ссылку на параметры функции и локальные переменные. Чтобы этого избежать, нам нужен индекс привязки, который не меняется при увеличении стека.
Мы создадим якорь сразу после входа в функцию (перед тем, как выделить место для локальных), сохранив текущую вершину (значение SP) в регистре BP. Фрагмент № 8.3 :
void myAlgo_noLPR_withAnchor() { // `a` at 997, `b` at 998, SP == 997 push(BP); // save old BP, SP == 996 BP = SP; // create anchor, stack[BP] == old value of BP, now BP == 996 SP -= 2; // SP == 994 stack[BP - 1] = stack[BP + 1] * 3; stack[BP - 2] = stack[BP + 2] * 3; AX = stack[BP - 1] - stack[BP - 2]; SP = BP; // cleanup locals, SP == 996 pop(BP); // SP == 997 }
Срез стека, который принадлежит функции и полностью контролирует ее, называется кадром стека функции . Например
myAlgo_noLPR_withAnchor
, кадр стека равенstack[996 .. 994]
(включая оба идентификатора).Кадр начинается с BP функции (после того, как мы обновили его внутри функции) и длится до следующего кадра стека. Таким образом, параметры в стеке являются частью кадра стека вызывающей стороны (см. Примечание 8a).
Примечания:
8а. Википедия говорит иначе о параметрах, но здесь я придерживаюсь руководства разработчика программного обеспечения Intel , см. Т. 1, раздел 6.2.4.1 Указатель базы стека-кадра, и рисунок 6-2 в разделе 6.3.2 Операции удаленного вызова и возврата . Параметры функции и фрейм стека являются частью записи активации функции (см. «Проблемы генерации функции» ).
8b. положительные смещения от точки BP к параметрам функции и отрицательные смещения указывают на локальные переменные. Это очень удобно для отладки
8c.
stack[BP]
хранит адрес предыдущего кадра стека,stack[stack[BP]]
сохраняет предыдущий кадр стека и так далее. Следуя этой цепочке, вы можете обнаружить фреймы всех функций в программе, которые еще не вернулись. Вот как отладчики показывают стек вызовов8d. первые 3 инструкции
myAlgo_noLPR_withAnchor
, в которых мы настраиваем фрейм (сохранить старый BP, обновить BP, зарезервировать место для локальных), называются прологом функции9. Соглашения о вызовах
Во фрагменте 8.1 мы поместили параметры для
myAlgo
справа налево и вернули результат в форматеAX
. Мы могли бы также передать параметры слева направо и вернутьсяBX
. Или передайте параметры в BX и CX и вернитесь в AX. Очевидно, что caller (main()
) и вызываемая функция должны согласовать, где и в каком порядке хранятся все эти данные.Соглашение о вызове - это набор правил о том, как передаются параметры и возвращается результат.
В приведенном выше коде мы использовали соглашение о вызовах cdecl :
myAlgo_noLPR_withAnchor
функцией в нашем случае), чтобы вызывающий объект (main
функция) мог полагаться на то, что эти регистры не были изменены вызовом.(Источник: пример «32-bit cdecl» из документации по переполнению стека; авторское право 2016 г. принадлежит icktoofay и Peter Cordes ; под лицензией CC BY-SA 3.0. Архив полного содержания документации по переполнению стека можно найти на сайте archive.org, в котором этот пример проиндексирован по ID темы 3261 и ID примера 11196.)
10. Вызов функций
Теперь самое интересное. Как и данные, исполняемый код также хранится в памяти (совершенно не связанной с памятью для стека), и каждая инструкция имеет адрес.
Если не указано иное, ЦП выполняет инструкции одну за другой в том порядке, в котором они хранятся в памяти. Но мы можем приказать процессору «перейти» в другое место в памяти и выполнить инструкции оттуда. В asm это может быть любой адрес, а в более высокоуровневых языках, таких как C ++, вы можете переходить только к адресам, отмеченным метками ( есть обходные пути, но они, мягко говоря, не очень хороши).
Возьмем эту функцию ( фрагмент 10.1 ):
int myAlgo_withCalls(int a, int b) { int t1 = triple(a); int t2 = triple(b); return t1 - t2; }
И вместо вызова
tripple
C ++ сделайте следующее:tripple
код в началоmyAlgo
телаmyAlgo
входе перепрыгивайте черезtripple
код с помощьюgoto
tripple
код, сохраните в стеке адрес строки кода сразу послеtripple
вызова, чтобы мы могли вернуться сюда позже и продолжить выполнение (PUSH_ADDRESS
макрос ниже)tripple
функции) и выполнить ее до конца (3. и 4. вместе составляютCALL
макрос)tripple
(после того, как мы очистили локальных), возьмите адрес возврата из вершины стека и перейдите туда (RET
макрос)Поскольку в C ++ нет простого способа перейти к конкретному адресу кода, мы будем использовать метки, чтобы отмечать места переходов. Я не буду вдаваться в подробности того, как работают макросы ниже, просто поверьте мне, они делают то, что я говорю ( фрагмент 10.2 ):
// pushes the address of the code at label's location on the stack // NOTE1: this gonna work only with 32-bit compiler (so that pointer is 32-bit and fits in int) // NOTE2: __asm block is specific for Visual C++. In GCC use https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html #define PUSH_ADDRESS(labelName) { \ void* tmpPointer; \ __asm{ mov [tmpPointer], offset labelName } \ push(reinterpret_cast<int>(tmpPointer)); \ } // why we need indirection, read https://stackoverflow.com/a/13301627/264047 #define TOKENPASTE(x, y) x ## y #define TOKENPASTE2(x, y) TOKENPASTE(x, y) // generates token (not a string) we will use as label name. // Example: LABEL_NAME(155) will generate token `lbl_155` #define LABEL_NAME(num) TOKENPASTE2(lbl_, num) #define CALL_IMPL(funcLabelName, callId) \ PUSH_ADDRESS(LABEL_NAME(callId)); \ goto funcLabelName; \ LABEL_NAME(callId) : // saves return address on the stack and jumps to label `funcLabelName` #define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__) // takes address at the top of stack and jump there #define RET() { \ int tmpInt; \ pop(tmpInt); \ void* tmpPointer = reinterpret_cast<void*>(tmpInt); \ __asm{ jmp tmpPointer } \ } void myAlgo_asm() { goto my_algo_start; triple_label: push(BP); BP = SP; SP -= 1; // stack[BP] == old BP, stack[BP + 1] == return address stack[BP - 1] = stack[BP + 2] * 3; AX = stack[BP - 1]; SP = BP; pop(BP); RET(); my_algo_start: push(BP); // SP == 995 BP = SP; // BP == 995; stack[BP] == old BP, // stack[BP + 1] == dummy return address, // `a` at [BP + 2], `b` at [BP + 3] SP -= 2; // SP == 993 push(AX); push(stack[BP + 2]); CALL(triple_label); stack[BP - 1] = AX; SP -= 1; pop(AX); push(AX); push(stack[BP + 3]); CALL(triple_label); stack[BP - 2] = AX; SP -= 1; pop(AX); AX = stack[BP - 1] - stack[BP - 2]; SP = BP; // cleanup locals, SP == 997 pop(BP); } int main() { push(AX); push(22); push(11); push(7777); // dummy value, so that offsets inside function are like we've pushed return address myAlgo_asm(); assert(myAlgo_withCalls(11, 22) == AX); SP += 1; // pop dummy "return address" SP += 2; pop(AX); }
Примечания:
10а. поскольку адрес возврата хранится в стеке, в принципе мы можем его изменить. Вот как работает атака с разбиванием стека
10b. последние 3 инструкции в "конце"
triple_label
(очистка локальных переменных, восстановление старого BP, возврат) называются эпилогом функции11. Сборка
Теперь посмотрим на настоящий asm для
myAlgo_withCalls
. Для этого в Visual Studio:Одно отличие от нашего asm-подобного C ++ состоит в том, что стек asm работает с байтами, а не с целыми числами. Таким образом, чтобы зарезервировать место для одного
int
, SP будет уменьшено на 4 байта.Вот и все ( фрагмент 11.1 , номера строк в комментариях взяты из сути ):
И asm для
tripple
( фрагмент 11.2 ):push ebp mov ebp,esp sub esp,0CCh push ebx push esi push edi lea edi,[ebp-0CCh] mov ecx,33h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] imul eax,dword ptr [ebp+8],3 mov dword ptr [ebp-8],eax mov eax,dword ptr [ebp-8] pop edi pop esi pop ebx mov esp,ebp pop ebp ret
Надеюсь, после прочтения этого поста сборка не выглядит такой загадочной, как раньше :)
Вот ссылки из тела сообщения и некоторые материалы для дальнейшего чтения:
источник
[SP]
это не является допустимым 16-битным режимом адресации. Наверное, лучше всего использоватьESP
. Кроме того, если вы объявляетеSP
как объектint
, вы должны изменять его на 4 для каждого элемента, а не на 1. (Если вы объявилиlong *SP
, тогдаSP += 2
будет увеличиваться на2 * sizeof(int)
и, таким образом, удалить 2 элемента. Но сint
SP это должно бытьSP += 8
, какadd esp, 8
. В 32 -bit asm.-fomit-frame-pointer
уже много лет используется в gcc и clang по умолчанию. Людям, интересующимся реальным asm, нужно знать, что EBP / RBP обычно не используется в качестве указателя кадра. Я бы сказал, что «традиционно людям нужен якорь, который не меняется с push / pop, но компиляторы могут отслеживать изменение смещения». Затем вы можете обновить раздел об обратных трассировках, указав, что это устаревший метод, который не используется по умолчанию, когда доступны.eh_frame
метаданные DWARF или метаданные Windows x86-64.Относительно того, реализован ли стек на оборудовании, эта статья в Википедии может помочь.
Эта и другие статьи, на которые она ссылается, могут быть полезны для понимания использования стека в процессорах.
источник
Концепт
Сначала подумайте обо всем, как если бы вы были человеком, который это изобрел. Как это:
Сначала подумайте о массиве и о том, как он реализован на низком уровне -> это в основном просто набор смежных ячеек памяти (ячеек памяти, которые находятся рядом друг с другом). Теперь, когда у вас есть этот мысленный образ в вашей голове, подумайте о том, что вы можете получить доступ к ЛЮБОЙ из этих областей памяти и удалить ее по своему желанию, когда вы удаляете или добавляете данные в свой массив. Теперь подумайте об этом же массиве, но вместо возможности удалить любое местоположение вы решите удалить только ПОСЛЕДНЕЕ местоположение при удалении или добавлении данных в свой массив. Теперь ваша новая идея по манипулированию данными в этом массиве таким образом называется LIFO, что означает «последний пришел - первым ушел». Ваша идея очень хороша, потому что она упрощает отслеживание содержимого этого массива без необходимости использования алгоритма сортировки каждый раз, когда вы что-то удаляете из него. Также, чтобы всегда знать, каков адрес последнего объекта в массиве, вы выделяете один регистр в ЦП, чтобы отслеживать его. Теперь, способ, которым регистр отслеживает это, состоит в том, что каждый раз, когда вы удаляете или добавляете что-либо в свой массив, вы также уменьшаете или увеличиваете значение адреса в своем регистре на количество объектов, которые вы удалили или добавили из массива (на объем занимаемого ими адресного пространства). Вы также хотите убедиться, что количество, на которое вы уменьшаете или увеличиваете этот регистр, фиксировано на одно количество (например, 4 ячейки памяти, т.е. 4 байта) на объект, опять же, чтобы упростить отслеживание, а также сделать это возможным чтобы использовать этот регистр с некоторыми конструкциями цикла, потому что в циклах используется фиксированное увеличение на итерацию (например, чтобы выполнить цикл через ваш массив с помощью цикла, вы создаете цикл для увеличения вашего регистра на 4 за каждую итерацию, что было бы невозможно, если бы в вашем массиве были объекты разных размеров). Наконец, вы решили назвать эту новую структуру данных «Стек», потому что она напоминает вам стопку тарелок в ресторане, где они всегда удаляют или добавляют тарелку наверху этой стопки.
Реализация
Как видите, стек - это не что иное, как массив смежных участков памяти, в которых вы решили, как им управлять. Из-за этого вы можете видеть, что вам даже не нужно использовать специальные инструкции и регистры для управления стеком. Вы можете реализовать это самостоятельно с помощью базовых инструкций mov, add и sub и использования регистров общего назначения вместо ESP и EBP следующим образом:
mov edx, 0FFFFFFFFh
; -> это будет начальный адрес вашего стека, наиболее удаленный от вашего кода и данных, он также будет служить тем регистром, который отслеживает последний объект в стеке, который я объяснил ранее. Вы называете его «указателем стека», поэтому вы выбираете регистр EDX в качестве того, для чего обычно используется ESP.
sub edx, 4
mov [edx], dword ptr [someVar]
; -> эти две инструкции уменьшат указатель стека на 4 ячейки памяти и скопируют 4 байта, начиная с ячейки памяти [someVar], в ячейку памяти, на которую теперь указывает EDX, точно так же, как инструкция PUSH уменьшает ESP, только здесь вы сделали это вручную, и вы использовали EDX. Таким образом, инструкция PUSH - это, по сути, просто более короткий код операции, который фактически делает это с помощью ESP.
mov eax, dword ptr [edx]
добавить edx, 4
; -> и здесь мы делаем наоборот, мы сначала копируем 4 байта, начиная с места в памяти, на которое теперь указывает EDX, в регистр EAX (произвольно выбранный здесь, мы могли бы скопировать его куда угодно). Затем мы увеличиваем указатель стека EDX на 4 ячейки памяти. Это то, что делает инструкция POP.
Теперь вы можете видеть, что инструкции PUSH и POP, а также регистры ESP и EBP были добавлены Intel только для того, чтобы упростить запись и чтение вышеуказанной концепции «стековой» структуры данных. Все еще есть некоторые процессоры RISC (сокращенный набор инструкций), которые не имеют инструкций PUSH и POP и выделенных регистров для управления стеком, и при написании программ сборки для этих процессоров вы должны реализовать стек самостоятельно, как и Я показал тебе.
источник
Вы путаете абстрактный стек и стек, реализованный на оборудовании. Последний уже реализован.
источник
Я думаю, что на главный ответ, который вы ищете, уже намекнули.
Когда загружается компьютер x86, стек не настраивается. Программист должен явно настроить его во время загрузки. Однако, если вы уже используете операционную систему, об этом позаботились. Ниже приведен пример кода простой программы начальной загрузки.
Сначала устанавливаются регистры сегмента данных и стека, а затем указатель стека устанавливается на 0x4000 сверх этого.
После этого кода стек можно использовать. Я уверен, что это можно сделать разными способами, но я думаю, что это должно проиллюстрировать идею.
источник
Стек - это просто способ использования памяти программами и функциями.
Стек меня всегда смущал, поэтому я сделал иллюстрацию:
( версия svg здесь )
Не уверен, помогает ли это кому-то или просто сбивает с толку. Я считаю, что это правильно. Не стесняйтесь использовать изображение SVG. Всеобщее достояние.
источник
Стек уже существует, поэтому вы можете предположить, что при написании кода. Стек содержит адреса возврата функций, локальные переменные и переменные, которые передаются между функциями. Существуют также встроенные регистры стека, такие как BP, SP (указатель стека), которые вы можете использовать, отсюда и упомянутые вами встроенные команды. Если стек еще не был реализован, функции не могли работать и поток кода не мог работать.
источник
Стек «реализуется» с помощью указателя стека, который (в предположении архитектуры x86) указывает на сегмент стека . Каждый раз, когда что-то помещается в стек (с помощью pushl, call или аналогичного кода операции стека), оно записывается по адресу, на который указывает указатель стека, и указатель стека уменьшается (стек растет вниз , т. Е. Меньшие адреса) . Когда вы извлекаете что-то из стека (popl, ret), указатель стека увеличивается и значение считывается из стека.
В приложении пользовательского пространства стек уже настроен для вас при запуске вашего приложения. В среде пространства ядра вы должны сначала настроить сегмент стека и указатель стека ...
источник
Я специально не видел ассемблера Gas, но в целом стек «реализуется» путем сохранения ссылки на место в памяти, где находится вершина стека. Ячейка памяти хранится в регистре, который имеет разные имена для разных архитектур, но может рассматриваться как регистр указателя стека.
Команды pop и push реализованы в большинстве архитектур для вас на основе микрокоманд. Однако некоторые «образовательные архитектуры» требуют, чтобы вы реализовали их самостоятельно. Функционально push будет реализован примерно так:
Кроме того, в некоторых архитектурах последний использованный адрес памяти сохраняется в качестве указателя стека. Некоторые сохраняют следующий доступный адрес.
источник
Что такое стек? Стек - это тип структуры данных - средство хранения информации в компьютере. Когда новый объект помещается в стек, он помещается поверх всех ранее введенных объектов. Другими словами, структура данных стека похожа на стопку карточек, бумаг, почтовых отправлений по кредитным картам или любых других реальных объектов, о которых вы можете подумать. При удалении объекта из стека первым удаляется тот, который находится сверху. Этот метод называется LIFO (последний пришел, первый ушел).
Термин «стек» также может быть сокращенным для обозначения стека сетевых протоколов. В сети соединения между компьютерами устанавливаются через серию более мелких соединений. Эти соединения или уровни действуют как структура данных стека в том смысле, что они строятся и удаляются одинаково.
источник
Вы правы, что стек - это структура данных. Часто структуры данных (включая стеки), с которыми вы работаете, являются абстрактными и существуют как представление в памяти.
Стек, с которым вы работаете в этом случае, имеет более материальное существование - он отображается непосредственно на реальные физические регистры в процессоре. Как структура данных, стеки представляют собой структуры FILO (first in, last out), которые обеспечивают удаление данных в порядке, обратном их введению. См. Логотип StackOverflow для визуализации! ;)
Вы работаете со стеком инструкций . Это стек фактических инструкций, которые вы скармливаете процессору.
источник
Стек вызовов реализуется набором инструкций x86 и операционной системой.
Такие инструкции, как push и pop, регулируют указатель стека, в то время как операционная система заботится о выделении памяти по мере увеличения стека для каждого потока.
Тот факт, что стек x86 «растет вниз» от более высоких адресов к более низким, делает эту архитектуру более уязвимой для атаки переполнения буфера.
источник
Вы правы, что стек - это «просто» структура данных. Здесь, однако, имеется в виду аппаратно реализованный стек, используемый для специального назначения - «Стек».
Многие люди комментировали аппаратно реализованный стек по сравнению со структурой данных (программного) стека. Я хотел бы добавить, что существует три основных типа структуры стека:
Я не уверен в этом, но я помню, что где-то читал, что есть аппаратный стек общего назначения, доступный в некоторых архитектурах. Если кто-нибудь знает, правильно ли это, прокомментируйте.
Первое, что нужно знать, это архитектура, для которой вы программируете, что объясняется в книге (я только что посмотрел - ссылка). Чтобы по-настоящему понять вещи, я предлагаю вам узнать о памяти, адресации, регистрах и архитектуре x86 (я предполагаю, что это то, что вы изучаете - из книги).
источник
Вызов функций, который требует сохранения и восстановления локального состояния в стиле LIFO (в отличие от обобщенного подхода совместной подпрограммы), оказывается такой невероятно распространенной потребностью, что языки сборки и архитектуры ЦП в основном встраивают эту функцию. вероятно, можно сказать о понятиях потоковой передачи, защиты памяти, уровней безопасности и т. д. Теоретически вы можете реализовать свой собственный стек, соглашения о вызовах и т. д., но я предполагаю, что некоторые коды операций и большинство существующих сред выполнения полагаются на эту родную концепцию «стека». .
источник
stack
это часть памяти. он используется дляinput
иoutput
изfunctions
. также он используется для запоминания возврата функции.esp
Регистр запоминает адрес стека.stack
иesp
реализованы аппаратно. также вы можете реализовать это самостоятельно. это сделает вашу программу очень медленной.пример:
nop //
esp
= 0012ffc4push 0 //
esp
= 0012ffc0, Dword [0012ffc0] = 00000000вызов proc01 //
esp
= 0012ffbc, Dword [0012ffbc] =eip
,eip
= adrr [proc01]pop
eax
//eax
= Dword [esp
],esp
=esp
+ 4источник
Я искал, как работает стек с точки зрения функций, и нашел этот блог его потрясающим и объясняющим концепцию стека с нуля и то, как стек сохраняет значение в стеке.
Теперь по вашему ответу. Я объясню на Python, но вы получите хорошее представление о том, как работает стек на любом языке.
Это программа:
Источник: Cryptroix
некоторые из его тем, которые освещаются в блоге:
Но это объясняется языком Python, поэтому, если хотите, можете взглянуть.
источник