Как работает стек на языке ассемблера?

В настоящее время я пытаюсь понять, как работает стек, поэтому я решил научить себя некоторому ассемблеру, я использую это книга:

http://savannah.nongnu.org/projects/pgubook/

Я использую Gas и делаю свое развитие на Linux Mint.

Я немного смущен чем-то:

Насколько мне известно, стек - это просто структура данных. Поэтому я предположил, что если я буду кодировать в сборке, мне придется самому реализовать стек. Однако, похоже, это не так, поскольку есть команды типа

pushl
popl

Итак, когда кодирование в сборке для архитектуры x86 и использование синтаксиса Gas: является ли стек только той структурой данных, которая уже реализована? Или это фактически реализовано на аппаратном уровне? Или что-то еще? Также будет ли большинство языков ассемблера для других наборов микросхем иметь уже реализованный стек?

Я знаю, что это немного глупый вопрос, но я действительно смущен этим.

Ответ 1

Я думаю, что в первую очередь вы путаетесь между program stack и any old stack.

Стек

Является абстрактной структурой данных, которая состоит из информации в системе Last In First Out. Вы помещаете произвольные объекты в стек, а затем вы снимаете их снова, подобно входу/выходу, верхний элемент всегда тот, который снят, и вы всегда нажимаете на верх.

Пакет программ

Является стеком, это часть памяти, которая используется во время выполнения, обычно имеет статический размер для каждой программы и часто используется для хранения параметров функции. Вы выталкиваете параметры в стек при вызове функции, а функция либо напрямую адресует стек, либо выталкивает переменные из стека.

Пакет программ обычно не является аппаратным (хотя он хранится в памяти, поэтому его можно утверждать как таковой), но указатель стека, указывающий на текущую область стека, обычно является регистром процессора. Это делает его немного более гибким, чем стек LIFO, поскольку вы можете изменить точку, в которой адресуется стек.

Вы должны прочитать и убедиться, что вы понимаете статью wikipedia, так как она дает хорошее описание аппаратного стека, который является тем, что вы имея дело с.

Существует также этот учебник, который объясняет стек в терминах старых 16-битных регистров, но может быть полезным и еще один специально о стеке.

От Нильса Пипренбринка:

Следует отметить, что некоторые процессоры не реализуют все инструкции для доступа и управления стеком (push, pop, указатель стека и т.д.), но x86 делает из-за этой частоты использования. В этих ситуациях, если вам нужен стек, вам придется его реализовать самостоятельно (некоторые MIPS и некоторые ARM-процессоры создаются без стеков).

Например, в MIP команда push будет реализована следующим образом:

addi $sp, $sp, -4  # Decrement stack pointer by 4  
sw   $t0, ($sp)   # Save $t0 to stack  

и инструкция Pop будет выглядеть так:

lw   $t0, ($sp)   # Copy from stack to $t0  
addi $sp, $sp, 4   # Increment stack pointer by 4  

Ответ 2

(Я составил суть всего кода в этом ответе на случай, если вы захотите поиграть с ним)

Я только когда-либо делал самые простые вещи в 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):

cin >> stack[333];
cin >> stack[517];
stack[555] = stack[333] + stack[517];

Но представьте, как трудно будет вспомнить, какие ячейки stack уже используются, а какие "свободны". Вот почему мы храним новые значения в стеке рядом друг с другом.

Одна из странностей в стеке asm (x86) состоит в том, что вы добавляете туда вещи, начиная с последнего индекса, и переходите к нижним индексам: stack [999], затем stack [998] и т.д. (Фрагмент # 2.2):

cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];

И все же (осторожно, сейчас вы будете сбиты с толку) "официальное" имя для stack[999] находится в нижней части стека.
Последняя использованная ячейка (stack[997] в приведенном выше примере) называется вершиной стека (см. Где верхняя часть стека находится на x86).


3. Указатель стека (SP)

Стек - не единственная вещь, видимая повсюду в вашем коде asm. Вы также можете управлять регистрами ЦП (см. Регистры общего назначения). Они действительно похожи на глобальные переменные:

int AX, BX, SP, BP, ...;
int main(){...}

Имеется специальный регистр процессора (SP) для отслеживания последнего элемента, добавленного в стек. Как следует из названия, это, ну, указатель (содержит адрес памяти, такой как 0xAAAABBCC). Но для целей этого поста я буду использовать его в качестве индекса.

В начале потока SP == STACK_CAPACITY, а затем вы уменьшаете его по мере необходимости. Правило состоит в том, что вы не можете писать в ячейки стека за пределами стека и любой индекс меньше SP недопустим, поэтому вы сначала уменьшите SP, а затем запишите значение во вновь выделенную ячейку.

