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


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

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

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

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

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

pushl
popl

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

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

17 67

17 ответов:

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

Стек

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

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

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

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

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

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

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

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

например, в MIPs толчок инструкция будет реализована следующим образом:

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

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

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

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

я только когда-либо делал самые основные вещи в asm во время моего курса CS101 еще в 2003 году. И я никогда не понимал, как работают asm и stack пока я не понял, что все это в основном похоже на программирование на 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 и затем запишите значение во вновь выделенную ячейку.

когда вы знаете, что добавите несколько значений в стек подряд, вы можете зарезервировать место для всех из них заранее (фрагмент #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 / pop

добавление нового элемента в верхней части стек-это такая частая операция, что у процессоров есть специальная инструкция для этого, 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 ' s кадр стека-это stack[996 .. 994] (оба idexes включительно).
Кадр начинается с BP функции (после того, как мы обновили его внутри функции) и длится до следующего кадра стека. Таким образом, параметры в стеке являются частью кадра стека вызывающего абонента (см. Примечание 8a).

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


9. Вызывающие соглашения

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

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

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

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

(источник: пример "32-разрядный cdecl" из документации переполнения стека; copyright 2016 by icktoofay и Питер Кордес ; лицензию под CC BY-SA 3.0. Ан архив полного содержимого документации переполнения стека можно найти по адресу archive.org, в котором этот пример индексируется по идентификатору темы 3261 и идентификатору примера 11196.)


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

теперь самая интересная часть. Как и данные, исполняемый код также хранится в памяти (полностью не связанной с памятью для стека), и каждая инструкция имеет адрес.
Когда нет в противном случае 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. at myAlgo запись перепрыгнуть через С goto
  3. когда нам нужно выполнить tripple's код, сохранить на адрес стека строки кода сразу после tripple позвоните, чтобы мы могли вернуться сюда позже и продолжить выполнение (PUSH_ADDRESS макрос ниже)
  4. перейти к адресу элемент tripple функция и выполнить ее до конца (3. и 4. вместе они CALL макрос)
  5. в конце 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а. поскольку обратный адрес хранится в стеке, в принципе мы можем его изменить. Вот как stack smashing attack работает
10b. последние 3 Инструкции В "конце"triple_label (очистка местных жителей, восстановление старого BP, возврат) называются функции эпилог


11. Сборка

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

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

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

;   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's 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  

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


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

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

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

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

Концепция

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

сначала подумайте о массиве и о том, как он реализован на низком уровне --> это в основном просто набор смежных ячеек памяти (ячеек памяти, которые находятся рядом друг с другом). Теперь, когда у вас есть этот мысленный образ в голове, подумайте о том, что вы можете получить доступ к любой из этих областей памяти и удалить его по своему желанию, когда вы удаляете или добавьте данные в свой массив. Теперь подумайте об этом же массиве, но вместо возможности удаления любого местоположения вы решите, что вы удалите только последнее местоположение, когда вы удалите или добавите данные в свой массив. Теперь Ваша новая идея манипулировать данными в этом массиве таким образом называется LIFO, что означает Last In First Out. Ваша идея очень хороша, потому что она облегчает отслеживание содержимого этого массива без необходимости использовать алгоритм сортировки каждый раз, когда вы удаляете что-то из него. Кроме того, чтобы всегда знать, какой адрес последнего объекта в массиве, вы выделяете один регистр в ЦП, чтобы отслеживать его. Теперь этот регистр отслеживает его так, что каждый раз, когда вы удаляете или добавляете что-то в свой массив, вы также уменьшаете или увеличиваете значение адреса в своем регистре на количество объектов, которые вы удалили или добавили из массива (на количество занимаемого ими адресного пространства). Вы также хотите убедиться, что та сумма, на которую вы уменьшаете или приращение этого регистра фиксируется на одну сумму (например, 4 ячейки памяти ie. 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 ячейки памяти. Это то, что поп инструкция делает.

теперь вы можете видеть, что инструкции PUSH и POP и регистры ESP ans EBP были просто добавлены Intel, чтобы упростить запись и чтение вышеупомянутой концепции структуры данных "стека". Есть еще некоторые RISC (сокращенный набор инструкций) Cpu-s, которые не имеют инструкций PUSH ans POP и выделенных регистров для манипуляций со стеком, и при написании сборочных программ для этих Cpu-s Вы должны реализовать стек самостоятельно, как я показал вам.

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

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

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

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


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

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

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

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

The stack is like stalactites

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

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

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

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

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

поп и пуш команды реализованы в большинстве архитектур для вас на основе микро инструкции. Однако некоторые " образовательные Архитектуры " требуют, чтобы вы реализовали их самостоятельно. Функционально 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

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

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

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

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

стек, с которым вы работаете в этом случае, имеет более материальное существование - он напрямую сопоставляется с реальными физическими регистрами в процессоре. Как структура данных, стеки-это структуры FILO (first in, last out), которые обеспечивают удаление данных в обратном порядке, в котором они были введены. Посмотри на логотип на сайте StackOverflow визуально! ;)

с инструкция стек. Это стек фактических инструкций, которые вы подаете процессору.

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

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

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

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

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

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

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

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

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

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

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

пример:

nop //esp = 0012ffc4

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

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

поп eax //eax = Dword[esp],esp= esp + 4

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

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

enter image description here

его программы :

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)

enter image description here

enter image description here

источник : 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, так что если вы хотите, вы можете взглянуть.