Что мешает вызываемому объекту очистить стек?

В рамках расширения моего учебного плана я медленно спускаюсь по лестнице абстракции программирования. Сейчас я хорошо владею C и готовлюсь к написанию некоторого ассемблера (в частности, ARM-ассемблера).

Я столкнулся с темой соглашений о вызовах, и хотя я в целом понимаю их смысл, вопрос, который, кажется, никогда не задавался и на который никогда не давался ответ:

Почему вызываемый объект не может обрабатывать переменные аргументы в стеке?

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

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


person Thomas K.    schedule 06.11.2013    source источник
comment
хорошо, я думал, что в некоторых соглашениях вызываемый объект очищает стек, например, в соглашении по паскалю вики/X86_calling_conventions#паскаль   -  person Hayri Uğur Koltuk    schedule 07.11.2013
comment
Мне любопытно, где ты повсюду; Вы, кажется, сделали очень плохой или неудачный выбор источников, чтобы включить их повсюду. :)   -  person Jason C    schedule 07.11.2013
comment
Кстати, я поместил эту ссылку в свой ответ, но она достаточно полезна, чтобы разместить ее и здесь: agner.org /optimize/calling_conventions.pdf содержит больше, чем вы когда-либо хотели знать. Хотя они обсуждают x86 и amd64, концепции абсолютно одинаковы для любой архитектуры на основе стека (я также часто работаю с устройствами OMAP и прямыми ARM, это одно и то же).   -  person Jason C    schedule 07.11.2013


Ответы (5)


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

Тот факт, что в настоящее время это не делается таким образом, не означает, что это должно делаться именно так. Хорошо задаваться вопросом, почему вещи такие, какие они есть, и в этом случае я думаю, что причина в том, что «немного проще реализовать varargs таким образом, и поскольку все другие крутые ребята делали это, мы тоже должны это делать». В конце концов, если все скомпилированные двоичные файлы C обрабатывают параметры таким образом, было бы очень сложно пытаться взаимодействовать с этими двоичными файлами, если бы у вас было другое соглашение о вызовах. (В качестве примера посмотрите на Windows API, где некоторые функции должны быть аннотированы, чтобы использовать нестандартное соглашение о вызовах для работы с ОС.)

Надеюсь это поможет!

person templatetypedef    schedule 06.11.2013
comment
Стоит отметить, что почти каждая функция Windows API использует __stdcall (очистка вызываемого объекта). - person Jason C; 07.11.2013

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

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

На некоторых архитектурах (обычно очень маленьких, таких как старые 8080 или 6800) нет инструкции ret n для автоматизации очистки стека и, как правило, они не могут выполнять арифметические действия с указателем стека.

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

    push arg1
    push arg2
    push arg3
    call proc

proc:
    pop  r1   ; the return address
    pop  r2
    pop  r2
    pop  r2
    push r1
    ret

При использовании соглашения cdecl 2 инструкции и одно использование регистра не используются:

    push arg1
    push arg2
    push arg3
    call proc
    pop  r2
    pop  r2
    pop  r2

proc:
    ret

И поскольку для одного языка лучше использовать одно соглашение о вызовах на всех платформах, CCALL как более простой и универсальный выглядит лучше. (Язык C создан во времена, когда 6800 был высокотехнологичным).

Но обратите внимание, что на этих платформах ассемблерные программы и родные языки (например, разные типы BASIC) обычно используют передачу аргументов регистра, что, конечно, намного быстрее на таких небольших системах.

В любом случае, это просто традиция. Вы можете настроить компилятор на использование любого соглашения, которое вы хотите. Например, WinAPI написан на C++, но по-прежнему использует соглашение stdcall, потому что оно лучше работает на платформе x86.

person johnfound    schedule 06.11.2013
comment
Какое соглашение о вызовах вы подразумеваете под CCALL? cdecl или что-то еще, о чем я не знаю? - person Thomas K.; 07.11.2013

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

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

Обзор общих соглашений о вызовах на платформе x86 см. в http://en.wikipedia.org/wiki/X86_calling_conventions< /а>.

Для подробного ознакомления с общими соглашениями о вызовах для многих компиляторов (концепции идентичны для любой функциональной архитектуры на основе стека, будь то x86, powerpc, arm, avr и т. д.), см. http://www.agner..org/optimize/calling_conventions.pdf.

Для общего соглашения о вызовах «stdcall», когда вызываемый объект очищает стек, приведен специальный документ Microsoft: http://msdn.microsoft.com/en-us/library/zxk0tw93.aspx Но это соглашение о вызовах поддерживается многими компиляторами. Обратите внимание, что компилятор MS вместо этого создает функции с переменными аргументами cdecl.

