Встроенная сборка 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 как-будто предполагает, что оптимизации не могут изменить наблюдаемое поведение. Я полагал, что компилятор не сможет предположить, что встроенная сборка фактически изменила наблюдаемое поведение или нет, поэтому компилятору не будет разрешено создавать встроенную сборку функций в другом порядке.