Сколько аргументов передается при вызове функции?

Я хочу проанализировать ассемблерный код, вызывающий функции, и для каждого «вызова» выяснить, сколько аргументов передается функции. Я предполагаю, что мне недоступны целевые функции, а только вызывающий код. Я ограничиваюсь кодом, который был скомпилирован только с помощью GCC, и соглашением о вызовах ABI в System V. Я попытался выполнить сканирование из каждой инструкции «вызова», но мне не удалось найти достаточно хорошего соглашения (например, где остановить сканирование? Что происходит при двух последующих вызовах с теми же аргументами?). Помощь очень ценится.


person Jaaz    schedule 18.12.2016    source источник
comment
У GCC есть две разные стратегии для вызова такой функции. Первый заключается в том, что он помещает аргументы в стек, а затем очищает их через некоторое время после вызова функции, а второй заключается в том, что он резервирует пространство для исходящих аргументов всех вызовов функций в начале функции и очищает его один раз в конце. . В любом случае аргументы функции в стеке изменчивы во время вызова, но только те аргументы, которые фактически передаются этой фракции. Это означает, что аргумент функции может быть помещен в стек задолго до вызова и между другими вызовами.   -  person Ross Ridge    schedule 19.12.2016
comment
Вы не можете достоверно сказать об этом в оптимизированном коде. И даже для хорошей работы большую часть времени, вероятно, требуется ИИ человеческого уровня. например оставила ли функция значение в RSI, потому что это второй аргумент, или она просто использовала RSI в качестве временного регистра при вычислении значения для RDI (первый аргумент)? Как говорит Росс, код, сгенерированный gcc для соглашений о вызове аргументов стека, имеет более очевидные шаблоны, но по-прежнему нелегко обнаружить.   -  person Peter Cordes    schedule 19.12.2016
comment
@PeterCordes Хм ... Я предполагал, что соглашение о вызовах основано на стеке, но да, использование регистров сделало бы это совершенно невозможным.   -  person Ross Ridge    schedule 19.12.2016
comment
что происходит при двух последующих вызовах с одними и теми же аргументами? Компиляторы всегда переписывают аргументы перед выполнением другого вызова, потому что они предполагают, что функции сбивают свои аргументы (даже в стеке). ABI утверждает, что аргументы принадлежат функциям. Сгенерированный компилятором код, который я видел, на самом деле никогда не изменяет память стека, содержащую его аргументы, даже если это позволило бы включить хвостовой вызов :(   -  person Peter Cordes    schedule 19.12.2016
comment
Это вызывает конфликт поддержки или не поддержки оптимизаций компиляции GCC. Если не поддерживать оптимизацию, то результирующий код, вероятно, будет более структурированным, однако, если он поддерживает оптимизацию, я, вероятно, не буду предполагать ситуаций, когда другой регистр используется в качестве блокнота для требуемого регистра, поскольку он часто требует дополнительных инструкций.   -  person Jaaz    schedule 19.12.2016
comment
Тем не менее, если аргументы передаются стеком, то, вероятно, это будет более простой случай (и я прихожу к выводу, что все 6 регистров также используются). Настоящее препятствие, кажется, касается только регистров.   -  person Jaaz    schedule 19.12.2016


Ответы (1)


Репост моих комментариев в качестве ответа.

Вы не можете достоверно сказать об этом в оптимизированном коде. И даже для хорошей работы большую часть времени, вероятно, требуется ИИ человеческого уровня. например оставила ли функция значение в RSI, потому что это второй аргумент, или она просто использовала RSI в качестве временного регистра при вычислении значения для RDI (первый аргумент)? Как говорит Росс, код, сгенерированный gcc для соглашений о вызовах stack-args, имеет более очевидные шаблоны, но по-прежнему нелегко обнаружить.

Также потенциально сложно отличить хранилища, которые передают локальные переменные в стек, и хранилища, которые хранят аргументы в стеке (поскольку gcc может и иногда использует mov хранилища для аргументов стека: см. -maccumulate-outgoing-args). Один из способов определить разницу состоит в том, что локальные переменные будут перезагружены позже, но всегда предполагается, что аргументы затираются.

что происходит при двух последующих вызовах с одними и теми же аргументами?

Компиляторы всегда перезаписывают аргументы перед тем, как сделать следующий вызов, потому что они предполагают, что функции сбивают их аргументы (даже в стеке). ABI говорит, что функции «владеют» своими аргументами. Компиляторы действительно создают код, который делает это (см. Комментарии), но код, сгенерированный компилятором, не всегда готов перенаправить память стека, содержащую свои аргументы, для хранения совершенно разных аргументов, чтобы включить оптимизацию хвостового вызова. :( Это ненормально, потому что я точно не помню, что я видел в отношении упущенных возможностей оптимизации хвостового вызова.


Тем не менее, если аргументы передаются стеком, то, вероятно, это будет более простой случай (и я прихожу к выводу, что все 6 регистров также используются).

Даже это ненадежно. System V x86-64 ABI не просто.

int foo(int, big_struct, int) передаст два целочисленных аргумента в регистрах, но передаст большую структуру по значению в стеке. Аргументы FP также являются серьезной проблемой. Вы не можете сделать вывод, что видение материала в стеке означает, что используются все 6 слотов для передачи целочисленных аргументов.

Windows x64 ABI значительно отличается: например, если второй аргумент (после добавления скрытого указателя возвращаемого значения, если необходимо) является целым числом / указателем, он всегда идет в RDX, независимо от того, был ли первый аргумент в RCX, XMM0, или в стеке. Это также требует, чтобы вызывающий абонент покинул «теневое пространство».


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

Что касается оптимизированного кода, сгенерированного разными компиляторами, я думаю, что было бы труднее реализовать что-то, даже близкое к полезному, чем вы когда-либо могли бы сэкономить, имея это.

person Peter Cordes    schedule 28.12.2016
comment
На самом деле не так уж и сложно заставить GCC генерировать код, который изменяет стековую память, используемую для передачи аргументов. Просто void foo(int arg) { arg = 0;} без оптимизации это сделает. При оптимизации требуется, чтобы компилятор возвращал выделенный для него регистр в стек. Например: godbolt.org/g/ogRl6n. - person Ross Ridge; 28.12.2016
comment
@RossRidge: спасибо. Я думал, что gcc имеет тенденцию выделять локальный объект для хранения значений, даже если входящий аргумент был мертв. Возможно, я никогда не рассматривал возможность непосредственного изменения аргумента C, поскольку обычно я пишу код не так. Хм, даже использование новой переменной C приводит к повторному использованию слота входящего стека аргументов. godbolt.org/g/8rRoxx. Круто, думаю, я ошибался в этом :) gcc 4.7 и старше используют другую стратегию и, похоже, оптимизируются для процессоров, где push работает медленно. Я не вижу здесь повторного использования слотов arg. godbolt.org/g/nmRGnV - person Peter Cordes; 28.12.2016
comment
Кажется, что для x86 и x64 это отличается, где в x64 GCC 6.3 не будет повторно использовать аргумент стека. godbolt.org/g/Xo15f8 - person Jaaz; 29.12.2016
comment
@Jaaz: Конечно, в 64-битном режиме в этом нет необходимости, потому что достаточно регистров с сохранением вызовов. gcc имеет тенденцию сохранять / восстанавливать регистр для функции и использовать его для сохранения вещей при вызовах функций, вместо того, чтобы проливать / перезагружать свои собственные значения. Это выигрыш для задержки, если вызываемая функция вообще не касается регистра. - person Peter Cordes; 29.12.2016