Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding Inline assembly in a pre-processor macro vs Inline assembly in a function

GGC's inline assembly can be difficult to implement properly and easy to get wrong1. From a higher level perspective inline assembly has some rules that have to be considered outside of what instructions an inline assembly statement may emit.

The C/C++ standards consider asm to be an option and implementation defined. Implementation defined behaviour is documented in GCC to include this:

Do not expect a sequence of asm statements to remain perfectly consecutive after compilation, even when you are using the volatile qualifier. If certain instructions need to remain consecutive in the output, put them in a single multi-instruction asm statement.

Basic inline assembly, or extended inline assembly without any output constraints are implicitly volatile. The documentation says that being volatile doesn't guarantee that successive statements will be ordered as the appear in the source code. This code would not have a guaranteed order:

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");

If the intention is to use CLI and STI to turn off (and turn back on) external interrupts and output some letters in the order MDP to the QEMU debug console (port 0xe9) then this isn't guaranteed. You can place all of them in a single inline assembly statement or you could use extended inline assembly templates to pass a dummy dependency to each statement guaranteeing ordering.

To make things more manageable OS developers in particular are known to create convenient wrappers around such code. Some developers do it as C pre-processor macros. In theory this looks useful:

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

#define cli() asm ("cli")

#define sti() asm ("sti")

You can then use them like this:

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

Of course the C pre-processor is done first before the C compiler begins to process the code itself. The pre-processor will generate these statements all in a row which is also not guaranteed to be emitted in a particular order by the code generator:

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");

My Questions

Some developers have taken it upon themselves to use macros that place the inline assembly statements inside a compound statement like this:

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

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

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

Using these macros as we did before would have the C pre-processor generating this code:

({ 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"); });

Question 1: Does placing asm statements inside a compound statement guarantee ordering? My view has been that I don't believe so, but I'm actually unsure. It is one of the reasons I avoid using pre-processor macros to generate inline assembly that I may use in a sequence like this.


For years I have used static inline functions in headers for inline assembly statements. Functions provide type checking, but I also believed that inline assembly in functions does guarantee that the side effects (including inline assembly) are emitted by the next sequence point (the ; on the end of a function call).

If I were to call actual functions my expectation is that each of these functions would have the inline assembly statements generated in order relative to one another:

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

Question 2 : Does placing the inline assembly statements in actual functions (whether external linkage or inlined) guarantee the order? My feeling is that if this weren't the case that code like:

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

Could be output as hello world or world hello. C's as-if rule suggests that optimizations can't alter observable behaviour. I believed the compiler wouldn't be able to assume that inline assembly actually had altered observable behaviour or not, so the compiler wouldn't be permitted to emit the inline assembly of the functions in another order.

like image 310
Michael Petch Avatar asked May 15 '19 21:05

Michael Petch


1 Answers

Do not expect a sequence of asm statements to remain perfectly consecutive after compilation, even when you are using the volatile qualifier. If certain instructions need to remain consecutive in the output, put them in a single multi-instruction asm statement.

You're actually misreading this (or overreading it). It is NOT saying that volatile asm statements can be reordered; they can't be reordered or removed -- that's the whole point of volatile. What it is saying is that other (non-volatile) things can be reordered with respect to the asm statements, and, in particular, might be moved in between any two of these asm statements. So they might not be consecutive after the optimizer gets through with them, but they will still be in order.

Note that this only applies to volatile asm blocks (which includes all block with no outputs -- they're implicitly volatile). Any other non-volatile asm blocks or statements might be moved between the volatile asm blocks, if otherwise allowed.

like image 54
Chris Dodd Avatar answered Oct 18 '22 00:10

Chris Dodd