Как именно работает стек вызовов?

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, но это будет означать, что ОС / ЦП (?) Должны выскочить 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 01
2
Другими словами, это LIFOозначает, что вы можете добавлять или удалять элементы только в конце стека, и вы всегда можете прочитать / изменить любой элемент.
HolyBlackCat 01
12
Почему бы вам не разобрать простую функцию после компиляции с -O0 и не посмотреть на сгенерированные инструкции? Довольно, ну поучительно ;-). Вы обнаружите, что код хорошо использует R-часть ОЗУ; он обращается к адресам напрямую по желанию. Вы можете думать об имени переменной как о смещении адресного регистра (указателя стека). Как говорили другие, стек - это просто LIFO в отношении стекирования (подходит для рекурсии и т. Д.). Это не LIFO в отношении доступа к нему. Доступ полностью случайный.
Питер - Восстановить Монику
6
Вы можете создать свою собственную структуру данных стека, используя массив и просто сохраняя индекс верхнего элемента, увеличивая его при нажатии и уменьшая при открытии. Если бы вы сделали это, вы по-прежнему могли бы получить доступ к любому отдельному элементу в массиве в любое время, не нажимая на него или выталкивая его, точно так же, как вы всегда можете с массивами. Здесь происходит примерно то же самое.
Crowman 01
3
В принципе, имя стека / кучи неудачное. Они мало похожи на stack и heap в терминологии структур данных, поэтому называть их одинаковыми очень сложно.
Сиюань Рен

