Як працює стек мовою асемблера?


84

На даний момент я намагаюся зрозуміти, як працює стек, тому я вирішив навчити себе деякій мові асемблера , я використовую цю книгу:

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

Я використовую Gas і розробляю Linux Mint .

Мене дещо бентежить:

Наскільки мені було відомо, стек - це просто структура даних. Тож я припустив, що, якщо кодую в збірці, мені доведеться реалізувати стек самостійно. Однак, схоже, це не так, оскільки існують такі команди

pushl
popl

Отже, під час кодування в збірці для архітектури x86 та використання синтаксису Gas: чи є стек лише структурою даних, яка вже реалізована? Або це насправді реалізовано на апаратному рівні? Або це щось інше? Чи більшість мов збірки для інших наборів мікросхем вже мають стек?

Я знаю, що це трохи дурне питання, але насправді мене це дуже бентежить.


2
Більшість із цих відповідей говорять про стек, яким його використовують мови, зокрема говорять про передачу аргументів у стек. У багатьох центральних процесорах це просто для полегшення реалізації мов - якби ви збирали ручне кодування, ви, як правило, передавали параметри функціям у регістри (принаймні до того, як центральний процесор був оптимізований для операцій стека, оскільки мови використовували його так сильно). Стек в основному підтримує виклики / повернення в порядку, також переривання (яке має зберегти стан процесора) буде виштовхувати існуючі значення в регістри, які він буде використовувати, і виводити їх перед поверненням.
Bill K

Відповіді:


82

Я думаю, насамперед ви плутаєтеся між program's stackі any old stack.

Стек

Це абстрактна структура даних, яка складається з інформації в системі Last In First Out. Ви кладете довільні предмети в стек, а потім знову знімаєте їх, подібно до вхідного / вихідного лотка, верхній предмет - це завжди той, який знімається, і ви завжди ставите на верхній.

Стек програм

Це стек, це розділ пам'яті, який використовується під час виконання, він зазвичай має статичний розмір для кожної програми і часто використовується для зберігання параметрів функції. Ви натискаєте параметри на стек, коли викликаєте функцію, і функція або звертається безпосередньо до стека, або вискакує змінні зі стеку.

