4

I invoke Clang 12.0.0 with -Os -march=haswell to compile the following C program:

int bar(int);

int foo(int x) {
  const int b = bar(x);
  if (x || b) {
      return 123;
  }
  return 456;
}

The following assembly is generated:

foo:                                    # @foo
        push    rbx
        mov     ebx, edi
        call    bar
        or      eax, ebx
        mov     ecx, 456
        mov     eax, 123
        cmove   eax, ecx
        pop     rbx
        ret

https://gcc.godbolt.org/z/WsGoM56Ez

As I understand it, the caller of foo sets up x in RAX/EAX. foo then calls bar, which doesn't require modifying RAX/EAX, since x is passed through as unmodified input.

The or eax, ebx instruction appears to be comparing the input x with the result of bar. How does that result end up in EBX? What purpose does mov ebx,edi serve?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Charles Nicholson
  • 888
  • 1
  • 8
  • 21
  • 4
    `x` is passed in via `edi`. `ebx` is a callee-saved register, the `mov ebx, edi` serves the purpose of preserving the value across the `call bar`. The result of `bar(x)` is what is in `eax` at the `or eax, ebx` with `x` still being in `ebx`. – Jester May 10 '21 at 21:42
  • 1
    The `or` combines two values in a bitwise OR operation. After this the destination register is overwritten here. However, the arithmetic status flags are still as set by the `or`. So `cmove` (conditional move, if equal) only writes the new value to its destination if the `or` found both of its inputs to equal zero. – ecm May 10 '21 at 21:47
  • 3
    `or eax, ebx` is implementing `x || b`, it's not comparing anything. – Barmar May 10 '21 at 21:49
  • 1
    I think all these misunderstandings stem from a wrong assumption about where the calling convention passes `x`, making this a duplicate. If not, we can reopen or find more duplicates. Matt Godbolt's CppCon talk (https://youtu.be/bSkpMdDe4g4) might be helpful. (Related: [How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116) ) – Peter Cordes May 10 '21 at 21:50
  • (Also related: [What registers are preserved through a linux x86-64 function call](https://stackoverflow.com/q/18024672) / [What are callee and caller saved registers?](https://stackoverflow.com/a/56178078), as well as [What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64](https://stackoverflow.com/q/2535989) which is still a possible duplicate) – Peter Cordes May 10 '21 at 21:55

1 Answers1

4

I'm afraid you are mistaken:

You can verify the basics by compiling a function like int foo(int x){return x;} - you'll see just a mov eax, edi.

Here is a commented version:

foo:                                    # @foo
        push    rbx           # save register rbx
        mov     ebx, edi      # save argument `x` in ebx
        call    bar           # a = bar()  (in eax)
        or      eax, ebx      # compute `x | a`, setting FLAGS
        mov     ecx, 456      # prepare 456 for conditional move
        mov     eax, 123      # eax = 123
        cmove   eax, ecx      # if `(x | a) == 0` set eax to 456
        pop     rbx           # restore register rbx
        ret                   # return value is in eax

The compiler optimizes x || b as (x | b) != 0 which allows for branchless code generation.

Note that mov doesn't modify the FLAGS, unlike most integer ALU instructions.

chqrlie
  • 131,814
  • 10
  • 121
  • 189