Если вы знаете, что добавите несколько значений в стек подряд, вы можете зарезервировать место для всех них заранее (фрагмент № 3):

SP -= 3;
cin >> stack[999];
cin >> stack[998];
stack[997] = stack[999] + stack[998];

Примечание. Теперь вы можете понять, почему "распределение" в стеке происходит так быстро. На самом деле вы ничего не выделяете (как в ключевом слове new или malloc), это всего лишь один целочисленный декремент.


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 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 (после того, как мы обновили его внутри функции) и продолжается до следующего кадра стека. Таким образом, параметры в стеке являются частью кадра стека вызывающего абонента (см. примечание 8а).

Примечания:
8а. Википедия иначе говорит о параметрах, но здесь я придерживаюсь руководства разработчика программного обеспечения Intel, см. вып. 1, раздел 6.2.4.1 Базовый указатель стекового каркаса и рис. 6-2 в разделе 6.3.2 Дальний вызов и RET. Параметры функции и кадр стека являются частью записи активации функции (см. Общие сведения о функциях).
8б. положительные смещения от точки ВР к параметрам функции и отрицательные смещения указывают на локальные переменные. Это очень удобно для отладки
8с. stack[BP] сохраняет адрес предыдущего кадра стека, stack[stack[BP]] сохраняет предыдущий кадр стека и так далее. Следуя этой цепочке, вы можете обнаружить кадры всех функций в программе, которые еще не вернулись. Вот как отладчики показывают вам стек вызовов
8d. первые 3 инструкции из myAlgo_noLPR_withAnchor, где мы настраиваем фрейм (сохранить старый БП, обновить БП, зарезервировать место для локальных жителей), называются прологом функции


9. Соглашение о вызовах

В фрагменте 8.1 мы поместили параметры для myAlgo справа налево и вернули результат в AX. Мы могли бы также передать параметры слева направо и вернуться в BX. Или передайте параметры в BX и CX и верните в AX. Очевидно, что вызывающий абонент (main()) и вызываемая функция должна согласовать, где и в каком порядке хранится весь этот материал.

Соглашение о вызовах - это набор правил о том, как передаются параметры и возвращается результат.

В приведенном выше коде мы использовали соглашение о вызовах cdecl:

  • Параметры передаются в стеке с первым аргументом по наименьшему адресу в стеке во время вызова (задано последним & lt;...>). Вызывающий отвечает за возврат параметров обратно в стек после вызова.
  • возвращаемое значение помещается в AX
  • EBP и ESP должны сохраняться вызываемым пользователем (в нашем случае это функция myAlgo_noLPR_withAnchor), чтобы вызывающий (функция main) мог полагаться на те регистры, которые не были изменены вызовом.
  • Все остальные регистры (EAX, & lt;...>) могут быть свободно изменены вызываемым пользователем; если вызывающая сторона желает сохранить значение до и после вызова функции, она должна сохранить это значение в другом месте (мы делаем это с AX)

(Source: example "32-bit cdecl" from Qaru Documentation; copyright 2016 by icktoofay and Peter Cordes ; licensed under CC BY-SA 3.0. An archive of the full Qaru Documentation content can be found at archive.org, in which this example is indexed by topic ID 3261 and example ID 11196.)


10. Избавляемся от вызовов функций

Теперь самая интересная часть. Как и данные, исполняемый код также хранится в памяти (полностью не связан с памятью для стека), и каждая инструкция имеет адрес.
Если не указано иное, CPU выполняет инструкции одну за другой в порядке их сохранения в памяти. Но мы можем дать команду CPU "перепрыгнуть" в другое место в памяти и выполнить оттуда инструкции. В asm это может быть любой адрес, а в более высокоуровневых языках, таких как C++, вы можете переходить только к адресам, отмеченным метками (есть обходные пути, но они не очень приятные, если не сказать больше).

