Почему нельзя использовать локальную переменную в выражениях GNU C basi c inline asm? - PullRequest
2 голосов
/ 14 февраля 2020

Почему я не могу использовать локальные переменные из main для использования в basi c asm inline? Это разрешено только в расширенном asm, но почему так?

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

И пример basi c asm:

int a = 10; //global a
int b = 20; //global b
int result;
int main()
{
 asm ( "pusha\n\t"
 "movl a, %eax\n\t"
 "movl b, %ebx\n\t"
 "imull %ebx, %eax\n\t"
 "movl %eax, result\n\t"
 "popa");
printf("the answer is %d\n", result);
return 0;
}

пример расширенного:

int main (void) {

    int data1 = 10;  //local var - could be used in extended
    int data2 = 20;
    int result;

    asm ("imull %%edx, %%ecx\n\t"
          "movl %%ecx, %%eax" 
          : "=a"(result)
          : "d"(data1), "c"(data2));

    printf("The result is %d\n",result);

    return 0;
}

Скомпилировано с: gcc -m32 somefile.c

платформа: uname -a: Linux 5.0.0-32-generic #34-Ubuntu SMP Wed Oct 2 02:06:48 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

Ответы [ 4 ]

4 голосов
/ 14 февраля 2020

Существует мало различий между "Basi c asm" и "Extended asm"; «basi c asm» - это просто особый случай, когда в операторе __asm__ нет списков выходов, входов или сбоев. Компилятор не выполняет подстановку % в строке сборки для Basi c asm. Если вы хотите вводить или выводить данные, вы должны указать их, и тогда это то, что люди называют «расширенным ассемблером».

На практике, возможно, будет возможен доступ к внешнему (или даже к области действия файла c) предметы из "basi c asm". Это связано с тем, что эти объекты будут (соответственно могут иметь) имена символов на уровне сборки. Однако для выполнения такого доступа вам нужно быть осторожным с тем, является ли он независимым от позиции (если ваш код будет связан с библиотеками или исполняемыми файлами P IE) и соответствует ли он другим ограничениям ABI, которые могут быть наложены во время компоновки, и есть различные соображения относительно совместимости с оптимизацией во время компоновки и другими преобразованиями, которые может выполнять компилятор. Короче говоря, это плохая идея, потому что вы не можете сказать компилятору, что оператор basi c asm изменил память. Нет никакого способа сделать это безопасным.

A "memory" clobber (Extended asm) может обеспечить безопасный доступ к переменным хранения stati c по имени из шаблона asm.

Вариант использования для basi c asm - это вещи, которые изменяют только машинное состояние, например asm("cli") в ядре для отключения прерываний, без чтения или записи каких-либо C переменных. (Даже в этом случае вы часто использовали бы «память», чтобы убедиться, что компилятор завершил более ранние операции с памятью перед изменением состояния машины.)

Локальный (автоматическое c хранилище, а не статическое c). переменные принципиально никогда не имеют имен символов, потому что они не существуют ни в одном экземпляре; есть один объект на каждый экземпляр блока, в котором они объявлены, во время выполнения. Таким образом, единственный возможный способ получить к ним доступ - через ограничения ввода / вывода.

Пользователи, пришедшие из MSV C, могут найти это удивительным, поскольку документы по встроенной сборке MSV C по этой проблеме путем преобразования ссылок на локальные переменные в их версии встроенного asm в доступы, относящиеся к указателю стека, между прочим. Однако предлагаемая версия встроенного ассемблера не совместима с оптимизирующим компилятором, и в функциях, использующих этот тип встроенного ассемблера, не может быть оптимизации. G CC и большой мир компиляторов, который вырос вместе с C из unix, не делает ничего подобного.

2 голосов
/ 14 февраля 2020

Вы не можете безопасно использовать глобальные переменные в Basi c Asm заявления либо ; случается, что работает с отключенной оптимизацией, но это небезопасно, и вы злоупотребляете синтаксисом.

