2

I am analyzing the following snippet of extended inline assembly code that returns the sum of two numbers given as parameters into a function.

int addAB(int a, int b){
    int result;

    __asm__(
        "movl %%ebx, %%eax;"
        "addl %%ecx, %%eax;":
        "=a" (result) :
        "b" (b), "c" (a) :
        "cc"
    );


    return result;
}

From my understanding, since EBX is a callee-saved register, it's value means it needs to be preserved across calls (is it necessary to do a push-pop here)? Furthermore, the CC clobber is specified which means that the condition codes are changed, but it seems that condition codes are almost always changed (from adding registers together to comparing registers)--when aren't condition codes potentially changed? If they are almost always potentially changed, would it just be a safe idea to put the "cc" clobber option for every clobber?

Adam Lee
  • 436
  • 1
  • 14
  • 49
  • 3
    You are not doing a call, so callee-saved does not apply. at&t syntax has order src, dst so only eax is changing and that is listed as output. cc is safe to put in clobber. This code does change flags, so it should be specified even though currently gcc assumes eflags is changed anyway. PS: of course this is quite a bad example of inline asm. The `mov` is unnecessary and the constraints are too strict. – Jester Dec 03 '20 at 00:36
  • Your conclusion that “cc” is pretty much always necessary for x86 is exactly why gcc makes it the default, which means that it is never necessary. – prl Dec 03 '20 at 03:14
  • Hint: mov+add reg,reg can always be replaced by LEA, unless you need the FLAGS result for something. Also, normally you'd use `"r"` input and output constraints to let the compiler pick convenient registers. (e.g. in a non-inlined version of this, it would typically pick edi and esi where the calling convention puts `a` and `b`.) I hope you just nailed down hard registers for the purposes of this example, to ask about a call-preserved register like EBX. – Peter Cordes Dec 03 '20 at 14:21

1 Answers1

4

There is really just one rule to know:

If you modify any(*) register, you must tell the compiler about it, either by declaring that register as an output operand, or by listing it as a clobber.

That's all. It doesn't matter whether the register is "volatile" (aka caller-saved) or not. And if you do inform the compiler appropriately, then you do not need to save and restore the register yourself, so no push/pop in your inline asm. The compiler will take care of it for you if needed. (Indeed, on x86-64, you must not push anything to the stack with inline asm, because of the red zone.)

(*) "Any" means "any register that compiled code might use"; in particular, all general-purpose integer, floating point, and vector registers. It does not apply to system or control registers that the compiler would not otherwise touch, nor to the x86 segment registers for the same reason. If you modify any of these, you're responsible for the consequences, and making sure the compiled code will still work correctly in the new state. (For example, leaving the direction flag DF set will typically cause compiled code to fail, as it expects it to always be clear.)

Your code doesn't actually modify ebx. It's true that the compiler will have to modify ebx to load your argument b there, but the compiler is well aware that ebx is non-volatile and so it will take care of saving and restoring it. (Indeed, by the time your asm gets control, it would be too late to save it anyway.) The only register your code does modify is eax, and that's declared as an output operand, so you're good. If you did modify ebx, you would need to list it as a "+b" input-output operand (since you need it as an input) or as a clobber (if it wasn't already an input) - but the same would be true if you modified the callee-saved ecx.

Note that if this function inlines into a larger function, the compiler might already want to save/restore EBX for some other purpose. Or this function might inline multiple times. You only want to save/restore a register once per function, not once per asm statement that might be in a loop, so leaving it to the compiler is a good and sensible design.

As for the "cc" clobber, my opinion is that it is good practice to list it whenever your code modifies the flags register, as your add instruction does. However, on x86, since so many instructions modify the flags, and so many programmers forget to list it as a clobber, the compiler assumes that every inline asm clobbers the flags. So, technically, you can safely omit the clobber. However, this may develop bad habits, since on other architectures the clobber is not assumed and must be specified if applicable. See What happens in the assembly output when we add "cc" to clobber list.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • 2
    There are some registers that should the same value on exit from the asm statement as on entry, because the compiler or its runtime library assume they don't change, like the direction flag or the segment registers. There are also some registers you may or may not be able to change like the frame pointer register or global pointer register, but if you do modify them when not allowed then the compiler will give an error if you've got your constraints and clobbers correct. – Ross Ridge Dec 03 '20 at 04:46
  • 1
    Not only any modified register: The compiler needs to know about *memory* that you write, or even read, if any. ([How can I indicate that the memory \*pointed\* to by an inline ASM argument may be used?](https://stackoverflow.com/q/56432259)). e.g. via pointer inputs, or by a symbol name for static storage. Not directly applicable to the OP's code, as it doesn't use pointers. – Peter Cordes Dec 03 '20 at 14:20