Давайте возьмем эту функцию (фрагмент # 10.1):

int myAlgo_withCalls(int a, int b) {
    int t1 = triple(a);
    int t2 = triple(b);
    return t1 - t2;
}

И вместо того, чтобы позвонить tripple C++, сделайте следующее:

  1. скопировать все тело tripple внутри myAlgo
  2. при вводе myAlgo перепрыгнуть через код tripple с помощью goto
  3. когда нам нужно выполнить код tripple, сохраните адрес стека строки кода сразу после вызова tripple, чтобы мы могли вернуться сюда позже и продолжить выполнение (макрос PUSH_ADDRESS ниже)
  4. перейти к адресу функции tripple и выполнить его до конца (3. и 4. вместе - макрос CALL)
  5. в конце tripple (после того, как мы очистили локальных), возьмите адрес возврата с вершины стека и прыгайте туда (макрос RET)

Поскольку в C++ нет простого способа перехода к определенному адресу кода, мы будем использовать метки для обозначения мест перехода. Я не буду вдаваться в подробности работы макросов ниже, просто поверьте мне, они делают то, что я говорю, они делают (фрагмент # 10.2):

// pushes the address of the code at label 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);
}

Примечание:
10a. поскольку обратный адрес хранится в стеке, в принципе, мы можем его изменить. Вот как работает атака с разбивкой стека works
10b. последние 3 инструкции в конце triple_label (очистка локальных узлов, восстановление старого BP, возврат) называются эпилогом функции


11. Сборка

Теперь давайте посмотрим на реальный asm для myAlgo_withCalls. Для этого в Visual Studio:

  • установить платформу сборки на x86
  • тип сборки: отладка
  • установить точку останова где-нибудь внутри myAlgo_withCalls
  • запустить, и когда выполнение остановится в точке останова, нажмите Ctrl + Alt + D

Одно отличие от нашего asm-like C++ состоит в том, что стек asm работает с байтами, а не с целыми числами. Таким образом, чтобы зарезервировать место для одного int, значение SP будет уменьшено на 4 байта.
Здесь мы идем (фрагмент # 11.1, номера строк в комментариях взяты из gist):

;   114: int myAlgo_withCalls(int a, int b) {
 push        ebp        ; create stack frame 
 mov         ebp,esp  
; return address at (ebp + 4), 'a' at (ebp + 8), 'b' at (ebp + 12)

 sub         esp,0D8h   ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal 

 push        ebx        ; cdecl requires to save all these registers
 push        esi  
 push        edi  

 ; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
 ; see https://stackoverflow.com/q/3818856/264047
 ; I guess that for ease of debugging, so that stack is filled with recognizable values
 ; 0CCCCCCCCh in binary is 110011001100...
 lea         edi,[ebp-0D8h]     
 mov         ecx,36h    
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  

;   115:    int t1 = triple(a);
 mov         eax,dword ptr [ebp+8]   ; push parameter 'a' on the stack
 push        eax  

 call        triple (01A13E8h)  
 add         esp,4                   ; clean up param 
 mov         dword ptr [ebp-8],eax   ; copy result from eax to 't1'

;   116:    int t2 = triple(b);
 mov         eax,dword ptr [ebp+0Ch] ; push 'b' (0Ch == 12)
 push        eax  

 call        triple (01A13E8h)  
 add         esp,4  
 mov         dword ptr [ebp-14h],eax ; t2 = eax

 mov         eax,dword ptr [ebp-8]   ; calculate and store result in eax
 sub         eax,dword ptr [ebp-14h]  

 pop         edi  ; restore registers
 pop         esi  
 pop         ebx  

 add         esp,0D8h  ; check we didn't mess up esp or ebp. this is only for debug builds
 cmp         ebp,esp  
 call        __RTC_CheckEsp (01A116Dh)  

 mov         esp,ebp  ; destroy frame
 pop         ebp  
 ret  

И 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  

Надеюсь, после прочтения этого поста сборка выглядит не так загадочно, как раньше :)


Вот ссылки из тела сообщения и некоторые дальнейшие чтения:

Ответ 3

Относительно того, будет ли стек реализован в аппаратном обеспечении, эта статья Википедии может помочь.

Некоторые семейства процессоров, такие как x86, имеют специальные инструкции для манипулирование стеком в настоящее время выполняется поток. Другие семейств процессоров, включая PowerPC и MIPS, не имеют явного стека поддержки, но вместо этого полагаться на соглашение и пакет делегатов управления операционной системой Бинарный интерфейс приложения (ABI).

Эта статья и другие ссылки, на которые она ссылается, могут быть полезны для восприятия использования стека в процессорах.

Ответ 4

Концепция

Сначала подумайте обо всем, как будто вы были тем, кто его изобрел. Вот так:

Сначала подумайте о массиве и о том, как он реализован на низком уровне → это в основном просто набор смежных ячеек памяти (места памяти, которые находятся рядом друг с другом). Теперь, когда у вас есть этот мысленный образ в голове, подумайте о том, что вы можете получить доступ к ЛЮБОЙ из этих мест памяти и удалить его по своему желанию, когда вы удаляете или добавляете данные в свой массив. Теперь подумайте об этом же массиве, но вместо возможности удалить любое местоположение вы решите, что вы удалите только LAST-местоположение при удалении или добавлении данных в ваш массив. Теперь ваша новая идея манипулировать данными в этом массиве таким образом называется LIFO, что означает Last In First Out. Ваша идея очень хороша, потому что упрощает отслеживание содержимого этого массива без необходимости использовать алгоритм сортировки каждый раз, когда вы удаляете что-то из него. Кроме того, чтобы знать, каков адрес последнего объекта в массиве, вы указываете один Регистр в Процессе, чтобы отслеживать его. Теперь способ отслеживания этого регистра так, что каждый раз, когда вы удаляете или добавляете что-то в свой массив, вы также уменьшаете или увеличиваете значение адреса в своем регистре на количество объектов, которые вы удалили или добавили из массива (по количество адресного пространства, которое они занимали). Вы также должны убедиться, что эта сумма, на которую вы уменьшаете или увеличиваете этот регистр, фиксируется на одну сумму (например, 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 (Reduced Instruction Set) Cpu-s, которые не имеют инструкций POPH и POP и выделенных регистров для манипулирования стеками, и при написании программ сборки для этих Cpu-s вам нужно реализовать стек самостоятельно, как я показал вам.

Ответ 5

Вы путаете абстрактный стек и реализованный аппаратный стек. Последнее уже реализовано.

Ответ 6

Я думаю, что основной ответ, который вы ищете, уже намекнул.

Когда компьютер x86 загружается, стек не настроен. Программист должен явно настроить его во время загрузки. Однако, если вы уже находитесь в операционной системе, об этом позаботились. Ниже приведен пример кода из простой программы начальной загрузки.

Сначала устанавливаются регистры сегментов данных и стека, а затем указатель стека устанавливается на 0x4000.


    movw    $BOOT_SEGMENT, %ax
    movw    %ax, %ds
    movw    %ax, %ss
    movw    $0x4000, %ax
    movw    %ax, %sp

После этого кода можно использовать стек. Теперь я уверен, что это можно сделать несколькими способами, но я думаю, что это должно проиллюстрировать идею.

Ответ 7

Стек - это просто способ, которым программы и функции используют память.

Стек всегда меня путал, поэтому я сделал иллюстрацию:

The stack is like stalactites

(версия svg здесь)

Ответ 8

Стек уже существует, поэтому вы можете предположить, что при написании кода. Стек содержит обратные адреса функций, локальные переменные и переменные, передаваемые между функциями. Существуют также регистры стека, такие как BP, SP (Stack Pointer), которые вы можете использовать, и, следовательно, встроенные команды, о которых вы упомянули. Если стек еще не реализован, функции не могут выполняться, и поток кода не может работать.

Ответ 9

Стек "реализуется" с помощью указателя стека, который (предполагая здесь архитектуру x86) указывает на сегмент стека. Каждый раз, когда что-то нажимается на стек (с помощью push-кода, вызова или аналогичного кода стека), он записывается в адрес, на который указывает указатель стека, и указатель стека уменьшается (стек растет вниз, т.е. меньшие адреса), Когда вы выталкиваете что-то из стека (popl, ret), указатель стека увеличивается и значение считывается со стека.

В приложении для пользовательского пространства стек уже настроен для вас, когда ваше приложение запускается. В среде ядра-пространства вы должны сначала настроить сегмент стека и указатель стека...

Ответ 10

Я не видел сборщика газа конкретно, но в целом стек "реализован", поддерживая ссылку на место в памяти, где находится верхняя часть стека. Место памяти хранится в регистре, который имеет разные имена для разных архитектур, но может считаться регистром указателя стека.

Команды pop и push реализованы в большинстве архитектур для вас, основываясь на микро-инструкциях. Однако некоторые "образовательные архитектуры" требуют, чтобы вы реализовали их самостоятельно. Функционально push будет реализован примерно так:

   load the address in the stack pointer register to a gen. purpose register x
   store data y at the location x
   increment stack pointer register by size of y

Кроме того, некоторые архитектуры хранят последний использованный адрес памяти в качестве указателя стека. Некоторые хранят следующий доступный адрес.

Ответ 11

Что такое стек? Стек - это тип структуры данных - средство хранения информации на компьютере. Когда новый объект вводится в стек, он помещается поверх всех ранее введенных объектов. Другими словами, структура данных стека - это как стек карт, документов, почтовых рассылок по кредитным картам или любые другие объекты реального мира, о которых вы можете думать. При удалении объекта из стека сначала удаляется один из них. Этот метод называется LIFO (последний раз, первый раз).

Термин "стек" также может быть коротким для стека сетевых протоколов. В сетях соединения между компьютерами выполняются с помощью ряда небольших соединений. Эти соединения или слои действуют как структура данных стека, поскольку они построены и удалены таким же образом.

Ответ 12

Вы правы, что стек представляет собой структуру данных. Часто структуры данных (включая стеки), с которыми вы работаете, являются абстрактными и существуют как представление в памяти.

Стек, с которым вы работаете, в этом случае имеет более существенное существование - он непосредственно сопоставляется с реальными физическими регистрами в процессоре. В качестве структуры данных стеки представляют собой структуры FILO (сначала в, последний), которые обеспечивают удаление данных в обратном порядке, в котором они были введены. См. Логотип StackOverflow для визуального отображения!;)

Вы работаете со стеком команд. Это стек фактических инструкций, которые вы загружаете процессору.

Ответ 13

Вы правы, что стек является "просто" структурой данных. Здесь, однако, это относится к аппаратно реализованному стеку, используемому для специального назначения - "The Stack".

Многие люди прокомментировали связанный с оборудованием стек по сравнению с структурой данных стека (программного обеспечения). Я хотел бы добавить, что существует три основных типа структуры стека -

  • Стек вызовов - о чем вы спрашиваете! Он хранит параметры функции и обратный адрес и т.д. Читайте в этой книге главу 4 (Все о 4-й странице, стр. 53). Существует хорошее объяснение.
  • Общий стек Что вы можете использовать в своей программе, чтобы сделать что-то особенное...
  • Общий аппаратный стек
    Я не уверен в этом, но я помню, как где-то читал, что в некоторых архитектурах есть стек с аппаратным обеспечением общего назначения. Если кто-нибудь знает, правильно ли это, сделайте комментарий.

Первое, что нужно знать, это архитектура, для которой вы программируете, что объясняет книга (я просто посмотрел ее вверх -link). Чтобы действительно понять вещи, я предлагаю вам узнать о памяти, адресе, реестрах и архитектуре x86 (я предполагаю, что вы изучаете - из книги).

Ответ 14

Стек вызовов реализуется набором команд x86 и операционной системой.

Инструкции, такие как push и pop, корректируют указатель стека, в то время как операционная система заботится о распределении памяти по мере роста стека для каждого потока.

Тот факт, что стек x86 "растет" от более высоких к более низким адресам, делает эту архитектуру более восприимчивой к атаке переполнения буфера.

Ответ 15

Функции вызова, требующие сохранения и восстановления локального состояния в режиме LIFO (в противоположность, например, обобщенному совместному подходу), оказываются настолько невероятно распространенной потребностью, что языки ассемблера и архитектуры процессора в основном создают эту функциональность в. То же самое можно было бы сказать и для понятий потоковой передачи, защиты памяти, уровней безопасности и т.д. Теоретически вы можете реализовать свой собственный стек, вызывать соглашения и т.д., Но я предполагаю, что некоторые коды операций и большинство существующих сред выполнения полагаются на эту родную концепцию "стек".

Ответ 16

stack является частью памяти. он использует для input и output of functions. также используется для запоминания возврата функции.

esp регистр запоминает адрес стека.

stack и esp реализованы с помощью аппаратного обеспечения. также вы можете реализовать его самостоятельно. это сделает вашу программу очень медленной.

Пример:

nop//esp= 0012ffc4

push 0// esp= 0012ffc0, Dword [0012ffc0] = 00000000

вызов proc01// esp= 0012ffbc, Dword [0012ffbc] = eip, eip= adrr [proc01]

pop eax//eax= Dword [ esp], esp= esp + 4

Ответ 17

Я искал, как стек работает с точки зрения функции, и я нашел этот блог его удивительный и его концепцию объяснения стека с нуля и как значение хранилища стека в стеке.

Теперь о вашем ответе. Я объясню с помощью python, но вы получите представление о том, как стек работает на любом языке.

введите описание изображения здесь

Его программа:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

введите описание изображения здесь

введите описание изображения здесь

Источник: Cryptroix

часть его темы, которую он охватывает в блоге:

How Function work ?
Calling a Function
 Functions In a Stack
 What is Return Address
 Stack
Stack Frame
Call Stack
Frame Pointer (FP) or Base Pointer (BP)
Stack Pointer (SP)
Allocation stack and deallocation of stack
StackoverFlow
What is Heap?

Но его объяснение с языком python, поэтому, если вы хотите, вы можете взглянуть.