Существует очень мало причин для когда-либо использования Basi c Asm. Даже для контроля состояния машины, такого как asm("cli"), для отключения прерываний, вы часто хотели бы, чтобы "memory" клоббер упорядочивал его относительно. загружает / сохраняет в глобалы. На самом деле, страница G CC https://gcc.gnu.org/wiki/ConvertBasicAsmToExtended рекомендует никогда не использовать Basi c Asm , поскольку она отличается в разных компиляторах, и G CC может измениться на ее обработку как забивая все вместо ничего (из-за существующего глючного кода, который делает неправильные предположения). Это сделает оператор Basi c Asm, который использует push / pop, еще более неэффективным, если компилятор также генерирует хранилища и перезагружает его.

По сути, это единственный вариант использования для Basi c Asm пишет тело функции __attribute__((naked)), в которой ввод / вывод данных / взаимодействие с другим кодом следует соглашению о вызовах ABI, а не любому пользовательскому соглашению, которое описывают ограничения / сгустки для действительно встроенного блока кода.


Конструкция встроенного ассемблера GNU C заключается в том, что это текст, который вы вводите в обычный вывод ассемблера компилятора (который затем подается на ассемблер, as). Расширенная asm делает строку шаблоном, в который она может подставить операнды. И ограничения описывают, как asm вписывается в поток данных программы logi c, а также регистрирует ее клобберы.

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

Ваш asm-блок Basi c поврежден, поскольку вы изменяете переменные C, не сообщая об этом компилятору . Это может сломаться с включенной оптимизацией (возможно, только с более сложным окружающим кодом, но работа не совсем то же самое, что на самом деле безопасно. Вот почему просто тестирование встроенного асм-кода GNU C даже близко не достаточно для того, чтобы он будущее против новых компиляторов и изменений в окружающем коде). Не существует неявного "memory" клоббера. (Basi c asm такой же, как Extended asm, за исключением того, что не выполняется подстановка % в строковом литерале. Поэтому вам не нужно %%, чтобы получить литерал % в выводе asm. Это неявно изменчиво, как Расширенный asm без выходов.)

Также обратите внимание, что если вы нацелены на i386 MacOS, вам потребуется _result в asm. result работает только потому, что имя символа asm точно совпадает с именем переменной C. Использование расширенных ограничений asm сделает его переносимым между GNU / Linux (без подчеркивания в начале) по сравнению с другими платформами, которые используют ведущие _.

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

Вместо этого вы можете сделать:

    asm ("imull %%edx, %%ecx\n\t"
          : "=c"(result)
          : "d"(data1), "c"(data2));

Или, лучше, используйте операнды "+r"(data2) и "r"(data1), чтобы дать компилятору свободный выбор при размещении регистров, вместо того, чтобы потенциально заставить компилятор выдавать ненужные mov инструкции. (См. Ответ @ Eri c с использованием именованных операндов и "=r" и соответствующего ограничения "0"; это эквивалентно "+r", но позволяет использовать разные имена C для ввода и вывода. )

Посмотрите на вывод asm компилятора, чтобы увидеть, как происходит генерация кода вокруг вашего оператора asm, если вы хотите убедиться, что он эффективен.


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

Даже для глобальных переменных вы хотите , чтобы компилятор мог максимально оптимизировать работу со встроенным ассемблером, поэтому вы хотите дать компилятору возможность использования копии глобальной переменной, которая уже есть в регистре, вместо получения значения в памяти в syn c с хранилищем, просто чтобы ваш ассемблер мог перезагрузить его.

Попытка компилятора проанализировать ваш asm и выяснить, какие C локальные имена переменных являются входными и выходными. (Но это было бы осложнением.)

