Why is this inline assembly not working with a separate asm volatile statement for each instruction?

前端 未结 3 1326
没有蜡笔的小新
没有蜡笔的小新 2020-12-19 02:12

For the the following code:

long buf[64];

register long rrax asm (\"rax\");
register long rrbx asm (\"rbx\");
register long rrsi asm (\"rsi\");

rrax = 0x34         


        
3条回答
  •  情书的邮戳
    2020-12-19 02:32

    Slightly off-topic but I'd like to follow up a bit on gcc inline assembly.

    The (non-)need for __volatile__ comes from the fact that GCC optimizes inline assembly. GCC inspects the assembly statement for side effects / prerequisites, and if it finds them not to exist it may choose to move the assembly instruction around or even decide to remove it. All __volatile__ does is to tell the compiler "stop caring and put this right there".

    Which is usually not what you really want.

    This is where the need for constraints come in. The name is overloaded and actually used for different things in GCC inline assembly:

    • constraints specify input / output operands used in the asm() block
    • constraints specify the "clobber list", which details what "state" (registers, condition codes, memory) are affected by the asm().
    • constraints specify classes of operands (registers, addresses, offsets, constants, ...)
    • constraints declare associations / bindings between assembler entities and C/C++ variables / expressions

    In many cases, developers abuse __volatile__ because they noticed their code either being moved around or even disappearing without it. If this happens, it's usually rather a sign that the developer has attempted not to tell GCC about side effects / prerequisites of the assembly. For example, this buggy code:

    register int foo __asm__("rax") = 1234;
    register int bar __adm__("rbx") = 4321;
    
    asm("add %rax, %rbx");
    printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
    

    It's got several bugs:

    • for one, it only compiles due to a gcc bug (!). Normally, to write register names in inline assembly, double %% are needed, but in the above if you actually specify them you get a compiler/assembler error, /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'.
    • second, it's not telling the compiler when and where you need/use the variables. Instead, it assumes the compiler honours asm() literally. That might be true for Microsoft Visual C++ but is not the case for gcc.

    If you compile it without optimization, it creates:

    0000000000400524 
    : [ ... ] 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: bb e1 10 00 00 mov $0x10e1,%ebx 40053e: 48 01 c3 add %rax,%rbx 400541: 48 89 da mov %rbx,%rdx 400544: b8 5c 06 40 00 mov $0x40065c,%eax 400549: 48 89 d6 mov %rdx,%rsi 40054c: 48 89 c7 mov %rax,%rdi 40054f: b8 00 00 00 00 mov $0x0,%eax 400554: e8 d7 fe ff ff callq 400430 [...]
    You can find your add instruction, and the initializations of the two registers, and it'll print the expected. If, on the other hand, you crank optimization up, something else happens:
    0000000000400530 
    : 400530: 48 83 ec 08 sub $0x8,%rsp 400534: 48 01 c3 add %rax,%rbx 400537: be e1 10 00 00 mov $0x10e1,%esi 40053c: bf 3c 06 40 00 mov $0x40063c,%edi 400541: 31 c0 xor %eax,%eax 400543: e8 e8 fe ff ff callq 400430 [ ... ]
    Your initializations of both the "used" registers are no longer there. The compiler discarded them because nothing it could see was using them, and while it kept the assembly instruction it put it before any use of the two variables. It's there but it does nothing (Luckily actually ... if rax / rbx had been in use who can tell what'd have happened ...).

    And the reason for that is that you haven't actually told GCC that the assembly is using these registers / these operand values. This has nothing whatsoever to do with volatile but all with the fact you're using a constraint-free asm() expression.

    The way to do this correctly is via constraints, i.e. you'd use:

    int foo = 1234;
    int bar = 4321;
    
    asm("add %1, %0" : "+r"(bar) : "r"(foo));
    printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
    

    This tells the compiler that the assembly:

    1. has one argument in a register, "+r"(...) that both needs to be initialized before the assembly statement, and is modified by the assembly statement, and associate the variable bar with it.
    2. has a second argument in a register, "r"(...) that needs to be initialized before the assembly statement and is treated as readonly / not modified by the statement. Here, associate foo with that.

    Notice no register assignment is specified - the compiler chooses that depending on the variables / state of the compile. The (optimized) output of the above:

    0000000000400530 
    : 400530: 48 83 ec 08 sub $0x8,%rsp 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: be e1 10 00 00 mov $0x10e1,%esi 40053e: bf 4c 06 40 00 mov $0x40064c,%edi 400543: 01 c6 add %eax,%esi 400545: 31 c0 xor %eax,%eax 400547: e8 e4 fe ff ff callq 400430 [ ... ]
    GCC inline assembly constraints are almost always necessary in some form or the other, but there can be multiple possible ways of describing the same requirements to the compiler; instead of the above, you could also write:

    asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
    

    This tells gcc:

    1. the statement has an output operand, the variable bar, that after the statement will be found in a register, "=r"(...)
    2. the statement has an input operand, the variable foo, which is to be placed into a register, "r"(...)
    3. operand zero is also an input operand and to be initialized with bar

    Or, again an alternative:

    asm("add %1, %0" : "+r"(bar) : "g"(foo));
    

    which tells gcc:

    1. bla (yawn - same as before, bar both input/output)
    2. the statement has an input operand, the variable foo, which the statement doesn't care whether it's in a register, in memory or a compile-time constant (that's the "g"(...) constraint)

    The result is different from the former:

    0000000000400530 
    : 400530: 48 83 ec 08 sub $0x8,%rsp 400534: bf 4c 06 40 00 mov $0x40064c,%edi 400539: 31 c0 xor %eax,%eax 40053b: be e1 10 00 00 mov $0x10e1,%esi 400540: 81 c6 d2 04 00 00 add $0x4d2,%esi 400546: e8 e5 fe ff ff callq 400430 [ ... ]
    because now, GCC has actually figured out foo is a compile-time constant and simply embedded the value in the add instruction ! Isn't that neat ?

    Admittedly, this is complex and takes getting used to. The advantage is that letting the compiler choose which registers to use for what operands allows optimizing the code overall; if, for example, an inline assembly statement is used in a macro and/or a static inline function, the compiler can, depending on the calling context, choose different registers at different instantiations of the code. Or if a certain value is compile-time evaluatable / constant in one place but not in another, the compiler can tailor the created assembly for it.

    Think of GCC inline assembly constraints as kind of "extended function prototypes" - they tell the compiler what types and locations for arguments / return values are, plus a bit more. If you don't specify these constraints, your inline assembly is creating the analogue of functions that operate on global variables/state only - which, as we probably all agree, are rarely ever doing exactly what you intended.

提交回复
热议问题