Як саме працює стопка дзвінків?


103

Я намагаюся глибше зрозуміти, як працюють операції мов програмування низького рівня, і особливо, як вони взаємодіють з ОС / процесором. Я, мабуть, читав кожну відповідь у кожній нитці, що стосується стека / купи, тут на Stack Overflow. Але є ще одне, чого я ще не повністю зрозумів.

Розглянемо цю функцію в псевдокоді, який, як правило, є дійсним кодом Rust ;-)

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(a, b);
    doAnotherThing(c, d);
}

Ось як я припускаю, що стек виглядає на лінії X:

Stack

a +-------------+
  | 1           | 
b +-------------+     
  | 2           |  
c +-------------+
  | 3           | 
d +-------------+     
  | 4           | 
  +-------------+ 

Тепер все, що я читав про те, як працює стек, - це те, що він суворо підкоряється правилам LIFO (останній, перший). Так само, як тип даних стека в .NET, Java або будь-якій іншій мові програмування.

Але якщо це так, то що буде після рядка X? Тому що, очевидно, наступне, що нам потрібно - це робота з aі b, але це означатиме, що ОС / CPU (?) Має вискочити dі cспочатку повернутися до aта b. Але тоді він би стріляв собі в ногу, бо це потрібно cі dв наступному рядку.

Отже, мені цікаво, що саме відбувається за лаштунками?

Ще одне пов'язане питання. Подумайте, ми передаємо посилання на одну з інших функцій, як ця:

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(&a, &b);
    doAnotherThing(c, d);
}

З того, як я розумію речі, це означатиме, що параметри в doSomethingпо суті вказують на ту саму адресу пам'яті, як aі bв foo. Але знову ж таки це означає, що до появи aтаb події не з'являється стек .

Ці два випадки змушують мене думати, що я не повністю зрозумів, як саме працює стек і як він суворо дотримується правил LIFO .


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

2
Іншими словами, LIFOозначає, що ви можете додавати або видаляти елементи лише в кінці стеку, і ви завжди можете прочитати / змінити будь-який елемент.
HolyBlackCat

12
Чому після компіляції з -00 ви не розбираєте просту функцію і не дивитесь створених інструкцій? Це досить, добре, повчально ;-). Ви побачите, що код добре використовує R частину ОЗУ; він звертається до адрес безпосередньо за бажанням. Ви можете вважати ім'я змінної як зміщення в регістрі адрес (покажчик стека). Як говорили інші, стек - це просто LIFO щодо укладання (добре для рекурсії тощо). Це не LIFO щодо доступу до нього. Доступ абсолютно випадковий.
Пітер - Відновіть Моніку

6
Ви можете створити власну структуру даних стека, використовуючи масив, і просто зберігати індекс верхнього елемента, збільшуючи його при натисканні, зменшуючи його при появі. Якби ви це зробили, ви все одно зможете отримати доступ до будь-якого окремого елемента в масиві в будь-який час, не натискаючи на нього або вискакуючи так, як ви завжди можете з масивами. Приблизно те саме відбувається і тут.
Кроумен

3
В основному, називання стека / купи невдало. Вони мало схожі на складання та нагромадження в термінології структур даних, тому називати їх так само дуже заплутано.
Сіюань Рен

Відповіді:


117

Стек виклику також може називатися стеком кадру.
Речі, що складаються за принципом LIFO, - це не локальні змінні, а цілі кадри стека ("виклики") функцій, що викликаються . Локальні змінні висуваються та спливають разом із цими кадрами відповідно до так званих функцій пролог та епілог .

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

Ця графіка з Вікіпедії показує, як структурований типовий стек викликів 1 :

Зображення стека

Додаємо зміщення змінної, до якої ми хочемо отримати доступ до адреси, що міститься в покажчику кадру, і ми отримуємо адресу нашої змінної. Так коротко сказано, код просто отримує доступ до них безпосередньо через постійні компенсації часу компіляції з базового покажчика; Це проста арифметика вказівника.

Приклад

#include <iostream>

int main()
{
    char c = std::cin.get();
    std::cout << c;
}

gcc.godbolt.org дає нам

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp

    movl    std::cin, %edi
    call    std::basic_istream<char, std::char_traits<char> >::get()
    movb    %al, -1(%rbp)
    movsbl  -1(%rbp), %eax
    movl    %eax, %esi
    movl    std::cout, %edi
    call    [... the insertion operator for char, long thing... ]

    movl    $0, %eax
    leave
    ret

.. за main. Я розділив код на три підрозділи. Пролог функції складається з перших трьох операцій:

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