Есть несколько широко используемых соглашений о вызовах (например, "cdecl", "stdcall", "fastcall"), которые обычно поддерживаются многими компиляторами, но если вы программируете на ассемблере или вам хочется писать патчи для компилятора, вы можете приходить с любым странным и дурацким соглашением, которое вы можете себе представить (ну, в пределах разумного).

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

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

person Jason C    schedule 06.11.2013
comment
«Везде» означает именно тот тип документов, что и документ от Microsoft, а также учебные материалы, обучающие x86. - person Thomas K.; 08.11.2013
comment
@ТомасК. Просто из любопытства, не могли бы вы опубликовать ссылку на один из этих документов Microsoft? Это не критика, а просто наблюдение, но я полагаю, что вы могли неверно истолковать то, что они говорили. Помимо функций Vararg, функция неявно точно знает, какие параметры передаются ей по ее определению. Рассмотрим функцию C void __stdcall something (int x). По соглашению о вызовах сгенерированный компилятором код для этой функции включает извлечение 4-байтового (например) целого числа из стека. Вызовы функций генерируются для передачи 4-байтового значения, его вызова и предположения о правильной обработке. - person Jason C; 08.11.2013
comment
См., например, en.wikipedia.org/wiki/X86_calling_conventions#stdcall. контракт, указанный функциями stdcall (кстати, для справки, содержащий раздел в этой вики-статье относится конкретно к соглашениям об очистке вызываемого абонента). Имейте в виду, что соглашение о вызовах является контрактом. Существует набор из них, который обычно используется, и это те, которые вы найдете описанными (например, cdecl, stdcall, fastcall). На самом деле вы можете придумать что угодно, но вы рискуете столкнуться с проблемами совместимости с существующими инструментами, созданными для работы только с общепринятыми соглашениями. - person Jason C; 08.11.2013
comment
Также стоит отметить, что при взаимодействии с другими двоичными объектами вы должны соблюдать соглашение о вызовах, которое они ожидают. Например, при вызове функции Windows API из ассемблера вы несете ответственность за размещение аргументов в стеке в порядке справа налево, и вы разумно делаете предположения о состоянии стека, когда эта функция возвращается. Для функций cdecl (обычное значение по умолчанию для компиляторов C) вы также несете ответственность за соблюдение этих правил при вызове этих функций и за выполнение соответствующих предположений о состоянии стека при возврате. - person Jason C; 08.11.2013

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

это просто условность. Это хорошая условность, потому что создатель вещи очищает ее. это самодостаточно. Пользователь вещи (фрейм/параметры стека) просто использует их. Если он становится вызывающим, то он управляет этим аспектом стека...

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

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

person old_timer    schedule 06.11.2013
comment
The called function absolutely knows how many parameters are defined at least, if the caller and callee dont agree on the number of parameters you are eventually going to get into trouble. Для языков, в которых используется поздняя оценка параметров (например, проверка того, действительно ли параметр содержит значение, был ли он инициализирован и т. д.), не это не так? Языки, которые поддерживают списки аргументов переменной длины, определяемые во время выполнения, — это контрпримеры? Может быть, я просто предполагаю... - person Chris Cirefice; 07.11.2013
comment
генерация кода как для вызывающего, так и для вызываемого абонента знает правила, если он объявлен как динамический, они оба знают, что код должен работать. Если они оба не знают (время компиляции или время выполнения), то это ошибка компилятора или ошибка программиста из-за несоответствия объявления для вызывающего и вызываемого. - person old_timer; 07.11.2013
comment
Хорошая точка зрения! Я просто хотел убедиться - я изучаю оценку параметров и контроль данных в своем университете; просто показалось, что ваше утверждение на самом деле не сходится. Хотя имеет смысл, когда я думаю об этом больше :) - person Chris Cirefice; 07.11.2013

Я могу назвать несколько причин:

  1. Очистка аргументов включает изменение указателя адреса стека (esp). Когда вызывающая сторона несет ответственность за это, все, что вызывающая сторона должна сделать с esp, это вернуть ее в то состояние, в котором она ее нашла, и когда вызывающая сторона восстанавливает контроль, esp остается таким же, каким он его оставил. Если бы вызываемый объект очищал аргументы, ему пришлось бы вычислять новое значение для esp (а не просто извлекать старое значение из стека), а вызывающий должен был бы учитывать, как вызываемый изменил его. Это сделало бы вещи более сложными как для вызывающего, так и для вызываемого абонента.

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

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

person Idan Arye    schedule 06.11.2013