Понимание встроенной сборки в макросе препроцессора против встроенной сборки в функции - PullRequest
5 голосов
/ 16 мая 2019

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

Стандарты C / C ++ рассматривают asm в качестве опции и определенной реализации. Поведение, определяемое реализацией, задокументировано в GCC , чтобы включить это:

Не ожидайте, что последовательность операторов asm останется идеально последовательной после компиляции, даже если вы используете префикс volatile . Если некоторые инструкции должны оставаться последовательными в выходных данных, поместите их в один оператор asm из нескольких инструкций.

Базовая встроенная сборка или расширенная встроенная сборка без каких-либо ограничений вывода неявно volatile. Документация говорит, что изменчивость не гарантирует, что последующие операторы будут упорядочены так, как они появляются в исходном коде. Этот код не будет иметь гарантированного заказа:

asm ("cli");
asm ("mov $'M', %%al; out %%al, $0xe9" ::: "eax");
asm ("mov $'D', %%al; out %%al, $0xe9" ::: "eax");
asm ("mov $'P', %%al; out %%al, $0xe9" ::: "eax");
asm ("sti");

Если предполагается использовать CLI и STI для отключения (и включения) внешних прерываний и вывода некоторых букв в порядке MDP на консоль отладки QEMU (порт 0xe9), тогда это не гарантируется. Вы можете поместить их все в один оператор встроенной сборки или использовать расширенные шаблоны встроенной сборки, чтобы передать фиктивную зависимость для каждого оператора, гарантирующего упорядочение.

Чтобы сделать вещи более управляемыми, разработчики ОС, в частности, создают удобные обертки вокруг такого кода. Некоторые разработчики делают это как макросы препроцессора C. Теоретически это выглядит полезным:

#define outb(port, value) \
        asm ("out %0, %1" \
             : \
             : "a"((uint8_t)value), "Nd"((uint16_t)port))

#define cli() asm ("cli")

#define sti() asm ("sti")

Затем вы можете использовать их так:

cli ();
outb (0xe9, 'M');
outb (0xe9, 'D');
outb (0xe9, 'P');
sti ();

Конечно, препроцессор C выполняется первым, прежде чем компилятор C начнет обрабатывать сам код. Препроцессор будет генерировать эти операторы все подряд, что также не гарантируется, что генератор кода генерирует в определенном порядке:

asm ("cli");
asm ("out %0, %1" : : "a"((uint8_t)'M'), "Nd"((uint16_t)0xe9));
asm ("out %0, %1" : : "a"((uint8_t)'D'), "Nd"((uint16_t)0xe9));
asm ("out %0, %1" : : "a"((uint8_t)'P'), "Nd"((uint16_t)0xe9));
asm ("sti");

Мои вопросы

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

#define outb(port, value) ({ \
        asm ("out %0, %1" \
             : \
             : "a"((uint8_t)value), "Nd"((uint16_t)port)); \
    })

#define cli() ({ \
        asm ("cli"); \
    })

#define sti() ({ \
        asm ("sti"); \
    })

При использовании этих макросов, как мы делали раньше, препроцессор C генерирует этот код:

({ asm ("cli"); });
({ asm ("out %0, %1" : : "a"((uint8_t)'M'), "Nd"((uint16_t)0xe9)); });
({ asm ("out %0, %1" : : "a"((uint8_t)'D'), "Nd"((uint16_t)0xe9)); });
({ asm ("out %0, %1" : : "a"((uint8_t)'P'), "Nd"((uint16_t)0xe9)); });
({ asm ("sti"); });

Вопрос 1 : Гарантирует ли размещение операторов asm внутри составного оператора порядок? Мое мнение таково, что я не верю в это, но я на самом деле не уверен. Это одна из причин, по которой я избегаю использования макросов препроцессора для генерации встроенной сборки, которую я могу использовать в такой последовательности:


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

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

cli ();
outb (0xe9, 'M');
outb (0xe9, 'D');
outb (0xe9, 'P');
sti ();

Вопрос 2 : Гарантирует ли порядок размещения операторов встроенной сборки в реальных функциях (внешней или встроенной)? У меня такое ощущение, что если бы это было не так, код наподобие

printf ("hello ");
printf ("world ");

Может быть выведено как hello world или world hello. Правило C как-будто предполагает, что оптимизации не могут изменить наблюдаемое поведение. Я полагал, что компилятор не сможет предположить, что встроенная сборка фактически изменила наблюдаемое поведение или нет, поэтому компилятору не будет разрешено создавать встроенную сборку функций в другом порядке.

1 Ответ

7 голосов
/ 16 мая 2019

Не ожидайте, что последовательность asm-операторов останется идеально последовательной после компиляции, даже если вы используете префикс volatile. Если определенные инструкции должны оставаться последовательными в выходных данных, поместите их в один мульти -инструкция asm.

Вы на самом деле неправильно это читаете (или перечитываете). Это НЕ говорит о том, что изменчивые asm-операторы могут быть переупорядочены; они не могут быть переупорядочены или удалены - в этом весь смысл изменчивости. Это говорит о том, что другие (энергонезависимые) вещи могут быть переупорядочены относительно операторов asm и, в частности, могут быть перемещены между любыми двумя из этих операторов asm. Таким образом, они могут не быть последовательными после того, как оптимизатор справится с ними, но они все равно будут в порядке.

Обратите внимание, что это относится только к volatile блокам asm (которые включают в себя все блоки без выходов - они неявно изменчивы). Любые другие энергонезависимые блоки или операторы asm могут быть перемещены между блоками as volatile, если это разрешено иным образом.

...