Search code examples
c++assemblygccinline-assembly

Why is this inline ASM function adding the first input operand twice?


I'm a beginner to assembly and a novice programmer in general. I'm trying to wrap my head around the basics of assembly by writing some basic inline asm functions in C++. I'm compiling with GNU g++, and the behavior is the same with every optimization setting. My PC is running windows with x86_64 architecture but inline assembly is always x86 32-bit to my understanding (is that correct? I have no formal CS/programming education so I might not know some basic things that seem obvious).

#include <iostream>

int sum(int a, int b, int c)
{
    int result = 0;
    asm(
        /* "addl %1, %0;" */
// The above line causes the result to be a + a + b + c.
// When commented out the result is a + b + c.
        "addl %2, %0;"
        "addl %3, %0;"
        : "=r" (result)
        : "r" (a), "r" (b), "r" (c)
    );
    return result;
}


int main()
{
    std::cout << sum(4, 10, 23) << std::endl;
// [Out] 37
}

My original expectation of how the above asm code was supposed to behave:

"addl %1, %0;"
--> Add the first input operand (to be assigned later) to the output operand

"addl %2, %0;"
--> Add the second input to the output

"addl %3, %0;"
--> Add the third input to the output

: "=r" (result)
--> Assign int result to be the output (%0) and store in a register (?)

: "r" (a), "r" (b), "r" (c)
--> Assign the inputs (%1, %2, %3) and store in a register (?)

chatGPT 4 insists that my original code was correct and should've resulted in a + b + c. Even when I explain the behavior of the program to chatGPT 4, it can't figure out what's happening, which is something I haven't seen before. It gave me a bunch of theories but the only one that seemed plausible was that, in plain terms, the value of the variable int a is being stored in the register that's being used to store the output, so the output is starting with the value of int a, despite int result being initialized with 0.

Can anyone help me understand what's happening?


Solution

  • Basically, what happens is that the expressions in the constraints are "bound" to resources as specified by the constraints, and then the asm code is emitted using the resources that were bound. In this case (with the "r" constraint), all of these things are bound to registers. An issue is that the inputs and outputs are bound independently, so a resource used for an input can be reused for an output.

    So for your specific example:

    : "=r" (result)
    --> bind the variable result to a register for output (only). So the register bound will contain the value at the end of the asm code, but not have any particular value at the start of the code.

    : "r" (a), "r" (b), "r" (c)
    --> bind the expressions a, b, and c to 3 (different) registers on input. That means these registers will be set to have the values of these expressions prior to the asm code, but no assumptions will be made about their values after the code.

    "addl %1, %0;"
    --> add the register bound to a to the register bound to result

    "addl %2, %0;"
    --> add the register bound to b to the register bound to result

    "addl %3, %0;"
    --> add the register bound to c to the register bound to result

    So the issue you have is that if result and a are bound to the same register (likely in such a simple case), you'll end up with a + a + b + c. You also have the issue that if result is bound to a different register, that register's value (which might be anything) will be added in.

    One obvious fix for both of these would be to use the constraint "+r" for result -- this binds the register to it FOR BOTH INPUT and output. That will ensure that result is bound to a different register to a, b, and c AND that register will be 0 on entry to the asm block.