Стек програм, як правило, не є апаратним забезпеченням (хоча він зберігається в пам'яті, тому його можна аргументувати як такий), але покажчик стека, який вказує на поточну область стека, як правило, є регістром ЦП. Це робить його трохи гнучкішим, ніж стек LIFO, оскільки ви можете змінити точку, в якій адресується стек.

Вам слід прочитати та переконатися, що ви розумієте статтю wikipedia, оскільки вона дає хороший опис Hardware Stack, з яким ви маєте справу.

Існує також цей підручник, який пояснює стек з точки зору старих 16-бітних регістрів, але може бути корисним і ще один, зокрема, про стек.

Від Нільса Піпенбринка:

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

Наприклад, в MIP-файлах буде реалізована push-інструкція, як:

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  

2
До речі - x86 має ці спеціальні вказівки щодо стека, тому що натискання та викидання речей зі стеку трапляється так часто, що було б гарною ідеєю використовувати для них короткий код операції (менше простору коду). Такі архітектури, як MIPS та ARM, їх не мають, тому вам доведеться впроваджувати стек самостійно.
Нільс Піпенбринк,

4
Майте на увазі, що ваш гарячий новий процесор певною мірою сумісний із бінарним процесором із 8086, а також сумісний із джерелом із 8080, розробкою 8008, першим мікропроцесором. Деякі з цих рішень мають довгий шлях.
Девід Торнлі,

4
В ARM існують єдині інструкції щодо маніпулювання стеком, вони просто не настільки очевидні, оскільки їх називають STMDB SP! (для PUSH) та LDMIA SP! (для POP).
Адам Гуд,

1
Боже, ця відповідь потребує +500 ... Я не знайшов нічого, що б це пояснювало з тих пір, як назавжди. Розглядаючи можливість створити нові акаунти, щоб ставити оцінку +1 від цього моменту ...
Габріель

1
@bplus Ви також можете звернутися до cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/stack.html
Suraj Jain

36

(У цій відповіді я склав суть всього коду на випадок, якщо ви хочете з ним пограти)

Я лише коли-небудь робив основні речі в 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вже використовуються, а які „вільні”. Ось чому ми зберігаємо нові значення в стеку поруч один з одним.

Одна дивна річ щодо стека (x86) asm полягає в тому, що ви додаєте туди речі, починаючи з останнього індексу, і переходите до нижчих індексів: стек [999], потім стек [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. Локальні змінні

Давайте подивимось на цю спрощену функцію ( фрагмент No4.1 ):

int triple(int a) {
    int result = a * 3;
    return result;
}

і перепишіть його без використання локальної змінної ( фрагмент No4.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];
}

і подивіться, як це називається ( фрагмент No 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 функції (після того, як ми оновили його всередині функції) і триває до наступного кадру стека. Отже, параметри стека є частиною кадру стека абонента (див. Примітку 8а).

Примітки:
8a. Про параметри Вікіпедія говорить інакше , але тут я дотримуюсь інструкції розробника програмного забезпечення Intel , див. Т. 1, розділ 6.2.4.1 Базовий вказівник стека та малюнок 6-2 у розділі 6.3.2 Далекі операції CALL та RET . Параметри функції та кадр стека є частиною запису активації функції (див . Генерацію в перилогах функції ).
8б. позитивні зсуви від точки BP до функціональних параметрів, а негативні - до локальних змінних. Це дуже зручно для налагодження
8c. stack[BP]зберігає адресу попереднього кадру стека,stack[stack[BP]]зберігає попередній кадр стека тощо. Слідуючи цьому ланцюжку, ви можете виявити фрейми всіх функцій програми, які ще не повернулися. Ось як налагоджувачі показують вам стек дзвінків
8d. перші 3 інструкції myAlgo_noLPR_withAnchor, де ми встановлюємо фрейм (зберегти старий BP, оновити BP, зарезервувати місце для місцевих жителів), називаються функційним прологом


9. Виклик конвенцій

У фрагменті 8.1 ми просунули параметри для myAlgoсправа наліво і повернули результат в AX. Ми могли б також передати параметри зліва направо та повернутися BX. Або передайте параметри в BX і CX і поверніть в AX. Очевидно, що main()функція caller ( ) та викликана функція повинні узгодити, де і в якому порядку зберігається все це.

Конвенція викликів - це набір правил передачі параметрів та повернення результату.

У наведеному вище коді ми використовували умову виклику cdecl :

  • Параметри передаються в стек, з першим аргументом за найнижчою адресою в стеку під час виклику (натискання останнього <...>). Абонент несе відповідальність за вискакування параметрів, повернутих із стеку після дзвінка.
  • повернене значення розміщується в AX
  • EBP та ESP повинні зберігатись у абонента ( myAlgo_noLPR_withAnchorфункція в нашому випадку) таким чином, щоб абонент ( mainфункція) міг покладатися на ті регістри, які не були змінені під час виклику.
  • Усі інші регістри (EAX, <...>) можуть вільно змінюватись абонентом; якщо абонент хоче зберегти значення до і після виклику функції, він повинен зберегти значення в іншому місці (ми робимо це за допомогою AX)

(Джерело: приклад «32-біта Cdecl» від переповнення стека документації, авторське право 2016 з icktoofay і Пітер Кордес ., Під ліцензією CC BY-SA 3.0 An архів повного змісту Stack Overflow документації можна знайти на 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;
}

І замість того, щоб викликати trippleC ++ способом, виконайте наступне:

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

Оскільки в C ++ немає простого способу перейти до певної адреси коду, ми будемо використовувати мітки для позначення місць стрибків. Я не буду вдаватися в подробиці, як працюють макроси нижче, просто повірте мені, що вони роблять те, що я кажу ( фрагмент No 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(очищення місцевих жителів, відновлення старого АТ, повернення) називаються епілогом функції


11. Збірка

А тепер давайте розглянемо справжній asm для myAlgo_withCalls. Для цього у Visual Studio:

  • встановити платформу збірки на x86 ( не x86_64)
  • тип збірки: налагодження
  • встановити точку розриву десь усередині 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  

Сподіваюся, після прочитання цієї публікації збірка не виглядає такою загадковою, як раніше :)


Ось посилання на тіло публікації та деякі подальші читання:


Це було дуже давно, коли я це запитав, це справді чудова відповідь у глибині. Дякую.
bplus

Чому ви використовуєте 16-розрядні імена для регістрів на початку вашої відповіді? Якщо ви говорили про фактичний 16-розрядний код, [SP]чи не є дійсним 16-розрядний режим адресації. Напевно, найкраще використовувати ESP. Крім того, якщо ви оголошуєте SPяк int, ви повинні модифікувати його на 4 для кожного елемента, а не на 1. (Якщо ви оголосили long *SP, то SP += 2збільшиться на 2 * sizeof(int), і, таким чином, вилучите 2 елементи. Але з intSP, це має бути SP += 8, як add esp, 8. -bit asm.
Пітер Кордес

Захоплююче! Я думаю, що цікаво, що ви намагаєтеся пояснити збірку за допомогою C. Я цього раніше не бачив. Охайний. Я можу запропонувати перейменувати "Немає локальних змінних" на "Як працюють локальні змінні", або просто "Локальні змінні".
Дейв Допсон

@PeterCordes причиною 16-розрядних імен (SP, BP) є чіткість - SP легко перекладається як "покажчик стека". Якщо я використовую належні 32-розрядні імена, мені потрібно буде пояснити різницю між 16/32/64 бітовими режимами або залишити це незрозумілим. Я мав намір, щоб той, хто знає лише Java або Python, міг стежити за публікацією, не сильно чухаючи голову. І я думаю, що адресація пам’яті лише відволікає читача. Плюс, я поставив посилання на wikibook на тему для допитливих і сказав кілька слів про ESP в кінці допису.
Олександр Малахов

1
Щоб уникнути цього, нам потрібен індекс прив’язки, який не змінюється, коли стек зростає. Потреба - неправильне слово; -fomit-frame-pointerє роком за замовчуванням у gcc та clang. Люди, які дивляться на справжній АСМ, повинні знати, що EBP / RBP зазвичай не буде використовуватися як вказівник кадру. Я б сказав, "традиційно люди хотіли якір, який не змінюється за допомогою push / pop, але компілятори можуть відстежувати зміщення зсувів". Потім ви можете оновити розділ про зворотні сліди, сказавши, що це застарілий метод, який не використовується за замовчуванням, коли .eh_frameдоступні метадані DWARF або метадані Windows x86-64.
Пітер Кордес,

7

Щодо того, чи реалізований стек в апаратному забезпеченні, ця стаття у Вікіпедії може допомогти.

Деякі сімейства процесорів, наприклад x86, мають спеціальні вказівки щодо маніпулювання стеком поточного потоку, що виконується. Інші сімейства процесорів, включаючи PowerPC та MIPS, не мають явної підтримки стека, але натомість покладаються на домовленості та делегують управління стеками в бінарний інтерфейс додатків операційної системи (ABI).

Ця стаття та інші, на які вона посилається, можуть бути корисними, щоб відчути використання стека в процесорах.


4

Концепція

Спочатку подумайте про все це так, ніби ви саме той, хто його винайшов. Подобається це:

Спочатку подумайте про масив і про те, як він реалізований на низькому рівні -> це в основному просто набір суміжних розташувань пам'яті (розташування пам'яті, які знаходяться поруч). Тепер, коли у вас є такий розумовий образ у вашій голові, подумайте про те, що ви можете отримати доступ до БУДЬ-ЯКОГО з цих місць пам'яті та видалити його за власним бажанням, коли ви видаляєте або додаєте дані у свій масив. Тепер подумайте про той самий масив, але замість можливості видалити будь-яке розташування ви вирішите, що ви видалите лише ОСТАННЕ розташування, коли ви видаляєте або додаєте дані у свій масив. Тепер ваша нова ідея таким чином маніпулювати даними в цьому масиві називається 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 (зменшений набір інструкцій), які не мають інструкцій PUSH і POP та виділених реєстрів для маніпулювання стеком, і під час написання програм збірки для цих процесорів вам доведеться реалізувати стек самостійно, як я тобі показав.


3

Ви плутаєте абстрактний стек та апаратно реалізований стек. Останнє вже реалізоване.


3

Я думаю, що головна відповідь, яку ви шукаєте, вже натякнута.

Коли комп'ютер x86 завантажується, стек не налаштовується. Програміст повинен чітко налаштувати його під час завантаження. Однак, якщо ви вже в операційній системі, про це подбали. Нижче наведено зразок коду з простої програми завантаження.

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


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

Після цього коду може використовуватися стек. Зараз я впевнений, що це можна зробити різними способами, але я думаю, що це має проілюструвати ідею.


3

Стек - це лише спосіб, яким програми та функції використовують пам’ять.

Стек завжди мене бентежив, тому я зробив ілюстрацію:

Стек такий, як сталактити

( версія SVG тут )

  • Поштовх "прикріплює новий сталактит до стелі".
  • Поп "вискакує сталактит".

Сподіваюся, це більше корисно, ніж заплутано.

Не соромтеся використовувати зображення SVG (ліцензія CC0).


1

Стек уже існує, тому ви можете припустити, що при написанні коду. Стек містить повернені адреси функцій, локальні змінні та змінні, які передаються між функціями. Є також вбудовані регістри стеків, такі як BP, SP (Stack Pointer), які ви можете використовувати, отже, вбудовані команди, про які ви згадали. Якщо стек ще не був реалізований, функції не могли працювати, а потік коду не міг працювати.


1

Стек «реалізований» за допомогою вказівника стека, який (припускаючи тут архітектуру x86) вказує на сегмент стека . Кожного разу, коли щось натискається на стек (за допомогою pushl, call або подібного коду операцій стека), воно записується на адресу, на яку вказує вказівник стека, а покажчик стека зменшується (стек зростає вниз , тобто менші адреси) . Коли ви викидаєте щось із стека (popl, ret), покажчик стека збільшується, а значення зчитується зі стека.

У програмі простору користувача стек вже налаштований для вас, коли програма запускається. У середовищі простору ядра спочатку потрібно налаштувати сегмент стека та вказівник стека ...


1

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

Команди 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

Крім того, деякі архітектури зберігають останню використовувану адресу пам'яті як покажчик стека. Деякі зберігають наступну доступну адресу.


1

Що таке стек? Стек - це тип структури даних - засіб зберігання інформації в комп’ютері. Коли новий об’єкт вводиться в стек, він розміщується поверх усіх раніше введених об’єктів. Іншими словами, структура даних стеку подібна до стопки карток, паперів, розсилок кредитних карток або будь-яких інших реальних об’єктів, про які ви можете подумати. Під час вилучення предмета зі стопки першим видаляється той, що знаходиться зверху. Цей метод називається LIFO (останній вхід, перший вихід).

Термін "стек" також може бути коротким для стека мережевого протоколу. У мережах зв’язок між комп’ютерами здійснюється через ряд менших з’єднань. Ці з'єднання, або шари, діють як структура даних стеку тим, що будуються та утилізуються однаково.


0

Ви праві, що стек є структурою даних. Часто структури даних (включаючи стеки), з якими ви працюєте, є абстрактними та існують як представлення в пам'яті.

Стек, з яким ви працюєте в цьому випадку, має більш матеріальне існування - він безпосередньо відображається у реальних фізичних регістрах в процесорі. Як структура даних, стеки - це структури FILO (перший вхід, останній вихід), які забезпечують видалення даних у зворотному порядку, коли вони були введені. Дивіться логотип StackOverflow для наочності! ;)

Ви працюєте зі стеком інструкцій . Це стек фактичних інструкцій, якими ви подаєте процесор.


неправильно. це не "стек інструкцій" (чи є щось таке?) це просто пам'ять, до якої здійснюється доступ через регістр стека. використовується для тимчасового зберігання, параметрів процедури та (найважливішого) адреси повернення для викликів функцій
Хав'єр

0

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

Інструкції, такі як push і pop, регулюють вказівник стека, тоді як операційна система дбає про розподіл пам'яті в міру зростання стека для кожного потоку.

Той факт, що стек x86 "зростає" з вищих на нижчі адреси, робить цю архітектуру більш сприйнятливою до атаки переповнення буфера.


1
Чому той факт, що стек x86 зростає, робить його більш сприйнятливим до переповнення буфера? Чи не могли б ви отримати те саме переповнення з розширеним сегментом вгору?
Натан Феллман,

@nathan: лише якщо ви можете отримати програму для виділення негативного обсягу пам'яті в стеку.
Хав'єр

1
Атаки переповнення буфера записують після кінця масиву на основі стека - char userName [256], це записує пам'ять від нижчої до вищої, що дозволяє вам перезаписувати такі речі, як адреса зворотного зв'язку. Якби стек рос у тому ж напрямку, ви могли б переписати лише нерозподілений стек.
Моріс Фланаган

0

Ви правильно вважаєте, що стек є "просто" структурою даних. Однак тут йдеться про апаратно реалізований стек, що використовується спеціально для цього - "Стек".

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

  1. Стек дзвінків - про який ви запитуєте! У ньому зберігаються параметри функцій, адреса повернення тощо. Читайте функції Глави 4 (Усе про четверту сторінку, тобто сторінку 53) у цій книзі. Є гарне пояснення.
  2. Загальний стек, який ви можете використовувати у своїй програмі, щоб зробити щось особливе ...
  3. Універсальний апаратний стек
    Я не впевнений у цьому, але я пам’ятаю, десь читав, що в деяких архітектурах є загальновстановлений апаратно реалізований стек. Якщо хтось знає, чи це правильно, будь ласка, коментуйте.

Перше, що потрібно знати, це архітектура, для якої ви програмуєте, що і пояснюється в книзі (я щойно подивився - посилання). Щоб по-справжньому зрозуміти речі, я пропоную вам дізнатись про пам'ять, адресацію, регістри та архітектуру x86 (я припускаю, що це те, що ви вивчаєте - з книги).


0

Виклик функцій, що вимагає збереження та відновлення локального стану за способом LIFO (на противагу сказанню, узагальнений ко-рутинний підхід), виявляється настільки неймовірно загальною потребою, що мови збірки та архітектури центрального процесора в основному будують цю функціональність. Те саме напевно, можна сказати про поняття потоків, захист пам'яті, рівні безпеки тощо. Теоретично ви можете реалізувати власний стек, конвенції викликів тощо, але я припускаю, що деякі операційні коди та більшість існуючих середовищ виконання покладаються на цю природну концепцію "стека" .


0

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

esp register - запам'ятати адресу стека.

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

приклад:

nop // esp= 0012ffc4

натиснути 0 // esp= 0012ffc0, Dword [0012ffc0] = 00000000

виклик proc01 // esp= 0012ffbc, Dword [0012ffbc] = eip, eip= adrr [proc01]

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


0

Я шукав, як працює стек з точки зору функції, і я знайшов цей блог своїм чудовим і пояснював концепцію стека з нуля і те, як стек зберігає значення в стеку.

Тепер про вашу відповідь. Я поясню за допомогою 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, тому, якщо ви хочете, ви можете поглянути.


Сайт Criptoix мертвий, і копії на web.archive.org немає
Олександр Малахов

1
@AlexanderMalakhov Cryptroix не працював через проблему з хостингом. Cryptroix зараз працює і працює.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.