https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94113

            Bug ID: 94113
           Summary: Apparently incorrect register allocation in inline asm
                    when using CMOV.
           Product: gcc
           Version: 9.2.1
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: inline-asm
          Assignee: unassigned at gcc dot gnu.org
          Reporter: contact at pgazz dot com
  Target Milestone: ---

Created attachment 48001
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=48001&action=edit
The preprocessed C file

Version: gcc (Debian 9.2.1-21) 9.2.1 20191130
System type: Debian GNU/Linux bullseye/sid
Options and complete command-line: gcc -Wall -Wextra -o broken broken.i
Compiler output: no errors or warnings
Preprocessed file: broken.i

The included example (broken.c) illustrates what appears to be a bug in inline
assembly when using the CMOV instruction.  Two different C variables are
allocated the same register, causing incorrect behavior.

      __asm__ volatile("movl %3, %0;\n\t" \
                   "cmpl $0, %1;\n\t" \
                   "cmovne %2, %0;\n\t" \
                   : "=r"(final_result) \
                   : "r"(cond), "r"(if_true), "r"(if_false) \
                   : "cc" \
                   );

The inline assembly uses CMOV to assign final_result to either if_true or
if_false depending on the cond variable, which is read in from the first
command-line argument and converted via atoi.

I was expecting the behavior to be equivalent to testing the value of cond,
then assigning final_result to either if_true or if_false depending on whether
cond is non-zero or zero respectively.  But instead, it always assigns it to
if_true.  Looking at the compiler output in broken.s, the eax register seems to
be allocated to both cond and to final_result.  In the first line of the inline
assembly movl %ecx, %eax; it looks like final_result is eax.  But in the next
line cmpl $0, %eax; it looks like eax is allocated to cond, which has already
been overwritten by the value of if_false in the previous line.

        movl    $0, -16(%rbp)   # final_result
        # ...
        movl    %eax, -12(%rbp) # cond, after atoi
        movl    $333, -8(%rbp)  # if_true
        movl    $444, -4(%rbp)  # if_false
        movl    -12(%rbp), %eax # cond -> eax
        movl    -8(%rbp), %edx  # if_true -> edx
        movl    -4(%rbp), %ecx  # if_false -> ecx
    #APP
    # 17 "broken.c" 1
        movl %ecx, %eax;        # move if_false to eax, which 
        cmpl $0, %eax;          # should be comparing to the value of cond, not
if_false
        cmovne %edx, %eax;      # conditional move if_true to final_result, but
condition (not equal) is always true

    # 0 "" 2
    #NO_APP
        movl    %eax, -16(%rbp) # eax -> final_result

Is the register allocator somehow assuming that the write-only modifier =r
means the value is only written at the end of the inline assembly?

I could circumvent it by explicitly using another register (esi) and adding an
extra MOV from that register as shown below.

      __asm__ volatile("movl %3, %%esi;\n\t" \
                   "cmpl $0, %1;\n\t" \
                   "cmovne %2, %%esi;\n\t" \
                   "movl %%esi, %0;\n\t" \
                   : "=r"(final_result) \
                   : "r"(cond), "r"(if_true), "r"(if_false) \
                   : "cc", "esi" \
                   );

A somewhat similar-looking bug was reported almost 13 years ago, but that
particular bug does not seem to happen anymore:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=31386

When compiling with optimizations, the bug still exists when running with
./broken 1, which just turns the cond variable to true.

Reply via email to