Существуют ли какие-либо рекомендации в x86-64 относительно того, когда функция должна соответствовать рекомендациям System V, а когда это не имеет значения? Это ответ на ответ здесь, в котором упоминается использование других соглашений о вызовах для упрощения внутренней / локальной функции.
# gcc 32-bit regparm calling convention
is_even: # input in RAX, bool return value in AL
not %eax # 2 bytes
and $1, %al # 2 bytes
ret
# custom calling convention:
is_even: # input in RDI
# returns in ZF. ZF=1 means even
test $1, %dil # 4 bytes. Would be 2 for AL, 3 for DL or CL (or BL)
ret
См. Контекст в этом ответе.
Например, следует ли его использовать:
- Требуется только при вызове внешней функцией C.
- Требуется только тогда, когда эта метка / функция равна
globl
.
Или как лучше всего определить, когда использовать регистры «как мне нравится», а когда использовать их в соответствии с соглашением System V?
1 ответ
Это зависит от того, что вы пишете в asm. Если вы пишете небольшую автономную программу asm, которая чисто написана на asm, например, 16-битный загрузчик, определенно продолжайте и составляйте собственные соглашения о вызовах для всего (если вы делаете какие-либо функции вообще, а не просто встраивание). например взгляните на функцию disp_ax_hex
в устаревший загрузчик BIOS @ ecm в качестве интересного примера и см. обсуждение в комментариях о разрешении disp_al
затирать больше регистров.
Я бы сказал, что обычно следует стандартному соглашению о вызовах в большинстве других кодов (часть более крупной программы, которая включает код, созданный компилятором); x86-64 System V довольно хорошо спроектирована. Рассмотрите возможность использования пользовательского соглашения только для «частных» вспомогательных функций, особенно тех, которые вызываются только из разных частей других функций. Обычно все вызывающие абоненты хранятся в одном файле, поэтому не global
.
Функции, которые могут возвращать два отдельных значения, определенно могут получить выгоду от настраиваемого соглашения о вызовах в интересах вызывающих asm.
например C memcmp
не возвращает позицию первой разности, только - / 0 / +. Это действительно глупо и бесполезно, лишающее нас возможности воспользоваться преимуществами существующего ассемблера, оптимизированного вручную, чтобы найти несоответствие позиции. В asm мы можем легко вернуть и то, и другое, например указатель на позицию в RDI и результат cmp
в FLAGS.
В этом случае вы могли бы написать функцию memcmp
, которая была бы на 100% совместима с соглашением о вызовах x86-64 System V (так что вам нужно будет расширить оба байта нулями и выполнить двойное слово sub
, вместо простого выполнения байта cmp
) с выводом RDI в качестве бонуса для вызывающих asm.
Часть моего ответа, которую вы связали, была своего рода случайной мыслью, которую я решил упомянуть. Это не то, что вы обычно делаете (хотя ни один из них не пишет asm вручную), и вы никогда не захотите помещать test
в функцию отдельно, кроме как в качестве решения упражнения по игре в код. Это была настоящая идея: большая часть «стоимости» этой функции связана только с тем, что вы сделали ее функцией, а в реальной жизни вы всегда встраиваете что-то настолько простое.
Обычно вы изначально не пишете крошечные функции. Вы просто реализуете логику в виде пары инструкций в середине более крупной функции, точно так же, как компилятор встроил бы небольшую вспомогательную функцию. Тогда следовать платформе ABI (в данном случае x86-64 System V) для всех ваших функций не дорого.
Оптимизация логики для возврата 0/1 int
(а не только 8-битного bool
), и соблюдение стандартного соглашения о вызовах может быть забавным упражнением, но часто бесполезен, если только не выяснится, что ваш реальный вариант использования хочет сделать что-то вроде even_count += is_even(x);
. Но в этом случае вы должны сделать odds += x&1;
и вычислить четный счет один раз в конце, когда вам это нужно, как even = total-odd
. Помимо устранения накладных расходов на вызов / возврат, встраивание также позволяет подумать об оптимизации логики крошечной функции как части фактического варианта использования.
Вот пример использования частных вспомогательных функций:
Иногда вы хотите повторить блок из нескольких инструкций в качестве частной «вспомогательной» функции для более крупной функции, например, mov eax, 1
/ call func
/ сделай что-нибудь еще / mov eax 123
/ call func
. Тогда вы можете думать о «функции» как о теле цикла или о чем-то внутри более крупной функции, а о вызывающем элементе - как о настраиваемой итерации.
Иногда имеет смысл повторить блок кода с помощью макроса, но если последовательность несколько длинная, это приведет к раздутию вашего кода. (Макросы раскрываются каждый раз, когда вы их используете; в отличие от 5-байтового call rel32
.)
Для ясности: is_even
настолько прост, что не имеет смысла помещать его в отдельную функцию. Вызов функции вместо простого запуска test $1, %reg
/ {{X2 }} или jnz
для некоторых регистров были бы совершенно безумными и запутанными, а также более крупными и медленными. Или and $1, %eax
, чтобы получить целое число 0/1 из нечетного регистра, которое можно использовать с add
для подсчета нечетных чисел. (итого-нечетное в конце считать четным). Большинство программистов не стали бы заключать это в макрос; понимание двоичного кода является стандартом для языка ассемблера, и достаточно простого комментария к инструкции test
или jcc для описания семантического значения (# if odd
).
Теоретически для чисто рукописной программы вы можете просто использовать любое наиболее удобное соглашение о вызовах в каждом конкретном случае для каждой функции, документируя ее с комментариями. Но обычно преимущество невелико по сравнению со стандартным соглашением о вызовах, и отслеживание того, какие функции смыкаются, которые регистрируются и где требуются свои аргументы, быстро становится кошмаром обслуживания для функций общего назначения, у которых есть несколько разных вызывающих, которые не сильно связаны с друг друга, чем вызываемая функция.
Конечно, по той же причине мы пишем приложения на языках высокого уровня и очень редко пишем какие-либо asm вручную. Тот факт, что вы предлагаете писать функции на asm вручную, означает, что стоит подумать, не слишком ли ограничивает «мышление как компилятор». В этом суть моего ответа codegolf : если из функции стоит выжать каждый последний байт или цикл, то вся программа (или, по крайней мере, ее вызывающая сторона), вероятно, написана аналогично.
Единственная веская причина для написания целых программ на asm в наши дни - это оптимизировать дерьмо за счет размера их машинного кода, например демонстрационная сцена. https://en.wikipedia.org/wiki/Demoscene. (Или, если «программа» действительно является загрузчиком, работающим без ОС или до нее.)
На этом этапе не позволяйте ABI и соглашениям о вызовах ограничивать вашу оптимизацию. И ваша программа, как правило, будет достаточно маленькой, чтобы можно было отслеживать различные функции и их соглашения о вызовах, особенно если они имеют некоторый логический смысл (или в основном соответствуют регистрам, в которых их вызывающие стороны в любом случае сохраняют правильные переменные).
Похожие вопросы
Связанные вопросы
Новые вопросы
assembly
Вопросы по языку ассемблера. Отметьте процессор и/или набор инструкций, которые вы используете, а также ассемблер, допустимый набор должен быть таким: ([assembly] [x86] [gnu-assembler] или [att]). Вместо этого используйте тег [.net-assembly] для сборок .NET, [cil] для языка ассемблера .NET и вместо байт-кода Java используйте тег java-bytecode-asm.