Потім cinпереміщується в регістр EDI 2 і getвикликається; Повертається значення в EAX.

Все йде нормально. Тепер цікаве відбувається:

Байт низького порядку EAX, позначений 8-бітовим регістром AL, приймається і зберігається в байті відразу після базового вказівника : тобто -1(%rbp)зміщення базового покажчика є -1. Цей байт - наша зміннаc . Зсув є негативним, оскільки стек зростає вниз на x86. Наступна операція зберігається cв EAX: EAX переміщується в ESI, coutпереміщується в EDI і потім оператор вставки викликається аргументами coutта cє ними.

Нарешті,

  • Повернене значення mainзберігається в EAX: 0. Це через неявне returnтвердження. Ви можете побачити xorl rax raxзамість цього movl.
  • залишити та повернутися на сайт для дзвінків. leaveскорочує цей епілог та неявно
    • Замінює вказівник стека базовим вказівником та
    • Вискакує базовий покажчик.

Після закінчення цієї операції retкадр фактично вискочив, хоча абонент все ще повинен очистити аргументи, оскільки ми використовуємо конвенцію виклику cdecl. Інші конвенції, наприклад, stdcall, вимагають, щоб користувач прибирав, наприклад, передаючи кількість байтів до ret.

Опущення покажчика кадру

Можливо також не використовувати зсуви від базового / кадрового покажчика, а від покажчика стека (ESB). Це робить реєстр EBP, який інакше міститиме значення покажчика кадру, доступне для довільного використання - але це може зробити налагодження неможливим на деяких машинах , і буде неявно вимкнено для деяких функцій . Це особливо корисно при компілюванні для процесорів лише з декількома регістрами, включаючи x86.

Ця оптимізація відома як FPO (опущення покажчика кадру) і встановлюється -fomit-frame-pointerу GCC та -Oyу Clang; зауважте, що це неявно спрацьовує кожен рівень оптимізації> 0, і лише тоді, коли налагодження все ще можливе, оскільки воно не має жодних витрат, крім цього. Додаткову інформацію дивіться тут і тут .


1 Як зазначено в коментарях, вказівник кадру, ймовірно, призначений для вказівки на адресу після зворотної адреси.

2 Зауважте, що регістри, які починаються з R, - це 64-бітні аналоги тих, які починаються з E. EAX позначає чотири байти низького порядку RAX. Я використовував імена 32-розрядних регістрів для наочності.


1
Чудова відповідь. Справа у зверненні до даних за допомогою компенсації була для мене відсутнім бітом :)
Крістоф

