Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does "+&r" differ from "+r"?

GCC's inline assembler recognizes the declarators =r and =&r. These make sense to me: the =r lets the assembler reuse an input register for output.

However, GCC's inline assembler also recognizes the declarators +r and +&r. These make less sense to me. After all, isn't the distinction between +r and +&r a distinction without a difference? Does the +r alone not suffice to tell the compiler to reserve a register for the sole use of a single variable?

For example, what is wrong with the following GCC code?

#include <stdio.h>
int main()
{
    int a = 0;
    printf("Initially, a == %d.\n", a);
    /* The architecture is amd64/x86-64. */
    asm(
        "inc %[a]\n"
        : [a] "+r" (a)
        : : "cc"
    );
    printf("However, after incrementation, a == %d.\n", a);
    return 0;
}

Notice incidentally that my inline assembly lacks an input declaration because, in my (perhaps mistaken) mind, the +r covers input, clobbering, output, everything. What have I misunderstood, please?

BACKGROUND

I have programmed 8- and 16-bit microcontrollers in assembly a bit, but have little or no experience at coding assembly in a hosted environment.

like image 532
thb Avatar asked Aug 26 '17 12:08

thb


1 Answers

GCC assumes by default that inline assembly statements consist of a simple single assembly instruction that consumes all of its input operands before writing to any of its output operands. When writing inline assembly statements that use multiple assembly instructions this assumption is often broken, so the early clobber constraint modifier & needs to be used to indicate which output operands are written to before all the input operands are consumed. This is necessary with both output operands that use the = modifier and read/write output operands that use +. For example consider the two following functions:

int
foo() {
    int a = 1;
    asm("add %1, %0" : "+r" (a) : "r" (1));
    return a;
}

int
bar() {
    int a = 1;
    asm("add %1, %0\n\t"
        "add %1, %0"
        : "+r" (a) : "r" (1));
    return a;
}

Both inline assembly statements use the same operands and the same constraints, but the only the inline assembly statement in foo is correct, the one in bar is broken. With optimizations enabled GCC generates the following code for the two functions:

_foo:
    movl    $1, %eax
/APP
    add %eax, %eax
/NO_APP
    ret

_bar:
    movl    $1, %eax
/APP
    add %eax, %eax
    add %eax, %eax
/NO_APP
    ret

GCC sees no reason not to use the same register, EAX, for both operands in both inline assembly statements. While this isn't a problem in foo, it causes bar to calculate the wrong result of 4 instead of the expected 3.

A correct version of bar would use the early clobber modifier:

int
baz() {
    int a = 1;
    asm("add %1, %0\n\t"
        "add %1, %0"
        : "+&r" (a) : "r" (1));
    return a;
}
_baz:
    movl    $1, %eax
    movl    %eax, %edx
/APP
    add %edx, %eax
    add %edx, %eax
/NO_APP
    ret

When compiling baz GCC knows to use a different register for both operands so it doesn't matter that the read/write output operand is modified before the input operand is read for the second time.

like image 59
Ross Ridge Avatar answered Oct 17 '22 06:10

Ross Ridge