Но если вы хотите, чтобы это было эффективно, вам нужно выяснить, когда x в asm может быть регистром, подобным EAX, вместо того, чтобы делать что-то braindead как всегда хранить x в памяти перед оператором asm, а затем заменять x на 8(%rsp) или что-то еще. Если вы хотите дать оператору asm контроль над тем, где могут находиться входные данные, вам понадобятся ограничения в некоторой форме. Выполнение этого для каждого операнда имеет смысл, и означает, что обработка inline-asm не Необходимо знать, что bts может принимать непосредственный или регистрируемый источник, но не память, для и других машинно-специфических деталей c, подобных этому. (Помните, G CC - переносимый компилятор; запоминание огромного количества информации о машине в парсер inline-asm было бы плохо.)

(MSV C заставляет все C переменные в _asm{} блокирует память. Невозможно эффективно использовать обернуть одну инструкцию, потому что ввод должен отскочить через память, даже если вы обернули ее в функцию, чтобы вы могли использовать официально поддерживаемый хак оставить значение в EAX и опустить конец непустой функции. В чем разница между 'asm', '__asm' и '__asm ​​__'? И на практике MSV C s реализация была, по-видимому, довольно хрупкой и трудной в обслуживании, настолько, что они удалили ее для x86-64, и она была задокументирована как не поддерживаемая в функции с аргументами регистров даже в 32-битном режиме! Это не ошибка синтаксического дизайна, хотя, только фактическая реализация.)

Clang поддерживает -fasm-blocks для синтаксиса в стиле _asm { ... } MSV C, где он анализирует asm и вы используете C var name. Вероятно, он вводит входы и выходы в память, но я не проверял.