1
Я думаю, що в малюнку є незначна помилка. Покажчик кадру повинен бути з іншого боку зворотної адреси. Залишення функції зазвичай виконується наступним чином: переміщення покажчика стека на покажчик кадру, виведення покажчика кадру виклику з стека, повернення (тобто
вискакуйте

kasperd абсолютно прав. Ви або взагалі не використовуєте покажчик кадру (дійсна оптимізація та особливо для архітектурних архітектур, таких як x86, надзвичайно корисно), або ви використовуєте його та зберігаєте попередній на стеці - зазвичай відразу після зворотної адреси. Від того, як кадр буде встановлений і видалений, багато в чому залежить від архітектури та ABI. Є досить багато архітектур (привіт Itanium), де вся справа .. цікавіше (а є такі речі, як аргументи зі змінними розмірами аргументів!)
Voo

3
@Christoph Я думаю, що ти підходиш до цього з концептуальної точки зору. Ось коментар, який, сподіваємось, очистить це - RTS або RunTime Stack трохи відрізняються від інших стеків тим, що це "брудний стек" - насправді нічого не заважає тобі подивитися на значення, яке не є " t зверху. Зауважте, що на діаграмі "Зворотна адреса" для зеленого методу - який потрібен синьому методу! є після параметрів. Як блакитний метод отримує повернене значення після появи попереднього кадру? Ну, це брудна стопка, тому вона може просто дотягнутися і схопити її.
Рікінг

1
Покажчик кадру насправді не потрібен, тому що завжди можна використовувати зсуви від покажчика стека. GCC, орієнтована на архітектури x64, за замовчуванням використовує покажчик стека та звільняє rbpвиконувати інші роботи.
Сіюань Рен

27

Тому що очевидно, що наступне, що нам потрібно, - це робота з a і b, але це означатиме, що ОС / CPU (?) Повинен спочатку вискочити d і c, щоб повернутися до a і b. Але тоді він би стріляв собі в ногу, бо їй потрібно c і d у наступному рядку.

Коротко:

Аргументи не потрібно викладати. Аргументи, передані абонентом fooдля функціонування, doSomethingі локальні змінні у doSomething всьому можна посилатися як зсув від базового покажчика .
Так,

  • Коли виконується виклик функції, аргументи функції PUSHed у стеці. На ці аргументи далі посилається базовий покажчик.
  • Коли функція повертається до свого виклику, аргументи функції, що повертається, POPed з стека, використовуючи метод LIFO.

Детально:

Правило полягає в тому, що кожен виклик функції призводить до створення кадру стека (мінімальним є адреса для повернення до). Отже, якщо funcAдзвінки funcBта funcBдзвінки funcC, три кадри стека встановлюються один поверх іншого. Коли функція повертається, її кадр стає недійсним . Добре поводиться функція діє лише на власному кадрі стека і не переходить на чужий. Іншими словами, POPing виконується до рамки стеку вгорі (при поверненні з функції).

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

Стек у вашому запитанні встановлюється абонентом foo. Коли doSomethingі doAnotherThingвикликаються, вони встановлюють власний стек. Цифра може допомогти вам зрозуміти це:

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

Зауважте, що для доступу до аргументів, функціональному тілу доведеться перейти вниз (більш високі адреси) з місця, де зберігається зворотна адреса, і для доступу до локальних змінних тіло функції доведеться перебирати стек (нижчі адреси ) щодо місця, де зберігається зворотна адреса. Насправді, типовий код, створений компілятором для функції, зробить саме це. Для цього компілятор виділяє реєстр під назвою EBP (Base Pointer). Інша назва цього ж покажчика кадру. Зазвичай компілятор, як перше, що стосується функції функції, висуває поточне значення EBP на стек і встановлює EBP на поточний ESP. Це означає, що після цього в будь-якій частині коду функції аргумент 1 відсутній EBP + 8 (4 байти для EBP кожного абонента та зворотна адреса), аргумент 2 - EBP + 12 (десяткова), локальні змінні знаходяться в EBP-4n.

.
.
.
[ebp - 4]  (1st local variable)
[ebp]      (old ebp value)
[ebp + 4]  (return address)
[ebp + 8]  (1st argument)
[ebp + 12] (2nd argument)
[ebp + 16] (3rd function argument) 

Погляньте на наступний код C для формування кадру функції стека:

void MyFunction(int x, int y, int z)
{
     int a, int b, int c;
     ...
}

Коли дзвонить, телефонуйте

MyFunction(10, 5, 2);  

буде створено наступний код

^
| call _MyFunction  ; Equivalent to: 
|                   ; push eip + 2
|                   ; jmp _MyFunction
| push 2            ; Push first argument  
| push 5            ; Push second argument  
| push 10           ; Push third argument  

і код збірки для функції буде (встановлення викликом перед поверненням)

^
| _MyFunction:
|  sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
|  ;x = [ebp + 8], y = [ebp + 12], z = [ebp + 16]
|  ;a = [ebp - 4] = [esp + 8], b = [ebp - 8] = [esp + 4], c = [ebp - 12] =   [esp]
|  mov ebp, esp
|  push ebp
 

Список літератури:


1
Спасибі за вашу відповідь. Також посилання дійсно круті і допомагають мені пролити більше світла на нескінченне питання про те, як насправді працюють комп'ютери :)
Крістоф

Що ви маєте на увазі під "натисканням поточного значення EBP на стек", а також, чи вказівник стека зберігається в регістрі, або занадто займає позицію в стеку ... я трохи не збентежений
Suraj Jain

І хіба це не повинно бути * [ebp + 8] не [ebp + 8].?
Сурай Джайн

@Suraj Jain; Ви знаєте, що є EBPі ESP?
хакі

esp - покажчик стека, а ebp - базовий покажчик. Якщо у мене є якісь пропущені знання, будь ласка, будь ласка, виправте їх.
Сурай Джайн

19

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

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

Ось код:

void X() 
{
  int a = 1;
  int b = 2;

  // T1
  Y(a);

  // T3
  Y(b);

  // T5
}

void Y(int p) 
{
  int q;
  q = p + 2;
  // T2 (first time through), T4 (second time through)
}

Бали часу T1, T2, etc. позначаються в коді, а стан пам'яті на той час показано на кресленні:

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


2
Прекрасне візуальне пояснення. Я погуглив і знайшов папір тут: cslibrary.stanford.edu/102/PointersAndMemory.pdf Дуже корисний папір!
Крістоф

7

Різні процесори та мови використовують кілька різних конструкцій стека. Два традиційних шаблони як 8x86, так і 68000 називаються конвенцією виклику Паскаля та конвенцією C виклику; кожна конвенція обробляється однаково в обох процесорах, за винятком імен регістрів. Кожен використовує два регістри для управління стеком та пов'язаними змінними, які називаються покажчиком стека (SP або A7) та покажчиком кадру (BP або A6).

Під час виклику підпрограми з використанням будь-якої конвенції будь-які параметри висуваються на стек перед тим, як викликати процедуру. Код розпорядження потім висуває поточне значення покажчика кадру на стек, копіює поточне значення вказівника стека в покажчик кадру і віднімає з покажчика стека кількість байтів, використовуваних локальними змінними [якщо такі є]. Як тільки це буде зроблено, навіть якщо додаткові дані будуть висунуті на стек, всі локальні змінні зберігатимуться у змінних з постійним від'ємним зміщенням від покажчика стека, і всі параметри, які висунуті абонентом на стек, можуть мати доступ у постійне позитивне зміщення від покажчика кадру.

Різниця між двома умовами полягає в тому, як вони обробляють вихід із підпрограми. У конвенції C функція, що повертається, копіює покажчик кадру на покажчик стека [повертаючи його до значення, яке воно мало після натискання старого покажчика кадру], з'являється старе значення покажчика кадру та виконує повернення. Будь-які параметри, які абонент натиснув на стек, перш ніж дзвінок залишиться там. У конвенції Паскаля, після виведення старого покажчика кадру, процесор вискакує функцію повернення функції, додає до покажчика стека кількість байтів параметрів, що висуваються абонентом, а потім переходить до висказаної адреси повернення. На вихідних 68000 потрібно було використовувати 3-інструкційну послідовність для видалення параметрів абонента; 8x86 і всі 680x0 процесори після оригіналу включали "ret N"

Конвенція Pascal має перевагу в тому, щоб зберегти трохи коду на стороні абонента, оскільки абоненту не потрібно оновлювати покажчик стека після виклику функції. Однак потрібно, щоб викликана функція точно знала, скільки байтів варіює параметри, які абонент збирається поставити на стек. Якщо не вдавити належну кількість параметрів до стека перед тим, як викликати функцію, яка використовує конвенцію Pascal, майже гарантовано спричинить збій. Однак це компенсується тим, що трохи додаткового коду в межах кожного виклику методу збереже код у місцях виклику методу. З цієї причини більшість оригінальних програм програми Macintosh використовували конвенцію виклику Паскаля.

Конвенція про виклик C має перевагу в тому, що дозволяє підпрограмам приймати змінну кількість параметрів і бути надійною, навіть якщо звичайна програма не використовує всі передані параметри (абонент буде знати, скільки байтів коштує параметрів, які вона натиснула, і таким чином зможе їх почистити). Крім того, не потрібно виконувати очищення стека після кожного виклику функції. Якщо рутина викликає чотири функції послідовно, кожна з яких використовує параметри, що мають чотири байти, вона може замість використання ADD SP,4після кожного дзвінка використовувати один ADD SP,16після останнього дзвінка, щоб очистити параметри від усіх чотирьох викликів.

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


1
Оце Так! Чи можу я позичити ваш мозок на тиждень або близько того. Потрібно витягти якісь азотно-зернисті речі! Чудова відповідь!
Крістоф

Де знаходиться покажчик кадру та стека, що зберігається у самій стеці чи деінде?
Сурай Джайн

@SurajJain: Зазвичай кожна збережена копія покажчика кадру зберігатиметься на фіксованому переміщенні відносно нового значення покажчика кадру.
supercat

Сер, я давно маю це сумніви. Якщо в своїй функції я пишу, якщо (g==4)тоді, int d = 3і gя приймаю введення, використовуючи scanfпісля цього, я визначаю іншу змінну int h = 5. Тепер, як компілятор тепер дає d = 3простір у стеку. Як виконується зміщення, тому що якщо gце не так 4, то в dі стеку не буде пам'яті для d, і буде надано просто зміщення, hі якщо g == 4тоді зсув буде спочатку для g, а потім для h. Як компілятор робить це під час компіляції, він не знає наш вклад дляg
Suraj Jain

@SurajJain: Ранні версії C вимагають, щоб всі автоматичні змінні в межах функції повинні з'являтися перед будь-якими виконаними операторами. Трохи розслабивши цю складну компіляцію, але один підхід полягає у формуванні коду на початку функції, яка віднімає з SP значення попередньо оголошеної мітки. У межах функції компілятор може в кожному пункті коду відслідковувати, скільки байтів коштує місцевих жителів, як і раніше, а також відстежувати максимальну кількість байтів, що вартують місцевих жителів, які коли-небудь знаходяться в області дії. В кінці функції він може подати значення для більш раннього ...
supercat

5

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

Звичайно, з цього є винятки. Локальні змінні всього ланцюжка викликів все ще виділяються та доступні. Але до них не можна отримати доступ безпосередньо. Натомість вони передаються за посиланням (або за вказівником, який насправді лише семантично відрізняється). У цьому випадку можна отримати доступ до локальної змінної кадру стека набагато далі. Але навіть у цьому випадку функція, що виконується в даний час, все ще працює лише на власних локальних даних. Це доступ до посилання, що зберігається у власному кадрі стека, який може бути посиланням на щось на купі, в статичній пам'яті або далі вниз по стеку.

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

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


4

Стек викликів насправді не є структурою даних стека. Комп'ютери, які ми використовуємо, є позадуходовими проектами архітектури машини з випадковим доступом. Отже, a і b можна отримати прямий доступ.

За лаштунками машина робить:

  • get "a" дорівнює значенню четвертого елемента нижче вершини стека.
  • get "b" дорівнює значенню третього елемента під вершиною стека.

http://en.wikipedia.org/wiki/Random-access_machine


1

Ось діаграма, яку я створив для стека викликів C. Він більш точний і сучасний, ніж версії google image

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

І відповідно до точної структури наведеної вище діаграми, ось налагодження notepad.exe x64 на Windows 7.

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

Низькі та високі адреси заміняються, так що стек піднімається вгору на цій діаграмі. Червоний позначає кадр точно так само, як на першій схемі (в якій використовувались червоний і чорний, але тепер чорний був змінений); чорний - домашній простір; синій - це зворотна адреса, яка є зміщенням функції виклику до інструкції після дзвінка; помаранчевий - це вирівнювання, а рожевий - там, де вказівник інструкції вказує відразу після дзвінка та перед першою інструкцією. Домашнє простір + повернене значення є найменшим дозволеним кадром у вікнах, оскільки вирівнювання 16 байт rsp праворуч на початку виклику функції має підтримуватися, це завжди включає також вирівнювання у 8 байтів.BaseThreadInitThunk і так далі.

Червоний кадр функції окреслює те, що логічно функціонує функція виклику + читає / модифікує (вона може змінювати параметр, переданий на стеку, який був занадто великим, щоб передати його в регістр на -Ofast). Зелені лінії розмежовують простір, який функція виділяє від початку до кінця функції.


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

@PeterCordes висновок ASM goldbolt показує, що викликає clang і gcc callees, що висуває параметр, переданий в регістр, до стека як поведінку за замовчуванням, тому він має адресу. Для gcc, що використовує registerпараметр, оптимізує це, але ви вважаєте, що це все одно буде оптимізоване, бачачи, що адреса ніколи не приймається у межах функції. Я зафіксую верхню раму; зізнаюся, я повинен був поставити еліпсис в окрему порожню рамку. "Callee володіє своїми стеками аргументів", що, в тому числі, ті, що викликає абонент, якщо вони не можуть бути передані в регістри?
Льюїс Келсі

Так, якщо компіляція з відключеною оптимізацією, виклик її кудись проллє. Але на відміну від позиції стекових арг (і, можливо, збережених-RBP), нічого не стандартизовано, де саме. Re: callee володіє своїми стеками аргументів: так, функціям дозволено змінювати свої вхідні аргументи. Reg args, який вона розтікає, не є arg. Компілятори іноді роблять це, але IIRC часто вони марнують простір стеків, використовуючи простір під зворотною адресою, навіть якщо вони ніколи не перечитують аргумент. Якщо абонент хоче здійснити черговий дзвінок з тими ж аргументами, щоб бути безпечним, він повинен зберігати ще одну копію перед повтореннямcall
Пітер Кордес,

@PeterCordes Ну, я зробив аргументи частиною стека виклику, тому що я розмежував рамки стека на основі пункту rbp. Деякі діаграми показують це як частину стека виклику (як це робить перша діаграма в цьому питанні), а деякі показують це як частину стека виклику, але, можливо, має сенс зробити їх частиною стека виклику, бачачи як область параметра недоступний для абонента в коді вищого рівня. Так, здається , registerі constоптимізації тільки зробити різницю на -O0.
Льюїс Келсі

@PeterCordes Я змінив це. Я можу знову змінити це
Льюїс Келсі,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.