Ответы:

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перемещаются в ЭОД , а затем оператор вставки вызываются 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
Думаю, в рисунке есть небольшая ошибка. Указатель кадра должен быть по другую сторону от адреса возврата. Выход из функции обычно выполняется следующим образом: переместить указатель стека на указатель кадра, извлечь указатель кадра вызывающего абонента из стека, вернуть (т.е.
вывести
касперд абсолютно прав. Вы либо вообще не используете указатель кадра (допустимая оптимизация и особенно для архитектур с нехваткой регистров, таких как x86, чрезвычайно полезно), либо вы используете его и сохраняете предыдущий в стеке - обычно сразу после адреса возврата. То, как фрейм устанавливается и удаляется, во многом зависит от архитектуры и ABI. Есть довольно много архитектур (привет, Itanium), где все это ... более интересно (и есть такие вещи, как списки аргументов переменного размера!)
Voo
3
@Christoph Я думаю, вы подходите к этому с концептуальной точки зрения. Вот комментарий, который, мы надеемся, прояснит это - RTS, или стек RunTime, немного отличается от других стеков тем, что это «грязный стек» - на самом деле нет ничего, что мешает вам посмотреть на значение, которое не является t сверху. Обратите внимание, что на диаграмме «Обратный адрес» для зеленого метода - он необходим синему методу! стоит после параметров. Как синий метод получает возвращаемое значение после того, как был вытянут предыдущий кадр? Ну, это грязная стопка, так что можно просто потянуться и схватить ее.
Riking 02
1
Указатель кадра на самом деле не нужен, потому что вместо него всегда можно использовать смещения от указателя стека. GCC, ориентированный на архитектуры x64, по умолчанию использует указатель стека и освобождает его rbpдля выполнения другой работы.
Сиюань Рен
27

Потому что, очевидно, следующее, что нам нужно, - это работать с a и b, но это будет означать, что ОС / ЦП (?) Должны сначала вывести d и c, чтобы вернуться к a и b. Но тогда он выстрелит себе в ногу, потому что ему нужны c и d в следующей строке.

Коротко:

Нет необходимости выдвигать аргументы. На аргументы, передаваемые вызывающей стороной fooфункции doSomethingи локальные переменные в, doSomething можно ссылаться как на смещение от базового указателя .
Так,

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

В деталях:

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

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

Стек в вашем вопросе настраивается вызывающим абонентом foo. Когда вызываются doSomethingи doAnotherThing, они устанавливают свой собственный стек. Рисунок может помочь вам понять это:

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

Обратите внимание, что для доступа к аргументам тело функции должно пройти вниз (более высокие адреса) от места, где хранится адрес возврата, а для доступа к локальным переменным тело функции должно пройти вверх по стеку (более низкие адреса ) относительно места, где хранится обратный адрес. Фактически, типичный код, сгенерированный компилятором для функции, будет делать именно это. Компилятор выделяет для этого регистр EBP (базовый указатель). Другое название этого - указатель кадра. Обычно компилятор первым делом для тела функции помещает текущее значение 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 в стек», а также сохраняет ли указатель стека в регистре или он тоже занимает позицию в стеке ... Я немного запутался
Сурадж Джайн
И разве это не должно быть * [ebp + 8], а не [ebp + 8]?
Сурадж Джайн
@Suraj Jain; Вы знаете, что такое EBPи ESP?
haccks
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 Действительно полезная статья!
Christoph
7

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

При вызове подпрограммы с использованием любого соглашения, любые параметры помещаются в стек перед вызовом подпрограммы. Затем код подпрограммы помещает текущее значение указателя фрейма в стек, копирует текущее значение указателя стека в указатель фрейма и вычитает из указателя стека количество байтов, используемых локальными переменными [если есть]. Как только это будет сделано, даже если дополнительные данные помещены в стек, все локальные переменные будут сохранены в переменных с постоянным отрицательным смещением от указателя стека, и все параметры, которые были помещены в стек вызывающей стороной, могут быть доступны в постоянное положительное смещение от указателя кадра.

Разница между этими двумя соглашениями заключается в том, как они обрабатывают выход из подпрограммы. В соглашении C возвращающая функция копирует указатель кадра в указатель стека [восстанавливает его до значения, которое он имел сразу после того, как был помещен указатель старого кадра], извлекает значение указателя старого кадра и выполняет возврат. Любые параметры, которые вызывающий объект поместил в стек перед вызовом, останутся там. Согласно соглашению Паскаля, после извлечения старого указателя кадра процессор извлекает адрес возврата функции, добавляет к указателю стека количество байтов параметров, переданных вызывающей стороной, и затем переходит к извлеченному адресу возврата. На исходном 68000 необходимо было использовать последовательность из 3 инструкций для удаления параметров вызывающего абонента; Процессоры 8x86 и все 680x0 после оригинала включали "ret N"

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

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

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

суперкар
источник
1
Вот Это Да! Могу я одолжить твой мозг на неделю или около того. Нужно извлечь некоторые мелочи! Отличный ответ!
Christoph
Где хранится указатель кадра и стека в самом стеке или где-либо еще?
Сурадж Джайн
@SurajJain: Как правило, каждая сохраненная копия указателя кадра будет сохранена с фиксированным смещением относительно нового значения указателя кадра.
supercat
Сэр, я долго сомневаюсь. Если в своей функции я пишу if (g==4)then int d = 3и gберу входные данные, scanfпосле этого я определяю другую переменную int h = 5. Теперь, как компилятор теперь предоставляет d = 3место в стеке. Как выполняется смещение, потому что если gэто не так 4, тогда в стеке не будет памяти для d, и будет просто смещение, hи если g == 4тогда смещение будет сначала для g, а затем для h. Как компилятор делает это во время компиляции, он не знает наших входных данных дляg
Сурадж Джайн
@SurajJain: Ранние версии C требовали, чтобы все автоматические переменные внутри функции появлялись перед любыми исполняемыми операторами. Немного ослабив эту сложную компиляцию, но один из подходов состоит в том, чтобы сгенерировать код в начале функции, который вычитает из SP значение заранее объявленной метки. Внутри функции компилятор может в каждой точке кода отслеживать, сколько байтов локальных переменных все еще находится в области видимости, а также отслеживать максимальное количество байтов локальных переменных, которые когда-либо были в области видимости. В конце функции он может предоставить значение для более раннего ...
supercat
5

Здесь уже есть действительно хорошие ответы. Однако, если вас по-прежнему беспокоит поведение стека LIFO, считайте его стеком кадров, а не стеком переменных. Я хочу сказать, что, хотя функция может обращаться к переменным, которые не находятся на вершине стека, она по-прежнему работает только с элементом наверху стека: одним кадром стека.

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

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

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

Джереми Уэст
источник
4

Стек вызовов на самом деле не является структурой данных стека. За кулисами используемые нами компьютеры являются реализациями архитектуры машин с произвольным доступом. Таким образом, можно получить прямой доступ к a и b.

За кадром машина делает:

  • get "a" равно чтению значения четвертого элемента ниже вершины стека.
  • get "b" равно чтению значения третьего элемента ниже вершины стека.

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

HDante
источник
1

Вот диаграмма, которую я создал для стека вызовов C. Это более точно и современно, чем версии изображений Google

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

И в соответствии с точной структурой диаграммы выше, вот отладка notepad.exe x64 на Windows 7.

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

Младшие адреса и старшие адреса меняются местами, поэтому стек на этой диаграмме поднимается вверх. Красный обозначает рамку точно так же, как на первой диаграмме (где использовались красный и черный, но теперь черный был изменен); черный - домашнее пространство; синий - это адрес возврата, который представляет собой смещение вызывающей функции к инструкции после вызова; оранжевый - это выравнивание, а розовый - то место, куда указывает указатель инструкции сразу после вызова и перед первой инструкцией. Значение homespace + return - это наименьший допустимый фрейм в окнах, и, поскольку 16-байтовое выравнивание rsp в самом начале вызываемой функции должно поддерживаться, это всегда также включает 8-байтовое выравнивание.BaseThreadInitThunk и так далее.

Красные фреймы функций обрисовывают, что вызываемая функция логически «владеет» + читает / изменяет (она может изменять переданный в стек параметр, который был слишком большим для передачи в регистр на -Ofast). Зеленые линии ограничивают пространство, которое функция выделяет себе от начала до конца функции.

Льюис Келси
источник
RDI и другие аргументы регистров вообще попадают в стек только при компиляции в режиме отладки, и нет гарантии, что компиляция выберет этот порядок. Кроме того, почему аргументы стека не отображаются в верхней части диаграммы для самого старого вызова функции? На вашей диаграмме нет четкого разграничения, между каким фреймом какие данные «принадлежат». (Вызываемый объект владеет аргументами стека). Отсутствие аргументов стека в верхней части диаграммы еще больше усложняет понимание того, что «параметры, которые нельзя передать в регистры» всегда находятся прямо над адресом возврата каждой функции.
Питер Кордес,
Выходные данные @PeterCordes goldbolt asm показывают, что вызываемые clang и gcc проталкивают параметр, переданный в регистре, в стек в качестве поведения по умолчанию, поэтому у него есть адрес. В gcc использование registerпараметра позади параметра оптимизирует это, но вы могли бы подумать, что это будет оптимизировано в любом случае, поскольку адрес никогда не берется внутри функции. Закреплю верхнюю раму; по общему признанию, я должен был поместить многоточие в отдельную пустую рамку. 'вызываемый объект владеет своими аргументами стека', что включает в себя те, которые вызывает вызывающий, если они не могут быть переданы в регистры?
Льюис Келси,
Ага, если компилировать с отключенной оптимизацией, вызываемый куда-то разольется. Но в отличие от позиции аргументов стека (и, возможно, сохраненного RBP), ничего не стандартизировано относительно того, где. Re: вызываемый объект владеет аргументами стека: да, функциям разрешено изменять свои входящие аргументы. Аргументы реестра, которые он передает, не являются аргументами стека. Компиляторы иногда делают это, но IIRC часто тратят пространство стека, используя пространство под адресом возврата, даже если они никогда не перечитывают аргумент. Если абонент хочет сделать еще один звонок с теми же аргументами, чтобы быть в безопасности , они должны хранить еще одну копию , прежде чем повторятьcall
Питер Кордес
@PeterCordes Ну, я сделал аргументы частью стека вызывающей стороны, потому что я разграничивал кадры стека на основе того, куда указывает rbp. На некоторых диаграммах это показано как часть стека вызываемого абонента (как и на первой диаграмме по этому вопросу), а на некоторых показано как часть стека вызывающего абонента, но, возможно, имеет смысл сделать их частью стека вызываемого абонента, рассматривая его как область действия параметра. недоступен для вызывающего в коде более высокого уровня. Да вроде registerи constоптимизации имеют значение только на -O0.
Льюис Келси,
@PeterCordes Я его изменил. Я мог бы изменить это снова
Льюис Келси,