Также обратите внимание, что встроенный синтаксис asm с ограничениями G CC разработан вокруг той же системы ограничений, что G CC - внутренние файлы описания машин, используемые для описания ISA для компилятора . (Файлы .md в источнике G CC, которые сообщают компилятору об инструкции по добавлению чисел, которые принимают входные данные в регистры "r" и содержат текстовую строку для мнемони c. Обратите внимание на "r" и "m" в некоторых примерах в https://gcc.gnu.org/onlinedocs/gccint/RTL-Template.html).

Проектная модель asm в GNU C заключается в том, что это черный ящик для оптимизатора; Вы должны полностью описать эффекты кода (для оптимизатора), используя ограничения. Если вы закроете регистр, вы должны сообщить компилятору. Если у вас есть входной операнд, который вы хотите уничтожить, вам нужно использовать фиктивный выходной операнд с соответствующим ограничением или операнд "+r" для обновления значения соответствующей переменной C.

Если вы читаете или записать память, указанную регистром ввода, вы должны сообщить компилятору. Как я могу указать, что можно использовать память, * указанную * встроенным аргументом ASM?

Если вы используете стек, вы должны сообщить компилятору (но вы не можете, поэтому вместо этого вы должны избегать наступления на красную зону: / Использование базового регистра указателя в C ++ inline asm ) См. Также тег встраиваемой сборки вики

G CC. Конструкция позволяет компилятору вводить данные в регистр и использовать то же самое. зарегистрироваться для другого выхода. (Используйте ограничение Early-Clobber, если оно не в порядке; синтаксис G CC разработан для эффективного переноса одной инструкции, которая читает все ее входные данные перед записью любого из ее выходных данных .)

Если бы G CC мог вывести все эти вещи только из C имен var, появляющихся в источнике asm, я не думаю, что уровень контроля был бы возможен . (По крайней мере, неправдоподобно.) И повсюду, вероятно, будут неожиданные эффекты, не говоря уже о пропущенных оптимизациях. Вы всегда используете встроенный asm только тогда, когда хотите получить максимальный контроль над вещами, поэтому последнее, что вам нужно, это компилятор, использующий множество сложных непрозрачных логик c, чтобы выяснить, что делать.

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


GNU C встроенный ассемблер не предназначен для низкоэффективной работы с минимальными усилиями. Если вы хотите легко, просто напишите на чистом C или используйте встроенные функции и позвольте компилятору выполнить свою работу. (И файл сообщает об ошибке при пропущенной оптимизации, если он делает неоптимальный код.)

2 голосов
/ 14 февраля 2020

Вы можете использовать локальные переменные в расширенной сборке, но вам необходимо сообщить о них расширенной сборке. Рассмотрим:

#include <stdio.h>


int main (void)
{
    int data1 = 10;
    int data2 = 20;
    int result;

    __asm__(
        "   movl    %[mydata1], %[myresult]\n"
        "   imull   %[mydata2], %[myresult]\n"
        : [myresult] "=&r" (result)
        : [mydata1] "r" (data1), [mydata2] "r" (data2));

    printf("The result is %d\n",result);

    return 0;
}

В этом [myresult] "=&r" (result) говорится, чтобы выбрать регистр (r), который будет использоваться в качестве выходного (=) значения для lvalue result, и этот регистр будет в сборке указываются как %[myresult] и должны отличаться от входных регистров (&). (Вы можете использовать один и тот же текст в обоих местах, result вместо myresult; я просто сделал его другим для иллюстрации.)

Аналогичным образом [mydata1] "r" (data1) говорит, что нужно поместить значение выражения data1 в регистр, и он будет называться в сборке как %[mydata1].

Я изменил код в сборке, чтобы он только изменял регистр вывода. Ваш исходный код изменяет %ecx, но не сообщает компилятору, что он это делает. Вы могли бы сказать компилятору, что, поставив "ecx" после третьего :, куда идет список «забитых» регистров. Однако, так как мой код позволяет компилятору назначить регистр, у меня не было бы специального регистра c для перечисления в регистре с засечками. Может быть способ сообщить компилятору, что один из входных регистров будет изменен, но не нужен для вывода, но я не знаю. (Документация: здесь .] Для этой задачи лучшим решением будет указать компилятору использовать тот же регистр для одного из входных данных в качестве выходных:

    __asm__(
        "   imull   %[mydata1], %[myresult]\n"
        : [myresult] "=r" (result)
        : [mydata1] "r" (data1), [mydata2] "0" (data2));

В этом 0 с data2 говорит, чтобы сделать его таким же, как операнд 0. Операнды нумеруются в том порядке, в котором они появляются, начиная с 0 для первого выходного операнда и продолжая во входные операнды. Таким образом, когда код сборки начинается , %[myresult] будет ссылаться на некоторый регистр, в который было помещено значение data2, и компилятор будет ожидать, что новое значение result будет в этом регистре, когда сборка будет завершена.

При этом вы должны сопоставить ограничение с тем, как вещь будет использоваться в сборке. Для ограничения r компилятор предоставляет некоторый текст, который можно использовать на языке ассемблера, где принимается общий регистр процессора. Другие включают m для ссылки на память и i для непосредственного операнда.

1 голос
/ 14 февраля 2020

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

Но у него нет определенного интерфейса для C, и давайте будем честны, если вы не связываете ваш ассемблер с вашим C кодом, тогда почему он там?

Примеры полезного очень простого asm: генерировать прерывание отладки; установить режим регистра с плавающей запятой (исключения / точность);

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

Этот механизм является «расширением» для стандарт ASM.

Конечно, асм на самом деле не является стандартом. Измените процессор и ваш код asm tra sh. Когда мы вообще говорим о соблюдении стандартов c / c ++ и не используем расширения, мы не говорим об asm, потому что вы уже нарушаете все существующие правила переносимости.

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

Но если вы просто пишете свои собственные функции asm и вызываете их из asm, вы не можете быть ограничены таким образом многое по соглашениям c / c ++: создайте свои собственные правила аргументов регистра; вернуть значения в любой регистр, который вы хотите; делать кадры стека или нет; сохранить кадр стека через исключения - кого это волнует?

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

...