Встроенная сборка GCC считывает значение из массива - PullRequest
0 голосов
/ 24 октября 2019

Во время изучения встроенной сборки gcc я немного играл с доступом к памяти. Я пытаюсь прочитать значение из массива, используя значение из другого массива в качестве индекса. Оба массива инициализируются чем-то.

Инициализация:

uint8_t* index = (uint8_t*)malloc(256);
memset(index, 33, 256);

uint8_t* data = (uint8_t*)malloc(256);
memset(data, 44, 256);

Доступ к массиву:

unsigned char read(void *index,void *data) {
        unsigned char value;

        asm __volatile__ (
        "  movzb (%1), %%edx\n"
        "  movzb (%2, %%edx), %%eax\n"
        : "=r" (value)
        : "c" (index), "c" (data)
        : "%eax", "%edx");

        return value;
    }

Вот как я использую функцию:

unsigned char value = read(index, data);

Теперь я ожидаюэто вернуть 44. Но это на самом деле возвращает мне какое-то случайное значение. Я читаю из неинициализированной памяти? Также я не уверен, как сказать компилятору, что он должен присваивать значение от eax переменной value.

1 Ответ

2 голосов
/ 24 октября 2019

Вы сказали компилятору, что собираетесь поместить вывод в %0, и он может выбрать любой регистр для этого "=r". Но вместо этого вы никогда не пишете %0 в своем шаблоне.

И вы используете два временных значения без видимой причины, когда вы могли бы использовать %0 в качестве временного.

Как обычно, вы можетеотладьте свой встроенный asm, добавив комментарии типа # 0 = %0 и просмотрев выходные данные asm компилятора. (Не разбирать, просто gcc -S, чтобы увидеть, что он заполняет. Например, # 0 = %ecx. (Вы не использовали ранний клоббер "=&r", поэтому он может выбрать тот же регистр, что и входные данные).


Кроме того, здесь есть еще 2 ошибки:

  1. не компилируется. Запрос 2 разных операндов в ECX с ограничениями "c" не может работать, если компилятор не может доказать при компиляции-время, когда они имеют одинаковое значение, так что %1 и %2 могут быть одинаковыми регистрами. https://godbolt.org/z/LgR4xS

  2. Вы разыменовываете указатели ввода, не сообщая компилятору, что вы читаете указатель напамять. * Использовать "memory" операнды-заглушки или фиктивные запоминания. Как я могу указать, что можно использовать память *, на которую указывает * встроенный аргумент ASM?

Или лучше https://gcc.gnu.org/wiki/DontUseInlineAsm, потому что это бесполезно для этого; просто позвольте GCC самому запускать загрузки movzb. unsigned char* безопасен от строго псевдонимов UB, поэтому вы можете безопасно привести любой указатель к unsigned char* и разыщите его, даже не используя memcpy или другие хаки для борьбыt против языковых правил для более широкого доступа с выравниванием или без ввода текста.

Но если вы настаиваете на встроенном asm, читайте руководства и учебные пособия, ссылки на https://stackoverflow.com/tags/inline-assembly/info. Вы не можете просто бросить коду стены, пока он не придерживается встроенного asm: вы должны понять, почему ваш код безопасен, чтобы надеяться на его безопасность. Есть много способов, как встроенный asm может сработать, но на самом деле не работает или ждет, чтобы сломаться с другим окружающим кодом.


Это безопасная и не совсем ужасная версия (кроме неизбежной оптимизации). поражение частей встроенного асма). Вам все еще нужна загрузка movzbl для обеих загрузок, даже если возвращаемое значение составляет всего 8 бит. movzbl - это естественный эффективный способ загрузки байта, заменяющий вместо слияния старое содержимое полного регистра.

unsigned char read(void *index, void *data)
{
    uintptr_t value;
    asm (
        " movzb (%[idx]), %k[out] \n\t"
        " movzb (%[arr], %[out]), %k[out]\n"
        : [out] "=&r" (value)              // early-clobber output
        : [idx] "r" (index), [arr] "r" (data)
        : "memory"  // we deref some inputs as pointers
    );

    return value;
}

Обратите внимание на ранний клоббер на выходе: это останавливает gcc от выборатот же регистр для вывода, что и один из входов. Было бы безопасно уничтожить регистр [idx] при первой загрузке, но я не знаю, как сообщить GCC об этом в одном операторе asm. Вы можете разделить ваш оператор asm на два отдельных, каждый со своими операндами ввода и вывода, соединяя выход первого с входом второго через локальную переменную. Тогда никому не понадобится ранний клоббер, потому что они просто обертывают отдельные инструкции, такие как встроенный синтаксис asm GNU C, предназначенный для хорошей работы.

Godbolt с тестовым вызывающим абонентом, чтобы увидетьон вызывается / оптимизирует при двойном вызове, с i386 clang и x86-64 gcc. например, запрос index в регистре заставляет LEA вместо того, чтобы позволить компилятору увидеть разыскивание и позволить ему выбрать режим адресации для *index. Также дополнительные movzbl %al, %eax, сделанные компилятором при добавлении к unsigned sum, потому что мы использовали узкий тип возврата.

Я использовал uintptr_t value, так что это может компилироваться для 32-битных и 64-битныхx86. Нет ничего плохого в том, чтобы сделать вывод из оператора asm более широким, чем возвращаемое значение функции, и это избавляет нас от необходимости использовать модификаторы размера, такие как movzbl (%1), %k0, чтобы GCC печатал 32-битное имя регистра(например, EAX), если он выбрал AL для 8-битной выходной переменной, например.

Я действительно решил использовать %k[out] в интересах 64-битного режима: мы хотим movzbl (%rdi), %eax, а неmovzb (%rdi), %rax (потеря префикса REX).

Вы также можете объявить функцию, возвращающую unsigned int или uintptr_t, поэтому компилятор знает, что ему не нужно возвращать нулевое расширение . OTOH иногда может помочь компилятору узнать, что диапазон значений составляет всего 0..255. Вы можете сказать, что вы производите значение с нулевым расширением, используя if(retval>255) __builtin_unreachable() или что-то еще. Или вы можете просто не использовать встроенный asm .

Вам не нужно asm volatile. (Предполагая, что вы хотите позволить ему оптимизировать, если результат не используется, или быть выведенным из циклов для постоянных входов). Вам нужен только "memory" клоббер, поэтому, если он все-таки привыкнет, компилятор знает, что он читает память.

("memory" клоббер считается как вся память, являющаяся входом, и вся память, являющаяся выходом. Поэтому он не может использовать CSE, например, вывод из цикла, поскольку, поскольку компилятор знает, что один вызов может прочитать что-то, что написал предыдущий, на практике "memory" клоббер примерно так же плох, как asm volatile. Даже двапоследовательные вызовы этой функции без прикосновения к массиву ввода вынуждают компилятор дважды выдавать инструкции.)

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


Но, как я сказал, нет никаких причин использовать встроенный ассемблер:

Это подойдетТо же самое в 100% портативном и безопасном ISO C:

// safe from strict-aliasing violations 
// because  unsigned char* can alias anything
inline
unsigned char read(void *index, void *data) {
    unsigned idx = *(unsigned char*)index;
    unsigned char * dp = data;
    return dp[idx];
}

Вы можете привести один или оба указателя к volatile unsigned char*, если вы настаиваете на том, чтобы доступ происходил каждый раз и не был оптимизирован.

Или, может быть, даже до atomic<unsigned char> * в зависимости от того, что вы делаете. (Это хак, предпочтение C ++ 20 atomic_ref для атомарной загрузки / хранения объектов, которые обычно не являются атомарными.)

...