Есть некоторые соглашения о вызовах (например, pascal, stdcall), но, насколько я понимаю, C действительно использует cdecl (объявленный C). Каждое из этих соглашений немного отличается тем, как вызывающий объект загружает параметры в стек, соответственно, который (вызывающий / вызываемый) выполняет очистку .

Говоря об очистке, вот мой вопрос. Я не понимаю: есть три разные вещи?

  1. стек чистый
  2. перемещение указателя обратно на предпоследний кадр стека
  3. восстановление стека

Или как мне их увидеть?

Кроме того, цель этого вопроса в основном заключается в том, как вариативная функция может работать в соглашениях о вызовах, таких как Pascal или stdcall, где вызываемый должен очистить / очистить / восстановить (я не знаю, какая операция) стек - но он этого не делает. не знаю, сколько параметров он получит.

ИЗМЕНИТЬ

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

"Таким образом, указатель стека ESP может перемещаться вверх и вниз, но регистр EBP остается фиксированным. Это удобно, потому что это означает, что мы всегда можем ссылаться на первый аргумент как [EBP + 8] независимо от того, сколько нажатий и выталкиваний выполняется функция."

Перемещаемые переменные и локальные переменные являются последовательными в памяти. В чем преимущество направления их с помощью EBP? Между ними никогда не будет динамического смещения, даже если размер стека изменится.

Один из прочитанных мною материалов - это этот сайт (только начало) для лучшее понимание того, что такое фрейм стека . Затем я зашел на yt и нашел эти обзор стека и стек вызовов, но они почему-то пропустили ту часть, которая мне была нужна. Что именно происходит, когда вы вызываете функцию (я не понимаю инструкцию " адрес вызова ", за которым следует следующая инструкция a push значение в стеке, что означает возвращаемое значение). Кто контролирует, какой будет обратный адрес? Звонящий? вызываемый? Когда вызываемый возвращается, программа продолжает работу, выполняя инструкцию, которая является операцией чтения из регистра или как?

7
Cătălina Sîrbu 2 Ноя 2020 в 10:16

2 ответа

Лучший ответ

насколько мне известно, C использует cdecl

Несмотря на его название, соглашение cdecl не универсально для кода C, даже для архитектуры x86. Его преимущество в том, что его легко определить и реализовать, но он не использует регистры ЦП для передачи аргументов, что более эффективно. Это имеет значение даже для x86 с нехваткой регистров, но намного больше для архитектур с более доступными регистрами, таких как x86_64.

Говоря об очистке, вот мой вопрос. Я не понимаю: есть три разные вещи?

  1. стек чистый
  2. перемещение указателя обратно на предпоследний кадр стека
  3. восстановление стека

Или как мне их увидеть?

Я был бы склонен интерпретировать (1) и (3) как разные способы выражения одного и того же, но возможно, что кто-то проведет между ними различия. (3) и связанные с ним формулировки - это то, с чем я сталкиваюсь чаще всего. (2) не обязательно одно и то же, потому что могут быть восстановлены два соответствующих параметра стека: основание кадра стека (см. Ниже) и верх стека. База кадра стека важна в том случае, если кадр стека содержит больше информации, чем значения аргументов и локальных переменных, таких как база предыдущего кадра стека.

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

Стек - это не обязательно вся картина.

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

Взяв в качестве примера x86 (для которого был разработан cdecl), ЦП имеет регистры как для базы стека (кадра), так и для текущего указателя стека. База стека вызывающего хранится в стеке с известным смещением (0) от базы стека вызываемого объекта. Независимо от количества аргументов вызываемый восстанавливает стек, перемещая верхнюю часть стека в свою базу стека и вставляя туда значение, чтобы получить базу стека вызывающего.

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

Почему так важен порядок, в котором параметры помещаются в стек?

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

Что касается стековых фреймов: это больше материал, который не указан в C и который варьируется, по крайней мере, до некоторой степени. Однако концептуально кадр стека вызова функции - это часть стека, которая обеспечивает контекст выполнения для этого вызова. Обычно он предоставляет хранилище для локальных переменных и может содержать дополнительную информацию, такую ​​как адрес возврата и / или значение указателя кадра стека вызывающего объекта. Он также может содержать другую информацию о вызове функции, подходящую для среды выполнения. Подробности являются частью используемого соглашения о вызовах.

4
John Bollinger 2 Ноя 2020 в 18:02

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

Например, в 32-битном коде для Windows я думаю, что stdcall является значением по умолчанию для многих функций Windows DLL, но вариативные используют cdecl. (Системы x86, отличные от Windows, такие как Linux и MacOS, обычно используют соглашения о вызовах вызывающих абонентов по умолчанию для всех функций. Так что это действительно только для 32-разрядной Windows, если мы говорим об основных системах.)

Таким образом, printf не нужно подсчитывать размер аргументов, на которые ссылается строка формата (или получать счет, переданный вызывающей стороной), а затем эмулировать ret 12 или ret 8 или что-то еще. ret n доступен только в машинном коде с непосредственным операндом, поэтому вы не можете сделать ret ecx или что-то в этом роде. Эмулировать счетчик переменных ret n можно различными способами, например одним из наименее плохих было бы копирование адреса возврата выше в стеке и настройка ESP до простого ret. Но это все еще довольно неэффективно по сравнению с простым использованием соглашения о вызывающих абонентах.

Кроме того, это сделало бы программы хрупкими: передача неиспользуемого аргумента в printf является неопределенным поведением в ISO C, но некоторый код зависит от того, что он молча игнорируется (случайно или из-за несоответствия типов).

Windows также гарантирует, что вызывающий и вызываемый согласны в том, сколько места в стеке будет выделяться вызываемым, путем «украшения» имен символов asm, таких как _foo@12, для таких функций, как int foo(int, int, int). (Три int аргумента = 12 байтов пространства стека для чистого соглашения об аргументах стека). Поэтому, если вы объявите его неправильным (или не объявляете его вообще, а в неявном объявлении используются более крупные типы), вы получите ошибку ссылки вместо ошибки, которую трудно отладить, которая может произойти только в оптимизированных сборках. (Если отладочная сборка, использующая EBP в качестве указателя кадра, исправляет несоответствие стека, прежде чем что-то может пойти не так.)

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


Как сказал @johnfound, ключевым моментом в соглашениях о вызовах является то, что вызывающий и вызываемый соглашаются с правилами. Любой однозначный набор правил работает до тех пор, пока обе стороны согласны.

Хорошие (эффективные) соглашения о вызовах (например, x86-64 System V и, в меньшей степени, Windows x64 и 32-разрядный fastcall / vectorcall) передаст первые несколько аргументов в регистры, избегая сохранения / перезагрузки в стек или любых манипуляций со стеком для простых функций. Эффективные соглашения о вызовах также хорошо сочетают в себе сохранение вызовов и вызов замкнутые регистры. Простые соглашения о вызовах передают все в стеке, при этом вызывающий или вызываемый объект отвечает за извлечение аргументов. Даже более простые (например, Irvine32 для начинающих asm) сохраняют все регистры.

Подробную информацию см. В руководстве по соглашениям о вызовах Agner Fog.

0
Peter Cordes 3 Ноя 2020 в 11:24